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

362 lines
12 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ImportGesconIncDaEcCommand extends Command
{
protected $signature = 'import:gescon-inc-da-ec
{tenant : Tenant ID}
{--mdb= : Percorso file MDB contenente Inc_da_ec}
{--table=Inc_da_ec : Nome tabella Access}
{--stabile-id= : ID stabile locale}
{--stabile-code= : Codice stabile legacy (es. 0019)}
{--anno= : Anno gestione}
{--pdf-base=/mnt/gescon-archives/gescon : Root archivio PDF}
{--preview : Mostra preview senza scrivere}
{--limit=20 : Limite righe in preview o import}';
protected $description = 'Importa Inc_da_ec e mappa incassi_estratto_conto collegando incassi.n_riferimento = Num_incasso + Nome_file_pdf';
public function handle(): int
{
if (! Schema::hasTable('incassi_estratto_conto')) {
$this->error('Tabella incassi_estratto_conto non trovata.');
return self::FAILURE;
}
$tenant = (string) $this->argument('tenant');
$mdb = trim((string) ($this->option('mdb') ?: ''));
$table = trim((string) ($this->option('table') ?: 'Inc_da_ec'));
$anno = $this->option('anno') ? (int) $this->option('anno') : null;
$limit = max(1, (int) ($this->option('limit') ?: 20));
$preview = (bool) $this->option('preview');
if ($mdb === '' || ! is_file($mdb)) {
$this->error('File MDB non trovato. Passa --mdb=/path/file.mdb');
return self::FAILURE;
}
$stabileId = $this->resolveStabileId();
$stabileCode = $this->resolveStabileCode();
if (! $stabileId || $stabileCode === null) {
$this->error('Stabile non risolto: passa --stabile-id e/o --stabile-code.');
return self::FAILURE;
}
$gestioneId = $this->resolveGestioneId($tenant, $stabileId, $anno);
$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 non trovata o MDB non leggibile.');
return self::FAILURE;
}
$rows = $this->parseDelimited($csv, '|');
if ($rows === []) {
$this->warn('Nessuna riga da importare.');
return self::SUCCESS;
}
if ($preview) {
foreach (array_slice($rows, 0, $limit) as $row) {
$mapped = $this->mapRow($row, $stabileCode, (string) $this->option('pdf-base'));
$this->line(json_encode($mapped, JSON_UNESCAPED_UNICODE));
}
$this->info('Preview completata. Righe sorgente: ' . count($rows));
return self::SUCCESS;
}
$toProcess = $limit > 0 ? array_slice($rows, 0, $limit) : $rows;
$imported = 0;
$matched = 0;
foreach ($toProcess as $row) {
$mapped = $this->mapRow($row, $stabileCode, (string) $this->option('pdf-base'));
$numIncasso = $mapped['num_incasso'];
$incassoId = null;
if ($numIncasso !== null && Schema::hasTable('incassi') && Schema::hasColumn('incassi', 'n_riferimento')) {
$incassoQuery = DB::table('incassi')
->where('n_riferimento', (string) $numIncasso)
->when($stabileId && Schema::hasColumn('incassi', 'condominio_id'), fn ($q) => $q->where('condominio_id', (int) $stabileId));
$incassoId = $incassoQuery->orderByDesc('id')->value('id');
}
if ($incassoId) {
$matched++;
}
$hash = hash('sha256', implode('|', [
$tenant,
$gestioneId ?? 0,
$mapped['num_incasso'] ?? '',
$mapped['data_operazione'] ?? '',
$mapped['importo'] ?? 0,
]));
$payload = [
'tenant_id' => $tenant,
'gestione_id' => $gestioneId,
'id_gescon' => $mapped['num_incasso'],
'data_operazione' => $mapped['data_operazione'],
'data_valuta' => $mapped['data_valuta'],
'importo' => $mapped['importo'],
'descrizione' => $mapped['descrizione'],
'tipo_movimento' => $mapped['importo'] < 0 ? 'uscita' : 'entrata',
'riferimento_bancario' => $mapped['num_incasso'] !== null ? (string) $mapped['num_incasso'] : null,
'incasso_id' => $incassoId,
'stato' => $incassoId ? 'riconciliato' : 'da_riconciliare',
'confidenza_match' => $incassoId ? 100 : 0,
'tipo_riconciliazione' => $incassoId ? 'automatica' : null,
'file_origine' => $mapped['nome_file_pdf'],
'formato_file' => 'mdb',
'hash_movimento' => $hash,
'metadati_gescon' => json_encode([
'source_table' => $table,
'num_incasso' => $mapped['num_incasso'],
'nome_file_pdf' => $mapped['nome_file_pdf'],
'pdf_path' => $mapped['pdf_path'],
'raw' => $row,
], JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
];
$existing = DB::table('incassi_estratto_conto')
->where('tenant_id', $tenant)
->where('hash_movimento', $hash)
->first(['id']);
if ($existing) {
DB::table('incassi_estratto_conto')
->where('id', (int) $existing->id)
->update($payload);
} else {
$payload['created_at'] = now();
DB::table('incassi_estratto_conto')->insert($payload);
}
$imported++;
}
$this->info("Import completato: {$imported} righe, match incassi={$matched}");
return self::SUCCESS;
}
private function mapRow(array $row, string $stabileCode, string $pdfBase): array
{
$get = function (array $keys) use ($row) {
foreach ($keys as $key) {
$lk = strtolower($key);
if (array_key_exists($lk, $row)) {
$value = $row[$lk];
return is_string($value) ? trim($value) : $value;
}
}
return null;
};
$numIncasso = $this->toInt($get(['Num_incasso', 'num_incasso', 'id_incasso', 'id']));
$nomeFilePdf = $this->nullIfEmpty($get(['Nome_file_pdf', 'nome_file_pdf', 'file_pdf', 'pdf']));
$pdfPath = null;
if ($nomeFilePdf) {
$pdfPath = rtrim($pdfBase, '/') . '/' . $stabileCode . '/INC_EC/' . ltrim($nomeFilePdf, '/');
}
return [
'num_incasso' => $numIncasso,
'data_operazione' => $this->toDate($get(['data_oper', 'data_operazione', 'data'])),
'data_valuta' => $this->toDate($get(['data_valuta', 'valuta', 'data_val'])),
'importo' => $this->money($get(['importo_euro', 'importo'])),
'descrizione' => $this->nullIfEmpty($get(['descrizione', 'causale', 'note'])),
'nome_file_pdf' => $nomeFilePdf,
'pdf_path' => $pdfPath,
];
}
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 resolveStabileId(): ?int
{
$fromOption = $this->option('stabile-id');
if (is_numeric($fromOption)) {
return (int) $fromOption;
}
$code = trim((string) ($this->option('stabile-code') ?: ''));
if ($code !== '' && Schema::hasTable('stabili')) {
$id = DB::table('stabili')->where('codice_stabile', $code)->value('id');
return is_numeric($id) ? (int) $id : null;
}
return null;
}
private function resolveStabileCode(): ?string
{
$fromOption = trim((string) ($this->option('stabile-code') ?: ''));
if ($fromOption !== '') {
return str_pad($fromOption, 4, '0', STR_PAD_LEFT);
}
$stabileId = $this->resolveStabileId();
if ($stabileId && Schema::hasTable('stabili')) {
$code = DB::table('stabili')->where('id', (int) $stabileId)->value('codice_stabile');
if (is_string($code) && trim($code) !== '') {
return str_pad(trim($code), 4, '0', STR_PAD_LEFT);
}
}
return null;
}
private function resolveGestioneId(string $tenant, int $stabileId, ?int $anno): ?int
{
if (! Schema::hasTable('gestioni_contabili')) {
return null;
}
$query = DB::table('gestioni_contabili')
->where('tenant_id', $tenant)
->where('stabile_id', $stabileId)
->where('tipo_gestione', 'ordinaria');
if ($anno) {
$query->where('anno_gestione', $anno);
}
return $query->orderByDesc('anno_gestione')->value('id');
}
private function runShell(string $cmd): ?string
{
$out = shell_exec($cmd . ' 2>/dev/null');
if ($out === null || $out === '') {
return null;
}
return $out;
}
private function nullIfEmpty($value): ?string
{
if ($value === null) {
return null;
}
$trim = trim((string) $value);
return $trim === '' ? null : $trim;
}
private function toInt($value): ?int
{
if ($value === null) {
return null;
}
$trim = trim((string) $value);
return $trim !== '' && is_numeric($trim) ? (int) $trim : null;
}
private function toDate($value): ?string
{
if ($value === null) {
return null;
}
$raw = trim((string) $value);
if ($raw === '') {
return null;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $raw)) {
return substr($raw, 0, 10);
}
if (preg_match('/^\d{2}\/\d{2}\/\d{2} /', $raw)) {
$dt = \DateTime::createFromFormat('m/d/y H:i:s', $raw);
if ($dt) {
$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}$/', $raw)) {
$dt = \DateTime::createFromFormat('d/m/Y', $raw);
return $dt?->format('Y-m-d');
}
$ts = @strtotime($raw);
return $ts ? date('Y-m-d', $ts) : null;
}
private function money($value): float
{
if ($value === null || $value === '') {
return 0.0;
}
$raw = str_replace(["\r", "\n", "\t", ' '], '', (string) $value);
if (str_contains($raw, ',') && str_contains($raw, '.')) {
$raw = str_replace('.', '', $raw);
$raw = str_replace(',', '.', $raw);
} elseif (str_contains($raw, ',')) {
$raw = str_replace(',', '.', $raw);
}
return is_numeric($raw) ? (float) $raw : 0.0;
}
}