522 lines
26 KiB
PHP
522 lines
26 KiB
PHP
<?php
|
|
namespace App\Services\FattureElettroniche;
|
|
|
|
use Carbon\Carbon;
|
|
use DOMDocument;
|
|
use DOMXPath;
|
|
|
|
class FatturaElettronicaXmlParser
|
|
{
|
|
/**
|
|
* @return array{
|
|
* tipo_documento: string|null,
|
|
* sdi_file: string|null,
|
|
* numero_fattura: string|null,
|
|
* data_fattura: Carbon|null,
|
|
* data_scadenza: Carbon|null,
|
|
* fornitore_piva: string|null,
|
|
* fornitore_cf: string|null,
|
|
* fornitore_denominazione: string|null,
|
|
* fornitore_nome: string|null,
|
|
* fornitore_cognome: string|null,
|
|
* fornitore_indirizzo: string|null,
|
|
* fornitore_civico: string|null,
|
|
* fornitore_cap: string|null,
|
|
* fornitore_citta: string|null,
|
|
* fornitore_provincia: string|null,
|
|
* fornitore_nazione: string|null,
|
|
* fornitore_telefono: string|null,
|
|
* fornitore_email: string|null,
|
|
* fornitore_fax: string|null,
|
|
* fornitore_regime_fiscale: string|null,
|
|
* imponibile: float|null,
|
|
* iva: float|null,
|
|
* totale: float|null,
|
|
* codice_destinatario: string|null,
|
|
* pec_destinatario: string|null,
|
|
* destinatario_cf: string|null,
|
|
* destinatario_piva: string|null,
|
|
* destinatario_denominazione: string|null,
|
|
* pagamento_modalita: string|null,
|
|
* pagamento_iban: string|null,
|
|
* pagamento_bic: string|null,
|
|
* consumo_valore: float|null,
|
|
* consumo_unita: string|null,
|
|
* consumo_riferimento: string|null,
|
|
* consumo_raw: string|null,
|
|
* trasmissione: array{
|
|
* id_paese: string|null,
|
|
* id_codice: string|null,
|
|
* progressivo_invio: string|null,
|
|
* formato_trasmissione: string|null,
|
|
* },
|
|
* ritenuta: array{
|
|
* tipo: string|null,
|
|
* importo: float|null,
|
|
* aliquota: float|null,
|
|
* causale: string|null,
|
|
* },
|
|
* righe: array<int, array{
|
|
* numero_linea: int|null,
|
|
* descrizione: string|null,
|
|
* quantita: float|null,
|
|
* prezzo_unitario: float|null,
|
|
* prezzo_totale: float|null,
|
|
* aliquota_iva: float|null,
|
|
* ritenuta: bool|null,
|
|
* }>,
|
|
* }
|
|
*/
|
|
public function parse(string $xml): array
|
|
{
|
|
$xml = $this->sanitizeXml($xml);
|
|
|
|
$dom = new DOMDocument();
|
|
$dom->preserveWhiteSpace = false;
|
|
$dom->formatOutput = false;
|
|
|
|
// Se l'XML è sporco, proviamo comunque a caricarlo
|
|
$loaded = @$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
|
|
if (! $loaded) {
|
|
throw new \RuntimeException('XML non valido');
|
|
}
|
|
|
|
$xpath = new DOMXPath($dom);
|
|
|
|
// IdentificativoSdI non è sempre presente negli XML (dipende dal flusso/origine).
|
|
// Se presente, lo salviamo per matching deterministico con la “Lista AdE”.
|
|
$sdiFile = $this->firstString($xpath, "//*[local-name()='IdentificativoSdI']");
|
|
|
|
$tipoDocumento = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']/*[local-name()='TipoDocumento']");
|
|
$numero = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']/*[local-name()='Numero']");
|
|
$data = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']/*[local-name()='Data']");
|
|
$scadenza = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiPagamento']//*[local-name()='DettaglioPagamento']/*[local-name()='DataScadenzaPagamento']");
|
|
|
|
$modalitaPagamento = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiPagamento']//*[local-name()='DettaglioPagamento']/*[local-name()='ModalitaPagamento']");
|
|
$iban = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiPagamento']//*[local-name()='DettaglioPagamento']/*[local-name()='IBAN']");
|
|
$bic = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiPagamento']//*[local-name()='DettaglioPagamento']/*[local-name()='BIC']");
|
|
|
|
$totaleDocumento = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']/*[local-name()='ImportoTotaleDocumento']");
|
|
|
|
$fornitorePiva = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='DatiAnagrafici']//*[local-name()='IdFiscaleIVA']/*[local-name()='IdCodice']");
|
|
$fornitoreCf = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='DatiAnagrafici']/*[local-name()='CodiceFiscale']");
|
|
|
|
$denominazione = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='DatiAnagrafici']//*[local-name()='Anagrafica']/*[local-name()='Denominazione']");
|
|
$nome = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='DatiAnagrafici']//*[local-name()='Anagrafica']/*[local-name()='Nome']");
|
|
$cognome = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='DatiAnagrafici']//*[local-name()='Anagrafica']/*[local-name()='Cognome']");
|
|
$regimeFiscale = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='DatiAnagrafici']/*[local-name()='RegimeFiscale']");
|
|
$fornitoreIndirizzo = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Sede']/*[local-name()='Indirizzo']");
|
|
$fornitoreCivico = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Sede']/*[local-name()='NumeroCivico']");
|
|
$fornitoreCap = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Sede']/*[local-name()='CAP']");
|
|
$fornitoreCitta = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Sede']/*[local-name()='Comune']");
|
|
$fornitoreProvincia = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Sede']/*[local-name()='Provincia']");
|
|
$fornitoreNazione = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Sede']/*[local-name()='Nazione']");
|
|
$fornitoreTelefono = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Contatti']/*[local-name()='Telefono']");
|
|
$fornitoreFax = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Contatti']/*[local-name()='Fax']");
|
|
$fornitoreEmail = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CedentePrestatore']//*[local-name()='Contatti']/*[local-name()='Email']");
|
|
if (! $denominazione) {
|
|
$denominazione = trim(implode(' ', array_filter([$nome, $cognome])));
|
|
}
|
|
|
|
$codiceDest = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='DatiTrasmissione']/*[local-name()='CodiceDestinatario']");
|
|
$pecDest = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='DatiTrasmissione']/*[local-name()='PECDestinatario']");
|
|
|
|
$destinatarioCf = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CessionarioCommittente']//*[local-name()='DatiAnagrafici']/*[local-name()='CodiceFiscale']");
|
|
$destinatarioPiva = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CessionarioCommittente']//*[local-name()='DatiAnagrafici']//*[local-name()='IdFiscaleIVA']/*[local-name()='IdCodice']");
|
|
|
|
$destDenominazione = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CessionarioCommittente']//*[local-name()='DatiAnagrafici']//*[local-name()='Anagrafica']/*[local-name()='Denominazione']");
|
|
$destNome = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CessionarioCommittente']//*[local-name()='DatiAnagrafici']//*[local-name()='Anagrafica']/*[local-name()='Nome']");
|
|
$destCognome = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='CessionarioCommittente']//*[local-name()='DatiAnagrafici']//*[local-name()='Anagrafica']/*[local-name()='Cognome']");
|
|
if (! $destDenominazione) {
|
|
$destDenominazione = trim(implode(' ', array_filter([$destNome, $destCognome])));
|
|
}
|
|
|
|
[$imponibile, $iva] = $this->sumRiepilogo($xpath);
|
|
|
|
$consumi = $this->extractConsumi($xpath);
|
|
|
|
$ritenuta = $this->extractRitenuta($xpath);
|
|
$righe = $this->extractRighe($xpath);
|
|
|
|
// Alcuni XML (soprattutto vecchi) esprimono componenti economiche fuori da DettaglioLinee,
|
|
// ad esempio Cassa Previdenziale e Bollo. Se non li trasformiamo in righe, la quadratura
|
|
// tra totale documento e somma righe risulta errata (tipico: delta = ImportoContributoCassa).
|
|
$righe = $this->appendExtraRigheFromGenerali($xpath, $righe);
|
|
|
|
$trasmissione = [
|
|
'id_paese' => $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='DatiTrasmissione']//*[local-name()='IdTrasmittente']/*[local-name()='IdPaese']"),
|
|
'id_codice' => $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='DatiTrasmissione']//*[local-name()='IdTrasmittente']/*[local-name()='IdCodice']"),
|
|
'progressivo_invio' => $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='DatiTrasmissione']/*[local-name()='ProgressivoInvio']"),
|
|
'formato_trasmissione' => $this->firstString($xpath, "//*[local-name()='FatturaElettronicaHeader']//*[local-name()='DatiTrasmissione']/*[local-name()='FormatoTrasmissione']"),
|
|
];
|
|
|
|
$totale = $this->toFloat($totaleDocumento);
|
|
if ($totale === null) {
|
|
$totale = null;
|
|
if ($imponibile !== null || $iva !== null) {
|
|
$totale = round((float) ($imponibile ?? 0) + (float) ($iva ?? 0), 2);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'tipo_documento' => $tipoDocumento ?: null,
|
|
'sdi_file' => $sdiFile ?: null,
|
|
'numero_fattura' => $numero ?: null,
|
|
'data_fattura' => $this->toDate($data),
|
|
'data_scadenza' => $this->toDate($scadenza),
|
|
'fornitore_piva' => $this->normalizeId($fornitorePiva),
|
|
'fornitore_cf' => $this->normalizeId($fornitoreCf),
|
|
'fornitore_denominazione' => $denominazione ?: null,
|
|
'fornitore_nome' => $nome ?: null,
|
|
'fornitore_cognome' => $cognome ?: null,
|
|
'fornitore_indirizzo' => $fornitoreIndirizzo ?: null,
|
|
'fornitore_civico' => $fornitoreCivico ?: null,
|
|
'fornitore_cap' => $fornitoreCap ?: null,
|
|
'fornitore_citta' => $fornitoreCitta ?: null,
|
|
'fornitore_provincia' => $fornitoreProvincia ?: null,
|
|
'fornitore_nazione' => $fornitoreNazione ?: null,
|
|
'fornitore_telefono' => $fornitoreTelefono ?: null,
|
|
'fornitore_email' => $fornitoreEmail ?: null,
|
|
'fornitore_fax' => $fornitoreFax ?: null,
|
|
'fornitore_regime_fiscale' => $regimeFiscale ?: null,
|
|
'imponibile' => $imponibile,
|
|
'iva' => $iva,
|
|
'totale' => $totale,
|
|
'codice_destinatario' => $codiceDest ?: null,
|
|
'pec_destinatario' => $pecDest ?: null,
|
|
'destinatario_cf' => $this->normalizeId($destinatarioCf),
|
|
'destinatario_piva' => $this->normalizeId($destinatarioPiva),
|
|
'destinatario_denominazione' => $destDenominazione ?: null,
|
|
|
|
'pagamento_modalita' => $modalitaPagamento ?: null,
|
|
'pagamento_iban' => $this->normalizeIban($iban),
|
|
'pagamento_bic' => $this->normalizeId($bic),
|
|
|
|
'consumo_valore' => $consumi['valore'],
|
|
'consumo_unita' => $consumi['unita'],
|
|
'consumo_riferimento' => $consumi['riferimento'],
|
|
'consumo_raw' => $consumi['raw'],
|
|
|
|
'trasmissione' => $trasmissione,
|
|
'ritenuta' => $ritenuta,
|
|
'righe' => $righe,
|
|
];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{numero_linea: int|null, descrizione: string|null, quantita: float|null, prezzo_unitario: float|null, prezzo_totale: float|null, aliquota_iva: float|null, ritenuta: bool|null}> $righe
|
|
* @return array<int, array{numero_linea: int|null, descrizione: string|null, quantita: float|null, prezzo_unitario: float|null, prezzo_totale: float|null, aliquota_iva: float|null, ritenuta: bool|null}>
|
|
*/
|
|
private function appendExtraRigheFromGenerali(DOMXPath $xpath, array $righe): array
|
|
{
|
|
$nextNumero = 1;
|
|
foreach ($righe as $r) {
|
|
if (isset($r['numero_linea']) && is_int($r['numero_linea']) && $r['numero_linea'] >= $nextNumero) {
|
|
$nextNumero = $r['numero_linea'] + 1;
|
|
}
|
|
}
|
|
|
|
// DatiCassaPrevidenziale può comparire 1..N volte.
|
|
$nodes = $xpath->query("//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']//*[local-name()='DatiCassaPrevidenziale']");
|
|
if ($nodes && $nodes->length > 0) {
|
|
foreach ($nodes as $node) {
|
|
$tipoCassa = $this->firstStringNode($xpath, "./*[local-name()='TipoCassa']", $node);
|
|
$aliquotaIva = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='AliquotaIVA']", $node));
|
|
$importo = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='ImportoContributoCassa']", $node));
|
|
if ($importo === null || abs($importo) < 0.00001) {
|
|
continue;
|
|
}
|
|
|
|
$label = 'Cassa previdenziale';
|
|
if (is_string($tipoCassa) && trim($tipoCassa) !== '') {
|
|
$label .= ' ' . trim($tipoCassa);
|
|
}
|
|
|
|
$righe[] = [
|
|
'numero_linea' => $nextNumero++,
|
|
'descrizione' => $label,
|
|
'quantita' => null,
|
|
'prezzo_unitario' => null,
|
|
'prezzo_totale' => $importo,
|
|
'aliquota_iva' => $aliquotaIva,
|
|
'ritenuta' => null,
|
|
];
|
|
}
|
|
}
|
|
|
|
// DatiBollo: in alcuni tracciati può essere conteggiato nel totale documento.
|
|
$bollo = $this->toFloat($this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']//*[local-name()='DatiBollo']/*[local-name()='ImportoBollo']"));
|
|
if ($bollo !== null && abs($bollo) >= 0.00001) {
|
|
$righe[] = [
|
|
'numero_linea' => $nextNumero++,
|
|
'descrizione' => 'Bollo',
|
|
'quantita' => null,
|
|
'prezzo_unitario' => null,
|
|
'prezzo_totale' => $bollo,
|
|
'aliquota_iva' => 0.0,
|
|
'ritenuta' => null,
|
|
];
|
|
}
|
|
|
|
return $righe;
|
|
}
|
|
|
|
/**
|
|
* @return array{tipo: string|null, importo: float|null, aliquota: float|null, causale: string|null}
|
|
*/
|
|
private function extractRitenuta(DOMXPath $xpath): array
|
|
{
|
|
$tipo = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']//*[local-name()='DatiRitenuta']/*[local-name()='TipoRitenuta']");
|
|
$importo = $this->toFloat($this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']//*[local-name()='DatiRitenuta']/*[local-name()='ImportoRitenuta']"));
|
|
$aliquota = $this->toFloat($this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']//*[local-name()='DatiRitenuta']/*[local-name()='AliquotaRitenuta']"));
|
|
$causale = $this->firstString($xpath, "//*[local-name()='FatturaElettronicaBody']//*[local-name()='DatiGeneraliDocumento']//*[local-name()='DatiRitenuta']/*[local-name()='CausalePagamento']");
|
|
|
|
return [
|
|
'tipo' => $tipo ?: null,
|
|
'importo' => $importo,
|
|
'aliquota' => $aliquota,
|
|
'causale' => $causale ?: null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{numero_linea: int|null, descrizione: string|null, quantita: float|null, prezzo_unitario: float|null, prezzo_totale: float|null, aliquota_iva: float|null, ritenuta: bool|null}>
|
|
*/
|
|
private function extractRighe(DOMXPath $xpath): array
|
|
{
|
|
$out = [];
|
|
$nodes = $xpath->query("//*[local-name()='DatiBeniServizi']//*[local-name()='DettaglioLinee']");
|
|
if (! $nodes || $nodes->length < 1) {
|
|
return $out;
|
|
}
|
|
|
|
foreach ($nodes as $node) {
|
|
$numeroLinea = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='NumeroLinea']", $node));
|
|
$descrizione = $this->firstStringNode($xpath, "./*[local-name()='Descrizione']", $node);
|
|
$quantita = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='Quantita']", $node));
|
|
$prezzoUnitario = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='PrezzoUnitario']", $node));
|
|
$prezzoTotale = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='PrezzoTotale']", $node));
|
|
$aliquotaIva = $this->toFloat($this->firstStringNode($xpath, "./*[local-name()='AliquotaIVA']", $node));
|
|
$ritenuta = $this->firstStringNode($xpath, "./*[local-name()='Ritenuta']", $node);
|
|
|
|
$ritenutaBool = null;
|
|
if (is_string($ritenuta) && $ritenuta !== '') {
|
|
$ritenutaBool = strtoupper(trim($ritenuta)) === 'SI';
|
|
}
|
|
|
|
$out[] = [
|
|
'numero_linea' => $numeroLinea !== null ? (int) $numeroLinea : null,
|
|
'descrizione' => $descrizione ?: null,
|
|
'quantita' => $quantita,
|
|
'prezzo_unitario' => $prezzoUnitario,
|
|
'prezzo_totale' => $prezzoTotale,
|
|
'aliquota_iva' => $aliquotaIva,
|
|
'ritenuta' => $ritenutaBool,
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function firstStringNode(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;
|
|
}
|
|
|
|
/**
|
|
* @return array{valore: float|null, unita: string|null, riferimento: string|null, raw: string|null}
|
|
*/
|
|
private function extractConsumi(DOMXPath $xpath): array
|
|
{
|
|
$texts = [];
|
|
|
|
$nodes = $xpath->query("//*[local-name()='DatiBeniServizi']//*[local-name()='DettaglioLinee']/*[local-name()='Descrizione']");
|
|
if ($nodes) {
|
|
foreach ($nodes as $n) {
|
|
$v = trim((string) $n->textContent);
|
|
if ($v !== '') {
|
|
$texts[] = $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
// AltriDatiGestionali spesso contiene codici (utenza/POD/PDR/letture) in forma testuale.
|
|
$nodes = $xpath->query("//*[local-name()='DatiBeniServizi']//*[local-name()='DettaglioLinee']//*[local-name()='AltriDatiGestionali']//*[local-name()='RiferimentoTesto']");
|
|
if ($nodes) {
|
|
foreach ($nodes as $n) {
|
|
$v = trim((string) $n->textContent);
|
|
if ($v !== '') {
|
|
$texts[] = $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
$raw = trim(implode("\n", array_values(array_unique($texts))));
|
|
if ($raw === '') {
|
|
return ['valore' => null, 'unita' => null, 'riferimento' => null, 'raw' => null];
|
|
}
|
|
|
|
$riferimento = null;
|
|
if (preg_match('/\butenza\s*([0-9]{3,})\b/i', $raw, $m)) {
|
|
$riferimento = 'utenza ' . $m[1];
|
|
} elseif (preg_match('/\bPOD\s*([A-Z0-9]{10,})\b/i', $raw, $m)) {
|
|
$riferimento = 'POD ' . strtoupper($m[1]);
|
|
} elseif (preg_match('/\bPDR\s*([0-9]{8,})\b/i', $raw, $m)) {
|
|
$riferimento = 'PDR ' . $m[1];
|
|
}
|
|
|
|
// Priorità: MC (acqua), kWh (energia), Smc (gas)
|
|
$valore = null;
|
|
$unita = null;
|
|
|
|
if (preg_match('/\bMC\s*([0-9]+(?:[\.,][0-9]+)?)\b/i', $raw, $m)) {
|
|
$valore = $this->toFloat($m[1]);
|
|
$unita = 'MC';
|
|
} elseif (preg_match('/\bkWh\s*([0-9]+(?:[\.,][0-9]+)?)\b/i', $raw, $m)) {
|
|
$valore = $this->toFloat($m[1]);
|
|
$unita = 'kWh';
|
|
} elseif (preg_match('/\bSmc\s*([0-9]+(?:[\.,][0-9]+)?)\b/i', $raw, $m)) {
|
|
$valore = $this->toFloat($m[1]);
|
|
$unita = 'Smc';
|
|
}
|
|
|
|
return [
|
|
'valore' => $valore,
|
|
'unita' => $unita,
|
|
'riferimento' => $riferimento,
|
|
'raw' => $raw,
|
|
];
|
|
}
|
|
|
|
private function firstString(DOMXPath $xpath, string $expr): ?string
|
|
{
|
|
$nodes = $xpath->query($expr);
|
|
if (! $nodes || $nodes->length < 1) {
|
|
return null;
|
|
}
|
|
|
|
$value = trim((string) $nodes->item(0)?->textContent);
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: float|null, 1: float|null}
|
|
*/
|
|
private function sumRiepilogo(DOMXPath $xpath): array
|
|
{
|
|
$nodes = $xpath->query("//*[local-name()='DatiBeniServizi']//*[local-name()='DatiRiepilogo']");
|
|
if (! $nodes || $nodes->length < 1) {
|
|
return [null, null];
|
|
}
|
|
|
|
$imponibile = 0.0;
|
|
$iva = 0.0;
|
|
$hasAny = false;
|
|
|
|
foreach ($nodes as $node) {
|
|
$childXpath = new DOMXPath($node->ownerDocument);
|
|
$imp = $childXpath->query(".//*[local-name()='ImponibileImporto']", $node);
|
|
$tax = $childXpath->query(".//*[local-name()='Imposta']", $node);
|
|
|
|
$impVal = $imp && $imp->length ? $this->toFloat($imp->item(0)?->textContent) : null;
|
|
$taxVal = $tax && $tax->length ? $this->toFloat($tax->item(0)?->textContent) : null;
|
|
|
|
if ($impVal !== null) {
|
|
$imponibile += $impVal;
|
|
$hasAny = true;
|
|
}
|
|
if ($taxVal !== null) {
|
|
$iva += $taxVal;
|
|
$hasAny = true;
|
|
}
|
|
}
|
|
|
|
if (! $hasAny) {
|
|
return [null, null];
|
|
}
|
|
|
|
return [round($imponibile, 2), round($iva, 2)];
|
|
}
|
|
|
|
private function toDate(?string $value): ?Carbon
|
|
{
|
|
$value = $value ? trim($value) : '';
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return Carbon::parse($value);
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function toFloat(?string $value): ?float
|
|
{
|
|
$value = $value ? trim($value) : '';
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
$value = str_replace(['.', ','], ['.', '.'], $value);
|
|
if (! is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
return (float) $value;
|
|
}
|
|
|
|
private function normalizeId(?string $value): ?string
|
|
{
|
|
$value = $value ? trim($value) : '';
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
$value = strtoupper(preg_replace('/\s+/', '', $value));
|
|
return $value !== '' ? $value : null;
|
|
}
|
|
|
|
private function normalizeIban(?string $value): ?string
|
|
{
|
|
$value = $this->normalizeId($value);
|
|
if (! $value) {
|
|
return null;
|
|
}
|
|
|
|
// IBAN tipico: 15-34 caratteri alfanumerici
|
|
if (! preg_match('/^[A-Z0-9]{15,34}$/', $value)) {
|
|
return null;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|