netgescon-day0/app/Services/Consumi/AcquaContractSyncService.php

644 lines
27 KiB
PHP
Executable File

<?php
namespace App\Services\Consumi;
use App\Filament\Pages\Supporto\TicketAcquaGenerale;
use App\Models\Scadenza;
use App\Models\StabileContratto;
use App\Models\StabileServizio;
use App\Models\StabileServizioLettura;
use Carbon\Carbon;
use Illuminate\Support\Facades\URL;
class AcquaContractSyncService
{
/** @return array<string,int> */
public function normalizeStabile(int $stabileId, int $userId): array
{
$stats = [
'services' => 0,
'synced' => 0,
'contracts_before' => 0,
'contracts_after' => 0,
'merged' => 0,
];
if ($stabileId <= 0) {
return $stats;
}
$stats['contracts_before'] = StabileContratto::query()
->where('stabile_id', $stabileId)
->where('tipo_contratto', 'acqua')
->count();
$services = StabileServizio::query()
->with('fornitore:id,ragione_sociale,nome,cognome')
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->orderByDesc('attivo')
->orderBy('id')
->get();
$stats['services'] = $services->count();
foreach ($services as $service) {
if ($this->syncForService($service, $userId) instanceof StabileContratto) {
$stats['synced']++;
}
}
$stats['contracts_after'] = StabileContratto::query()
->where('stabile_id', $stabileId)
->where('tipo_contratto', 'acqua')
->count();
$stats['merged'] = max(0, $stats['contracts_before'] - $stats['contracts_after']);
return $stats;
}
public function syncForService(StabileServizio $service, int $userId): ?StabileContratto
{
if ((string) ($service->tipo ?? '') !== 'acqua') {
return null;
}
$service->loadMissing('fornitore:id,ragione_sociale,nome,cognome');
$contracts = StabileContratto::query()
->where('stabile_id', (int) $service->stabile_id)
->where('tipo_contratto', 'acqua')
->orderByRaw("CASE WHEN stato = 'attivo' THEN 0 ELSE 1 END")
->orderByDesc('updated_at')
->orderByDesc('id')
->get();
$contract = $this->resolveCanonicalContract($service, $contracts);
if (! $contract instanceof StabileContratto) {
$contract = new StabileContratto([
'stabile_id' => (int) $service->stabile_id,
'tipo_contratto' => 'acqua',
]);
}
$serviceIdentifiers = $this->collectServiceIdentifiers($service);
$fornitoreLabel = trim((string) ($service->fornitore?->ragione_sociale ?: trim(($service->fornitore?->nome ?? '') . ' ' . ($service->fornitore?->cognome ?? ''))));
$title = trim('Contratto acqua' . ($fornitoreLabel !== '' ? ' - ' . $fornitoreLabel : ''));
$contract->fill([
'fornitore_id' => (int) ($service->fornitore_id ?? 0) ?: null,
'titolo' => $title !== '' ? $title : 'Contratto acqua',
'categoria_impianto' => 'acqua',
'servizio_label' => $service->nome,
'codice_contratto' => $this->nullableString($service->codice_contratto),
'riferimento_esterno' => $this->nullableString($service->codice_utenza ?: $service->codice_cliente),
'stato' => $contract->exists ? ($contract->stato ?: 'attivo'): 'attivo',
'decorrenza_dal' => $contract->decorrenza_dal ?: now()->startOfYear()->toDateString(),
'frequenza_scadenze' => $contract->frequenza_scadenze ?: 'annuale',
'giorni_preavviso' => $contract->giorni_preavviso ?: 30,
'rinnovo_automatico' => $contract->exists ? (bool) $contract->rinnovo_automatico : true,
'codici_collegati' => $this->buildLinkedCodes($service),
'dati_contratto' => array_merge(is_array($contract->dati_contratto) ? $contract->dati_contratto : [], [
'origine' => 'acqua_contract_sync',
'servizio_id' => (int) $service->id,
'last_synced_at' => now()->toIso8601String(),
'service_identifiers' => $serviceIdentifiers,
]),
'updated_by' => $userId,
]);
if (! $contract->exists) {
$contract->created_by = $userId;
$contract->valuta = 'EUR';
}
$contract->save();
$duplicates = $this->resolveDuplicateContracts($contracts, $service, $contract);
if ($duplicates !== []) {
$this->mergeDuplicateContracts($contract, $duplicates, $userId);
$contract->refresh();
}
$this->syncGeneratedScadenze($contract, $service);
return $contract;
}
/** @param \\Illuminate\\Support\\Collection<int,StabileContratto> $contracts */
private function resolveCanonicalContract(StabileServizio $service, $contracts): ?StabileContratto
{
$serviceIdentifiers = $this->collectServiceIdentifiers($service);
$serviceStrongIdentifiers = $this->collectServiceStrongIdentifiers($service);
$serviceWeakIdentifiers = $this->collectServiceWeakIdentifiers($service);
$serviceCount = StabileServizio::query()
->where('stabile_id', (int) $service->stabile_id)
->where('tipo', 'acqua')
->count();
$best = null;
$bestScore = 0;
foreach ($contracts as $contract) {
$score = 0;
$contractIdentifiers = $this->collectContractIdentifiers($contract);
$contractStrongIdentifiers = $this->collectContractStrongIdentifiers($contract);
$contractWeakIdentifiers = $this->collectContractWeakIdentifiers($contract);
$linkedServiceId = (int) data_get($contract->dati_contratto ?? [], 'servizio_id', 0);
if ($linkedServiceId > 0 && $linkedServiceId === (int) $service->id) {
$score += 250;
}
$serviceContract = $this->normalizeIdentifier((string) ($service->codice_contratto ?? ''));
$contractCode = $this->normalizeIdentifier((string) ($contract->codice_contratto ?? ''));
if ($serviceContract !== '' && $contractCode !== '' && $serviceContract === $contractCode) {
$score += 200;
}
$serviceReference = $this->normalizeIdentifier((string) ($service->codice_utenza ?: ''));
$contractReference = $this->normalizeIdentifier((string) ($contract->riferimento_esterno ?? ''));
if ($serviceReference !== '' && $contractReference !== '' && $serviceReference === $contractReference) {
$score += 90;
}
$overlap = array_intersect($serviceStrongIdentifiers, $contractStrongIdentifiers);
if ($overlap !== []) {
$score += 50 * count($overlap);
}
if ($serviceStrongIdentifiers === [] && $serviceCount === 1) {
$weakOverlap = array_intersect($serviceWeakIdentifiers, $contractWeakIdentifiers);
if ($weakOverlap !== []) {
$score += 20 * count($weakOverlap);
}
}
if ((string) ($contract->stato ?? '') === 'attivo') {
$score += 10;
}
if ((int) ($contract->fornitore_id ?? 0) > 0 && (int) ($contract->fornitore_id ?? 0) === (int) ($service->fornitore_id ?? 0)) {
$score += 5;
}
if ($score > $bestScore) {
$best = $contract;
$bestScore = $score;
}
}
if ($best instanceof StabileContratto && $bestScore >= 50) {
return $best;
}
if ($contracts->count() === 1 && $serviceCount === 1) {
return $contracts->first();
}
return null;
}
/** @param \\Illuminate\\Support\\Collection<int,StabileContratto> $contracts
* @return array<int,StabileContratto>
*/
private function resolveDuplicateContracts($contracts, StabileServizio $service, StabileContratto $canonical): array
{
$canonicalCode = $this->normalizeIdentifier((string) ($canonical->codice_contratto ?? ''));
$canonicalIdentifiers = $this->collectContractStrongIdentifiers($canonical);
$serviceIdentifiers = $this->collectServiceStrongIdentifiers($service);
return $contracts
->filter(function (StabileContratto $contract) use ($canonical, $canonicalCode, $canonicalIdentifiers, $serviceIdentifiers): bool {
if ((int) $contract->id === (int) $canonical->id) {
return false;
}
$contractCode = $this->normalizeIdentifier((string) ($contract->codice_contratto ?? ''));
if ($canonicalCode !== '' && $contractCode !== '' && $canonicalCode === $contractCode) {
return true;
}
$contractIdentifiers = $this->collectContractStrongIdentifiers($contract);
if ($canonicalIdentifiers !== [] && $contractIdentifiers !== [] && array_intersect($canonicalIdentifiers, $contractIdentifiers) !== []) {
return true;
}
return $serviceIdentifiers !== []
&& $contractIdentifiers !== []
&& array_intersect($serviceIdentifiers, $contractIdentifiers) !== [];
})
->values()
->all();
}
/** @param array<int,StabileContratto> $duplicates */
private function mergeDuplicateContracts(StabileContratto $canonical, array $duplicates, int $userId): void
{
$mergedIds = [];
$linkedCodes = is_array($canonical->codici_collegati) ? $canonical->codici_collegati : [];
$meta = is_array($canonical->dati_contratto) ? $canonical->dati_contratto : [];
$dirty = false;
foreach ($duplicates as $duplicate) {
$mergedIds[] = (int) $duplicate->id;
if (! $canonical->documento_principale_id && $duplicate->documento_principale_id) {
$canonical->documento_principale_id = $duplicate->documento_principale_id;
$dirty = true;
}
if (! $canonical->data_stipula && $duplicate->data_stipula) {
$canonical->data_stipula = $duplicate->data_stipula;
$dirty = true;
}
if (! $canonical->decorrenza_dal && $duplicate->decorrenza_dal) {
$canonical->decorrenza_dal = $duplicate->decorrenza_dal;
$dirty = true;
}
if (! $canonical->decorrenza_al && $duplicate->decorrenza_al) {
$canonical->decorrenza_al = $duplicate->decorrenza_al;
$dirty = true;
}
if (! filled($canonical->note) && filled($duplicate->note)) {
$canonical->note = $duplicate->note;
$dirty = true;
}
$linkedCodes = $this->mergeLinkedCodes($linkedCodes, is_array($duplicate->codici_collegati) ? $duplicate->codici_collegati : []);
Scadenza::query()
->where('stabile_contratto_id', (int) $duplicate->id)
->update(['stabile_contratto_id' => (int) $canonical->id]);
$duplicate->delete();
}
if ($mergedIds !== []) {
$meta['merged_from_contract_ids'] = array_values(array_unique(array_map('intval', array_merge(
array_map('intval', (array) ($meta['merged_from_contract_ids'] ?? [])),
$mergedIds,
))));
$meta['last_merged_at'] = now()->toIso8601String();
$meta['last_merged_by'] = $userId;
$canonical->codici_collegati = $linkedCodes;
$canonical->dati_contratto = $meta;
$canonical->updated_by = $userId;
$dirty = true;
}
if ($dirty) {
$canonical->save();
}
}
private function syncGeneratedScadenze(StabileContratto $contract, StabileServizio $service): void
{
Scadenza::query()
->where('stabile_contratto_id', (int) $contract->id)
->where(function ($query): void {
$query->where('natura', 'contratto_stabile')
->orWhere(function ($nested): void {
$nested->where('natura', 'acqua')
->where('legacy_payload->source', 'acqua_contract_sync');
});
})
->delete();
foreach ($this->buildContractScheduleDates($contract) as $date) {
$popupDal = $date->copy()->subDays(max(0, (int) ($contract->giorni_preavviso ?? 0)));
Scadenza::query()->create([
'stabile_id' => (int) $contract->stabile_id,
'stabile_contratto_id' => (int) $contract->id,
'descrizione_sintetica' => $contract->titolo,
'data_scadenza' => $date->toDateString(),
'natura' => 'contratto_stabile',
'natura_label' => 'Scadenza contratto',
'gestione_tipo' => $contract->tipo_contratto,
'anno' => $date->format('Y'),
'fatto' => 'N',
'tipo_riga' => trim((string) ($contract->servizio_label ?: $contract->tipo_contratto)),
'richiede_pop_up' => true,
'giorni_anticipo' => (int) ($contract->giorni_preavviso ?? 0),
'popup_dal' => $popupDal->toDateString(),
'legacy_payload' => [
'source' => 'acqua_contract_sync',
'schedule_type' => 'contract',
'codice_contratto' => $contract->codice_contratto,
'riferimento_esterno' => $contract->riferimento_esterno,
'codici_collegati' => $contract->codici_collegati ?? [],
],
]);
}
$this->syncGeneralReadingScadenze($contract, $service);
$this->syncUnitReadingScadenze($contract, $service);
}
private function syncGeneralReadingScadenze(StabileContratto $contract, StabileServizio $service): void
{
$rows = StabileServizioLettura::query()
->where('stabile_id', (int) $service->stabile_id)
->where('stabile_servizio_id', (int) $service->id)
->whereNull('unita_immobiliare_id')
->whereNull('lettura_fine')
->whereNotNull('deadline_lettura_at')
->orderBy('deadline_lettura_at')
->get(['id', 'stabile_id', 'deadline_lettura_at', 'riferimento_acquisizione', 'periodo_dal', 'periodo_al']);
foreach ($rows as $reading) {
$deadline = $reading->deadline_lettura_at instanceof Carbon
? $reading->deadline_lettura_at->copy()
: Carbon::parse((string) $reading->deadline_lettura_at);
Scadenza::query()->create([
'stabile_id' => (int) $service->stabile_id,
'stabile_contratto_id' => (int) $contract->id,
'descrizione_sintetica' => 'Lettura acqua generale ' . $this->serviceContractLabel($service),
'data_scadenza' => $deadline->toDateString(),
'natura' => 'acqua',
'natura_label' => 'Lettura acqua generale',
'gestione_tipo' => 'operativa',
'anno' => $deadline->format('Y'),
'fatto' => 'N',
'tipo_riga' => trim((string) ($service->nome ?: 'Acqua')),
'richiede_pop_up' => true,
'giorni_anticipo' => 2,
'popup_dal' => $deadline->copy()->subDays(2)->toDateString(),
'legacy_payload' => [
'source' => 'acqua_contract_sync',
'schedule_type' => 'general_reading',
'stabile_servizio_id' => (int) $service->id,
'reading_id' => (int) $reading->id,
'reading_reference' => $reading->riferimento_acquisizione,
'periodo_dal' => optional($reading->periodo_dal)->toDateString(),
'periodo_al' => optional($reading->periodo_al)->toDateString(),
'action_url' => TicketAcquaGenerale::getUrl([
'stabile' => (int) $service->stabile_id,
'servizio' => (int) $service->id,
], panel: 'admin-filament'),
],
]);
}
}
private function syncUnitReadingScadenze(StabileContratto $contract, StabileServizio $service): void
{
$rows = StabileServizioLettura::query()
->with('unitaImmobiliare:id,codice_unita,scala,interno')
->where('stabile_id', (int) $service->stabile_id)
->where('stabile_servizio_id', (int) $service->id)
->whereNotNull('unita_immobiliare_id')
->whereNull('lettura_fine')
->whereNotNull('deadline_lettura_at')
->orderBy('deadline_lettura_at')
->get(['id', 'stabile_id', 'stabile_servizio_id', 'unita_immobiliare_id', 'deadline_lettura_at', 'riferimento_acquisizione']);
foreach ($rows as $reading) {
$deadline = $reading->deadline_lettura_at instanceof Carbon
? $reading->deadline_lettura_at->copy()
: Carbon::parse((string) $reading->deadline_lettura_at);
$unitLabel = trim(implode(' ', array_filter([
trim((string) ($reading->unitaImmobiliare?->codice_unita ?? '')),
trim((string) ($reading->unitaImmobiliare?->interno ?? '')),
])));
Scadenza::query()->create([
'stabile_id' => (int) $service->stabile_id,
'stabile_contratto_id' => (int) $contract->id,
'descrizione_sintetica' => 'Autolettura acqua ' . ($unitLabel !== '' ? $unitLabel : ('UI #' . (int) $reading->unita_immobiliare_id)),
'data_scadenza' => $deadline->toDateString(),
'natura' => 'acqua',
'natura_label' => 'Autolettura acqua unità',
'gestione_tipo' => 'operativa',
'anno' => $deadline->format('Y'),
'fatto' => 'N',
'tipo_riga' => trim((string) ($service->nome ?: 'Acqua')),
'richiede_pop_up' => true,
'giorni_anticipo' => 3,
'popup_dal' => $deadline->copy()->subDays(3)->toDateString(),
'legacy_payload' => [
'source' => 'acqua_contract_sync',
'schedule_type' => 'unit_reading',
'stabile_servizio_id' => (int) $service->id,
'reading_id' => (int) $reading->id,
'unita_immobiliare_id' => (int) $reading->unita_immobiliare_id,
'reading_reference' => $reading->riferimento_acquisizione,
'action_url' => URL::temporarySignedRoute(
'public.water-reading.show',
now()->addDays(90),
[
'servizio' => (int) $service->id,
'unita' => (int) $reading->unita_immobiliare_id,
'request' => (int) $reading->id,
]
),
],
]);
}
}
/** @return array<int,Carbon> */
private function buildContractScheduleDates(StabileContratto $contract): array
{
$start = $contract->decorrenza_dal instanceof Carbon
? $contract->decorrenza_dal->copy()
: ($contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : now()->copy());
$end = $contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : null;
$frequency = trim((string) ($contract->frequenza_scadenze ?? 'una_tantum'));
if ($contract->giorno_scadenza) {
$start->day(min(28, (int) $contract->giorno_scadenza));
}
if ($frequency === '' || $frequency === 'una_tantum') {
return [$end ?: $start];
}
$dates = [];
$cursor = $start->copy();
$limit = $end
? $end->copy()
: ($contract->rinnovo_automatico ? $start->copy()->addMonths(24) : $start->copy()->addMonths(12));
while ($cursor <= $limit && count($dates) < 60) {
$dates[] = $cursor->copy();
$cursor = match ($frequency) {
'mensile' => $cursor->copy()->addMonthNoOverflow(),
'bimestrale' => $cursor->copy()->addMonthsNoOverflow(2),
'trimestrale' => $cursor->copy()->addMonthsNoOverflow(3),
'semestrale' => $cursor->copy()->addMonthsNoOverflow(6),
default => $cursor->copy()->addYearNoOverflow(),
};
if ($contract->giorno_scadenza) {
$cursor->day(min(28, (int) $contract->giorno_scadenza));
}
}
return $dates;
}
/** @return array<int,string> */
private function collectServiceIdentifiers(StabileServizio $service): array
{
return array_values(array_filter(array_unique([
$this->normalizeIdentifier((string) ($service->codice_utenza ?? '')),
$this->normalizeIdentifier((string) ($service->codice_cliente ?? '')),
$this->normalizeIdentifier((string) ($service->codice_contratto ?? '')),
$this->normalizeIdentifier((string) ($service->contatore_matricola ?? '')),
])));
}
/** @return array<int,string> */
private function collectServiceStrongIdentifiers(StabileServizio $service): array
{
return array_values(array_filter(array_unique([
$this->normalizeIdentifier((string) ($service->codice_utenza ?? '')),
$this->normalizeIdentifier((string) ($service->codice_contratto ?? '')),
$this->normalizeIdentifier((string) ($service->contatore_matricola ?? '')),
])));
}
/** @return array<int,string> */
private function collectServiceWeakIdentifiers(StabileServizio $service): array
{
return array_values(array_filter(array_unique([
$this->normalizeIdentifier((string) ($service->codice_cliente ?? '')),
])));
}
/** @return array<int,string> */
private function collectContractIdentifiers(StabileContratto $contract): array
{
$identifiers = [
$this->normalizeIdentifier((string) ($contract->codice_contratto ?? '')),
$this->normalizeIdentifier((string) ($contract->riferimento_esterno ?? '')),
];
foreach ((array) ($contract->codici_collegati ?? []) as $row) {
if (! is_array($row)) {
continue;
}
$identifiers[] = $this->normalizeIdentifier((string) ($row['valore'] ?? ''));
}
return array_values(array_filter(array_unique($identifiers)));
}
/** @return array<int,string> */
private function collectContractStrongIdentifiers(StabileContratto $contract): array
{
$identifiers = [
$this->normalizeIdentifier((string) ($contract->codice_contratto ?? '')),
];
foreach ((array) ($contract->codici_collegati ?? []) as $row) {
if (! is_array($row)) {
continue;
}
$key = (string) ($row['chiave'] ?? '');
if (! in_array($key, ['utenza', 'codice_contratto', 'matricola_contatore'], true)) {
continue;
}
$identifiers[] = $this->normalizeIdentifier((string) ($row['valore'] ?? ''));
}
if ($identifiers === [] || count(array_filter($identifiers)) === 0) {
$identifiers[] = $this->normalizeIdentifier((string) ($contract->riferimento_esterno ?? ''));
}
return array_values(array_filter(array_unique($identifiers)));
}
/** @return array<int,string> */
private function collectContractWeakIdentifiers(StabileContratto $contract): array
{
$identifiers = [];
foreach ((array) ($contract->codici_collegati ?? []) as $row) {
if (! is_array($row) || (string) ($row['chiave'] ?? '') !== 'codice_cliente') {
continue;
}
$identifiers[] = $this->normalizeIdentifier((string) ($row['valore'] ?? ''));
}
if ($identifiers === []) {
$identifiers[] = $this->normalizeIdentifier((string) ($contract->riferimento_esterno ?? ''));
}
return array_values(array_filter(array_unique($identifiers)));
}
/** @return array<int,array{chiave:string,valore:string}> */
private function buildLinkedCodes(StabileServizio $service): array
{
return array_values(array_filter([
filled($service->codice_utenza) ? ['chiave' => 'utenza', 'valore' => (string) $service->codice_utenza] : null,
filled($service->codice_cliente) ? ['chiave' => 'codice_cliente', 'valore' => (string) $service->codice_cliente] : null,
filled($service->codice_contratto) ? ['chiave' => 'codice_contratto', 'valore' => (string) $service->codice_contratto] : null,
filled($service->contatore_matricola) ? ['chiave' => 'matricola_contatore', 'valore' => (string) $service->contatore_matricola] : null,
]));
}
/** @param array<int,array<string,mixed>> $left
* @param array<int,array<string,mixed>> $right
* @return array<int,array{chiave:string,valore:string}>
*/
private function mergeLinkedCodes(array $left, array $right): array
{
$map = [];
foreach (array_merge($left, $right) as $row) {
if (! is_array($row)) {
continue;
}
$chiave = trim((string) ($row['chiave'] ?? ''));
$valore = trim((string) ($row['valore'] ?? ''));
if ($chiave === '' || $valore === '') {
continue;
}
$map[$chiave . '|' . $valore] = [
'chiave' => $chiave,
'valore' => $valore,
];
}
return array_values($map);
}
private function normalizeIdentifier(string $value): string
{
return preg_replace('/[^A-Z0-9]/', '', strtoupper(trim($value))) ?? '';
}
private function nullableString(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function serviceContractLabel(StabileServizio $service): string
{
$bits = array_filter([
trim((string) ($service->nome ?? 'Acqua')),
trim((string) ($service->codice_contratto ?? '')),
trim((string) ($service->codice_utenza ?? '')),
]);
return implode(' ', $bits);
}
}