dryRun = (bool) $this->option('dry-run'); $code = $this->normalizeCode((string) ($this->option('stabile') ?? '')); if ($code === '') { $this->error('Specificare --stabile=0000'); return self::FAILURE; } if (! Schema::connection('gescon_import')->hasTable('f24')) { $this->error('Tabella gescon_import.f24 non disponibile.'); return self::FAILURE; } $root = rtrim((string) ($this->option('base-path') ?? '/mnt/gescon-archives/gescon'), '/'); $mdb = $root . '/' . $code . '/generale_stabile.mdb'; if (! is_file($mdb)) { $this->error('File generale_stabile.mdb non trovato: ' . $mdb); return self::FAILURE; } $rows = $this->exportTable($mdb, ['F24', 'f24']); if (empty($rows)) { $this->warn('Tabella F24 non trovata o vuota.'); return self::SUCCESS; } $yearsFilter = $this->parseYearsCsv((string) ($this->option('years') ?? '')); if ((bool) $this->option('truncate')) { $deleteQ = DB::connection('gescon_import')->table('f24')->where('cod_stabile', $code); $toDelete = (int) $deleteQ->count(); if (! $this->dryRun && $toDelete > 0) { $deleteQ->delete(); } $this->line('Purge f24 staging: ' . $toDelete . ($this->dryRun ? ' (dry-run)' : '')); } $seen = []; $stats = [ 'rows' => count($rows), 'slots_seen' => 0, 'filtered_year' => 0, 'duplicates' => 0, 'inserted' => 0, 'already_present' => 0, 'invalid' => 0, ]; foreach ($rows as $row) { $rowYear = $this->extractYear((string) ($row['anno'] ?? '')); $rowMonth = $this->toInt($row['mese'] ?? null); for ($i = 1; $i <= 6; $i++) { $stats['slots_seen']++; $codTributo = trim((string) ($row['cod_tributo_' . $i] ?? '')); if ($codTributo === '') { continue; } $slotYear = $this->extractYear((string) ($row['anno_' . $i] ?? '')) ?? $rowYear; if (! empty($yearsFilter) && ($slotYear === null || ! in_array($slotYear, $yearsFilter, true))) { $stats['filtered_year']++; continue; } $deb = $this->toAmount($row['importo_tributo_deb_' . $i] ?? null); $cre = $this->toAmount($row['importo_tributo_cre_' . $i] ?? null); $importo = round($deb - $cre, 2); $date = $this->resolveDateVersamento($row, $slotYear ?? $rowYear, $rowMonth); if ($date === null) { $stats['invalid']++; continue; } $periodo = trim((string) ($row['rateiz_' . $i] ?? '')); if ($slotYear !== null) { $periodo = $periodo !== '' ? ($periodo . '/' . $slotYear) : (string) $slotYear; } $noteParts = [ 'slot=' . $i, 'rif_f24=' . trim((string) ($row['rif_f24_mio'] ?? '')), 'banca=' . trim((string) ($row['banca_f24'] ?? '')), 'agenzia=' . trim((string) ($row['agenzia'] ?? '')), ]; if ((bool) $this->option('mark-obsolete')) { $noteParts[] = 'obsolete=1'; } $note = implode('; ', array_filter($noteParts, fn($v) => trim((string) $v) !== '')); $importoFormatted = number_format($importo, 2, '.', ''); $fingerprint = implode('|', [$code, $date, $codTributo, $periodo, $importoFormatted]); if (isset($seen[$fingerprint])) { $stats['duplicates']++; continue; } $seen[$fingerprint] = true; $existsQ = DB::connection('gescon_import') ->table('f24') ->where('cod_stabile', $code) ->where('data_versamento', $date) ->where('codice_tributo', $codTributo) ->where('periodo_riferimento', $periodo) ->where('importo', $importoFormatted) ->where('importo_euro', $importoFormatted); if ($existsQ->exists()) { $stats['already_present']++; continue; } if ($this->dryRun) { $stats['inserted']++; continue; } DB::connection('gescon_import')->table('f24')->insert([ 'cod_stabile' => $code, 'data_versamento' => $date, 'codice_tributo' => $codTributo, 'importo' => $importoFormatted, 'importo_euro' => $importoFormatted, 'periodo_riferimento' => $periodo !== '' ? $periodo : null, 'note' => $note !== '' ? $note : null, 'created_at' => now(), 'updated_at' => now(), ]); $stats['inserted']++; } } $this->info('Import F24 completato' . ($this->dryRun ? ' (DRY RUN)' : '')); $this->line('- righe legacy lette: ' . $stats['rows']); $this->line('- slot analizzati (_1.._6): ' . $stats['slots_seen']); $this->line('- filtrati per anno: ' . $stats['filtered_year']); $this->line('- duplicati intra-run: ' . $stats['duplicates']); $this->line('- gia presenti: ' . $stats['already_present']); $this->line('- record inseriti: ' . $stats['inserted']); $this->line('- record invalidi: ' . $stats['invalid']); return self::SUCCESS; } private function normalizeCode(string $code): string { $code = trim($code); if ($code === '') { return ''; } if (is_numeric($code)) { return str_pad((string) ((int) $code), 4, '0', STR_PAD_LEFT); } return $code; } /** @return array */ private function parseYearsCsv(string $value): array { $years = []; foreach (explode(',', $value) as $raw) { $v = trim($raw); if (preg_match('/^\d{4}$/', $v)) { $years[] = (int) $v; } } $years = array_values(array_unique($years)); sort($years); return $years; } private function extractYear(string $value): ?int { $value = trim($value); if ($value === '') { return null; } if (preg_match('/(19|20)\d{2}/', $value, $m)) { return (int) $m[0]; } return null; } private function toInt(mixed $value): ?int { if ($value === null) { return null; } $s = trim((string) $value); return $s !== '' && is_numeric($s) ? (int) $s : null; } private function toAmount(mixed $value): float { if ($value === null) { return 0.0; } $s = trim((string) $value); if ($s === '') { return 0.0; } $s = preg_replace('/[^0-9,.-]/', '', $s) ?? '0'; if (str_contains($s, ',') && str_contains($s, '.')) { $s = str_replace('.', '', $s); $s = str_replace(',', '.', $s); } elseif (str_contains($s, ',')) { $s = str_replace(',', '.', $s); } if (! is_numeric($s)) { $s = '0'; } return round((float) $s, 2); } private function resolveDateVersamento(array $row, ?int $fallbackYear, ?int $fallbackMonth): ?string { $raw = trim((string) ($row['dt_versamento'] ?? $row['dt_pagamento'] ?? '')); if ($raw !== '') { if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/', $raw, $m)) { $y = (int) $m[3]; if ($y < 100) { $y += 2000; } $a = (int) $m[1]; $b = (int) $m[2]; // Legacy MDB usa talvolta mm/dd, talvolta dd/mm: gestiamo entrambi in modo difensivo. $month = $a; $day = $b; if ($a > 12 && $b <= 12) { $month = $b; $day = $a; } $month = max(1, min(12, $month)); $day = max(1, min(31, $day)); return sprintf('%04d-%02d-%02d', $y, $month, $day); } if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $raw, $m)) { return sprintf('%04d-%02d-%02d', (int) $m[1], (int) $m[2], (int) $m[3]); } } if ($fallbackYear === null) { return null; } $month = $fallbackMonth !== null && $fallbackMonth >= 1 && $fallbackMonth <= 12 ? $fallbackMonth : 1; return sprintf('%04d-%02d-01', $fallbackYear, $month); } /** @return array> */ private function exportTable(string $mdbPath, array $candidates): array { if (! is_file($mdbPath) || ! is_readable($mdbPath)) { return []; } $binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables'; $binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export'; $tablesRaw = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: ''; if ($tablesRaw === '') { return []; } $tables = array_values(array_filter(preg_split('/\R/', trim($tablesRaw)) ?: [])); $table = null; foreach ($candidates as $cand) { foreach ($tables as $name) { if (strcasecmp($name, $cand) === 0) { $table = $name; break 2; } } } if ($table === null) { return []; } $tmp = tempnam(sys_get_temp_dir(), 'mdbf24_'); $cmd = sprintf( '%s -D %s -d %s -q %s %s %s > %s 2>/dev/null', escapeshellarg($binExport), escapeshellarg('%Y-%m-%d'), escapeshellarg('|'), escapeshellarg('"'), escapeshellarg($mdbPath), escapeshellarg($table), escapeshellarg($tmp) ); @shell_exec($cmd); if (! is_file($tmp) || filesize($tmp) === 0) { @unlink($tmp); return []; } $fh = fopen($tmp, 'r'); if (! $fh) { @unlink($tmp); return []; } $headers = fgetcsv($fh, 0, '|'); if (! is_array($headers) || empty($headers)) { fclose($fh); @unlink($tmp); return []; } $headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers); $rows = []; while (($cols = fgetcsv($fh, 0, '|')) !== false) { if (! is_array($cols)) { continue; } if (count($cols) < count($headers)) { $cols = array_pad($cols, count($headers), null); } if (count($cols) > count($headers)) { $cols = array_slice($cols, 0, count($headers)); } $assoc = @array_combine($headers, $cols); if (is_array($assoc)) { $rows[] = $assoc; } } fclose($fh); @unlink($tmp); return $rows; } }