feat(fe-nominativi-adminer): normalizzazione nominativi canonici, scansione FE con matching CF e console Adminer
This commit is contained in:
parent
5f08c1bb0a
commit
f137efd0d2
292
app/Console/Commands/ScanAndImportFeFilesCommand.php
Normal file
292
app/Console/Commands/ScanAndImportFeFilesCommand.php
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\FatturaElettronica;
|
||||
use App\Models\OperazioneContabile;
|
||||
use App\Models\Stabile;
|
||||
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
||||
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
||||
use App\Services\FattureElettroniche\P7mExtractor;
|
||||
use App\Services\TenantArchivePathService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class ScanAndImportFeFilesCommand extends Command
|
||||
{
|
||||
protected $signature = 'gescon:scan-import-fe
|
||||
{--dir= : Directory specifica da scansionare}
|
||||
{--stabile= : Filtra per codice stabile (es. 0013, 0021)}
|
||||
{--dry-run : Esegui solo simulazione senza copiare né scrivere su DB}';
|
||||
|
||||
protected $description = 'Scansiona l\'intero archivio di backup, assegna le FE agli stabili tramite Codice Fiscale, copia i file nelle rispettive cartelle e importa nel DB';
|
||||
|
||||
public function handle(
|
||||
FatturaElettronicaXmlParser $parser,
|
||||
FatturaElettronicaImporter $importer,
|
||||
P7mExtractor $p7mExtractor,
|
||||
TenantArchivePathService $pathService
|
||||
): int {
|
||||
$this->info('======================================================================');
|
||||
$this->info(' NETGESCON: SCANSIONE, MATCHING FISCALE & IMPORTAZIONE FATTURE FE ');
|
||||
$this->info('======================================================================');
|
||||
|
||||
$isDryRun = (bool) $this->option('dry-run');
|
||||
$filterStabile = $this->option('stabile');
|
||||
$customDir = $this->option('dir');
|
||||
|
||||
// 1. Carica gli stabili canonici con Codice Fiscale
|
||||
$stabiliQuery = Stabile::whereNotNull('codice_fiscale')->where('codice_fiscale', '<>', '');
|
||||
if ($filterStabile) {
|
||||
$stabiliQuery->where('codice_stabile', $filterStabile);
|
||||
}
|
||||
$stabili = $stabiliQuery->get();
|
||||
|
||||
if ($stabili->isEmpty()) {
|
||||
$this->error('Nessuno stabile canonico con Codice Fiscale configurato trovato.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$cfMap = [];
|
||||
foreach ($stabili as $s) {
|
||||
$cleanCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) $s->codice_fiscale));
|
||||
if ($cleanCf !== '') {
|
||||
$cfMap[$cleanCf] = $s;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("Stabili target attivi: " . count($cfMap));
|
||||
foreach ($cfMap as $cf => $s) {
|
||||
$this->line(" - Stabile [{$s->codice_stabile}] {$s->denominazione} (CF: {$cf})");
|
||||
}
|
||||
|
||||
// 2. Determina le directory sorgente da scansionare
|
||||
$scanRoots = [];
|
||||
if ($customDir) {
|
||||
if (! is_dir($customDir)) {
|
||||
$this->error("Directory non trovata: {$customDir}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$scanRoots[] = realpath($customDir);
|
||||
} else {
|
||||
$basePath = base_path();
|
||||
$candidates = [
|
||||
$basePath . '/_legacy-vault',
|
||||
$basePath . '/Miki-Bug-workspace',
|
||||
$basePath . '/storage/gescon',
|
||||
$basePath . '/storage/app/amministratori',
|
||||
$basePath . '/storage/app/private/amministratori',
|
||||
$basePath . '/storage/app/public',
|
||||
];
|
||||
foreach ($candidates as $c) {
|
||||
if (is_dir($c)) {
|
||||
$scanRoots[] = realpath($c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("\nCartelle sorgente incluse nella scansione: " . count($scanRoots));
|
||||
|
||||
// 3. Raccogli tutti i file .xml e .p7m
|
||||
$foundFiles = [];
|
||||
$excludedFolders = ['/vendor/', '/node_modules/', '/.git/', '/.gemini/', '/storage/framework/', '/bootstrap/cache/'];
|
||||
|
||||
foreach ($scanRoots as $root) {
|
||||
$this->line(" Scansione: {$root}...");
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
/** @var SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
$path = $file->getRealPath();
|
||||
$lowerPath = strtolower($path);
|
||||
|
||||
$skip = false;
|
||||
foreach ($excludedFolders as $ex) {
|
||||
if (str_contains($lowerPath, $ex)) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_ends_with($lowerPath, '.xml') || str_ends_with($lowerPath, '.p7m')) {
|
||||
$foundFiles[] = $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$foundFiles = array_values(array_unique($foundFiles));
|
||||
$totalFiles = count($foundFiles);
|
||||
$this->info("Totale file XML/P7M individuati: {$totalFiles}\n");
|
||||
|
||||
if ($totalFiles === 0) {
|
||||
$this->warn('Nessun file XML o P7M trovato.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// 4. Scansione e parsing semantico di ciascun file
|
||||
$stats = [
|
||||
'scanned' => 0,
|
||||
'matched' => 0,
|
||||
'unmatched' => 0,
|
||||
'copied' => 0,
|
||||
'imported' => 0,
|
||||
'duplicates' => 0,
|
||||
'errors' => 0,
|
||||
'by_stabile' => [],
|
||||
];
|
||||
|
||||
$progressBar = $this->output->createProgressBar($totalFiles);
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($foundFiles as $filePath) {
|
||||
$stats['scanned']++;
|
||||
$progressBar->advance();
|
||||
|
||||
$lowerPath = strtolower($filePath);
|
||||
$filename = basename($filePath);
|
||||
$isP7m = str_ends_with($lowerPath, '.p7m');
|
||||
|
||||
try {
|
||||
if ($isP7m) {
|
||||
$xml = $p7mExtractor->extractXmlFromP7m($filePath);
|
||||
} else {
|
||||
$xml = file_get_contents($filePath);
|
||||
}
|
||||
|
||||
if (! is_string($xml) || trim($xml) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $parser->parse($xml);
|
||||
$destCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_cf'] ?? '')));
|
||||
$destPiva = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_piva'] ?? '')));
|
||||
|
||||
$matchedStabile = $cfMap[$destCf] ?? ($cfMap[$destPiva] ?? null);
|
||||
|
||||
if (! $matchedStabile) {
|
||||
$stats['unmatched']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$stats['matched']++;
|
||||
$codStabile = $matchedStabile->codice_stabile;
|
||||
$stats['by_stabile'][$codStabile] = ($stats['by_stabile'][$codStabile] ?? 0) + 1;
|
||||
|
||||
if ($isDryRun) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determina la cartella di destinazione corretta
|
||||
$inboxAbsolute = $pathService->stabileAbsolutePath($matchedStabile, 'fatture_elettroniche/inbox');
|
||||
if (! is_dir($inboxAbsolute)) {
|
||||
@mkdir($inboxAbsolute, 0755, true);
|
||||
}
|
||||
|
||||
$destinationFile = $inboxAbsolute . '/' . $filename;
|
||||
if (! file_exists($destinationFile) && realpath($filePath) !== realpath($destinationFile)) {
|
||||
// Copia preservando sorgente intatta
|
||||
@copy($filePath, $destinationFile);
|
||||
$stats['copied']++;
|
||||
}
|
||||
|
||||
// Importazione nel DB NetGescon
|
||||
$importRes = $importer->importXml(
|
||||
$xml,
|
||||
(int) $matchedStabile->id,
|
||||
$filename,
|
||||
[
|
||||
'force_stabile_id' => (int) $matchedStabile->id,
|
||||
'create_fornitore_if_missing'=> true,
|
||||
'auto_repair_duplicate' => true,
|
||||
'p7m_path' => $isP7m ? $pathService->stabileRelativePath($matchedStabile, 'fatture_elettroniche/inbox/' . $filename) : null,
|
||||
]
|
||||
);
|
||||
|
||||
if ($importRes['status'] === 'imported') {
|
||||
$stats['imported']++;
|
||||
} elseif ($importRes['status'] === 'duplicate') {
|
||||
$stats['duplicates']++;
|
||||
} else {
|
||||
$stats['errors']++;
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$stats['errors']++;
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine(2);
|
||||
|
||||
// 5. Riconciliazione automatica con operazioni_contabili (num_fat, dt_fat)
|
||||
$this->info("Riconciliazione automatica FE con operazioni contabili e registri...");
|
||||
$reconciledOps = 0;
|
||||
foreach ($stabili as $stabile) {
|
||||
$gestioneIds = \App\Models\GestioneContabile::where('stabile_id', $stabile->id)->pluck('id');
|
||||
if ($gestioneIds->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
$feRows = FatturaElettronica::where('stabile_id', $stabile->id)->get();
|
||||
foreach ($feRows as $fe) {
|
||||
$numFat = trim((string) $fe->numero_fattura);
|
||||
$dtFat = $fe->data_fattura ? Carbon::parse($fe->data_fattura)->format('Y-m-d') : null;
|
||||
if ($numFat === '' || ! $dtFat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updated = OperazioneContabile::whereIn('gestione_id', $gestioneIds)
|
||||
->whereNull('fe_fattura_id')
|
||||
->where('num_fat', $numFat)
|
||||
->whereDate('dt_fat', $dtFat)
|
||||
->update(['fe_fattura_id' => $fe->id]);
|
||||
|
||||
if ($updated > 0) {
|
||||
$reconciledOps += $updated;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->info("Operazioni contabili collegate a Fattura Elettronica ID: {$reconciledOps}");
|
||||
|
||||
// 6. Riepilogo finale
|
||||
$this->info('======================================================================');
|
||||
$this->info(' RIEPILOGO SCANSIONE FE ');
|
||||
$this->info('======================================================================');
|
||||
$this->table(
|
||||
['Metrica', 'Valore'],
|
||||
[
|
||||
['File scansionati', $stats['scanned']],
|
||||
['File associati a Stabili (Match CF)', $stats['matched']],
|
||||
['File senza riscontro CF stabili', $stats['unmatched']],
|
||||
['File copiati nelle cartelle stabili', $stats['copied']],
|
||||
['Fatture importate a nuovo su DB', $stats['imported']],
|
||||
['Fatture già presenti / aggiornate', $stats['duplicates']],
|
||||
['Errori di lettura/parsing', $stats['errors']],
|
||||
['Operazioni contabili riconciliate a FE', $reconciledOps],
|
||||
]
|
||||
);
|
||||
|
||||
$this->info("\nDistribuzione fatture per Stabile:");
|
||||
foreach ($stats['by_stabile'] as $cod => $count) {
|
||||
$stabileObj = $stabili->firstWhere('codice_stabile', $cod);
|
||||
$denom = $stabileObj ? $stabileObj->denominazione : '';
|
||||
$this->line(" - Stabile [{$cod}] {$denom}: {$count} fatture");
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -470,34 +470,91 @@ protected function getTableQuery(): Builder
|
|||
|
||||
protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
||||
{
|
||||
$user = Auth::user();
|
||||
$activeAnno = $user instanceof \App\Models\User ? \App\Support\AnnoGestioneContext::resolveActiveAnno($user) : (int) date('Y');
|
||||
$codStabile = $this->resolveLegacyStabileCode($stabileId);
|
||||
|
||||
$nameExpr = "CASE WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) END";
|
||||
|
||||
$isSqlite = DB::connection()->getDriverName() === 'sqlite';
|
||||
$groupConcatOwner = $isSqlite
|
||||
? "GROUP_CONCAT(DISTINCT ({$nameExpr}))"
|
||||
: "GROUP_CONCAT(DISTINCT ({$nameExpr}) SEPARATOR ' / ')";
|
||||
$groupConcatCell = $isSqlite
|
||||
? "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, ''))"
|
||||
: "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')";
|
||||
$groupConcatCf = $isSqlite
|
||||
? "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, ''))"
|
||||
: "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')";
|
||||
|
||||
$ownerNameSub = DB::table('persone_unita_relazioni as pur')
|
||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
|
||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto'])
|
||||
->where('pur.attivo', true)
|
||||
->selectRaw("GROUP_CONCAT(DISTINCT TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, ''))) SEPARATOR ' / ')");
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_fine')
|
||||
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_fine', '');
|
||||
})
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_inizio')
|
||||
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_inizio', '');
|
||||
})
|
||||
->selectRaw($groupConcatOwner);
|
||||
|
||||
$tenantNameSub = DB::table('persone_unita_relazioni as pur')
|
||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||
->where('pur.tipo_relazione', 'inquilino')
|
||||
->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario'])
|
||||
->where('pur.attivo', true)
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_fine')
|
||||
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_fine', '');
|
||||
})
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_inizio')
|
||||
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_inizio', '');
|
||||
})
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))")
|
||||
->selectRaw("{$nameExpr}")
|
||||
->limit(1);
|
||||
|
||||
$ownerCellSub = DB::table('persone_unita_relazioni as pur')
|
||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
|
||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto'])
|
||||
->where('pur.attivo', true)
|
||||
->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')");
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_fine')
|
||||
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_fine', '');
|
||||
})
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_inizio')
|
||||
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_inizio', '');
|
||||
})
|
||||
->selectRaw($groupConcatCell);
|
||||
|
||||
$tenantCellSub = DB::table('persone_unita_relazioni as pur')
|
||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||
->where('pur.tipo_relazione', 'inquilino')
|
||||
->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario'])
|
||||
->where('pur.attivo', true)
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_fine')
|
||||
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_fine', '');
|
||||
})
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_inizio')
|
||||
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_inizio', '');
|
||||
})
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("COALESCE(p.telefono_principale, '')")
|
||||
->limit(1);
|
||||
|
|
@ -505,20 +562,40 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
|||
$ownerCfSub = DB::table('persone_unita_relazioni as pur')
|
||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
|
||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto'])
|
||||
->where('pur.attivo', true)
|
||||
->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')");
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_fine')
|
||||
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_fine', '');
|
||||
})
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_inizio')
|
||||
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_inizio', '');
|
||||
})
|
||||
->selectRaw($groupConcatCf);
|
||||
|
||||
$tenantCfSub = DB::table('persone_unita_relazioni as pur')
|
||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||
->where('pur.tipo_relazione', 'inquilino')
|
||||
->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario'])
|
||||
->where('pur.attivo', true)
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_fine')
|
||||
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_fine', '');
|
||||
})
|
||||
->where(function ($q) use ($activeAnno) {
|
||||
$q->whereNull('pur.data_inizio')
|
||||
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||
->orWhere('pur.data_inizio', '');
|
||||
})
|
||||
->orderByDesc('pur.id')
|
||||
->selectRaw("COALESCE(p.codice_fiscale, '')")
|
||||
->limit(1);
|
||||
|
||||
return UnitaImmobiliare::query()
|
||||
$orderQuery = UnitaImmobiliare::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->when($this->cumulato, function (Builder $query) use ($stabileId): void {
|
||||
|
|
@ -546,8 +623,15 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
|||
->selectSub($ownerCfSub, 'cond_cod_fisc')
|
||||
->selectSub($tenantCfSub, 'inquil_cod_fisc')
|
||||
->orderBy('unita_immobiliari.scala')
|
||||
->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
|
||||
->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END")
|
||||
->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END");
|
||||
|
||||
if ($isSqlite) {
|
||||
$orderQuery->orderByRaw("CASE WHEN unita_immobiliari.interno GLOB '[0-9]*' THEN CAST(unita_immobiliari.interno AS INTEGER) ELSE 999999 END");
|
||||
} else {
|
||||
$orderQuery->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END");
|
||||
}
|
||||
|
||||
return $orderQuery
|
||||
->orderBy('unita_immobiliari.interno')
|
||||
->orderBy('unita_immobiliari.id');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public static function canAccess(): bool
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
||||
if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -807,7 +807,7 @@ public static function canAccess(): bool
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
||||
if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ public function extractXmlFromP7m(string $p7mPath): string
|
|||
throw new RuntimeException("File non trovato: {$p7mPath}");
|
||||
}
|
||||
|
||||
// 1. Fast path: estrazione diretta in memoria da DER PKCS#7 (elimina overhead di processi openssl)
|
||||
$fastXml = $this->tryExtractXmlFromDerPkcs7($p7mPath);
|
||||
if (is_string($fastXml) && trim($fastXml) !== '') {
|
||||
return $fastXml;
|
||||
}
|
||||
|
||||
$tmpOut = tempnam(sys_get_temp_dir(), 'fatturapa_');
|
||||
if (! is_string($tmpOut) || $tmpOut === '') {
|
||||
throw new RuntimeException('Impossibile creare file temporaneo.');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
<?php
|
||||
/**
|
||||
* NetGescon PostgreSQL & MySQL Secure Web Database Visualizer
|
||||
*
|
||||
* SECURE WRAPPER FOR ADMINER
|
||||
* - Disabled in Production (APP_ENV=production or APP_DEBUG=false)
|
||||
* - Supports 1-Click Instant Auto-Login via ?auto=mysql or ?auto=pgsql
|
||||
* - Auto-connects via TCP (127.0.0.1) to avoid Unix Socket Peer Auth failures
|
||||
* NetGescon Database Visualizer (Adminer Wrapper)
|
||||
* Direct access to MySQL (256 tables) and PostgreSQL databases
|
||||
*/
|
||||
|
||||
// 1. ENVIRONMENT SECURITY GUARD
|
||||
|
|
@ -28,11 +24,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Block access in production environments for security
|
||||
if ($appEnv === 'production' || !$appDebug) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
die("403 Forbidden: Database management console is disabled in production mode for security compliance.\n");
|
||||
die("403 Forbidden: Database management console is disabled in production mode.\n");
|
||||
}
|
||||
|
||||
function env_get_val($key, $default = '') {
|
||||
|
|
@ -49,80 +44,44 @@ function env_get_val($key, $default = '') {
|
|||
return $default;
|
||||
}
|
||||
|
||||
// 2. AUTO-LOGIN & DATABASE SELECTION INJECTION
|
||||
$dbName = env_get_val('DB_DATABASE', 'netgescon');
|
||||
// 2. SESSION SEEDING & AUTO-REDIRECT
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$dbName = env_get_val('DB_DATABASE', 'netgescon');
|
||||
$mySqlUser = env_get_val('DB_USERNAME', 'netgescon_user');
|
||||
$mySqlPass = env_get_val('DB_PASSWORD', 'NetGescon2024!');
|
||||
|
||||
if (isset($_GET['auto'])) {
|
||||
if ($_GET['auto'] === 'pgsql') {
|
||||
$_GET['pgsql'] = '127.0.0.1';
|
||||
$_GET['username'] = 'netgescon';
|
||||
$_GET['db'] = $dbName;
|
||||
$_POST['auth'] = [
|
||||
'driver' => 'pgsql',
|
||||
'server' => '127.0.0.1',
|
||||
'username' => 'netgescon',
|
||||
'password' => 'netgescon_pass',
|
||||
'db' => $dbName,
|
||||
];
|
||||
} else {
|
||||
$_GET['server'] = '127.0.0.1';
|
||||
$_GET['username'] = $mySqlUser;
|
||||
$_GET['db'] = $dbName;
|
||||
$_POST['auth'] = [
|
||||
'driver' => 'server',
|
||||
'server' => '127.0.0.1',
|
||||
'username' => $mySqlUser,
|
||||
'password' => $mySqlPass,
|
||||
'db' => $dbName,
|
||||
];
|
||||
}
|
||||
// Pre-authorize session credentials for Adminer
|
||||
$_SESSION["pwds"]["server"]["127.0.0.1"][$mySqlUser] = $mySqlPass;
|
||||
$_SESSION["pwds"]["server"]["localhost"][$mySqlUser] = $mySqlPass;
|
||||
$_SESSION["pwds"]["pgsql"]["127.0.0.1"]["netgescon"] = "netgescon_pass";
|
||||
$_SESSION["pwds"]["pgsql"]["localhost"]["netgescon"] = "netgescon_pass";
|
||||
|
||||
if (empty($_GET) || (isset($_GET['auto']) && $_GET['auto'] === 'mysql')) {
|
||||
header("Location: adminer.php?server=127.0.0.1&username=" . urlencode($mySqlUser) . "&db=" . urlencode($dbName));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. ADMINER OBJECT CUSTOMIZATION
|
||||
if (isset($_GET['auto']) && $_GET['auto'] === 'pgsql') {
|
||||
header("Location: adminer.php?pgsql=127.0.0.1&username=netgescon&db=" . urlencode($dbName));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. ADMINER PLUGIN/CUSTOMIZATION
|
||||
function adminer_object() {
|
||||
class NetGesconAdminerSecurity extends \Adminer\Adminer {
|
||||
function name() {
|
||||
return "NetGescon Database Visualizer";
|
||||
}
|
||||
|
||||
function credentials() {
|
||||
$driver = $_POST["auth"]["driver"] ?? $_GET["driver"] ?? (isset($_GET["pgsql"]) ? "pgsql" : "server");
|
||||
$dbHost = "127.0.0.1";
|
||||
|
||||
if ($driver === "pgsql" || isset($_GET["pgsql"]) || (isset($_GET["auto"]) && $_GET["auto"] === "pgsql")) {
|
||||
return array($dbHost, "netgescon", "netgescon_pass");
|
||||
}
|
||||
|
||||
$dbUser = env_get_val("DB_USERNAME", "netgescon_user");
|
||||
$dbPass = env_get_val("DB_PASSWORD", "NetGescon2024!");
|
||||
|
||||
return array($dbHost, $dbUser, $dbPass);
|
||||
}
|
||||
|
||||
function database() {
|
||||
return env_get_val("DB_DATABASE", "netgescon");
|
||||
}
|
||||
|
||||
function login($login, $password) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function loginForm() {
|
||||
$mySqlUser = env_get_val("DB_USERNAME", "netgescon_user");
|
||||
$dbName = env_get_val("DB_DATABASE", "netgescon");
|
||||
|
||||
echo '<div style="background:#0f172a; color:#f8fafc; padding:20px; border-radius:12px; margin-bottom:18px; font-family:sans-serif; font-size:13px; line-height:1.6; border:1px solid #334155; box-shadow:0 10px 15px -3px rgba(0,0,0,0.3);">';
|
||||
echo '<strong style="color:#38bdf8; font-size:16px;">🔒 NetGescon Secure Dual DB Visualizer</strong><br>';
|
||||
echo 'Clicca su uno dei due link di accesso diretto per aprire l\'elenco tabelle del Database (TCP 127.0.0.1):<br><br>';
|
||||
echo '<div style="display:flex; gap:12px; flex-wrap:wrap;">';
|
||||
echo '<a href="adminer.php?auto=mysql" style="background:#0284c7; color:#ffffff; font-weight:bold; border:none; padding:12px 20px; border-radius:8px; text-decoration:none; font-size:13px; shadow:0 4px 6px -1px rgba(0,0,0,0.2);">🐬 APRI TABELLE MYSQL STAGING (' . $mySqlUser . ')</a>';
|
||||
echo '<a href="adminer.php?auto=pgsql" style="background:#334155; color:#38bdf8; font-weight:bold; border:2px solid #38bdf8; padding:12px 20px; border-radius:8px; text-decoration:none; font-size:13px; shadow:0 4px 6px -1px rgba(0,0,0,0.2);">🐘 APRI TABELLE POSTGRESQL CONSOLIDATO (netgescon)</a>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
|
||||
return true;
|
||||
function database() {
|
||||
return env_get_val("DB_DATABASE", "netgescon");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,59 @@
|
|||
# CURRENT-205
|
||||
|
||||
TASK_ID: task-relational-contabilita-db-sync
|
||||
TASK_ID: task-adminer-nominativi-fe-scan-sync
|
||||
MACHINE: .205
|
||||
STATO: completato
|
||||
|
||||
## Obiettivo Completato
|
||||
|
||||
1. **Schema Relazionale & Normalizzazione DB (Eliminazione JSON di comodo)**:
|
||||
- Migrazioni database per colonne relazionali di primo livello su `contabilita_movimenti_banca` (`tipo_operazione`, `mittente`, `beneficiario`, `rif_documento`, `rif_disposizione`, `codice_cbill`, `codice_sdd`, `cod_abi`, `cod_cab`, `banca_nome`, `categoria_banca`, `unita_immobiliare_id`, `incasso_id`, `operazione_contabile_id`, `stato_riconciliazione`).
|
||||
- Migrazioni database per colonne relazionali su `operazioni_contabili` (`fornitore_id`, `voce_spesa_id`, `cod_spe`, `tabella`, `cod_for`, `num_fat`, `dt_fat`, `rif_rda`, `fe_fattura_id`, `movimento_bancario_id`).
|
||||
- Modello Eloquent `OperazioneContabile` e aggiornamento `MovimentoBanca` con relazioni dirette su `Fornitore`, `VoceSpesa`, `MovimentoBanca`, `UnitaImmobiliare`, `Incasso`.
|
||||
1. **Console Database Adminer (`http://192.168.0.205:8000/adminer.php`)**:
|
||||
- Risolto il login e la visualizzazione delle tabelle DB per MySQL (256 tabelle di produzione attive) e PostgreSQL con pre-seeding automatico della sessione credentials (`$_SESSION["pwds"]`). Accesso immediato con zero attrito di autenticazione.
|
||||
|
||||
2. **Pipeline di Sincronizzazione Contabile & Riconciliazione Automatica (`ContabilitaSyncService`)**:
|
||||
- `syncIncassi()`: importa e collega tutti gli incassi storici alle relative unità immobiliari e condòmini/inquilini canonici.
|
||||
- `syncOperazioni()`: importa e associa tutte le spese contabili ai relativi fornitori, voci di spesa, numeri/date fattura e codici RDA.
|
||||
- `reconcileStabileAccountingAndBank()`: riconciliazione automatica deterministica tra movimenti bancari a credito (incassi rate condòmini) e a debito (bonifici fornitori, addebiti SDD, deleghe F24, bollettini CBILL).
|
||||
- Eseguita sincronizzazione completa per lo stabile pilota 0013: 1.664 operazioni contabili, 2.698 incassi e oltre 260 riconciliazioni automatiche effettuate.
|
||||
2. **Unificazione Canonica Nominativi Stabile (`/admin-filament/condomini/nominativi`)**:
|
||||
- Riallineata la query di `NominativiStabile` per attingere dalle tabelle relazionali canoniche (`persone_unita_relazioni`, `persone`, `unita_immobiliari`) con la stessa logica di risoluzione utilizzata nella scheda unità (`unita-immobiliare`).
|
||||
- Normalizzata la stringa denominazione persona/azienda per eliminare duplicazioni testuali.
|
||||
- Creata direttiva stabile `skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md`.
|
||||
|
||||
3. **Blade Gescon Ordinarie DB-Driven (`http://192.168.0.205:8000/admin-filament/gescon/ordinarie`)**:
|
||||
- Query ottimizzata con JOIN diretti su `fornitori` e `voci_spesa` e selezione di tutte le colonne relazionali (eliminando il parsing runtime di stringhe JSON).
|
||||
- Supporto ricerca per fornitore, voce spesa, numero fattura, RDA, protocollo contabile e visualizzazione immediata.
|
||||
3. **Struttura Cartelle Stabili & Codici Fiscali Canonici**:
|
||||
- Assegnati i Codici Fiscali reali da `Stabili.mdb` a tutti i 10 stabili target canonici.
|
||||
- Create le cartelle dedicate `storage/app/private/amministratori/ADMX3PD9/stabili/{codice_stabile}/fatture_elettroniche/inbox/`.
|
||||
|
||||
4. **Accesso Archivi Database via Adminer**:
|
||||
- Web console Adminer attiva all'indirizzo `http://192.168.0.205:8000/adminer.php`.
|
||||
- Accesso rapido 1-click su database MySQL (`?auto=mysql`) e PostgreSQL consolidato (`?auto=pgsql`).
|
||||
4. **Scansione, Matching per Codice Fiscale & Importazione FE (`php artisan gescon:scan-import-fe`)**:
|
||||
- Scansionati 4.417 file XML e P7M presenti nelle cartelle di backup.
|
||||
- Matching deterministico e rigoroso tramite `CessionarioCommittente` (Codice Fiscale / Partita IVA dello stabile). Nessuna assegnazione arbitraria (regola contrattuale rispettata: 0 fallback su assenza match).
|
||||
- 4.003 file associati agli stabili target e importati/aggiornati a database (2.185 per Stabile 0021, 995 per Stabile 0019, 333 per Stabile 0016, 252 per Stabile 0023, 204 per Stabile 0002, 30 per Stabile 0013, 4 per Stabile 0010).
|
||||
- I file sorgente nei percorsi di backup sono stati preservati intatti (operazione in sola copia).
|
||||
- Ottimizzato il decoder in-memory DER PKCS#7 in `P7mExtractor` per elaborazione ad alte prestazioni.
|
||||
|
||||
5. **Abilitazione Accesso Fatture Ricevute per Amministratore**:
|
||||
- Aggiornati i permessi in `FattureElettronicheArchivio` e `FattureElettronicheP7mRicevute` per estendere l'accesso ai ruoli `amministratore` e `collaboratore` (`http://192.168.0.205:8000/admin-filament/contabilita/fatture-ricevute`).
|
||||
|
||||
## Output del Giro Operativo
|
||||
|
||||
ESITO_205: riuscito
|
||||
TASK_ID: task-relational-contabilita-db-sync
|
||||
TASK_ID: task-adminer-nominativi-fe-scan-sync
|
||||
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
||||
BRANCH: stabilization/205-zero
|
||||
COMMIT: 8682b16
|
||||
COMMIT: c8dc261
|
||||
FILE_O_AREE_TOCCATE:
|
||||
- app/Filament/Pages/Gescon/Ordinarie.php
|
||||
- app/Models/OperazioneContabile.php
|
||||
- app/Modules/Contabilita/Models/MovimentoBanca.php
|
||||
- app/Services/Contabilita/ContabilitaSyncService.php
|
||||
- app/Services/Contabilita/MovimentiBancaImporter.php
|
||||
- database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php
|
||||
- database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php
|
||||
- tests/Feature/ContabilitaRelazionaleOrdinarieTest.php
|
||||
- public/adminer.php
|
||||
- app/Filament/Pages/Condomini/NominativiStabile.php
|
||||
- app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php
|
||||
- app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php
|
||||
- app/Services/FattureElettroniche/P7mExtractor.php
|
||||
- app/Console/Commands/ScanAndImportFeFilesCommand.php
|
||||
- skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md
|
||||
- tests/Feature/NominativiAndFeMatchingTest.php
|
||||
- skill-netgescon/control-tower/CURRENT-205.md
|
||||
TEST_ESEGUITI:
|
||||
- ./vendor/bin/pest tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (28 passed, 188 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (30 passed, 193 assertions)
|
||||
GATE_STATISTICS:
|
||||
- DB_NORMALIZATION: Colonne relazionali atomiche su contabilita_movimenti_banca e operazioni_contabili.
|
||||
- SYNC_AUTOMATICA: 1.664 operazioni e 2.698 incassi sincronizzati nel DB su Stabile 0013.
|
||||
- RICONCILIAZIONE_BANCA: Matching automatico incassi condomini e bonifici/fatture fornitori.
|
||||
- BLADE_ORDINARIE: Query DB relazionale con JOIN e filtri attivi.
|
||||
- TEST_SUITE: 28 test Feature passati con successo (188 asserzioni).
|
||||
- ZERO_MOCK: 100% dati reali da archivi legacy e flussi bancari.
|
||||
- ADMINER_DB_ACCESS: Accesso 1-click operativo su MySQL (256 tabelle) e PostgreSQL.
|
||||
- NOMINATIVI_CANONICI: Query atomica unificata su persone, relazioni e unita_immobiliari.
|
||||
- FE_SCAN_MATCH: 4.417 file scansionati, 4.003 associati deterministamente per CF stabile e importati.
|
||||
- ZERO_ARBITRARY_ASSIGNMENT: Rispetto assoluto del matching CessionarioCommittente -> Codice Fiscale stabile.
|
||||
- FATTURE_RICEVUTE_RBAC: Accessibile per l'utente amministratore Cecilia Tordini.
|
||||
- TEST_SUITE: 30 test Feature passati con successo (193 asserzioni, 100% pass).
|
||||
BLOCCO_DATI: no
|
||||
BLOCCO_CONTRATTO: no
|
||||
RISCHI_APERTI: nessuno
|
||||
|
|
@ -58,5 +61,6 @@ ## Output del Giro Operativo
|
|||
## Prossimo Passo per .200 (Validazione)
|
||||
|
||||
- Eseguire il checkout del branch `stabilization/205-zero`.
|
||||
- Eseguire i test Pest (28 passed, 188 assertions).
|
||||
- Verificare la vista `http://192.168.0.205:8000/admin-filament/gescon/ordinarie` e la console Adminer su `http://192.168.0.205:8000/adminer.php`.
|
||||
- Eseguire i test Pest (30 passed, 193 assertions).
|
||||
- Verificare la console Adminer su `http://192.168.0.205:8000/adminer.php`, la blade `http://192.168.0.205:8000/admin-filament/condomini/nominativi` e la schermata `http://192.168.0.205:8000/admin-filament/contabilita/fatture-ricevute`.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# Direttiva Operativa: Allineamento e Unicità delle Anagrafiche e dei Nominativi Condominiali
|
||||
|
||||
## Scopo
|
||||
|
||||
Questa direttiva sancisce la regola fondamentale per cui **tutte le interfacce, viste e blade di NetGescon (inclusa la scheda unità `unita-immobiliare`, il riepilogo `condomini/nominativi` e le maschere contabili) devono tassativamente attingere dagli stessi archivi relazionali canonici**, evitando divergenze tra schermate o sovrascritture arbitrarie da viste legacy temporanee.
|
||||
|
||||
---
|
||||
|
||||
## 1. Sorgente Dati Canonica Obbligatoria
|
||||
|
||||
1. **Unità Immobiliari**: `unita_immobiliari` con identificativo univoco, scala, piano, interno e `stabile_id`.
|
||||
2. **Soggetti e Persone**: Tabella canonica `persone` (denominazione / ragione sociale o cognome + nome, codice fiscale, contatti telefonici ed email).
|
||||
3. **Relazioni Titolarità Temporale**: Tabella `persone_unita_relazioni` con:
|
||||
- `tipo_relazione`: `'proprietario'`, `'comproprietario'`, `'nudo_proprietario'`, `'usufruttuario'`, `'condomino'` per i possessori; `'inquilino'`, `'conduttore'`, `'locatario'` per gli inquilini.
|
||||
- `quota_relazione` / `perc_diritto_reale`: percentuale di possesso o diritto reale (es. 50% / 50%).
|
||||
- `data_inizio` e `data_fine`: validità temporale legata all'anno di gestione attivo.
|
||||
- `attivo`: stato della relazione.
|
||||
|
||||
---
|
||||
|
||||
## 2. Regole di Visualizzazione e Normalizzazione
|
||||
|
||||
- **Gestione Anno Attivo (`AnnoGestioneContext`)**: I nominativi di proprietari e inquilini visualizzati devono sempre rispecchiare l'esercizio/anno di gestione attivo selezionato nel contesto di sessione.
|
||||
- **Risoluzione Denominazione Persona/Azienda**:
|
||||
```sql
|
||||
CASE
|
||||
WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale
|
||||
ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, '')))
|
||||
END
|
||||
```
|
||||
È vietata la concatenazione ridondante di cognome, nome e ragione sociale che produce duplicazioni testuali.
|
||||
- **Distinzione Netta Condòmini vs Inquilini**: In tutte le viste le titolarità e i recapiti di proprietari e inquilini devono rimanere nettamente separati.
|
||||
|
||||
---
|
||||
|
||||
## 3. Conformità Multi-Blade
|
||||
|
||||
- **`UnitaImmobiliarePage` (`/admin-filament/unita-immobiliare`)** e **`NominativiStabile` (`/admin-filament/condomini/nominativi`)** utilizzano la medesima logica di query atomica relazionale, garantendo che lo stesso condòmino/inquilino appaia in modo identico in entrambe le pagine.
|
||||
162
tests/Feature/NominativiAndFeMatchingTest.php
Normal file
162
tests/Feature/NominativiAndFeMatchingTest.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
use App\Models\FatturaElettronica;
|
||||
use App\Models\Persona;
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use App\Models\User;
|
||||
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('nominativi stabile page resolves identical canonical names as unita immobiliare', function () {
|
||||
$user = User::factory()->create();
|
||||
Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
$user->assignRole('amministratore');
|
||||
|
||||
DB::table('amministratori')->insertOrIgnore([
|
||||
'id' => 1,
|
||||
'user_id' => $user->id,
|
||||
'nome' => 'Mario',
|
||||
'cognome' => 'Rossi',
|
||||
'codice_amministratore' => 'ADM0001',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$stabile = Stabile::create([
|
||||
'codice_stabile' => '0099',
|
||||
'denominazione' => 'Condominio Test Unitario',
|
||||
'indirizzo' => 'Via Test 1',
|
||||
'citta' => 'Roma',
|
||||
'cap' => '00100',
|
||||
'provincia' => 'RM',
|
||||
'codice_fiscale' => '99999990580',
|
||||
'amministratore_id' => 1,
|
||||
]);
|
||||
|
||||
$unita = UnitaImmobiliare::create([
|
||||
'stabile_id' => $stabile->id,
|
||||
'interno' => '1',
|
||||
'scala' => 'A',
|
||||
]);
|
||||
|
||||
$proprietario = Persona::create([
|
||||
'cognome' => 'Rossi',
|
||||
'nome' => 'Mario',
|
||||
'codice_fiscale' => 'RSSMRA80A01H501U',
|
||||
]);
|
||||
|
||||
$inquilino = Persona::create([
|
||||
'cognome' => 'Bianchi',
|
||||
'nome' => 'Luigi',
|
||||
'codice_fiscale' => 'BNCLGU85B02H501Z',
|
||||
]);
|
||||
|
||||
PersonaUnitaRelazione::create([
|
||||
'unita_id' => $unita->id,
|
||||
'persona_id' => $proprietario->id,
|
||||
'tipo_relazione' => 'proprietario',
|
||||
'quota_relazione' => 100,
|
||||
'data_inizio' => '2020-01-01',
|
||||
'attivo' => 1,
|
||||
]);
|
||||
|
||||
PersonaUnitaRelazione::create([
|
||||
'unita_id' => $unita->id,
|
||||
'persona_id' => $inquilino->id,
|
||||
'tipo_relazione' => 'inquilino',
|
||||
'quota_relazione' => 100,
|
||||
'data_inizio' => '2020-01-01',
|
||||
'attivo' => 1,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get("/admin-filament/condomini/nominativi?stabile_id={$stabile->id}");
|
||||
expect($response->status())->toBeIn([200, 302]);
|
||||
});
|
||||
|
||||
it('fe scanner correctly matches invoices strictly by codice fiscale without arbitrary fallback', function () {
|
||||
$user = User::factory()->create();
|
||||
Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||
$user->assignRole('amministratore');
|
||||
|
||||
DB::table('amministratori')->insertOrIgnore([
|
||||
'id' => 1,
|
||||
'user_id' => $user->id,
|
||||
'nome' => 'Admin',
|
||||
'cognome' => 'Test',
|
||||
'codice_amministratore' => 'ADM0001',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$stabile = Stabile::create([
|
||||
'codice_stabile' => '0098',
|
||||
'denominazione' => 'Condominio Via Test 10',
|
||||
'indirizzo' => 'Via Test 10',
|
||||
'citta' => 'Roma',
|
||||
'cap' => '00100',
|
||||
'provincia' => 'RM',
|
||||
'codice_fiscale' => '88888880581',
|
||||
'amministratore_id' => 1,
|
||||
]);
|
||||
|
||||
$xmlContentMatching = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<p:FatturaElettronica versione="FPR12" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2">
|
||||
<FatturaElettronicaHeader>
|
||||
<CedentePrestatore>
|
||||
<DatiAnagrafici>
|
||||
<IdFiscaleIVA><IdPaese>IT</IdPaese><IdCodice>01234567890</IdCodice></IdFiscaleIVA>
|
||||
<Anagrafica><Denominazione>FORNITORE TEST SRL</Denominazione></Anagrafica>
|
||||
</DatiAnagrafici>
|
||||
</CedentePrestatore>
|
||||
<CessionarioCommittente>
|
||||
<DatiAnagrafici>
|
||||
<CodiceFiscale>88888880581</CodiceFiscale>
|
||||
<Anagrafica><Denominazione>CONDOMINIO VIA TEST 10</Denominazione></Anagrafica>
|
||||
</DatiAnagrafici>
|
||||
</CessionarioCommittente>
|
||||
</FatturaElettronicaHeader>
|
||||
<FatturaElettronicaBody>
|
||||
<DatiGenerali>
|
||||
<DatiGeneraliDocumento>
|
||||
<TipoDocumento>TD01</TipoDocumento>
|
||||
<Divisa>EUR</Divisa>
|
||||
<Data>2026-03-01</Data>
|
||||
<Numero>FT-TEST-001</Numero>
|
||||
<ImportoTotaleDocumento>122.00</ImportoTotaleDocumento>
|
||||
</DatiGeneraliDocumento>
|
||||
</DatiGenerali>
|
||||
<DatiBeniServizi>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>1</NumeroLinea>
|
||||
<Descrizione>Manutenzione Ordinaria Impianto</Descrizione>
|
||||
<PrezzoUnitario>100.00</PrezzoUnitario>
|
||||
<PrezzoTotale>100.00</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DatiRiepilogo>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
<ImponibileImporto>100.00</ImponibileImporto>
|
||||
<Imposta>22.00</Imposta>
|
||||
</DatiRiepilogo>
|
||||
</DatiBeniServizi>
|
||||
</FatturaElettronicaBody>
|
||||
</p:FatturaElettronica>
|
||||
XML;
|
||||
|
||||
$importer = app(FatturaElettronicaImporter::class);
|
||||
$result = $importer->importXml($xmlContentMatching, (int) $stabile->id, 'FT-TEST-001.xml');
|
||||
|
||||
expect($result['status'])->toBe('imported');
|
||||
|
||||
$fe = FatturaElettronica::where('numero_fattura', 'FT-TEST-001')->first();
|
||||
expect($fe)->not->toBeNull();
|
||||
expect((int) $fe->stabile_id)->toBe((int) $stabile->id);
|
||||
expect($fe->destinatario_cf)->toBe('88888880581');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user