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

259 lines
8.6 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\Stabile;
use App\Models\StgFatturaAde;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class GesconImportFattureAdeCsv extends Command
{
protected $signature = 'gescon:import-fatture-ade-csv
{amministratore_id : ID amministratore target}
{--stabile_id= : ID stabile target (opzionale)}
{--cartella= : Cartella legacy stabile (es. 0021) opzionale, usata solo come metadato}
{--path= : File CSV specifico da importare}
{--dir=/home/michele/netgescon/docs/Importazione : Directory da cui importare (ricorsiva)}
{--dry-run : Non scrive su DB}
{--limit=0 : Limita righe per file (0 = nessun limite)}';
protected $description = 'Importa i CSV AdE (lista fatture FE disponibili) nello staging, deduplicando per Sdi/file e target (amministratore+stabile).';
public function handle(): int
{
$amministratoreId = (int) $this->argument('amministratore_id');
$stabileId = $this->option('stabile_id') !== null ? (int) $this->option('stabile_id') : null;
$cartella = $this->option('cartella') ? trim((string) $this->option('cartella')) : null;
$path = $this->option('path') ? (string) $this->option('path') : null;
$dir = (string) ($this->option('dir') ?: '');
$dry = (bool) $this->option('dry-run');
$limit = max(0, (int) $this->option('limit'));
if ($amministratoreId <= 0) {
$this->error('amministratore_id non valido');
return self::FAILURE;
}
if ($stabileId !== null && $stabileId > 0) {
$exists = Stabile::query()->whereKey($stabileId)->where('amministratore_id', $amministratoreId)->exists();
if (!$exists) {
$this->error("stabile_id={$stabileId} non appartiene ad amministratore_id={$amministratoreId}");
return self::FAILURE;
}
}
$files = [];
if ($path) {
if (!is_file($path)) {
$this->error("CSV non trovato: {$path}");
return self::FAILURE;
}
$files = [$path];
} else {
if ($dir === '' || !is_dir($dir)) {
$this->error("Directory non valida: {$dir}");
return self::FAILURE;
}
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
foreach ($it as $file) {
if (!$file instanceof \SplFileInfo) {
continue;
}
if (!$file->isFile()) {
continue;
}
$name = $file->getFilename();
if (!Str::endsWith(Str::lower($name), '.csv')) {
continue;
}
$files[] = $file->getPathname();
}
sort($files);
}
if (empty($files)) {
$this->warn('Nessun CSV trovato');
return self::SUCCESS;
}
$stats = [
'files' => count($files),
'rows' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
];
$runner = function () use (&$stats, $files, $amministratoreId, $stabileId, $cartella, $dry, $limit): void {
foreach ($files as $filePath) {
[$rows, $headers] = $this->readCsvSemicolon($filePath);
if (empty($rows)) {
continue;
}
if ($limit > 0) {
$rows = array_slice($rows, 0, $limit);
}
foreach ($rows as $row) {
$stats['rows']++;
$sdi = $this->clean($row['sdi/file'] ?? null);
if ($sdi === null) {
$stats['skipped']++;
continue;
}
$payload = [
'amministratore_id' => $amministratoreId,
'stabile_id' => $stabileId,
'cartella_legacy' => $cartella,
'tipo_documento' => $this->clean($row['tipo documento'] ?? null),
'numero_documento' => $this->clean($row['numero fattura / documento'] ?? null),
'data_emissione' => $this->parseDateIt($row['data emissione fattura'] ?? null),
'identificativo_fornitore' => $this->clean($row['identificativo fornitore'] ?? null),
'denominazione_fornitore' => $this->clean($row['denominazione fornitore'] ?? null),
'imponibile' => $this->parseDecimalIt($row['imponibile / importo'] ?? null),
'imposta' => $this->parseDecimalIt($row['imposta (totale in euro)'] ?? null),
'sdi_file' => $sdi,
'stato_visualizzazione' => $this->clean($row['fatture visualizzate'] ?? null),
'source_file' => $filePath,
'raw' => $row,
];
if ($dry) {
continue;
}
$existing = StgFatturaAde::query()
->where('amministratore_id', $amministratoreId)
->where('stabile_id', $stabileId)
->where('sdi_file', $sdi)
->first();
if ($existing) {
$existing->fill($payload);
if ($existing->isDirty()) {
$existing->save();
$stats['updated']++;
}
} else {
StgFatturaAde::create($payload);
$stats['created']++;
}
}
}
};
if ($dry) {
$runner();
} else {
DB::transaction($runner);
}
$this->info('✅ Import AdE CSV completato' . ($dry ? ' (dry-run)' : ''));
$this->line(json_encode($stats, JSON_UNESCAPED_UNICODE));
return self::SUCCESS;
}
/** @return array{0: array<int,array<string,mixed>>, 1: array<int,string>} */
private function readCsvSemicolon(string $filePath): array
{
$content = @file_get_contents($filePath);
if (!is_string($content) || trim($content) === '') {
return [[], []];
}
// Normalize line endings
$content = str_replace(["\r\n", "\r"], "\n", $content);
$fh = fopen('php://temp', 'r+');
fwrite($fh, $content);
rewind($fh);
$headers = fgetcsv($fh, separator: ';');
if (!$headers) {
return [[], []];
}
$headers = array_map(fn($h) => mb_strtolower(trim((string) $h)), $headers);
$rows = [];
while (($cols = fgetcsv($fh, separator: ';')) !== false) {
if (empty($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 (!$assoc) {
continue;
}
$rows[] = $assoc;
}
return [$rows, $headers];
}
private function parseDateIt($v): ?string
{
$s = $this->clean($v);
if ($s === null) {
return null;
}
// Expected dd/mm/yyyy
$parts = explode('/', $s);
if (count($parts) !== 3) {
return null;
}
[$d, $m, $y] = $parts;
$d = (int) $d;
$m = (int) $m;
$y = (int) $y;
if ($d <= 0 || $m <= 0 || $y <= 0) {
return null;
}
return sprintf('%04d-%02d-%02d', $y, $m, $d);
}
private function parseDecimalIt($v): float
{
$s = $this->clean($v);
if ($s === null) {
return 0.0;
}
// Remove thousand separators and convert comma decimal
$s = str_replace('.', '', $s);
$s = str_replace(',', '.', $s);
$s = preg_replace('/[^0-9\.-]/', '', $s) ?? $s;
if ($s === '' || !is_numeric($s)) {
return 0.0;
}
return (float) $s;
}
private function clean($v): ?string
{
if ($v === null) {
return null;
}
$s = trim((string) $v);
return $s === '' ? null : $s;
}
}