netgescon-day0/app/Console/Commands/ImportGesconFattureLegacyMdbCommand.php

360 lines
12 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ImportGesconFattureLegacyMdbCommand extends Command
{
protected $signature = 'import:gescon-fatture-legacy
{tenant : Tenant ID}
{--mdb= : Percorso file generale_stabile.mdb}
{--table=Fatture : Nome tabella Access}
{--into=stg_fatture_gescon : Tabella staging destinazione}
{--truncate : Svuota la tabella staging prima dell\'import}
{--preview : Mostra una preview e non importa}
{--limit=10 : Limite righe per preview}';
protected $description = 'Importa fatture legacy (tabella Fatture) da generale_stabile.mdb in staging (idempotente per legacy_id)';
public function handle(): int
{
$tenant = (string) $this->argument('tenant');
$mdb = (string) ($this->option('mdb') ?: '');
$table = (string) ($this->option('table') ?: 'Fatture');
$into = (string) ($this->option('into') ?: 'stg_fatture_gescon');
if ($mdb === '' || !is_file($mdb)) {
$this->error('File MDB non trovato. Passa --mdb=/path/generale_stabile.mdb');
return self::FAILURE;
}
if (!DB::getSchemaBuilder()->hasTable($into)) {
$this->error("Tabella staging non trovata: {$into} (esegui migrate)");
return self::FAILURE;
}
$bin = trim((string) @shell_exec('command -v mdb-export')) ?: '';
if ($bin === '') {
$this->error('mdb-export non trovato (installa mdbtools)');
return self::FAILURE;
}
$csv = $this->runShell(sprintf(
"%s -D %s -d %s -q %s -R \"\\n\" %s %s",
escapeshellarg($bin),
escapeshellarg('%Y-%m-%d %H:%M:%S'),
escapeshellarg('|'),
escapeshellarg('"'),
escapeshellarg($mdb),
escapeshellarg($table),
));
if ($csv === null || trim($csv) === '') {
$this->error('Nessun dato esportato (tabella inesistente o file non leggibile)');
return self::FAILURE;
}
$rows = $this->parseDelimited($csv, '|');
if ($this->option('preview')) {
$limit = max(1, (int) $this->option('limit'));
$this->line('Preview:');
foreach (array_slice($rows, 0, $limit) as $r) {
$this->line(json_encode($this->mapRow($tenant, $mdb, $r), JSON_UNESCAPED_UNICODE));
}
$this->line('Totale righe: ' . count($rows));
return self::SUCCESS;
}
if ($this->option('truncate')) {
DB::table($into)->truncate();
}
$now = now();
$payloads = [];
$count = 0;
foreach ($rows as $row) {
$payload = $this->mapRow($tenant, $mdb, $row);
if ($payload['legacy_id_fatture'] === null) {
continue;
}
$payload['created_at'] = $now;
$payload['updated_at'] = $now;
$payloads[] = $payload;
if (count($payloads) >= 500) {
DB::table($into)->upsert($payloads, ['tenant_id', 'legacy_id_fatture'], [
'legacy_id_stabile',
'riferimento',
'cod_fornitore',
'data_fattura',
'data_pagamento',
'numero_fattura',
'descrizione_sintetica',
'descrizione_corpo',
'imponibile',
'importo_iva',
'totale_fattura',
'importo_netto',
'onorario',
'rimborsi',
'aliq_cassa',
'importo_cassa',
'aliq_rda',
'importo_rda',
'aliq_inps',
'importo_inps',
'aliq_iva',
'legacy_id_anno',
'reg_gestione',
'reg_nstra',
'note',
'raw',
'sorgente_archivio',
'updated_at',
]);
$count += count($payloads);
$payloads = [];
}
}
if (!empty($payloads)) {
DB::table($into)->upsert($payloads, ['tenant_id', 'legacy_id_fatture'], [
'legacy_id_stabile',
'riferimento',
'cod_fornitore',
'data_fattura',
'data_pagamento',
'numero_fattura',
'descrizione_sintetica',
'descrizione_corpo',
'imponibile',
'importo_iva',
'totale_fattura',
'importo_netto',
'onorario',
'rimborsi',
'aliq_cassa',
'importo_cassa',
'aliq_rda',
'importo_rda',
'aliq_inps',
'importo_inps',
'aliq_iva',
'legacy_id_anno',
'reg_gestione',
'reg_nstra',
'note',
'raw',
'sorgente_archivio',
'updated_at',
]);
$count += count($payloads);
}
$this->info("✅ Import completato: {$count} righe elaborate (upsert)");
return self::SUCCESS;
}
private function runShell(string $cmd): ?string
{
$out = shell_exec($cmd . ' 2>/dev/null');
if ($out === null || $out === '') {
return null;
}
return $out;
}
private function parseDelimited(string $text, string $sep): array
{
$lines = preg_split("/\r?\n/", trim($text));
if (!$lines || count($lines) === 0) {
return [];
}
$headers = str_getcsv(array_shift($lines), $sep);
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
$rows = [];
foreach ($lines as $line) {
if ($line === '') {
continue;
}
$cols = str_getcsv($line, $sep);
if (count($cols) < count($headers)) {
$cols = array_pad($cols, count($headers), null);
}
if (count($cols) > count($headers)) {
$cols = array_slice($cols, 0, count($headers));
}
$row = @array_combine($headers, $cols);
if (!$row) {
continue;
}
$rows[] = $row;
}
return $rows;
}
private function mapRow(string $tenant, string $mdb, array $row): array
{
$get = function (string ...$keys) use ($row) {
foreach ($keys as $k) {
$lk = strtolower($k);
if (array_key_exists($lk, $row)) {
$v = $row[$lk];
return is_string($v) ? trim($v) : $v;
}
}
return null;
};
$legacyId = $this->toInt($get('id_fatture', 'id_fattura', 'id'));
$raw = $row;
return [
'tenant_id' => $tenant,
'sorgente_archivio' => $mdb,
'legacy_id_fatture' => $legacyId,
'legacy_id_stabile' => $this->toInt($get('id_stabile')),
'riferimento' => $this->toInt($get('riferimento')),
'cod_fornitore' => $this->toInt($get('cod_fornitore', 'cod_fornitore')),
'data_fattura' => $this->toDate($get('data_fattura')),
'data_pagamento' => $this->toDate($get('data_pagamento')),
'numero_fattura' => $this->nullIfEmpty($get('num_fattura', 'numero_fattura')),
'descrizione_sintetica' => $this->nullIfEmpty($get('descrizione_sintetica')),
'descrizione_corpo' => $this->nullIfEmpty($get('descriz_corpo', 'descrizione_corpo')),
'imponibile' => $this->money($get('imponibile') ?? 0),
'importo_iva' => $this->money($get('importo_iva') ?? 0),
'totale_fattura' => $this->money($get('totale_fattura') ?? 0),
'importo_netto' => $this->money($get('importo_netto') ?? 0),
'onorario' => $this->money($get('onorario') ?? 0),
'rimborsi' => $this->money($get('rimborsi') ?? 0),
'aliq_cassa' => $this->decimal($get('aliq_cassa')),
'importo_cassa' => $this->money($get('importo_cassa') ?? 0),
'aliq_rda' => $this->decimal($get('aliq_rda')),
'importo_rda' => $this->money($get('importo_rda') ?? 0),
'aliq_inps' => $this->decimal($get('aliq_inps')),
'importo_inps' => $this->money($get('importo_inps') ?? 0),
'aliq_iva' => $this->decimal($get('aliq_iva')),
'legacy_id_anno' => $this->toInt($get('id_anno')),
'reg_gestione' => $this->nullIfEmpty($get('reg_gestione')),
'reg_nstra' => $this->toInt($get('reg_nstra')),
'note' => $this->nullIfEmpty($get('note')),
'raw' => json_encode($raw, JSON_UNESCAPED_UNICODE),
];
}
private function nullIfEmpty($v): ?string
{
if ($v === null) {
return null;
}
$s = trim((string) $v);
return $s === '' ? null : $s;
}
private function toInt($v): ?int
{
if ($v === null) {
return null;
}
$s = trim((string) $v);
if ($s === '' || !is_numeric($s)) {
return null;
}
return (int) $s;
}
private function toDate($v): ?string
{
if ($v === null) {
return null;
}
$s = trim((string) $v);
if ($s === '') {
return null;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $s)) {
return substr($s, 0, 10);
}
if (preg_match('/^\d{2}\/\d{2}\/\d{2} /', $s)) {
$dt = \DateTime::createFromFormat('m/d/y H:i:s', $s);
if (!$dt) {
return null;
}
// Fix 2-digit years (e.g. 69 should be 1969, not 2069)
$yy = (int) $dt->format('y');
$curYY = (int) now()->format('y');
$year = ($yy > $curYY) ? (1900 + $yy) : (2000 + $yy);
return sprintf('%04d-%02d-%02d', $year, (int) $dt->format('m'), (int) $dt->format('d'));
}
if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $s)) {
$dt = \DateTime::createFromFormat('d/m/Y', $s);
return $dt?->format('Y-m-d');
}
return null;
}
private function money($v): float
{
if ($v === null || $v === '') {
return 0.0;
}
$s = trim((string) $v);
if ($s === '') {
return 0.0;
}
// Remove spaces (including non-breaking)
$s = str_replace([" ", "\u{00A0}"], '', $s);
// If it looks like a number with dots as thousand separators and comma as decimal:
// e.g. 1.234.567,89
if (preg_match('/^-?\d{1,3}(?:\.\d{3})+(?:,\d+)?$/', $s)) {
$s = str_replace('.', '', $s);
$s = str_replace(',', '.', $s);
return (float) $s;
}
// If it looks like a number with commas as thousand separators and dot as decimal:
// e.g. 1,234,567.89
if (preg_match('/^-?\d{1,3}(?:,\d{3})+(?:\.\d+)?$/', $s)) {
$s = str_replace(',', '', $s);
return (float) $s;
}
// Otherwise: treat comma as decimal separator if present, keep dot as decimal.
// This correctly handles MDB exports like 79.95999999999999
if (str_contains($s, ',') && !str_contains($s, '.')) {
$s = str_replace(',', '.', $s);
} elseif (str_contains($s, ',') && str_contains($s, '.')) {
// Mixed but not matching a clear thousands pattern: assume comma is decimal and dots are thousands
$s = str_replace('.', '', $s);
$s = str_replace(',', '.', $s);
}
// Strip anything not number-ish
$s = preg_replace('/[^0-9\-\.]/', '', $s) ?? '';
if ($s === '' || $s === '-' || $s === '.') {
return 0.0;
}
return (float) $s;
}
private function decimal($v): ?float
{
if ($v === null || $v === '') {
return null;
}
return $this->money($v);
}
}