} $parsed * @return array{status: string, servizio_id?: int, created_letture?: int, updated_letture?: int, tickets?: int, message?: string} */ public function ingest( FatturaElettronica $fattura, array $parsed, ?int $voceSpesaId, int $userId, bool $createTicketOnMismatch = true, ?int $forceServizioId = null, ): array { if (! Schema::hasTable('stabile_servizi') || ! Schema::hasTable('stabile_servizio_letture')) { return ['status' => 'skipped', 'message' => 'Tabelle servizi/letture non presenti']; } $stabileId = (int) ($fattura->stabile_id ?: 0); if ($stabileId <= 0) { return ['status' => 'error', 'message' => 'Stabile non disponibile']; } $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; $matricola = is_string($contatore['matricola'] ?? null) ? trim((string) $contatore['matricola']) : ''; $utenza = is_string($codici['utenza'] ?? null) ? trim((string) $codici['utenza']) : ''; $cliente = is_string($codici['cliente'] ?? null) ? trim((string) $codici['cliente']) : ''; $contratto = is_string($codici['contratto'] ?? null) ? trim((string) $codici['contratto']) : ''; $norm = static function (?string $value): string { $v = is_string($value) ? $value : ''; $v = strtoupper(trim($v)); $v = preg_replace('/\s+/', '', $v) ?? $v; return $v; }; $error = is_string($parsed['error'] ?? null) ? trim((string) $parsed['error']) : ''; $tickets = 0; // Se non abbiamo dati utili, è molto probabile che il layout PDF sia cambiato o che non sia un PDF “utenze”. // In modalità batch, vogliamo segnalarlo con un ticket (se richiesto). $hasAnyId = ($matricola !== '') || ($utenza !== '') || ($cliente !== '') || ($contratto !== ''); if (! $hasAnyId && count($consumi) < 1) { if ($createTicketOnMismatch) { $tickets += $this->openTicket( $stabileId, $userId, (int) ($fattura->fornitore_id ?: 0) ?: null, 'Parser acqua: dati non trovati', 'FE #' . (int) $fattura->id . ' (' . ($fattura->sdi_file ?? 'n/d') . ")\n" . ($error !== '' ? ('Errore: ' . $error . "\n") : '') . 'Nessun codice utenza/cliente/contratto, nessuna matricola contatore e nessun periodo consumo rilevato.', ) ? 1 : 0; } return [ 'status' => 'no-data', 'tickets' => $tickets, 'message' => $error !== '' ? $error : 'Dati acqua non trovati nel PDF', ]; } $servizio = null; if ($forceServizioId) { $servizio = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->whereKey($forceServizioId) ->first(); if (! $servizio) { return ['status' => 'error', 'message' => 'Servizio acqua selezionato non trovato']; } // Mismatch detection: se il servizio ha già un identificativo e il PDF ne porta un altro, apri ticket e non agganciare. $mismatch = false; $why = []; if (is_string($servizio->contatore_matricola) && $servizio->contatore_matricola !== '' && $matricola !== '' && $servizio->contatore_matricola !== $matricola) { $mismatch = true; $why[] = 'matricola diversa (servizio ' . $servizio->contatore_matricola . ' vs PDF ' . $matricola . ')'; } if (is_string($servizio->codice_utenza) && $servizio->codice_utenza !== '' && $utenza !== '' && $servizio->codice_utenza !== $utenza) { $mismatch = true; $why[] = 'utenza diversa (servizio ' . $servizio->codice_utenza . ' vs PDF ' . $utenza . ')'; } if (is_string($servizio->codice_cliente) && $servizio->codice_cliente !== '' && $cliente !== '' && $servizio->codice_cliente !== $cliente) { $mismatch = true; $why[] = 'cliente diverso (servizio ' . $servizio->codice_cliente . ' vs PDF ' . $cliente . ')'; } if (is_string($servizio->codice_contratto) && $servizio->codice_contratto !== '' && $contratto !== '' && $servizio->codice_contratto !== $contratto) { $mismatch = true; $why[] = 'contratto diverso (servizio ' . $servizio->codice_contratto . ' vs PDF ' . $contratto . ')'; } if ($mismatch && $createTicketOnMismatch) { $tickets += $this->openTicket( $stabileId, $userId, (int) ($fattura->fornitore_id ?: 0) ?: null, 'Parser acqua: mismatch utenza/contatore', 'FE #' . (int) $fattura->id . ' (' . ($fattura->sdi_file ?? 'n/d') . ")\n" . implode("; ", $why), ) ? 1 : 0; return ['status' => 'mismatch', 'tickets' => $tickets, 'message' => implode('; ', $why)]; } } if (! $servizio) { $candidates = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderByDesc('id') ->limit(50) ->get(); $targetMatricola = $norm($matricola); $targetUtenza = $norm($utenza); $targetCliente = $norm($cliente); $targetContratto = $norm($contratto); $best = null; $bestScore = 0; $bestTie = 0; foreach ($candidates as $cand) { $score = 0; $candMatricola = $norm(is_string($cand->contatore_matricola) ? $cand->contatore_matricola : null); $candUtenza = $norm(is_string($cand->codice_utenza) ? $cand->codice_utenza : null); $candCliente = $norm(is_string($cand->codice_cliente) ? $cand->codice_cliente : null); $candContratto = $norm(is_string($cand->codice_contratto) ? $cand->codice_contratto : null); if ($targetMatricola !== '' && $candMatricola !== '' && $targetMatricola === $candMatricola) { $score += 4; } if ($targetUtenza !== '' && $candUtenza !== '' && $targetUtenza === $candUtenza) { $score += 3; } if ($targetCliente !== '' && $candCliente !== '' && $targetCliente === $candCliente) { $score += 2; } if ($targetContratto !== '' && $candContratto !== '' && $targetContratto === $candContratto) { $score += 2; } if ($score > $bestScore) { $best = $cand; $bestScore = $score; $bestTie = 0; } elseif ($score > 0 && $score === $bestScore) { $bestTie++; } } if ($best && $bestScore > 0) { if ($bestTie > 0 && $createTicketOnMismatch) { $tickets += $this->openTicket( $stabileId, $userId, (int) ($fattura->fornitore_id ?: 0) ?: null, 'Parser acqua: utenza ambigua', 'FE #' . (int) $fattura->id . ' (' . ($fattura->sdi_file ?? 'n/d') . ")\n" . 'Più utenze acqua compatibili con gli identificativi del PDF. ' . 'Matricola=' . ($matricola !== '' ? $matricola : 'n/d') . ', Utenza=' . ($utenza !== '' ? $utenza : 'n/d') . ', Cliente=' . ($cliente !== '' ? $cliente : 'n/d') . ', Contratto=' . ($contratto !== '' ? $contratto : 'n/d'), ) ? 1 : 0; } $servizio = $best; } if (! $servizio) { $name = 'Acqua'; if ($matricola !== '') { $name .= ' - Contatore ' . $matricola; } elseif ($utenza !== '') { $name .= ' - Utenza ' . $utenza; } if ($candidates->count() > 0 && $createTicketOnMismatch) { $tickets += $this->openTicket( $stabileId, $userId, (int) ($fattura->fornitore_id ?: 0) ?: null, 'Parser acqua: utenza non riconosciuta', 'FE #' . (int) $fattura->id . ' (' . ($fattura->sdi_file ?? 'n/d') . ")\n" . 'Identificativi PDF non agganciati a nessuna utenza acqua esistente. ' . 'Matricola=' . ($matricola !== '' ? $matricola : 'n/d') . ', Utenza=' . ($utenza !== '' ? $utenza : 'n/d') . ', Cliente=' . ($cliente !== '' ? $cliente : 'n/d') . ', Contratto=' . ($contratto !== '' ? $contratto : 'n/d'), ) ? 1 : 0; } $servizio = new StabileServizio([ 'stabile_id' => $stabileId, 'tipo' => 'acqua', 'nome' => $name, 'attivo' => true, ]); } } if ((int) ($fattura->fornitore_id ?? 0) > 0 && ! $servizio->fornitore_id) { $servizio->fornitore_id = (int) $fattura->fornitore_id; } if ($voceSpesaId && $voceSpesaId > 0) { $servizio->voce_spesa_id = $voceSpesaId; } // Aggiorna codici servizio solo se non presenti (non sovrascrive). if ($utenza !== '' && (! is_string($servizio->codice_utenza) || $servizio->codice_utenza === '')) { $servizio->codice_utenza = $utenza; } if ($cliente !== '' && (! is_string($servizio->codice_cliente) || $servizio->codice_cliente === '')) { $servizio->codice_cliente = $cliente; } if ($contratto !== '' && (! is_string($servizio->codice_contratto) || $servizio->codice_contratto === '')) { $servizio->codice_contratto = $contratto; } if ($matricola !== '' && (! is_string($servizio->contatore_matricola) || $servizio->contatore_matricola === '')) { $servizio->contatore_matricola = $matricola; } $servizio->save(); $created = 0; $updated = 0; $importo = null; if (count($consumi) === 1 && is_numeric($fattura->totale)) { $importo = (float) $fattura->totale; } foreach ($consumi as $c) { $dal = is_string($c['dal'] ?? null) ? (string) $c['dal'] : null; $al = is_string($c['al'] ?? null) ? (string) $c['al'] : null; $val = isset($c['valore']) && is_numeric($c['valore']) ? (float) $c['valore'] : null; $unita = is_string($c['unita'] ?? null) ? trim((string) $c['unita']) : ''; $tipo = is_string($c['tipologia_lettura'] ?? null) ? trim((string) $c['tipologia_lettura']) : null; $lettura = StabileServizioLettura::query()->firstOrNew([ 'stabile_id' => $stabileId, 'stabile_servizio_id' => (int) $servizio->id, 'fattura_elettronica_id' => (int) $fattura->id, 'periodo_dal' => $dal, 'periodo_al' => $al, ]); $isNew = ! $lettura->exists; $lettura->fill([ 'fornitore_id' => (int) ($fattura->fornitore_id ?: 0) ?: null, 'voce_spesa_id' => $voceSpesaId ?: ($servizio->voce_spesa_id ? (int) $servizio->voce_spesa_id : null), 'tipologia_lettura' => $tipo, 'consumo_valore' => $val, 'consumo_unita' => $unita !== '' ? $unita : 'mc', 'importo_totale' => $importo, 'raw' => [ 'source' => 'pdf_ocr', 'codici' => $parsed['codici'] ?? null, 'contatore' => $parsed['contatore'] ?? null, 'consumo' => $c, ], 'created_by' => $userId > 0 ? $userId : null, ]); $lettura->save(); if ($isNew) { $created++; } else { $updated++; } } return [ 'status' => 'ok', 'servizio_id' => (int) $servizio->id, 'created_letture' => $created, 'updated_letture' => $updated, 'tickets' => $tickets, ]; } private function openTicket(int $stabileId, int $userId, ?int $fornitoreId, string $title, string $body): bool { try { $cat = CategoriaTicket::query()->firstOrCreate( ['nome' => 'Parser PDF'], ['descrizione' => 'Anomalie parsing PDF (acqua/energia/gas) e import consumi'] ); $feId = null; if (preg_match('/\bFE\s*#\s*(\d+)\b/i', $body, $m)) { $feId = (int) $m[1]; if ($feId <= 0) { $feId = null; } } if ($feId !== null) { $existing = Ticket::query() ->where('stabile_id', $stabileId) ->where('categoria_ticket_id', (int) $cat->id) ->where('titolo', $title) ->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']) ->when($fornitoreId !== null, fn($q) => $q->where('assegnato_a_fornitore_id', $fornitoreId)) ->where('descrizione', 'like', 'FE #' . $feId . '%') ->orderByDesc('id') ->first(); if ($existing) { return false; } } Ticket::query()->create([ 'stabile_id' => $stabileId, 'aperto_da_user_id' => $userId > 0 ? $userId : 1, 'categoria_ticket_id' => (int) $cat->id, 'assegnato_a_fornitore_id' => $fornitoreId, 'titolo' => $title, 'descrizione' => $body, 'stato' => 'Aperto', 'priorita' => 'Media', ]); return true; } catch (\Throwable) { return false; } } }