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

359 lines
12 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ImportGesconF24LegacyCommand extends Command
{
protected $signature = 'gescon:import-f24-legacy
{--stabile= : Codice stabile (es. 0023)}
{--base-path=/mnt/gescon-archives/gescon : Root archivi legacy}
{--years= : Anni da importare (csv, es. 2024,2025)}
{--truncate : Pulisce f24 staging per lo stabile prima dell\'import}
{--mark-obsolete : Marca record importati come storici in note}
{--dry-run : Simulazione senza scrittura}';
protected $description = 'Importa i dati F24 da generale_stabile.mdb espandendo i campi _1.._6 in record normalizzati.';
private bool $dryRun = false;
public function handle(): int
{
$this->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<int, int> */
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<int, array<string, string>> */
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;
}
}