365 lines
16 KiB
PHP
365 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Consumi;
|
|
|
|
use App\Models\CategoriaTicket;
|
|
use App\Models\FatturaElettronica;
|
|
use App\Models\StabileServizio;
|
|
use App\Models\StabileServizioLettura;
|
|
use App\Models\Ticket;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class ConsumiAcquaIngestionService
|
|
{
|
|
/**
|
|
* @param array{codici?: array{utenza?: string|null, cliente?: string|null, contratto?: string|null}, contatore?: array{matricola?: string|null}, consumi?: array<int, array{dal?: string|null, al?: string|null, valore?: float|null, unita?: string|null, tipologia_lettura?: string|null}>} $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'];
|
|
}
|
|
|
|
$storedPayload = [];
|
|
$storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : '';
|
|
if ($storedRaw !== '') {
|
|
$decoded = json_decode($storedRaw, true);
|
|
if (is_array($decoded) && (($decoded['type'] ?? null) === 'acqua')) {
|
|
$storedPayload = $decoded;
|
|
}
|
|
}
|
|
|
|
if ((empty($parsed['codici']) || ! is_array($parsed['codici'])) && is_array($storedPayload['codici'] ?? null)) {
|
|
$parsed['codici'] = $storedPayload['codici'];
|
|
}
|
|
if ((empty($parsed['contatore']) || ! is_array($parsed['contatore'])) && is_array($storedPayload['contatore'] ?? null)) {
|
|
$parsed['contatore'] = $storedPayload['contatore'];
|
|
}
|
|
if ((empty($parsed['consumi']) || ! is_array($parsed['consumi'])) && is_array($storedPayload['consumi'] ?? null)) {
|
|
$parsed['consumi'] = $storedPayload['consumi'];
|
|
}
|
|
|
|
$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;
|
|
}
|
|
}
|
|
}
|