sanitizeXml($xml); $xml = trim($xml); if ($xml === '') { return ['status' => 'error', 'message' => 'File vuoto']; } $hash = hash('sha256', $xml); $existingByHash = FatturaElettronica::query()->where('xml_hash', $hash)->first(); if ($existingByHash) { // Auto-repair: se stiamo importando di nuovo lo stesso XML ma ci portiamo dietro // percorsi mancanti (p7m_path/xml_path) o vogliamo completare PDF/protocollo, aggiorna l'esistente. if (empty($extra['force_update']) && ! empty($extra['auto_repair_duplicate']) && $this->shouldAutoRepairDuplicate($existingByHash, $extra)) { try { $updated = $this->updateExisting($existingByHash, $xml, $originalFilename, $extra); $this->postProcess($updated, $xml, $extra); return ['status' => 'imported', 'id' => (int) $updated->id, 'message' => 'Aggiornata (auto-repair duplicato)']; } catch (\Throwable $e) { return ['status' => 'error', 'id' => (int) $existingByHash->id, 'message' => $e->getMessage()]; } } if (! empty($extra['force_update'])) { try { $updated = $this->updateExisting($existingByHash, $xml, $originalFilename, $extra); $this->postProcess($updated, $xml, $extra); return ['status' => 'imported', 'id' => (int) $updated->id, 'message' => 'Aggiornata (force)']; } catch (\Throwable $e) { return ['status' => 'error', 'id' => (int) $existingByHash->id, 'message' => $e->getMessage()]; } } $msg = 'Già importata (hash)'; $msg .= ' — id=' . (int) $existingByHash->id; $msg .= ' — hash=' . substr((string) $hash, 0, 10); if (is_string($originalFilename) && $originalFilename !== '') { $msg .= ' — file=' . $originalFilename; } return [ 'status' => 'duplicate', 'id' => (int) $existingByHash->id, 'message' => $msg, 'debug' => [ 'method' => 'hash', 'hash' => $hash, 'existing_id' => (int) $existingByHash->id, 'existing_stabile_id' => (int) ($existingByHash->stabile_id ?? 0), ], ]; } $data = $this->parser->parse($xml); $numero = isset($data['numero_fattura']) ? trim((string) $data['numero_fattura']) : null; if ($numero !== null) { $n = strtoupper(trim($numero)); if ($n === '' || $n === 'ND' || $n === 'N/D' || $n === 'N.D.' || $n === 'N. D.' || $n === 'N.D') { $numero = null; } } $dataFattura = $data['data_fattura'] ?? null; $fornitorePiva = $data['fornitore_piva'] ?? null; $fornitoreCf = $data['fornitore_cf'] ?? null; // La tabella richiede fornitore_piva: se manca, ripieghiamo sul CF. if (! $fornitorePiva && $fornitoreCf) { $fornitorePiva = $fornitoreCf; } if (! $fornitorePiva) { $fornitorePiva = 'ND'; } $fornitorePivaForDedup = $fornitorePiva; if (is_string($fornitorePivaForDedup)) { $tmp = strtoupper(trim($fornitorePivaForDedup)); if ($tmp === '' || $tmp === 'ND' || $tmp === 'N/D' || $tmp === 'N.D.' || $tmp === 'N. D.') { $fornitorePivaForDedup = null; } } $preferredStabileId = (int) ($extra['force_stabile_id'] ?? 0); $stabileRouting = $this->resolveStabileRoutingFromXml( $data, $preferredStabileId > 0 ? $preferredStabileId : $fallbackStabileId, ! empty($extra['allow_fallback_stabile']) ); $stabileId = (int) $stabileRouting['stabile_id']; // Dedup “logico” anche se l'XML differisce (formattazione ecc.) // Nota: evitiamo dedup “logico” se i campi sono placeholder (es. numero = ND) oppure se P.IVA/CF non è disponibile. if ($numero && $dataFattura && $fornitorePivaForDedup) { $existingLogical = FatturaElettronica::query() ->where('fornitore_piva', $fornitorePivaForDedup) ->where('numero_fattura', $numero) ->whereDate('data_fattura', $dataFattura) ->first(); if ($existingLogical) { // Auto-repair: se il record esistente è incompleto (mancano path/file) e il file corrente può completarlo. if (empty($extra['force_update']) && ! empty($extra['auto_repair_duplicate']) && $this->shouldAutoRepairDuplicate($existingLogical, $extra)) { try { $updated = $this->updateExisting($existingLogical, $xml, $originalFilename, $extra); $this->postProcess($updated, $xml, $extra); return ['status' => 'imported', 'id' => (int) $updated->id, 'message' => 'Aggiornata (auto-repair duplicato)']; } catch (\Throwable $e) { return ['status' => 'error', 'id' => (int) $existingLogical->id, 'message' => $e->getMessage()]; } } if (! empty($extra['force_update'])) { try { $updated = $this->updateExisting($existingLogical, $xml, $originalFilename, $extra); $this->postProcess($updated, $xml, $extra); return ['status' => 'imported', 'id' => (int) $updated->id, 'message' => 'Aggiornata (force)']; } catch (\Throwable $e) { return ['status' => 'error', 'id' => (int) $existingLogical->id, 'message' => $e->getMessage()]; } } $msg = 'Già presente (fornitore/numero/data)'; $msg .= ' — id=' . (int) $existingLogical->id; $msg .= ' — piva=' . (string) $fornitorePivaForDedup; $msg .= ' — num=' . (string) $numero; try { $msg .= ' — data=' . (string) (is_object($dataFattura) && method_exists($dataFattura, 'format') ? $dataFattura->format('Y-m-d') : $dataFattura); } catch (\Throwable) { // ignore } return [ 'status' => 'duplicate', 'id' => (int) $existingLogical->id, 'message' => $msg, 'debug' => [ 'method' => 'logical', 'fornitore_piva' => (string) $fornitorePivaForDedup, 'numero_fattura' => (string) $numero, 'data_fattura' => is_object($dataFattura) && method_exists($dataFattura, 'format') ? $dataFattura->format('Y-m-d') : (string) $dataFattura, 'existing_id' => (int) $existingLogical->id, 'existing_stabile_id' => (int) ($existingLogical->stabile_id ?? 0), ], ]; } } $stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId); $amministratoreId = (int) ($stabile?->amministratore_id ?: 0); $fornitoreId = $this->matchFornitoreIdForAmministratore($amministratoreId, $fornitorePiva, $fornitoreCf); if (! $fornitoreId && ! empty($extra['create_fornitore_if_missing']) && $amministratoreId > 0) { try { $fornitoreId = $this->ensureFornitoreFromFeData($amministratoreId, $data, $extra); } catch (\Throwable) { // ignore: l'import FE deve poter proseguire anche senza creare il fornitore. $fornitoreId = null; } } // Best-effort: se il fornitore esiste, aggiorna i suoi dati base dalla FE (P.IVA/CF/denominazione, contatti, pagamenti). try { if ($fornitoreId) { $this->syncFornitoreFromFeData((int) $fornitoreId, $data, $extra); } } catch (\Throwable) { // ignore } $importRighe = $this->shouldImportRighe($fornitoreId, $extra); $contabilitaFatturaId = null; try { /** @var FatturaElettronica $created */ $created = DB::transaction(function () use ($stabileId, $fornitoreId, $data, $xml, $hash, $originalFilename, $fornitorePiva, $extra, $importRighe, &$contabilitaFatturaId) { $created = FatturaElettronica::query()->create([ 'stabile_id' => $stabileId, 'fornitore_id' => $fornitoreId, 'sdi_file' => isset($data['sdi_file']) ? trim((string) $data['sdi_file']) : null, 'numero_fattura' => $data['numero_fattura'] ?? 'ND', 'data_fattura' => $data['data_fattura'] ?? now()->startOfDay(), 'data_scadenza' => $data['data_scadenza'] ?? null, 'fornitore_piva' => $fornitorePiva, 'fornitore_cf' => $data['fornitore_cf'] ?? null, 'fornitore_denominazione' => $data['fornitore_denominazione'] ?? 'ND', 'imponibile' => (float) ($data['imponibile'] ?? 0), 'iva' => (float) ($data['iva'] ?? 0), 'totale' => (float) ($data['totale'] ?? 0), 'pagamento_modalita' => $data['pagamento_modalita'] ?? null, 'pagamento_iban' => $data['pagamento_iban'] ?? null, 'pagamento_bic' => $data['pagamento_bic'] ?? null, 'consumo_valore' => $data['consumo_valore'] ?? null, 'consumo_unita' => $data['consumo_unita'] ?? null, 'consumo_riferimento' => $data['consumo_riferimento'] ?? null, 'consumo_raw' => $data['consumo_raw'] ?? null, 'codice_destinatario' => $data['codice_destinatario'] ?? null, 'pec_destinatario' => $data['pec_destinatario'] ?? null, 'destinatario_cf' => $data['destinatario_cf'] ?? null, 'destinatario_piva' => $data['destinatario_piva'] ?? null, 'destinatario_denominazione' => $data['destinatario_denominazione'] ?? null, 'stato' => 'ricevuta', 'xml_content' => $xml, 'nome_file_xml' => $originalFilename, 'xml_hash' => $hash, 'nome_file_p7m' => $extra['nome_file_p7m'] ?? null, 'p7m_path' => $extra['p7m_path'] ?? null, 'xml_path' => $extra['xml_path'] ?? null, ]); // Crea anche la testata contabile (debito fornitore) collegata alla FE. if (Schema::hasTable('contabilita_fatture_fornitori')) { $ritenuta = is_array($data['ritenuta'] ?? null) ? $data['ritenuta'] : []; $ritenutaImporto = (float) ($ritenuta['importo'] ?? 0); $ritenutaAliquota = $ritenuta['aliquota'] ?? null; $netto = round(((float) ($data['totale'] ?? 0)) - $ritenutaImporto, 2); $dataDoc = $data['data_fattura'] ?? null; $dataRegistrazione = null; if ($dataDoc instanceof \Carbon\CarbonInterface) { $dataRegistrazione = $dataDoc->copy()->startOfDay(); } elseif (is_string($dataDoc) && trim($dataDoc) !== '') { try { $dataRegistrazione = \Carbon\Carbon::parse($dataDoc)->startOfDay(); } catch (\Throwable) { $dataRegistrazione = null; } } $fatturaContabile = FatturaFornitore::query()->create([ 'stabile_id' => $stabileId, 'fattura_elettronica_id' => $created->id, 'fornitore_id' => $fornitoreId, 'data_registrazione' => $dataRegistrazione ?: now()->startOfDay(), 'data_documento' => $dataDoc ?: null, 'numero_documento' => $data['numero_fattura'] ?? null, 'descrizione' => $data['fornitore_denominazione'] ?? null, 'valuta' => 'EUR', 'cambio' => 1, 'imponibile' => round((float) ($data['imponibile'] ?? 0), 2), 'iva' => round((float) ($data['iva'] ?? 0), 2), 'totale' => round((float) ($data['totale'] ?? 0), 2), 'data_scadenza' => $data['data_scadenza'] ?? null, 'modalita_pagamento' => $data['pagamento_modalita'] ?? null, 'ritenuta_aliquota' => $ritenutaAliquota, 'ritenuta_importo' => round($ritenutaImporto, 2), 'ritenuta_codice_tributo' => null, 'netto_da_pagare' => $netto, 'stato' => 'inserito', 'note' => null, ]); $contabilitaFatturaId = (int) $fatturaContabile->id; $defaultCostAccountId = null; if ($fornitoreId && Schema::hasTable('fornitore_stabile_impostazioni')) { $defaultCostAccountId = FornitoreStabileImpostazione::query() ->where('stabile_id', (int) $stabileId) ->where('fornitore_id', (int) $fornitoreId) ->value('conto_costo_default_id'); $defaultCostAccountId = is_numeric($defaultCostAccountId) ? (int) $defaultCostAccountId : null; if ($defaultCostAccountId !== null && $defaultCostAccountId <= 0) { $defaultCostAccountId = null; } } if ($importRighe) { $righe = is_array($data['righe'] ?? null) ? $data['righe'] : []; if (count($righe) < 1) { $righe = [[ 'numero_linea' => 1, 'descrizione' => $data['numero_fattura'] ?? 'Fattura elettronica', 'quantita' => null, 'prezzo_unitario' => null, 'prezzo_totale' => (float) ($data['imponibile'] ?? 0), 'aliquota_iva' => null, 'ritenuta' => null, ]]; } foreach ($righe as $idx => $r) { $imponibileRiga = (float) ($r['prezzo_totale'] ?? 0); if ($imponibileRiga <= 0 && isset($r['prezzo_unitario'], $r['quantita'])) { $imponibileRiga = (float) $r['prezzo_unitario'] * (float) $r['quantita']; } $aliquota = $r['aliquota_iva'] ?? null; $ivaRiga = 0.0; if ($aliquota !== null) { $ivaRiga = round($imponibileRiga * ((float) $aliquota / 100), 2); } $fatturaContabile->righe()->create([ 'riga' => (int) (($r['numero_linea'] ?? null) ?: ($idx + 1)), 'descrizione' => Str::limit(trim((string) ($r['descrizione'] ?? '')), 255, ''), 'imponibile_euro' => round($imponibileRiga, 2), 'iva_euro' => round($ivaRiga, 2), 'totale_euro' => round($imponibileRiga + $ivaRiga, 2), 'aliquota_iva' => $aliquota, 'conto_costo_id' => $defaultCostAccountId, 'tabella_millesimale_id' => null, ]); } // Quadratura: alcuni XML hanno componenti (cassa/bollo/arrotondamenti) fuori dalle DettaglioLinee. // Se restano differenze tra totale testata e somma righe, aggiungiamo una riga di riepilogo. $this->ensureQuadraturaRighe($fatturaContabile, $defaultCostAccountId); $this->applyFornitoreVocePreset($fatturaContabile, (int) $stabileId, (int) $fornitoreId); } } return $created; }); } catch (\Throwable $e) { return ['status' => 'error', 'message' => $e->getMessage()]; } $this->postProcess($created, $xml, $extra); if ($fornitoreId && Schema::hasTable('products')) { try { $fornitore = Fornitore::query()->find((int) $fornitoreId); if ($fornitore instanceof Fornitore) { app(FornitoreProductCatalogService::class)->extractFromFattureElettroniche($fornitore, (int) $stabileId, 1); } } catch (\Throwable) { // Il catalogo prodotti non deve interrompere l'import della FE. } } return ['status' => 'imported', 'id' => (int) $created->id, 'contabilita_fattura_id' => $contabilitaFatturaId]; } private function sanitizeXml(string $xml): string { $xml = preg_replace('/^\xEF\xBB\xBF/', '', $xml) ?? $xml; if (function_exists('mb_check_encoding') && ! mb_check_encoding($xml, 'UTF-8')) { if (function_exists('mb_convert_encoding')) { $xml = mb_convert_encoding($xml, 'UTF-8', 'UTF-8,ISO-8859-1,Windows-1252'); } } $fixed = @iconv('UTF-8', 'UTF-8//IGNORE', $xml); if (is_string($fixed) && $fixed !== '') { $xml = $fixed; } $xml = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $xml) ?? $xml; return $xml; } private function ensureQuadraturaRighe(FatturaFornitore $fatturaContabile, ?int $defaultCostAccountId = null): void { try { $sum = (float) $fatturaContabile->righe()->sum('totale_euro'); $totale = (float) ($fatturaContabile->totale ?? 0); $delta = round($totale - $sum, 2); if (abs($delta) <= 0.01) { return; } $already = $fatturaContabile->righe() ->where('descrizione', 'Riepilogo FE / arrotondamenti') ->exists(); if ($already) { return; } $maxRiga = $fatturaContabile->righe()->max('riga'); $nextRiga = (int) (is_numeric($maxRiga) ? $maxRiga : 0) + 1; $fatturaContabile->righe()->create([ 'riga' => $nextRiga, 'descrizione' => 'Riepilogo FE / arrotondamenti', 'imponibile_euro' => round($delta, 2), 'iva_euro' => 0.0, 'totale_euro' => round($delta, 2), 'aliquota_iva' => null, 'conto_costo_id' => $defaultCostAccountId, 'tabella_millesimale_id' => null, ]); } catch (\Throwable) { // best-effort } } private function applyFornitoreVocePreset(FatturaFornitore $fatturaContabile, int $stabileId, int $fornitoreId): void { if (! Schema::hasTable('fornitore_stabile_impostazioni')) { return; } $voceSpesaId = null; $gestioneId = null; $tabellaId = null; $impostazione = FornitoreStabileImpostazione::query() ->where('stabile_id', $stabileId) ->where('fornitore_id', $fornitoreId) ->first(['voce_spesa_default_id', 'meta']); if (! $impostazione) { return; } if (! empty($impostazione->voce_spesa_default_id)) { $voceSpesaId = (int) $impostazione->voce_spesa_default_id; if ($voceSpesaId <= 0) { $voceSpesaId = null; } } $meta = is_array($impostazione->meta) ? $impostazione->meta : []; if (isset($meta['gestione_id']) && is_numeric($meta['gestione_id'])) { $gestioneId = (int) $meta['gestione_id']; if ($gestioneId <= 0) { $gestioneId = null; } } if (isset($meta['tabella_millesimale_id']) && is_numeric($meta['tabella_millesimale_id'])) { $tabellaId = (int) $meta['tabella_millesimale_id']; if ($tabellaId <= 0) { $tabellaId = null; } } if ($voceSpesaId && (! $gestioneId || ! $tabellaId)) { $voce = DB::table('voci_spesa') ->where('id', $voceSpesaId) ->first(['gestione_contabile_id', 'tabella_millesimale_default_id']); if ($voce) { if (! $gestioneId && is_numeric($voce->gestione_contabile_id ?? null)) { $gestioneId = (int) $voce->gestione_contabile_id; if ($gestioneId <= 0) { $gestioneId = null; } } if (! $tabellaId && is_numeric($voce->tabella_millesimale_default_id ?? null)) { $tabellaId = (int) $voce->tabella_millesimale_default_id; if ($tabellaId <= 0) { $tabellaId = null; } } } } if ($gestioneId && empty($fatturaContabile->gestione_id)) { $fatturaContabile->gestione_id = $gestioneId; $fatturaContabile->save(); } $update = []; if ($voceSpesaId) { $update['voce_spesa_id'] = $voceSpesaId; } if ($gestioneId) { $update['gestione_id'] = $gestioneId; } if ($tabellaId) { $update['tabella_millesimale_id'] = $tabellaId; } if ($update !== []) { $fatturaContabile->righe() ->whereNull('voce_spesa_id') ->where('descrizione', '!=', 'Riepilogo FE / arrotondamenti') ->update($update); } } private function updateContabilitaFornituraDaPdf(FatturaElettronica $fattura): void { if (! Schema::hasTable('contabilita_fatture_fornitori')) { return; } if (! Schema::hasColumn('contabilita_fatture_fornitori', 'dati_fornitura')) { return; } $fatturaContabile = FatturaFornitore::query() ->where('fattura_elettronica_id', (int) $fattura->id) ->first(); if (! $fatturaContabile) { return; } $existing = is_array($fatturaContabile->dati_fornitura) ? $fatturaContabile->dati_fornitura : []; if (count($existing) > 0) { return; } $pdfPath = is_string($fattura->allegato_pdf_path ?? null) ? trim((string) $fattura->allegato_pdf_path) : ''; if ($pdfPath === '') { return; } $disk = Storage::disk('local'); $resolvedPath = $pdfPath; if (! $disk->exists($resolvedPath) && $disk->exists('private/' . $resolvedPath)) { $resolvedPath = 'private/' . $resolvedPath; } if (! $disk->exists($resolvedPath)) { return; } $fullPath = $disk->path($resolvedPath); $data = $this->extractGasFornituraDataFromPdf($fullPath); if (! is_array($data) || $data === []) { return; } $fatturaContabile->dati_fornitura = $data; $fatturaContabile->save(); } /** @return array|null */ private function extractGasFornituraDataFromPdf(string $fullPath): ?array { try { $parser = new Parser(); $text = $parser->parseFile($fullPath)->getText(); } catch (\Throwable) { return null; } if (! is_string($text) || trim($text) === '') { return null; } $codCliente = $this->matchFirst($text, '/Cod\.?\s*Cliente\s*([A-Z0-9]+)/i'); $contratto = $this->matchFirst($text, '/Contratto\s*([A-Z0-9_]+)/i'); $pdr = $this->matchFirst($text, '/PDR\s*([0-9]{8,})/i'); $periodoFatturato = null; $consumoFatturatoRaw = null; if (preg_match('/([0-9\.]+)\s*Consumo\s*Fatturato\s*Smc\s*([A-ZÀ-Ù]+\s*\d{4})/u', $text, $m)) { $consumoFatturatoRaw = trim((string) $m[1]); $periodoFatturato = trim((string) $m[2]); } else { $periodoFatturato = $this->matchFirst($text, '/Periodo\s*([A-ZÀ-Ù]+\s+\d{4})/u'); $consumoFatturatoRaw = $this->matchFirst($text, '/Consumo\s*Fatturato\s*([0-9\.]+)\s*Smc/i'); } $consumoFatturatoSmc = $this->parseSmcValue($consumoFatturatoRaw); $periodoAnnuo = $this->matchFirst($text, '/([A-Za-zà-ù]+\s+\d{4}\s*-\s*[A-Za-zà-ù]+\s+\d{4})\s*Periodo/i'); if (! $periodoAnnuo) { $periodoAnnuo = $this->matchFirst($text, '/Periodo\s*([A-Za-zà-ù]+\s+\d{4}\s*-\s*[A-Za-zà-ù]+\s+\d{4})/i'); } $consumoAnnuoRaw = null; if (preg_match_all('/([0-9\.]+)\s*Smc[\s\S]{0,120}?Consumo\s*annuo/i', $text, $m) && isset($m[1])) { $all = array_filter(array_map('trim', $m[1])); if ($all !== []) { usort($all, fn($a, $b) => ($this->parseSmcValue($b) ?? 0) <=> ($this->parseSmcValue($a) ?? 0)); $consumoAnnuoRaw = $all[0] ?? null; } } if (! $consumoAnnuoRaw) { $consumoAnnuoRaw = $this->matchFirst($text, '/([0-9\.]+)\s*Smc\s*Consumo\s*annuo/i'); } if (preg_match('/Consumo\s*annuo([\s\S]{0,400})/i', $text, $m)) { if (preg_match_all('/([0-9\.]+)\s*Smc/i', (string) $m[1], $vals) && isset($vals[1])) { $all = array_filter(array_map('trim', $vals[1])); if ($all !== []) { usort($all, fn($a, $b) => ($this->parseSmcValue($b) ?? 0) <=> ($this->parseSmcValue($a) ?? 0)); $consumoAnnuoRaw = $all[0] ?? null; } } } $consumoAnnuoSmc = $this->parseSmcValue($consumoAnnuoRaw); [$offerta, $scadenzaOfferta] = $this->extractOffertaInfo($text); $payload = [ 'tipo' => 'gas_naturale', 'codice_cliente' => $codCliente, 'contratto' => $contratto, 'pdr' => $pdr, 'periodo_fatturato' => $periodoFatturato, 'consumo_fatturato_raw' => $consumoFatturatoRaw, 'consumo_fatturato_smc' => $consumoFatturatoSmc, 'periodo_annuo' => $periodoAnnuo, 'consumo_annuo_raw' => $consumoAnnuoRaw, 'consumo_annuo_smc' => $consumoAnnuoSmc, 'offerta' => $offerta, 'offerta_scadenza' => $scadenzaOfferta, ]; $payload = array_filter($payload, fn($v) => $v !== null && $v !== ''); return $payload !== [] ? $payload : null; } /** @param array $lines */ private function findValueAfterLabel(array $lines, string $labelPattern, int $maxLookahead = 6, ?string $valuePattern = null): ?string { $count = count($lines); for ($i = 0; $i < $count; $i++) { if (! preg_match($labelPattern, $lines[$i])) { continue; } $limit = min($count - 1, $i + $maxLookahead); for ($j = $i + 1; $j <= $limit; $j++) { $val = trim($lines[$j]); if ($val === '') { continue; } if ($valuePattern && ! preg_match($valuePattern, $val)) { continue; } return $val; } } return null; } /** @param array $lines */ private function findFirstMatchInLines(array $lines, string $pattern): ?string { foreach ($lines as $line) { if (preg_match($pattern, $line, $m) && isset($m[1])) { $value = trim((string) $m[1]); if ($value !== '') { return $value; } } } return null; } /** @param array $lines */ private function findSmcAfterLabel(array $lines, string $labelPattern, int $maxLookahead, string $mode): ?string { $count = count($lines); for ($i = 0; $i < $count; $i++) { if (! preg_match($labelPattern, $lines[$i])) { continue; } $limit = min($count - 1, $i + $maxLookahead); $values = []; for ($j = $i + 1; $j <= $limit; $j++) { if (! preg_match('/([0-9\.]+)\s*Smc/i', $lines[$j], $m)) { continue; } $values[] = $m[1]; } if ($values === []) { continue; } if ($mode === 'max') { usort($values, fn($a, $b) => ($this->parseSmcValue($b) ?? 0) <=> ($this->parseSmcValue($a) ?? 0)); } return $values[0] ?? null; } return null; } private function matchFirst(string $text, string $pattern): ?string { if (preg_match($pattern, $text, $m) && isset($m[1])) { $value = trim((string) $m[1]); return $value !== '' ? $value : null; } return null; } private function parseSmcValue(?string $raw): ?float { if ($raw === null) { return null; } $clean = str_replace(['.', ' '], '', $raw); $clean = str_replace(',', '.', $clean); if ($clean === '' || ! is_numeric($clean)) { return null; } return (float) $clean; } /** @return array{0:?string,1:?string} */ private function extractOffertaInfo(string $text): array { $offerta = null; $scadenza = null; $lines = preg_split('/\R+/', $text) ?: []; $prev = null; foreach ($lines as $line) { $line = trim((string) $line); if ($line === '') { continue; } if ($offerta === null && preg_match('/^Offerta/i', $line) && ! preg_match('/^Nome Offerta/i', $line)) { $candidate = trim(preg_replace('/^Offerta\s*/i', '', $line)); if ($candidate !== '' && stripos($candidate, 'Offerta') !== 0) { $offerta = $candidate; } } if ($scadenza === null && preg_match('/Scadenza\s*condizioni\s*economiche\s*([A-Za-zÀ-Ùà-ù]+)/u', $line, $m)) { $scadenza = trim((string) ($m[1] ?? '')); } if ($scadenza === null && preg_match('/Scadenza\s*condizioni/i', $line)) { if (is_string($prev) && trim($prev) !== '') { $scadenza = trim($prev); } } if ($scadenza === null && preg_match('/Scadenza\s*([A-Za-zÀ-Ùà-ù]+)/u', $line, $m)) { $candidate = trim((string) ($m[1] ?? '')); if ($candidate !== '' && ! preg_match('/^condizioni$/i', $candidate)) { $scadenza = $candidate; } } if ($scadenza === null && preg_match('/^Scadenza/i', $line)) { if (is_string($prev) && trim($prev) !== '') { $scadenza = trim($prev); } } $prev = $line; } return [$offerta, $scadenza]; } private function shouldAutoRepairDuplicate(FatturaElettronica $existing, array $extra): bool { // Se non ci stiamo portando dietro nulla (né path né filename), non facciamo nulla. $hasIncomingXmlPath = isset($extra['xml_path']) && is_string($extra['xml_path']) && trim($extra['xml_path']) !== ''; $hasIncomingP7mPath = isset($extra['p7m_path']) && is_string($extra['p7m_path']) && trim($extra['p7m_path']) !== ''; $hasIncomingP7mName = isset($extra['nome_file_p7m']) && is_string($extra['nome_file_p7m']) && trim($extra['nome_file_p7m']) !== ''; if (! $hasIncomingXmlPath && ! $hasIncomingP7mPath && ! $hasIncomingP7mName) { return false; } // Se mancano file/percorso sul record esistente, possiamo completarlo. if ($hasIncomingP7mPath && (! is_string($existing->p7m_path) || trim((string) $existing->p7m_path) === '')) { return true; } if ($hasIncomingXmlPath && (! is_string($existing->xml_path) || trim((string) $existing->xml_path) === '')) { return true; } if (! is_string($existing->xml_content) || trim((string) $existing->xml_content) === '') { return true; } // Se il file su storage non esiste più (pulizie/move), reimpostiamo i path. try { if ($hasIncomingP7mPath && is_string($existing->p7m_path) && $existing->p7m_path !== '' && ! Storage::disk('local')->exists($existing->p7m_path)) { return true; } if ($hasIncomingXmlPath && is_string($existing->xml_path) && $existing->xml_path !== '' && ! Storage::disk('local')->exists($existing->xml_path)) { return true; } } catch (\Throwable) { // ignore } // Se manca PDF, il postProcess può estrarlo dagli Allegati o creare/riparare Documento. if (Schema::hasColumn('fatture_elettroniche', 'allegato_pdf_path')) { if (! is_string($existing->allegato_pdf_path) || trim((string) $existing->allegato_pdf_path) === '') { return true; } } return false; } private function postProcess(FatturaElettronica $fattura, string $xml, array $extra = []): void { // Best-effort: se esiste una fattura contabile collegata ma senza fornitore_id, backfill automatico. try { if ( $fattura->fornitore_id && Schema::hasTable('contabilita_fatture_fornitori') && Schema::hasColumn('contabilita_fatture_fornitori', 'fattura_elettronica_id') ) { $fatturaContabile = FatturaFornitore::query() ->where('fattura_elettronica_id', (int) $fattura->id) ->first(); if ($fatturaContabile && empty($fatturaContabile->fornitore_id)) { $fatturaContabile->fornitore_id = (int) $fattura->fornitore_id; $fatturaContabile->save(); } } } catch (\Throwable) { // ignore } // Best-effort: aggiorna dati pagamento del fornitore se mancanti. try { if ($fattura->fornitore_id && Schema::hasColumn('fornitori', 'iban')) { $fornitore = Fornitore::query()->find($fattura->fornitore_id); if ($fornitore) { $dirty = false; if (Schema::hasColumn('fornitori', 'iban') && is_string($fattura->pagamento_iban) && trim($fattura->pagamento_iban) !== '') { $ibanFe = trim($fattura->pagamento_iban); $ibanDb = is_string($fornitore->iban) ? trim((string) $fornitore->iban) : ''; if ($ibanDb !== $ibanFe) { $fornitore->iban = $ibanFe; $dirty = true; } } if (Schema::hasColumn('fornitori', 'bic') && is_string($fattura->pagamento_bic) && trim($fattura->pagamento_bic) !== '') { $bicFe = trim($fattura->pagamento_bic); $bicDb = is_string($fornitore->bic) ? trim((string) $fornitore->bic) : ''; if ($bicDb !== $bicFe) { $fornitore->bic = $bicFe; $dirty = true; } } if (Schema::hasColumn('fornitori', 'modalita_pagamento_predefinita') && empty($fornitore->modalita_pagamento_predefinita) && is_string($fattura->pagamento_modalita) && $fattura->pagamento_modalita !== '') { $fornitore->modalita_pagamento_predefinita = $fattura->pagamento_modalita; $dirty = true; } if ($dirty) { $fornitore->save(); } } } } catch (\Throwable) { // ignore } // Best-effort: estrai e salva il PDF embedded in (se presente). try { if (Schema::hasColumn('fatture_elettroniche', 'allegato_pdf_path')) { $pdf = $this->extractFirstPdfAttachment($xml); if ($pdf && is_string($pdf['bytes']) && $pdf['bytes'] !== '') { $hashPdf = hash('sha256', $pdf['bytes']); $ym = optional($fattura->data_fattura)->format('Y/m') ?: now()->format('Y/m'); $base = $this->archiveBaseForStabile((int) $fattura->stabile_id, $ym); $originalName = (string) ($pdf['filename'] ?? 'allegato.pdf'); $originalName = str_replace(["\r", "\n"], '', $originalName); $safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME)); if ($safeName === '') { $safeName = 'allegato'; } $safeName .= '.pdf'; $path = $base . '/allegati/' . $hashPdf . '-' . $safeName; if (! Storage::disk('local')->exists($path)) { Storage::disk('local')->put($path, $pdf['bytes']); } $fattura->forceFill([ 'allegato_pdf_nome' => $originalName, 'allegato_pdf_mime' => (string) ($pdf['mime'] ?? 'application/pdf'), 'allegato_pdf_path' => $path, 'allegato_pdf_hash' => $hashPdf, ])->save(); } } } catch (\Throwable) { // ignore } // Best-effort: estrai dati fornitura da PDF e salva sulla fattura contabile. try { $this->updateContabilitaFornituraDaPdf($fattura); } catch (\Throwable) { // ignore } // Best-effort: crea documento protocollato e rinomina/sposta i file. try { if (Schema::hasTable('documenti')) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fattura); } } catch (\Throwable) { // ignore } // Best-effort: crea riga in registro ritenute d'acconto se presente (RA). try { if (! Schema::hasTable('registro_ritenute_acconto')) { return; } $parsed = $this->parser->parse($xml); $ritenuta = is_array($parsed['ritenuta'] ?? null) ? $parsed['ritenuta'] : []; $ritenutaImporto = (float) ($ritenuta['importo'] ?? 0); $ritenutaAliquota = (float) ($ritenuta['aliquota'] ?? 0); if ($ritenutaImporto <= 0 || $ritenutaAliquota <= 0) { return; } $dataCompetenza = $fattura->data_fattura ? $fattura->data_fattura->copy() : now(); $dataCompetenza = $dataCompetenza->startOfDay(); $scadenzaVersamento = $dataCompetenza->copy()->addMonthNoOverflow()->day(16); $gestione = GestioneContabile::query() ->where('stabile_id', (int) $fattura->stabile_id) ->where('gestione_attiva', true) ->orderByDesc('id') ->first(); if (! $gestione) { $gestione = GestioneContabile::query() ->where('stabile_id', (int) $fattura->stabile_id) ->where('anno_gestione', (int) $dataCompetenza->format('Y')) ->orderByDesc('id') ->first(); } if (! $gestione) { return; } $tenantId = (string) ($gestione->tenant_id ?? ''); if ($tenantId === '') { $stabile = Stabile::query()->with('amministratore:id,codice_amministratore')->select(['id', 'amministratore_id'])->find((int) $fattura->stabile_id); $tenantId = (string) ($stabile?->amministratore?->codice_amministratore ?: ''); } $fatturaContabileId = null; if (Schema::hasTable('contabilita_fatture_fornitori')) { $fatturaContabileId = FatturaFornitore::query()->where('fattura_elettronica_id', (int) $fattura->id)->value('id'); $fatturaContabileId = $fatturaContabileId ? (int) $fatturaContabileId : null; } $exists = RegistroRitenuteAcconto::query() ->where('gestione_id', (int) $gestione->id) ->where('fornitore_id', (int) ($fattura->fornitore_id ?: 0)) ->whereDate('data_competenza', $dataCompetenza->toDateString()) ->where('importo_ritenuta', round($ritenutaImporto, 4)) ->when($fatturaContabileId, fn($q) => $q->where('fattura_id', $fatturaContabileId)) ->exists(); if ($exists) { return; } DB::transaction(function () use ($gestione, $tenantId, $fattura, $fatturaContabileId, $dataCompetenza, $scadenzaVersamento, $ritenutaImporto, $ritenutaAliquota, $ritenuta) { $next = (int) (RegistroRitenuteAcconto::query() ->where('gestione_id', (int) $gestione->id) ->lockForUpdate() ->max('numero_progressivo') ?? 0); $next++; RegistroRitenuteAcconto::query()->create([ 'tenant_id' => $tenantId, 'gestione_id' => (int) $gestione->id, 'numero_progressivo' => $next, 'data_competenza' => $dataCompetenza->toDateString(), 'fornitore_id' => (int) ($fattura->fornitore_id ?: 0), 'operazione_id' => null, 'fattura_id' => $fatturaContabileId, 'imponibile' => round((float) ($fattura->imponibile ?? 0), 4), 'aliquota_ritenuta' => round((float) $ritenutaAliquota, 2), 'importo_ritenuta' => round((float) $ritenutaImporto, 4), 'codice_tributo' => '1040', 'tipo_ritenuta' => isset($ritenuta['tipo']) && is_string($ritenuta['tipo']) && $ritenuta['tipo'] !== '' ? $ritenuta['tipo'] : 'professionale', 'rif_rda' => null, 'stato_versamento' => 'da_versare', 'data_scadenza_versamento' => $scadenzaVersamento->toDateString(), 'anno_dichiarazione' => (int) $dataCompetenza->format('Y'), ]); }); } catch (\Throwable) { // ignore } // Best-effort: genera prima nota automatica (registrazione + movimenti) se configurata. try { if (! Schema::hasTable('contabilita_fatture_fornitori') || ! Schema::hasTable('contabilita_registrazioni') || ! Schema::hasTable('contabilita_movimenti')) { return; } // Se la gestione è già chiusa, non generiamo nuove registrazioni contabili. if (Schema::hasTable('contabilita_chiusure_gestione') && Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) { $gestioneId = (int) ($fattura->gestione_id ?? 0); if ($gestioneId > 0) { $chiusa = \App\Modules\Contabilita\Models\ChiusuraGestione::query() ->where('gestione_id', $gestioneId) ->where('stato', 'chiusa') ->exists(); if ($chiusa) { return; } } } $userId = (int) ($extra['user_id'] ?? 0); if ($userId <= 0) { // Fallback: manteniamo comportamento pre-esistente per evitare errori DB su colonne NOT NULL. $userId = 1; } $fatturaContabile = FatturaFornitore::query() ->where('fattura_elettronica_id', (int) $fattura->id) ->first(); if (! $fatturaContabile) { return; } if (! empty($fatturaContabile->registrazione_id)) { return; } if (! Schema::hasTable('contabilita_regole_prima_nota')) { return; } $regola = RegolaPrimaNota::query() ->where('stabile_id', (int) $fatturaContabile->stabile_id) ->where('origine', 'fattura') ->where('chiave', 'default') ->where('attiva', true) ->first(); if (! $regola || ! $regola->conto_dare_id || ! $regola->conto_avere_id) { return; } $descrizione = 'Fattura fornitore ' . (string) ($fattura->fornitore_denominazione ?? '') . ' ' . (string) ($fattura->numero_fattura ?? ''); $descrizione = trim($descrizione); if ($descrizione === '') { $descrizione = 'Fattura fornitore'; } $importo = (float) ($fatturaContabile->totale ?? 0); if ($importo <= 0) { return; } DB::transaction(function () use ($fatturaContabile, $regola, $descrizione, $importo, $userId) { $regPayload = [ 'stabile_id' => (int) $fatturaContabile->stabile_id, 'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id') ? \App\Modules\Contabilita\Models\Registrazione::resolveGestioneIdOrDefault( (int) ($fatturaContabile->gestione_id ?: 0), (int) $fatturaContabile->stabile_id, (string) ($fatturaContabile->data_registrazione ?: '') ) : null), 'data_registrazione' => ($fatturaContabile->data_registrazione ?: now()->toDateString()), 'descrizione' => $descrizione, 'created_by' => $userId, 'updated_by' => $userId, ]; if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) { $regPayload['user_id'] = $userId; } /** @var \App\Modules\Contabilita\Models\Registrazione $reg */ $reg = \App\Modules\Contabilita\Models\Registrazione::query()->create($regPayload); \App\Modules\Contabilita\Models\Movimento::query()->create([ 'registrazione_id' => (int) $reg->id, 'conto_id' => (int) $regola->conto_dare_id, 'tipo' => 'dare', 'importo' => round($importo, 2), ]); \App\Modules\Contabilita\Models\Movimento::query()->create([ 'registrazione_id' => (int) $reg->id, 'conto_id' => (int) $regola->conto_avere_id, 'tipo' => 'avere', 'importo' => round($importo, 2), ]); // Blindatura: mai consentire prime note sbilanciate, nemmeno di 0,01. $reg->assertBilanciata(); $fatturaContabile->registrazione_id = (int) $reg->id; $fatturaContabile->stato = 'contabilizzato'; $fatturaContabile->save(); }); } catch (\Throwable) { // ignore } } private function archiveBaseForStabile(int $stabileId, string $ym): string { $stabile = Stabile::query() ->with(['amministratore:id,codice_amministratore,codice_univoco']) ->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco']) ->find($stabileId); if ($stabile) { $base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore); if (is_string($base) && $base !== '') { return $base . '/fatture-ricevute/' . $ym; } } // fallback ultra-conservativo $adminId = (int) ($stabile?->amministratore_id ?: 0); return 'amministratori/ID-' . $adminId . '/stabili/ID-' . (int) $stabileId . '/fatture-ricevute/' . $ym; } /** * @return array{filename: string, mime: string, bytes: string}|null */ private function extractFirstPdfAttachment(string $xml): ?array { $dom = new DOMDocument(); $dom->preserveWhiteSpace = false; $dom->formatOutput = false; $loaded = @$dom->loadXML($xml); if (! $loaded) { return null; } $xpath = new DOMXPath($dom); $nodes = $xpath->query("//*[local-name()='Allegati']"); if (! $nodes || $nodes->length < 1) { return null; } foreach ($nodes as $allegatoNode) { $nome = $this->xpathFirstString($xpath, ".//*[local-name()='NomeAttachment']", $allegatoNode); $formato = $this->xpathFirstString($xpath, ".//*[local-name()='FormatoAttachment']", $allegatoNode); $attachment = $this->xpathFirstString($xpath, ".//*[local-name()='Attachment']", $allegatoNode); if (! $attachment) { continue; } $nomeLower = strtolower((string) $nome); $formatoLower = strtolower((string) $formato); $looksPdf = false; if ($nomeLower !== '' && str_ends_with($nomeLower, '.pdf')) { $looksPdf = true; } if (! $looksPdf && $formatoLower !== '' && str_contains($formatoLower, 'pdf')) { $looksPdf = true; } if (! $looksPdf) { continue; } $base64 = preg_replace('/\s+/', '', $attachment); $bytes = base64_decode((string) $base64, true); if (! is_string($bytes) || $bytes === '') { // Fallback: alcuni provider usano base64url o omettono il padding. $base64b = strtr((string) $base64, '-_', '+/'); $pad = strlen($base64b) % 4; if ($pad > 0) { $base64b .= str_repeat('=', 4 - $pad); } $bytes = base64_decode((string) $base64b, true); } if (! is_string($bytes) || $bytes === '') { // Ultimo tentativo: decode non strict (ignora caratteri non-base64). $bytes = base64_decode((string) $base64, false); } if (! is_string($bytes) || $bytes === '') { continue; } $filename = $nome ?: 'allegato.pdf'; if (! str_ends_with(strtolower($filename), '.pdf')) { $filename .= '.pdf'; } return [ 'filename' => $filename, 'mime' => 'application/pdf', 'bytes' => $bytes, ]; } return null; } private function xpathFirstString(DOMXPath $xpath, string $expr, \DOMNode $contextNode): ?string { $nodes = $xpath->query($expr, $contextNode); if (! $nodes || $nodes->length < 1) { return null; } $value = trim((string) $nodes->item(0)?->textContent); return $value !== '' ? $value : null; } private function resolveStabileIdFromXml(array $parsed, int $fallbackStabileId, bool $allowFallback): int { return (int) $this->resolveStabileRoutingFromXml($parsed, $fallbackStabileId, $allowFallback)['stabile_id']; } /** * @return array{stabile_id:int, source:string} */ private function resolveStabileRoutingFromXml(array $parsed, int $preferredStabileId, bool $allowPreferredFallback): array { $directMatch = $this->matchUniqueStabileIdFromParsedIdentifiers($parsed); if ($directMatch !== null) { return $directMatch; } if ($preferredStabileId > 0 && $this->stabileMatchesParsedRecipientContext($preferredStabileId, $parsed)) { return [ 'stabile_id' => $preferredStabileId, 'source' => 'preferred_context', ]; } if ($allowPreferredFallback && $preferredStabileId > 0) { return [ 'stabile_id' => $preferredStabileId, 'source' => 'preferred_fallback', ]; } throw new \RuntimeException('Impossibile determinare lo stabile dalla FE (identificativi destinatario non trovati o non compatibili)'); } /** * @return array{stabile_id:int, source:string}|null */ private function matchUniqueStabileIdFromParsedIdentifiers(array $parsed): ?array { $destCf = $this->normalizeId(isset($parsed['destinatario_cf']) ? (string) $parsed['destinatario_cf'] : ''); $destPiva = $this->normalizeId(isset($parsed['destinatario_piva']) ? (string) $parsed['destinatario_piva'] : ''); $codice = $this->normalizeId(isset($parsed['codice_destinatario']) ? (string) $parsed['codice_destinatario'] : ''); $pec = strtolower(trim((string) ($parsed['pec_destinatario'] ?? ''))); if ($destCf !== '') { $match = Stabile::query() ->whereNotNull('codice_fiscale') ->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$destCf]) ->where('attivo', true) ->get(); if ($match->count() === 1) { return [ 'stabile_id' => (int) $match->first()->id, 'source' => 'destinatario_cf', ]; } } if ($codice !== '') { $match = Stabile::query() ->whereNotNull('codice_destinatario_sdi') ->whereRaw("REPLACE(UPPER(codice_destinatario_sdi), ' ', '') = ?", [$codice]) ->where('attivo', true) ->get(); if ($match->count() === 1) { return [ 'stabile_id' => (int) $match->first()->id, 'source' => 'codice_destinatario', ]; } } if ($pec !== '') { $match = Stabile::query() ->where(function ($q) use ($pec) { $q->whereRaw('LOWER(pec_condominio) = ?', [$pec]); if (Schema::hasColumn('stabili', 'pec_amministratore')) { $q->orWhereRaw('LOWER(pec_amministratore) = ?', [$pec]); } }) ->where('attivo', true) ->get(); if ($match->count() === 1) { return [ 'stabile_id' => (int) $match->first()->id, 'source' => 'pec_destinatario', ]; } } if ($destPiva !== '') { $match = Stabile::query() ->select('stabili.id') ->join('amministratori', 'amministratori.id', '=', 'stabili.amministratore_id') ->where('stabili.attivo', true) ->whereRaw("REPLACE(UPPER(amministratori.partita_iva), ' ', '') = ?", [$destPiva]) ->get(); if ($match->count() === 1) { return [ 'stabile_id' => (int) $match->first()->id, 'source' => 'destinatario_piva_amministratore', ]; } } return null; } private function stabileMatchesParsedRecipientContext(int $stabileId, array $parsed): bool { $stabileColumns = ['id', 'amministratore_id', 'codice_fiscale', 'cod_fisc_amministratore', 'codice_destinatario_sdi', 'pec_condominio']; if (Schema::hasColumn('stabili', 'pec_amministratore')) { $stabileColumns[] = 'pec_amministratore'; } $stabile = Stabile::query() ->with('amministratore:id,partita_iva,codice_fiscale_studio') ->select($stabileColumns) ->find($stabileId); if (! $stabile) { return false; } $destCf = $this->normalizeId(isset($parsed['destinatario_cf']) ? (string) $parsed['destinatario_cf'] : ''); $destPiva = $this->normalizeId(isset($parsed['destinatario_piva']) ? (string) $parsed['destinatario_piva'] : ''); $codice = $this->normalizeId(isset($parsed['codice_destinatario']) ? (string) $parsed['codice_destinatario'] : ''); $pec = strtolower(trim((string) ($parsed['pec_destinatario'] ?? ''))); $candidateIds = array_filter([ $this->normalizeId((string) ($stabile->codice_fiscale ?? '')), $this->normalizeId((string) ($stabile->cod_fisc_amministratore ?? '')), $this->normalizeId((string) ($stabile->amministratore?->codice_fiscale_studio ?? '')), $this->normalizeId((string) ($stabile->amministratore?->partita_iva ?? '')), ]); if ($destCf !== '' && in_array($destCf, $candidateIds, true)) { return true; } if ($destPiva !== '' && in_array($destPiva, $candidateIds, true)) { return true; } if ($codice !== '' && $this->normalizeId((string) ($stabile->codice_destinatario_sdi ?? '')) === $codice) { return true; } if ($pec !== '') { $candidatePec = array_filter([ strtolower(trim((string) ($stabile->pec_condominio ?? ''))), strtolower(trim((string) ($stabile->pec_amministratore ?? ''))), ]); if (in_array($pec, $candidatePec, true)) { return true; } } return false; } private function shouldImportRighe(?int $fornitoreId, array $extra): bool { $mode = (string) ($extra['importa_righe'] ?? 'auto'); // auto|si|no $mode = strtolower(trim($mode)); if ($mode === 'si' || $mode === 'true' || $mode === '1') { return true; } if ($mode === 'no' || $mode === 'false' || $mode === '0') { return false; } if (! $fornitoreId) { return true; } if (! Schema::hasColumn('fornitori', 'escludi_righe_fe')) { return true; } $fornitore = Fornitore::query()->select(['id', 'escludi_righe_fe'])->find($fornitoreId); if (! $fornitore) { return true; } return ! (bool) ($fornitore->escludi_righe_fe ?? false); } private function updateExisting(FatturaElettronica $existing, string $xml, ?string $originalFilename, array $extra): FatturaElettronica { $xml = trim($xml); if ($xml === '') { throw new \RuntimeException('File vuoto'); } $data = $this->parser->parse($xml); $stabileId = $this->resolveStabileIdFromXml($data, (int) ($existing->stabile_id ?? 0), true); $fornitorePiva = $data['fornitore_piva'] ?? null; $fornitoreCf = $data['fornitore_cf'] ?? null; if (! $fornitorePiva && $fornitoreCf) { $fornitorePiva = $fornitoreCf; } if (! $fornitorePiva) { $fornitorePiva = $existing->fornitore_piva ?: 'ND'; } $fornitoreId = $this->matchFornitoreId($fornitorePiva, $fornitoreCf); $importRighe = $this->shouldImportRighe($fornitoreId, $extra); return DB::transaction(function () use ($existing, $data, $xml, $originalFilename, $extra, $stabileId, $fornitoreId, $fornitorePiva, $importRighe) { $existing->forceFill([ 'stabile_id' => $stabileId, 'fornitore_id' => $fornitoreId, 'sdi_file' => isset($data['sdi_file']) ? trim((string) $data['sdi_file']) : ($existing->sdi_file ?? null), 'numero_fattura' => $data['numero_fattura'] ?? $existing->numero_fattura, 'data_fattura' => $data['data_fattura'] ?? $existing->data_fattura, 'data_scadenza' => $data['data_scadenza'] ?? $existing->data_scadenza, 'fornitore_piva' => $fornitorePiva, 'fornitore_cf' => $data['fornitore_cf'] ?? $existing->fornitore_cf, 'fornitore_denominazione' => $data['fornitore_denominazione'] ?? $existing->fornitore_denominazione, 'imponibile' => (float) ($data['imponibile'] ?? $existing->imponibile ?? 0), 'iva' => (float) ($data['iva'] ?? $existing->iva ?? 0), 'totale' => (float) ($data['totale'] ?? $existing->totale ?? 0), 'pagamento_modalita' => $data['pagamento_modalita'] ?? $existing->pagamento_modalita, 'pagamento_iban' => $data['pagamento_iban'] ?? $existing->pagamento_iban, 'pagamento_bic' => $data['pagamento_bic'] ?? $existing->pagamento_bic, 'codice_destinatario' => $data['codice_destinatario'] ?? $existing->codice_destinatario, 'pec_destinatario' => $data['pec_destinatario'] ?? $existing->pec_destinatario, 'destinatario_cf' => $data['destinatario_cf'] ?? $existing->destinatario_cf, 'destinatario_piva' => $data['destinatario_piva'] ?? $existing->destinatario_piva, 'destinatario_denominazione' => $data['destinatario_denominazione'] ?? $existing->destinatario_denominazione, 'xml_content' => $xml, 'nome_file_xml' => $originalFilename ?: $existing->nome_file_xml, 'xml_hash' => hash('sha256', $xml), 'nome_file_p7m' => $extra['nome_file_p7m'] ?? $existing->nome_file_p7m, 'p7m_path' => $extra['p7m_path'] ?? $existing->p7m_path, 'xml_path' => $extra['xml_path'] ?? $existing->xml_path, ]); $existing->save(); if (Schema::hasTable('contabilita_fatture_fornitori')) { $fatturaContabile = FatturaFornitore::query()->where('fattura_elettronica_id', $existing->id)->first(); if (! $fatturaContabile) { $ritenuta = is_array($data['ritenuta'] ?? null) ? $data['ritenuta'] : []; $ritenutaImporto = (float) ($ritenuta['importo'] ?? 0); $ritenutaAliquota = $ritenuta['aliquota'] ?? null; $netto = round(((float) ($data['totale'] ?? 0)) - $ritenutaImporto, 2); $dataDoc = $data['data_fattura'] ?? null; $dataRegistrazione = null; if ($dataDoc instanceof \Carbon\CarbonInterface) { $dataRegistrazione = $dataDoc->copy()->startOfDay(); } elseif (is_string($dataDoc) && trim($dataDoc) !== '') { try { $dataRegistrazione = \Carbon\Carbon::parse($dataDoc)->startOfDay(); } catch (\Throwable) { $dataRegistrazione = null; } } $fatturaContabile = FatturaFornitore::query()->create([ 'stabile_id' => $stabileId, 'fattura_elettronica_id' => $existing->id, 'fornitore_id' => $fornitoreId, 'data_registrazione' => $dataRegistrazione ?: now()->startOfDay(), 'data_documento' => $dataDoc ?: null, 'numero_documento' => $data['numero_fattura'] ?? null, 'descrizione' => $data['fornitore_denominazione'] ?? null, 'valuta' => 'EUR', 'cambio' => 1, 'imponibile' => round((float) ($data['imponibile'] ?? 0), 2), 'iva' => round((float) ($data['iva'] ?? 0), 2), 'totale' => round((float) ($data['totale'] ?? 0), 2), 'data_scadenza' => $data['data_scadenza'] ?? null, 'modalita_pagamento' => $data['pagamento_modalita'] ?? null, 'ritenuta_aliquota' => $ritenutaAliquota, 'ritenuta_importo' => round($ritenutaImporto, 2), 'ritenuta_codice_tributo' => null, 'netto_da_pagare' => $netto, 'stato' => 'inserito', 'note' => null, ]); if ($importRighe) { $defaultCostAccountId = null; if ($fornitoreId && Schema::hasTable('fornitore_stabile_impostazioni')) { $defaultCostAccountId = FornitoreStabileImpostazione::query() ->where('stabile_id', (int) $stabileId) ->where('fornitore_id', (int) $fornitoreId) ->value('conto_costo_default_id'); $defaultCostAccountId = is_numeric($defaultCostAccountId) ? (int) $defaultCostAccountId : null; if ($defaultCostAccountId !== null && $defaultCostAccountId <= 0) { $defaultCostAccountId = null; } } $righe = is_array($data['righe'] ?? null) ? $data['righe'] : []; if (count($righe) < 1) { $righe = [[ 'numero_linea' => 1, 'descrizione' => $data['numero_fattura'] ?? 'Fattura elettronica', 'quantita' => null, 'prezzo_unitario' => null, 'prezzo_totale' => (float) ($data['imponibile'] ?? 0), 'aliquota_iva' => null, 'ritenuta' => null, ]]; } foreach ($righe as $idx => $r) { $imponibileRiga = (float) ($r['prezzo_totale'] ?? 0); if ($imponibileRiga <= 0 && isset($r['prezzo_unitario'], $r['quantita'])) { $imponibileRiga = (float) $r['prezzo_unitario'] * (float) $r['quantita']; } $aliquota = $r['aliquota_iva'] ?? null; $ivaRiga = 0.0; if ($aliquota !== null) { $ivaRiga = round($imponibileRiga * ((float) $aliquota / 100), 2); } $fatturaContabile->righe()->create([ 'riga' => (int) (($r['numero_linea'] ?? null) ?: ($idx + 1)), 'descrizione' => (string) ($r['descrizione'] ?? ''), 'imponibile_euro' => round($imponibileRiga, 2), 'iva_euro' => round($ivaRiga, 2), 'totale_euro' => round($imponibileRiga + $ivaRiga, 2), 'aliquota_iva' => $aliquota, 'conto_costo_id' => $defaultCostAccountId, 'tabella_millesimale_id' => null, ]); } $this->ensureQuadraturaRighe($fatturaContabile, $defaultCostAccountId); } } else { // Repair opzionale: utile per vecchi XML già importati che risultano “non quadrati” // (es. Cassa Previdenziale in DatiGeneraliDocumento). if (! empty($extra['repair_contabilita'])) { $defaultCostAccountId = null; if ($fornitoreId && Schema::hasTable('fornitore_stabile_impostazioni')) { $defaultCostAccountId = FornitoreStabileImpostazione::query() ->where('stabile_id', (int) $stabileId) ->where('fornitore_id', (int) $fornitoreId) ->value('conto_costo_default_id'); $defaultCostAccountId = is_numeric($defaultCostAccountId) ? (int) $defaultCostAccountId : null; if ($defaultCostAccountId !== null && $defaultCostAccountId <= 0) { $defaultCostAccountId = null; } } // Aggiunge righe mancanti se presenti nel parser (es. cassa/bollo), senza toccare righe esistenti. if ($importRighe) { $righeParsed = is_array($data['righe'] ?? null) ? $data['righe'] : []; foreach ($righeParsed as $r) { $desc = Str::limit(trim((string) ($r['descrizione'] ?? '')), 255, ''); $imp = (float) ($r['prezzo_totale'] ?? 0); $aliquota = $r['aliquota_iva'] ?? null; $iva = 0.0; if ($aliquota !== null) { $iva = round($imp * ((float) $aliquota / 100), 2); } $tot = round($imp + $iva, 2); if (trim($desc) === '' || abs($tot) < 0.00001) { continue; } $exists = $fatturaContabile->righe() ->where('descrizione', $desc) ->whereRaw('ABS(totale_euro - ?) <= 0.01', [$tot]) ->exists(); if ($exists) { continue; } $maxRiga = $fatturaContabile->righe()->max('riga'); $nextRiga = (int) (is_numeric($maxRiga) ? $maxRiga : 0) + 1; $fatturaContabile->righe()->create([ 'riga' => $nextRiga, 'descrizione' => $desc, 'imponibile_euro' => round($imp, 2), 'iva_euro' => round($iva, 2), 'totale_euro' => $tot, 'aliquota_iva' => $aliquota, 'conto_costo_id' => $defaultCostAccountId, 'tabella_millesimale_id' => null, ]); } } $this->ensureQuadraturaRighe($fatturaContabile, $defaultCostAccountId); } } } return $existing; }); } private function matchFornitoreId(?string $piva, ?string $cf): ?int { return $this->matchFornitoreIdForAmministratore(0, $piva, $cf); } private function matchFornitoreIdForAmministratore(int $amministratoreId, ?string $piva, ?string $cf): ?int { $query = Fornitore::query(); if ($amministratoreId > 0) { $query->where('amministratore_id', $amministratoreId); } $pivaNorm = $this->normalizeId($piva); $cfNorm = $this->normalizeId($cf); // Priorità assoluta: P.IVA (dato certo AdE/FE) if ($pivaNorm !== '' && $pivaNorm !== 'ND') { $match = (clone $query) ->whereRaw("REPLACE(UPPER(partita_iva), ' ', '') = ?", [$pivaNorm]) ->first(); if ($match) { return (int) $match->id; } // Fallback: spesso in DB è salvata senza prefisso IT. if (str_starts_with($pivaNorm, 'IT') && strlen($pivaNorm) > 2) { $pivaNoIt = substr($pivaNorm, 2); $match = (clone $query) ->whereRaw("REPLACE(UPPER(partita_iva), ' ', '') = ?", [$pivaNoIt]) ->first(); if ($match) { return (int) $match->id; } } } if ($cfNorm !== '' && $cfNorm !== 'ND') { $match = (clone $query) ->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$cfNorm]) ->first(); if ($match) { return (int) $match->id; } } return null; } private function normalizeId(?string $value): string { $value = strtoupper(trim((string) $value)); $value = str_replace(' ', '', $value); return $value; } /** * Aggiorna i dati del fornitore (e della Rubrica collegata) con informazioni affidabili da FE. * Non sovrascrive valori già presenti, salvo riallineamenti innocui. */ private function syncFornitoreFromFeData(int $fornitoreId, array $data, array $extra = []): void { $fornitore = Fornitore::query()->with('rubrica')->find($fornitoreId); if (! $fornitore) { return; } $userId = (int) ($extra['user_id'] ?? 0); $now = now(); $piva = isset($data['fornitore_piva']) && is_string($data['fornitore_piva']) ? trim($data['fornitore_piva']) : null; $cf = isset($data['fornitore_cf']) && is_string($data['fornitore_cf']) ? trim($data['fornitore_cf']) : null; $den = isset($data['fornitore_denominazione']) && is_string($data['fornitore_denominazione']) ? trim($data['fornitore_denominazione']) : ''; if ($piva === '' || $piva === 'ND') { $piva = null; } if ($cf === '' || $cf === 'ND') { $cf = null; } $fornitoreNome = $this->normalizeNullableString($data['fornitore_nome'] ?? null); $fornitoreCognome = $this->normalizeNullableString($data['fornitore_cognome'] ?? null); $fornitoreIndirizzo = $this->normalizeNullableString($data['fornitore_indirizzo'] ?? null); $fornitoreCivico = $this->normalizeNullableString($data['fornitore_civico'] ?? null); $fornitoreCap = $this->normalizeNullableString($data['fornitore_cap'] ?? null); $fornitoreCitta = $this->normalizeNullableString($data['fornitore_citta'] ?? null); $fornitoreProvincia = $this->normalizeNullableString($data['fornitore_provincia'] ?? null); $fornitoreNazione = $this->normalizeNullableString($data['fornitore_nazione'] ?? null); $fornitoreTelefono = $this->normalizeNullableString($data['fornitore_telefono'] ?? null); $fornitoreFax = $this->normalizeNullableString($data['fornitore_fax'] ?? null); $fornitoreEmailRaw = $this->normalizeNullableString($data['fornitore_email'] ?? null); $fornitorePec = $this->looksLikePec($fornitoreEmailRaw) ? $fornitoreEmailRaw : null; $fornitoreEmail = $fornitorePec ? null : $fornitoreEmailRaw; $dirty = false; if (empty($fornitore->partita_iva) && $piva) { $fornitore->partita_iva = $piva; $dirty = true; } if (empty($fornitore->codice_fiscale) && $cf) { $fornitore->codice_fiscale = $cf; $dirty = true; } if ((empty($fornitore->ragione_sociale) || $fornitore->ragione_sociale === 'ND') && $den !== '') { $fornitore->ragione_sociale = $den; $dirty = true; } if (Schema::hasColumn('fornitori', 'nome') && empty($fornitore->nome) && $fornitoreNome) { $fornitore->nome = $fornitoreNome; $dirty = true; } if (Schema::hasColumn('fornitori', 'cognome') && empty($fornitore->cognome) && $fornitoreCognome) { $fornitore->cognome = $fornitoreCognome; $dirty = true; } if (Schema::hasColumn('fornitori', 'indirizzo') && empty($fornitore->indirizzo) && $fornitoreIndirizzo) { $fornitore->indirizzo = $fornitoreIndirizzo; $dirty = true; } if (Schema::hasColumn('fornitori', 'civico') && empty($fornitore->civico) && $fornitoreCivico) { $fornitore->civico = $fornitoreCivico; $dirty = true; } if (Schema::hasColumn('fornitori', 'cap') && empty($fornitore->cap) && $fornitoreCap) { $fornitore->cap = $fornitoreCap; $dirty = true; } if (Schema::hasColumn('fornitori', 'citta') && empty($fornitore->citta) && $fornitoreCitta) { $fornitore->citta = $fornitoreCitta; $dirty = true; } if (Schema::hasColumn('fornitori', 'provincia') && empty($fornitore->provincia) && $fornitoreProvincia) { $fornitore->provincia = strtoupper($fornitoreProvincia); $dirty = true; } if (Schema::hasColumn('fornitori', 'nazione') && empty($fornitore->nazione) && $fornitoreNazione) { $fornitore->nazione = strtoupper($fornitoreNazione); $dirty = true; } if (Schema::hasColumn('fornitori', 'telefono') && empty($fornitore->telefono) && $fornitoreTelefono) { $fornitore->telefono = $fornitoreTelefono; $dirty = true; } if (Schema::hasColumn('fornitori', 'email') && empty($fornitore->email) && $fornitoreEmail) { $fornitore->email = $fornitoreEmail; $dirty = true; } if (Schema::hasColumn('fornitori', 'pec') && empty($fornitore->pec) && $fornitorePec) { $fornitore->pec = $fornitorePec; $dirty = true; } // Pagamenti: già gestiti anche nel postProcess, qui lo facciamo comunque (best-effort). if (Schema::hasColumn('fornitori', 'iban') && empty($fornitore->iban) && isset($data['pagamento_iban']) && is_string($data['pagamento_iban']) && trim($data['pagamento_iban']) !== '') { $fornitore->iban = trim($data['pagamento_iban']); $dirty = true; } if (Schema::hasColumn('fornitori', 'bic') && empty($fornitore->bic) && isset($data['pagamento_bic']) && is_string($data['pagamento_bic']) && trim($data['pagamento_bic']) !== '') { $fornitore->bic = trim($data['pagamento_bic']); $dirty = true; } if (Schema::hasColumn('fornitori', 'modalita_pagamento_predefinita') && empty($fornitore->modalita_pagamento_predefinita) && isset($data['pagamento_modalita']) && is_string($data['pagamento_modalita']) && trim($data['pagamento_modalita']) !== '') { $fornitore->modalita_pagamento_predefinita = trim($data['pagamento_modalita']); $dirty = true; } if ($dirty) { $fornitore->save(); } // Allinea anche Rubrica se presente. $rubrica = $fornitore->rubrica; if ($rubrica) { $rDirty = false; if (empty($rubrica->partita_iva) && $piva) { $rubrica->partita_iva = $piva; $rDirty = true; } if (empty($rubrica->codice_fiscale) && $cf) { $rubrica->codice_fiscale = $cf; $rDirty = true; } if ((empty($rubrica->ragione_sociale) || $rubrica->ragione_sociale === 'ND') && $den !== '') { $rubrica->ragione_sociale = $den; $rubrica->tipo_contatto = $rubrica->tipo_contatto ?: 'persona_giuridica'; $rubrica->categoria = $rubrica->categoria ?: 'fornitore'; $rubrica->stato = $rubrica->stato ?: 'attivo'; $rDirty = true; } if (empty($rubrica->indirizzo) && $fornitoreIndirizzo) { $rubrica->indirizzo = $fornitoreIndirizzo; $rDirty = true; } if (empty($rubrica->civico) && $fornitoreCivico) { $rubrica->civico = $fornitoreCivico; $rDirty = true; } if (empty($rubrica->cap) && $fornitoreCap) { $rubrica->cap = $fornitoreCap; $rDirty = true; } if (empty($rubrica->citta) && $fornitoreCitta) { $rubrica->citta = $fornitoreCitta; $rDirty = true; } if (empty($rubrica->provincia) && $fornitoreProvincia) { $rubrica->provincia = strtoupper($fornitoreProvincia); $rDirty = true; } if (empty($rubrica->nazione) && $fornitoreNazione) { $rubrica->nazione = strtoupper($fornitoreNazione); $rDirty = true; } if (empty($rubrica->telefono_ufficio) && $fornitoreTelefono) { $rubrica->telefono_ufficio = $fornitoreTelefono; $rDirty = true; } if (empty($rubrica->fax) && $fornitoreFax) { $rubrica->fax = $fornitoreFax; $rDirty = true; } if (empty($rubrica->email) && $fornitoreEmail) { $rubrica->email = $fornitoreEmail; $rDirty = true; } if (empty($rubrica->pec) && $fornitorePec) { $rubrica->pec = $fornitorePec; $rDirty = true; } if ($rDirty) { $rubrica->data_ultima_modifica = $now; if ($userId > 0) { $rubrica->modificato_da = $userId; } $rubrica->save(); } } } private function ensureFornitoreFromFeData(int $amministratoreId, array $data, array $extra = []): ?int { $piva = isset($data['fornitore_piva']) && is_string($data['fornitore_piva']) ? trim($data['fornitore_piva']) : null; $cf = isset($data['fornitore_cf']) && is_string($data['fornitore_cf']) ? trim($data['fornitore_cf']) : null; $den = isset($data['fornitore_denominazione']) && is_string($data['fornitore_denominazione']) ? trim($data['fornitore_denominazione']) : ''; if ($piva === '' || $piva === 'ND') { $piva = null; } if ($cf === '') { $cf = null; } if ($den === '') { $den = $piva ?: ($cf ?: 'Fornitore'); } $existing = $this->matchFornitoreIdForAmministratore($amministratoreId, $piva, $cf); if ($existing) { return $existing; } $userId = (int) ($extra['user_id'] ?? 0); $now = now(); $fornitoreNome = $this->normalizeNullableString($data['fornitore_nome'] ?? null); $fornitoreCognome = $this->normalizeNullableString($data['fornitore_cognome'] ?? null); $fornitoreIndirizzo = $this->normalizeNullableString($data['fornitore_indirizzo'] ?? null); $fornitoreCivico = $this->normalizeNullableString($data['fornitore_civico'] ?? null); $fornitoreCap = $this->normalizeNullableString($data['fornitore_cap'] ?? null); $fornitoreCitta = $this->normalizeNullableString($data['fornitore_citta'] ?? null); $fornitoreProvincia = $this->normalizeNullableString($data['fornitore_provincia'] ?? null); $fornitoreNazione = $this->normalizeNullableString($data['fornitore_nazione'] ?? null); $fornitoreTelefono = $this->normalizeNullableString($data['fornitore_telefono'] ?? null); $fornitoreFax = $this->normalizeNullableString($data['fornitore_fax'] ?? null); $fornitoreEmailRaw = $this->normalizeNullableString($data['fornitore_email'] ?? null); $fornitorePec = $this->looksLikePec($fornitoreEmailRaw) ? $fornitoreEmailRaw : null; $fornitoreEmail = $fornitorePec ? null : $fornitoreEmailRaw; $rubrica = null; if ($piva) { $rubrica = RubricaUniversale::query()->where('partita_iva', $piva)->first(); } if (! $rubrica && $cf) { $rubrica = RubricaUniversale::query()->where('codice_fiscale', $cf)->first(); } if (! $rubrica) { $rubrica = RubricaUniversale::query() ->where('tipo_contatto', 'persona_giuridica') ->where('categoria', 'fornitore') ->whereRaw('LOWER(ragione_sociale) = ?', [mb_strtolower($den)]) ->first(); } if (! $rubrica) { $rubrica = RubricaUniversale::query()->create([ 'ragione_sociale' => $den, 'tipo_contatto' => 'persona_giuridica', 'categoria' => 'fornitore', 'stato' => 'attivo', 'partita_iva' => $piva, 'codice_fiscale' => $cf, 'indirizzo' => $fornitoreIndirizzo, 'civico' => $fornitoreCivico, 'cap' => $fornitoreCap, 'citta' => $fornitoreCitta, 'provincia' => $fornitoreProvincia ? strtoupper($fornitoreProvincia) : null, 'nazione' => $fornitoreNazione ? strtoupper($fornitoreNazione) : null, 'telefono_ufficio' => $fornitoreTelefono, 'fax' => $fornitoreFax, 'email' => $fornitoreEmail, 'pec' => $fornitorePec, 'data_inserimento' => $now, 'data_ultima_modifica' => $now, 'creato_da' => $userId > 0 ? $userId : null, 'modificato_da' => $userId > 0 ? $userId : null, ]); } else { $dirty = false; if (empty($rubrica->ragione_sociale) && $den !== '') { $rubrica->ragione_sociale = $den; $dirty = true; } if (empty($rubrica->partita_iva) && $piva) { $rubrica->partita_iva = $piva; $dirty = true; } if (empty($rubrica->codice_fiscale) && $cf) { $rubrica->codice_fiscale = $cf; $dirty = true; } if (empty($rubrica->indirizzo) && $fornitoreIndirizzo) { $rubrica->indirizzo = $fornitoreIndirizzo; $dirty = true; } if (empty($rubrica->civico) && $fornitoreCivico) { $rubrica->civico = $fornitoreCivico; $dirty = true; } if (empty($rubrica->cap) && $fornitoreCap) { $rubrica->cap = $fornitoreCap; $dirty = true; } if (empty($rubrica->citta) && $fornitoreCitta) { $rubrica->citta = $fornitoreCitta; $dirty = true; } if (empty($rubrica->provincia) && $fornitoreProvincia) { $rubrica->provincia = strtoupper($fornitoreProvincia); $dirty = true; } if (empty($rubrica->nazione) && $fornitoreNazione) { $rubrica->nazione = strtoupper($fornitoreNazione); $dirty = true; } if (empty($rubrica->telefono_ufficio) && $fornitoreTelefono) { $rubrica->telefono_ufficio = $fornitoreTelefono; $dirty = true; } if (empty($rubrica->fax) && $fornitoreFax) { $rubrica->fax = $fornitoreFax; $dirty = true; } if (empty($rubrica->email) && $fornitoreEmail) { $rubrica->email = $fornitoreEmail; $dirty = true; } if (empty($rubrica->pec) && $fornitorePec) { $rubrica->pec = $fornitorePec; $dirty = true; } if ($dirty) { $rubrica->categoria = $rubrica->categoria ?: 'fornitore'; $rubrica->tipo_contatto = $rubrica->tipo_contatto ?: 'persona_giuridica'; $rubrica->stato = $rubrica->stato ?: 'attivo'; $rubrica->data_ultima_modifica = $now; if ($userId > 0) { $rubrica->modificato_da = $userId; } $rubrica->save(); } } $fornitore = Fornitore::query()->create([ 'amministratore_id' => $amministratoreId, 'rubrica_id' => (int) $rubrica->id, // Un solo codice: quello della Rubrica. 'codice_univoco' => (string) ($rubrica->codice_univoco ?? ''), 'ragione_sociale' => $den, 'nome' => $fornitoreNome, 'cognome' => $fornitoreCognome, 'partita_iva' => $piva, 'codice_fiscale' => $cf, 'indirizzo' => $fornitoreIndirizzo, 'civico' => $fornitoreCivico, 'cap' => $fornitoreCap, 'citta' => $fornitoreCitta, 'provincia' => $fornitoreProvincia ? strtoupper($fornitoreProvincia) : null, 'nazione' => $fornitoreNazione ? strtoupper($fornitoreNazione) : null, 'telefono' => $fornitoreTelefono, 'email' => $fornitoreEmail, 'pec' => $fornitorePec, 'iban' => isset($data['pagamento_iban']) && is_string($data['pagamento_iban']) ? $data['pagamento_iban'] : null, 'bic' => isset($data['pagamento_bic']) && is_string($data['pagamento_bic']) ? $data['pagamento_bic'] : null, 'modalita_pagamento_predefinita' => isset($data['pagamento_modalita']) && is_string($data['pagamento_modalita']) ? $data['pagamento_modalita'] : null, ]); return (int) $fornitore->id; } private function normalizeNullableString(mixed $value): ?string { if (! is_string($value)) { return null; } $normalized = trim($value); if ($normalized === '' || strtoupper($normalized) === 'ND') { return null; } return $normalized; } private function looksLikePec(?string $email): bool { if (! is_string($email) || trim($email) === '') { return false; } if (! filter_var($email, FILTER_VALIDATE_EMAIL)) { return false; } return preg_match('/\b(pec|legalmail|postecert|cert|mailpec)\b/i', $email) === 1; } }