1677 lines
73 KiB
PHP
Executable File
1677 lines
73 KiB
PHP
Executable File
<?php
|
|
namespace App\Services\Contabilita;
|
|
|
|
use App\Support\GestioneVisibility;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class IncassiHubReadService
|
|
{
|
|
public function visibleOrdinaryCompetenceLabels(int $stabileId): array
|
|
{
|
|
$policy = GestioneVisibility::policyForStabile($stabileId);
|
|
$years = array_values(array_filter(
|
|
array_map('intval', $policy['visible_years'] ?? []),
|
|
static fn(int $year): bool => $year >= 2000 && $year <= 2100,
|
|
));
|
|
|
|
rsort($years);
|
|
|
|
return array_values(array_unique(array_map(
|
|
fn(int $year): string => $this->legacyGestioneLabel($year),
|
|
$years,
|
|
)));
|
|
}
|
|
|
|
public function operativeOrdinaryContext(int $stabileId): array
|
|
{
|
|
$policy = GestioneVisibility::policyForStabile($stabileId);
|
|
$fallbackYears = array_values(array_filter(
|
|
array_map('intval', $policy['countable_years'] ?? []),
|
|
static fn(int $year): bool => $year >= 2000 && $year <= 2100,
|
|
));
|
|
|
|
if ($stabileId <= 0 || ! Schema::hasTable('gestioni_contabili')) {
|
|
return [
|
|
'years' => $fallbackYears,
|
|
'labels' => array_map(fn(int $year): string => $this->legacyGestioneLabel($year), $fallbackYears),
|
|
'date_start' => null,
|
|
'date_end' => null,
|
|
];
|
|
}
|
|
|
|
$rows = DB::table('gestioni_contabili')
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo_gestione', 'ordinaria')
|
|
->where(function ($query): void {
|
|
$query->where('gestione_attiva', true)
|
|
->orWhere('stato', 'aperta');
|
|
})
|
|
->orderBy('data_inizio')
|
|
->orderBy('id')
|
|
->get(['denominazione', 'anno_gestione', 'data_inizio', 'data_fine']);
|
|
|
|
if ($rows->isEmpty()) {
|
|
return [
|
|
'years' => $fallbackYears,
|
|
'labels' => array_map(fn(int $year): string => $this->legacyGestioneLabel($year), $fallbackYears),
|
|
'date_start' => null,
|
|
'date_end' => null,
|
|
];
|
|
}
|
|
|
|
$labels = [];
|
|
$years = [];
|
|
$start = null;
|
|
$end = null;
|
|
|
|
foreach ($rows as $row) {
|
|
$year = is_numeric($row->anno_gestione ?? null) ? (int) $row->anno_gestione : null;
|
|
if ($year) {
|
|
$years[] = $year;
|
|
}
|
|
|
|
$label = $this->extractLegacyGestioneLabel((string) ($row->denominazione ?? ''));
|
|
if ($label === null && $year) {
|
|
$label = $this->legacyGestioneLabel($year);
|
|
}
|
|
|
|
if ($label !== null) {
|
|
$labels[] = $label;
|
|
}
|
|
|
|
$rowStart = $this->normalizeDate($row->data_inizio ?? null);
|
|
$rowEnd = $this->normalizeDate($row->data_fine ?? null);
|
|
|
|
if ($rowStart !== null && ($start === null || $rowStart < $start)) {
|
|
$start = $rowStart;
|
|
}
|
|
|
|
if ($rowEnd !== null && ($end === null || $rowEnd > $end)) {
|
|
$end = $rowEnd;
|
|
}
|
|
}
|
|
|
|
$years = array_values(array_unique(array_map('intval', $years)));
|
|
sort($years);
|
|
|
|
$labels = array_values(array_unique(array_filter($labels)));
|
|
|
|
return [
|
|
'years' => $years !== [] ? $years : $fallbackYears,
|
|
'labels' => $labels !== [] ? $labels : array_map(fn(int $year): string => $this->legacyGestioneLabel($year), $fallbackYears),
|
|
'date_start' => $start,
|
|
'date_end' => $end,
|
|
];
|
|
}
|
|
|
|
public function searchReceivableTargets(int $stabileId, string $term, int $limit = 15): array
|
|
{
|
|
$needle = trim($term);
|
|
if ($stabileId <= 0 || $needle === '' || ! Schema::hasTable('unita_immobiliari')) {
|
|
return [];
|
|
}
|
|
|
|
$rows = DB::table('unita_immobiliari as ui')
|
|
->leftJoin('soggetti as s', 's.old_id', '=', 'ui.legacy_cond_id')
|
|
->where('ui.stabile_id', $stabileId)
|
|
->whereNotNull('ui.legacy_cond_id')
|
|
->where(function ($query) use ($needle): void {
|
|
$like = '%' . $needle . '%';
|
|
$query->where('ui.codice_unita', 'like', $like)
|
|
->orWhere('ui.denominazione', 'like', $like)
|
|
->orWhere('ui.scala', 'like', $like)
|
|
->orWhere('ui.interno', 'like', $like)
|
|
->orWhereRaw("CONCAT(COALESCE(ui.scala, ''), '/', COALESCE(ui.interno, '')) like ?", [$like])
|
|
->orWhere('s.nome', 'like', $like)
|
|
->orWhere('s.cognome', 'like', $like)
|
|
->orWhere('s.ragione_sociale', 'like', $like)
|
|
->orWhere('s.codice_fiscale', 'like', $like);
|
|
})
|
|
->orderBy('ui.scala')
|
|
->orderBy('ui.interno')
|
|
->orderBy('ui.id')
|
|
->limit(max(1, min($limit, 50)))
|
|
->get([
|
|
'ui.id as unita_id',
|
|
'ui.codice_unita',
|
|
'ui.denominazione',
|
|
'ui.scala',
|
|
'ui.interno',
|
|
'ui.legacy_cond_id',
|
|
's.id as soggetto_id',
|
|
's.nome as soggetto_nome',
|
|
's.cognome as soggetto_cognome',
|
|
's.ragione_sociale as soggetto_rs',
|
|
's.codice_fiscale as soggetto_cf',
|
|
]);
|
|
|
|
return $rows->map(function (object $row): array {
|
|
$subjectLabel = trim((string) (($row->soggetto_rs ?? '') ?: trim((string) (($row->soggetto_nome ?? '') . ' ' . ($row->soggetto_cognome ?? '')))));
|
|
$unitLabelParts = array_filter([
|
|
trim((string) ($row->codice_unita ?? '')),
|
|
trim((string) ($row->denominazione ?? '')),
|
|
trim((string) ($row->scala ?? '')) !== '' || trim((string) ($row->interno ?? '')) !== ''
|
|
? ('Scala ' . trim((string) ($row->scala ?? '—')) . ' / Int. ' . trim((string) ($row->interno ?? '—')))
|
|
: null,
|
|
]);
|
|
|
|
return [
|
|
'legacy_cond_id' => trim((string) ($row->legacy_cond_id ?? '')),
|
|
'unita_id' => is_numeric($row->unita_id ?? null) ? (int) $row->unita_id : null,
|
|
'soggetto_id' => is_numeric($row->soggetto_id ?? null) ? (int) $row->soggetto_id : null,
|
|
'label' => implode(' · ', $unitLabelParts),
|
|
'subject_label' => $subjectLabel,
|
|
'subject_cf' => trim((string) ($row->soggetto_cf ?? '')) ?: null,
|
|
'scala' => trim((string) ($row->scala ?? '')) ?: null,
|
|
'interno' => trim((string) ($row->interno ?? '')) ?: null,
|
|
];
|
|
})->all();
|
|
}
|
|
|
|
public function ledgerForLegacyCondomin(int $stabileId, string $legacyCondId, ?string $paymentDate = null, int $limit = 120, ?array $allowedGestioni = null): array
|
|
{
|
|
$stabile = $this->resolveStabile($stabileId);
|
|
$codiceStabile = trim((string) ($stabile['codice_stabile'] ?? ''));
|
|
$legacyCondId = trim($legacyCondId);
|
|
$operativeContext = $this->operativeOrdinaryContext($stabileId);
|
|
$allowedGestioni = $allowedGestioni !== null
|
|
? array_values(array_filter($allowedGestioni))
|
|
: array_values(array_filter($operativeContext['labels'] ?? []));
|
|
|
|
if ($stabileId <= 0 || $codiceStabile === '' || $legacyCondId === '' || ! Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) {
|
|
return [
|
|
'meta' => ['stabile_id' => $stabileId, 'codice_stabile' => $codiceStabile ?: null, 'legacy_cond_id' => $legacyCondId ?: null],
|
|
'paid' => [],
|
|
'due' => [],
|
|
'due_ordinary' => [],
|
|
'due_extraordinary' => [],
|
|
'totals' => ['paid' => 0.0, 'due' => 0.0],
|
|
];
|
|
}
|
|
|
|
$ordinaryQuery = DB::connection('gescon_import')
|
|
->table('rate_emissioni_dettaglio')
|
|
->where('cod_stabile', $codiceStabile)
|
|
->where('cod_cond', $legacyCondId);
|
|
|
|
if ($allowedGestioni !== []) {
|
|
$ordinaryQuery->where(function ($builder) use ($allowedGestioni): void {
|
|
foreach ($allowedGestioni as $label) {
|
|
$builder->orWhere('anno_gestione', $label);
|
|
}
|
|
$builder->orWhereNull('anno_gestione');
|
|
});
|
|
}
|
|
|
|
if ($paymentDate) {
|
|
$end = Carbon::parse($paymentDate)->copy()->addDays(60)->format('Y-m-d');
|
|
$start = Carbon::parse($paymentDate)->copy()->subMonths(24)->format('Y-m-d');
|
|
$ordinaryQuery->where(function ($builder) use ($start, $end): void {
|
|
$builder->whereBetween('data_emissione', [$start, $end])
|
|
->orWhereNull('data_emissione');
|
|
});
|
|
}
|
|
|
|
$rows = $ordinaryQuery
|
|
->orderBy('data_emissione')
|
|
->orderBy('numero_ricevuta')
|
|
->limit(max(1, min($limit, 300)))
|
|
->get([
|
|
'id',
|
|
'numero_emissione',
|
|
'anno_emissione',
|
|
'anno_gestione',
|
|
'cod_cond',
|
|
'cond_inq',
|
|
'data_emissione',
|
|
'descrizione',
|
|
'numero_ricevuta',
|
|
'numero_mese',
|
|
'importo_dovuto_euro',
|
|
'gia_pagato',
|
|
'residuo_emesso',
|
|
'raggruppamento',
|
|
'numero_straordinaria',
|
|
'legacy_year',
|
|
])
|
|
->map(function (object $row): array {
|
|
$importoOriginario = (float) ($row->importo_dovuto_euro ?? 0);
|
|
$residuo = $row->residuo_emesso !== null ? (float) $row->residuo_emesso : $importoOriginario;
|
|
$pagato = (float) ($row->gia_pagato ?? 0);
|
|
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'numero_emissione' => is_numeric($row->numero_emissione ?? null) ? (int) $row->numero_emissione : null,
|
|
'anno_emissione' => trim((string) ($row->anno_emissione ?? '')) ?: null,
|
|
'gestione' => $row->anno_gestione,
|
|
'cod_cond' => $row->cod_cond,
|
|
'ci' => $row->cond_inq,
|
|
'data_emissione' => $this->normalizeDate($row->data_emissione ?? null),
|
|
'descrizione' => trim((string) ($row->descrizione ?? '')) ?: null,
|
|
'ricevuta' => $row->numero_ricevuta,
|
|
'mese' => $row->numero_mese,
|
|
'importo_originario' => $importoOriginario,
|
|
'gia_pagato' => $pagato,
|
|
'residuo' => $residuo,
|
|
'raggruppamento' => $row->raggruppamento,
|
|
'numero_straordinaria' => is_numeric($row->numero_straordinaria ?? null) ? (int) $row->numero_straordinaria : null,
|
|
'legacy_year' => trim((string) ($row->legacy_year ?? '')) ?: null,
|
|
];
|
|
})
|
|
->values();
|
|
|
|
$extraordinaryContext = $this->buildExtraordinaryContext($stabileId, $codiceStabile);
|
|
$extraordinaryRows = $this->loadPersistentExtraordinaryRows($codiceStabile, $legacyCondId, $limit, $extraordinaryContext, $allowedGestioni);
|
|
|
|
$allRows = $rows->merge($extraordinaryRows)->unique('id')->values();
|
|
$emissionLabels = $this->loadEmissionDescriptions($codiceStabile, $allRows->pluck('numero_emissione')->filter()->all());
|
|
|
|
$classifiedRows = $allRows
|
|
->map(fn(array $row): array=> $this->classifyLedgerRow($row, $emissionLabels, $extraordinaryContext))
|
|
->values();
|
|
|
|
$paid = $classifiedRows
|
|
->filter(fn(array $row): bool => abs((float) ($row['gia_pagato'] ?? 0)) > 0.0001)
|
|
->values()
|
|
->all();
|
|
|
|
$due = $classifiedRows
|
|
->filter(fn(array $row): bool => abs((float) ($row['residuo'] ?? 0)) > 0.0001)
|
|
->values()
|
|
->all();
|
|
|
|
$dueOrdinary = array_values(array_filter($due, fn(array $row): bool => ($row['bucket'] ?? 'ordinary') !== 'extraordinary'));
|
|
$dueExtraordinary = array_values(array_filter($due, fn(array $row): bool => ($row['bucket'] ?? null) === 'extraordinary'));
|
|
$summary = [
|
|
'by_bucket' => $this->summarizeRowsByBucket($dueOrdinary, $dueExtraordinary),
|
|
'by_gestione' => $this->summarizeRowsByGestione($due),
|
|
'extraordinary_by_work' => $this->summarizeExtraordinaryByWork($dueExtraordinary),
|
|
'extraordinary_missing_works' => $this->detectMissingExtraordinaryWorks($dueExtraordinary, $extraordinaryContext),
|
|
];
|
|
|
|
return [
|
|
'meta' => [
|
|
'stabile_id' => $stabileId,
|
|
'codice_stabile' => $codiceStabile,
|
|
'legacy_cond_id' => $legacyCondId,
|
|
],
|
|
'paid' => $paid,
|
|
'due' => $due,
|
|
'due_ordinary' => $dueOrdinary,
|
|
'due_extraordinary' => $dueExtraordinary,
|
|
'summary' => $summary,
|
|
'totals' => [
|
|
'paid' => array_reduce($paid, fn(float $carry, array $row): float => $carry + (float) ($row['gia_pagato'] ?? 0), 0.0),
|
|
'due' => array_reduce($due, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0),
|
|
'due_ordinary' => array_reduce($dueOrdinary, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0),
|
|
'due_extraordinary' => array_reduce($dueExtraordinary, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0),
|
|
],
|
|
];
|
|
}
|
|
|
|
public function forBankMovement(int $movementId, int $rateLimit = 30): array
|
|
{
|
|
if (! Schema::hasTable('contabilita_movimenti_banca')) {
|
|
return [
|
|
'meta' => ['source' => 'contabilita_movimenti_banca', 'records_count' => 0],
|
|
'records' => [],
|
|
'export_text' => '',
|
|
];
|
|
}
|
|
|
|
$movement = DB::table('contabilita_movimenti_banca')
|
|
->where('id', $movementId)
|
|
->first();
|
|
|
|
if (! $movement) {
|
|
return [
|
|
'meta' => ['source' => 'contabilita_movimenti_banca', 'records_count' => 0],
|
|
'records' => [],
|
|
'export_text' => '',
|
|
];
|
|
}
|
|
|
|
$stabile = $this->resolveStabile((int) ($movement->stabile_id ?? 0));
|
|
$description = trim((string) (($movement->descrizione_estesa ?? '') ?: ($movement->descrizione ?? '')));
|
|
$parsed = $this->parseBankDescription($description);
|
|
$legacyCondomin = $this->resolveLegacyCondominByMovement((int) ($movement->stabile_id ?? 0), (string) ($stabile['codice_stabile'] ?? ''), $parsed);
|
|
$legacyCondId = trim((string) ($legacyCondomin['legacy_cond_id'] ?? '')) ?: null;
|
|
$scala = $parsed['scala'] ?? $legacyCondomin['scala'] ?? null;
|
|
$interno = $parsed['interno'] ?? $legacyCondomin['interno'] ?? null;
|
|
$nominativo = $legacyCondomin['nome'] ?? $parsed['nominativo'] ?? null;
|
|
$operativeContext = $this->operativeOrdinaryContext((int) ($movement->stabile_id ?? 0));
|
|
$visibleLabels = $this->visibleOrdinaryCompetenceLabels((int) ($movement->stabile_id ?? 0));
|
|
$subjectId = is_numeric($legacyCondomin['soggetto_id'] ?? null) ? (int) $legacyCondomin['soggetto_id'] : null;
|
|
$unitId = is_numeric($legacyCondomin['unita_id'] ?? null) ? (int) $legacyCondomin['unita_id'] : null;
|
|
|
|
$candidateRates = $this->findRateCandidates(
|
|
(string) ($stabile['codice_stabile'] ?? ''),
|
|
$legacyCondId,
|
|
null,
|
|
$this->normalizeDate($movement->data ?? null),
|
|
$visibleLabels !== [] ? $visibleLabels : ($operativeContext['labels'] ?? []),
|
|
$rateLimit,
|
|
);
|
|
|
|
$record = [
|
|
'key' => 'mov:' . (int) $movement->id,
|
|
'source' => 'contabilita_movimenti_banca',
|
|
'protocollo' => null,
|
|
'num_incasso' => null,
|
|
'data_pag' => $this->normalizeDate($movement->data ?? null),
|
|
'data_pag_label' => ! empty($movement->data) ? Carbon::parse((string) $movement->data)->format('d/m/Y') : '—',
|
|
'id_condomino' => $legacyCondId,
|
|
'sc_int' => $scala && $interno ? strtoupper((string) $scala) . '/' . $interno : null,
|
|
'nome_condomino' => $nominativo,
|
|
'descrizione_aggintiva' => $description,
|
|
'importo' => (float) ($movement->importo ?? 0),
|
|
'importo_label' => '€ ' . number_format((float) ($movement->importo ?? 0), 2, ',', '.'),
|
|
'subject_id' => $subjectId,
|
|
'unit_id' => $unitId,
|
|
'payer_reference' => [
|
|
'holder_name' => $parsed['ordinante_nome'] ?? null,
|
|
'holder_iban' => $parsed['ordinante_iban'] ?? null,
|
|
'bank_label' => $parsed['banca_ordinante'] ?? null,
|
|
'bic' => $parsed['banca_bic'] ?? null,
|
|
'cro' => $parsed['cro'] ?? null,
|
|
'fingerprint' => $parsed['bank_reference_fingerprint'] ?? null,
|
|
'matched_from_reference' => (bool) ($legacyCondomin['matched_from_reference'] ?? false),
|
|
],
|
|
'incasso_candidates' => [],
|
|
'movimento_candidates' => [[
|
|
'id' => (int) $movement->id,
|
|
'score' => 100.0,
|
|
'data' => $this->normalizeDate($movement->data ?? null),
|
|
'data_label' => ! empty($movement->data) ? Carbon::parse((string) $movement->data)->format('d/m/Y') : '—',
|
|
'importo' => (float) ($movement->importo ?? 0),
|
|
'importo_label' => '€ ' . number_format((float) ($movement->importo ?? 0), 2, ',', '.'),
|
|
'label' => $description,
|
|
'nome' => $nominativo,
|
|
'scala_interno' => $scala && $interno ? strtoupper((string) $scala) . '/' . $interno : null,
|
|
'riferimento' => 'movimento_banca',
|
|
'type' => 'movimento_operativo',
|
|
]],
|
|
'candidate_rates' => $candidateRates,
|
|
'ai_payload' => [
|
|
'source' => 'contabilita_movimenti_banca',
|
|
'stabile_id' => (int) ($movement->stabile_id ?? 0),
|
|
'codice_stabile' => $stabile['codice_stabile'] ?? null,
|
|
'bank_movement' => [
|
|
'id' => (int) $movement->id,
|
|
'data' => $this->normalizeDate($movement->data ?? null),
|
|
'importo' => (float) ($movement->importo ?? 0),
|
|
'descrizione_estesa' => $description,
|
|
'scala' => $scala,
|
|
'interno' => $interno,
|
|
'nominativo' => $nominativo,
|
|
'ordinante_nome' => $parsed['ordinante_nome'] ?? null,
|
|
'ordinante_iban' => $parsed['ordinante_iban'] ?? null,
|
|
'banca_ordinante' => $parsed['banca_ordinante'] ?? null,
|
|
'banca_bic' => $parsed['banca_bic'] ?? null,
|
|
'cro' => $parsed['cro'] ?? null,
|
|
'bank_reference_fingerprint' => $parsed['bank_reference_fingerprint'] ?? null,
|
|
],
|
|
'candidate_rates' => $candidateRates,
|
|
],
|
|
];
|
|
$record['ai_payload_pretty'] = json_encode($record['ai_payload'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
$meta = [
|
|
'stabile_id' => (int) ($movement->stabile_id ?? 0),
|
|
'codice_stabile' => $stabile['codice_stabile'] ?? null,
|
|
'stabile_label' => $stabile['label'] ?? null,
|
|
'source' => 'contabilita_movimenti_banca',
|
|
'source_label' => 'Movimento banca operativo',
|
|
'records_count' => 1,
|
|
];
|
|
|
|
return [
|
|
'meta' => $meta,
|
|
'records' => [$record],
|
|
'export_text' => $this->buildExportText($meta, [$record]),
|
|
];
|
|
}
|
|
|
|
public function forStabile(int $stabileId, int $limit = 20): array
|
|
{
|
|
$limit = max(1, min($limit, 100));
|
|
$stabile = $this->resolveStabile($stabileId);
|
|
|
|
$rows = $this->loadDomainRows($stabileId, $limit);
|
|
$source = 'incassi_estratto_conto';
|
|
|
|
if ($rows === []) {
|
|
$rows = $this->loadStagingRows($stabile['codice_stabile'] ?? null, $limit);
|
|
$source = 'gescon_import.in_da_ec';
|
|
}
|
|
|
|
if ($rows === []) {
|
|
$rows = $this->loadApplicationIncassiRows($stabileId, $limit);
|
|
$source = 'netgescon.incassi';
|
|
}
|
|
|
|
$records = array_map(function (object $row) use ($stabileId, $stabile, $source): array {
|
|
return $this->normalizeRow($row, $stabileId, $stabile, $source);
|
|
}, $rows);
|
|
|
|
$meta = [
|
|
'stabile_id' => $stabileId,
|
|
'codice_stabile' => $stabile['codice_stabile'] ?? null,
|
|
'stabile_label' => $stabile['label'] ?? null,
|
|
'source' => $source,
|
|
'source_label' => match ($source) {
|
|
'incassi_estratto_conto' => 'Dominio incassi_estratto_conto',
|
|
'netgescon.incassi' => 'Incassi importati da singolo_anno.mdb',
|
|
default => 'Staging gescon_import.in_da_ec',
|
|
},
|
|
'records_count' => count($records),
|
|
];
|
|
|
|
return [
|
|
'meta' => $meta,
|
|
'records' => $records,
|
|
'export_text' => $this->buildExportText($meta, $records),
|
|
];
|
|
}
|
|
|
|
private function resolveStabile(int $stabileId): array
|
|
{
|
|
if (! Schema::hasTable('stabili')) {
|
|
return [];
|
|
}
|
|
|
|
$row = DB::table('stabili')
|
|
->where('id', $stabileId)
|
|
->first(['id', 'codice_stabile', 'denominazione']);
|
|
|
|
if (! $row) {
|
|
return [];
|
|
}
|
|
|
|
$codice = trim((string) ($row->codice_stabile ?? ''));
|
|
$denominazione = trim((string) ($row->denominazione ?? ''));
|
|
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'codice_stabile' => $codice !== '' ? $codice : null,
|
|
'label' => trim($codice . ' ' . $denominazione) ?: ($codice !== '' ? $codice : ('Stabile #' . $row->id)),
|
|
];
|
|
}
|
|
|
|
private function loadDomainRows(int $stabileId, int $limit): array
|
|
{
|
|
if (! Schema::hasTable('incassi_estratto_conto') || ! Schema::hasTable('gestioni_contabili')) {
|
|
return [];
|
|
}
|
|
|
|
return DB::table('incassi_estratto_conto as iec')
|
|
->join('gestioni_contabili as gc', 'gc.id', '=', 'iec.gestione_id')
|
|
->where('gc.stabile_id', $stabileId)
|
|
->orderByDesc('iec.data_operazione')
|
|
->orderByDesc('iec.id')
|
|
->limit($limit)
|
|
->get([
|
|
'iec.*',
|
|
'gc.stabile_id as gestione_stabile_id',
|
|
'gc.anno_gestione as gestione_anno',
|
|
])
|
|
->all();
|
|
}
|
|
|
|
private function loadStagingRows(?string $codiceStabile, int $limit): array
|
|
{
|
|
if ($codiceStabile === null || ! Schema::connection('gescon_import')->hasTable('in_da_ec')) {
|
|
return [];
|
|
}
|
|
|
|
$trimmed = ltrim($codiceStabile, '0');
|
|
|
|
return DB::connection('gescon_import')
|
|
->table('in_da_ec')
|
|
->where(function ($query) use ($codiceStabile, $trimmed): void {
|
|
$query->where('cod_stabile', $codiceStabile);
|
|
if ($trimmed !== '' && $trimmed !== $codiceStabile) {
|
|
$query->orWhere('cod_stabile', $trimmed);
|
|
}
|
|
})
|
|
->orderByDesc('data_pag')
|
|
->orderByDesc('protocollo')
|
|
->limit($limit)
|
|
->get()
|
|
->all();
|
|
}
|
|
|
|
private function loadApplicationIncassiRows(int $stabileId, int $limit): array
|
|
{
|
|
if (! Schema::hasTable('incassi')) {
|
|
return [];
|
|
}
|
|
|
|
return DB::table('incassi')
|
|
->where('condominio_id', $stabileId)
|
|
->where(function ($query): void {
|
|
$query->whereNull('movimento_bancario_id')
|
|
->orWhere('movimento_bancario_id', 0);
|
|
})
|
|
->where(function ($query): void {
|
|
$query->whereNull('stato_riconciliazione')
|
|
->orWhereNotIn('stato_riconciliazione', ['automatica', 'manuale', 'riconciliato']);
|
|
})
|
|
->orderByDesc('data_pagamento')
|
|
->orderByDesc('dt_empag')
|
|
->orderByDesc('id')
|
|
->limit($limit)
|
|
->get([
|
|
'id',
|
|
'id_incasso_gescon',
|
|
'cod_cond_gescon',
|
|
'cond_inquil',
|
|
'n_riferimento',
|
|
'n_ricevuta',
|
|
'anno_rif',
|
|
'importo_pagato',
|
|
'importo_pagato_euro',
|
|
'dt_empag',
|
|
'data_pagamento',
|
|
'descrizione',
|
|
'stato_riconciliazione',
|
|
'metadati_gescon',
|
|
])
|
|
->all();
|
|
}
|
|
|
|
private function normalizeRow(object $row, int $stabileId, array $stabile, string $source): array
|
|
{
|
|
$meta = $this->decodeArray($row->metadati_gescon ?? $row->payload ?? null);
|
|
|
|
$protocollo = $meta['protocollo'] ?? $row->protocollo ?? $row->id_gescon ?? $row->id_incasso_gescon ?? null;
|
|
$numIncasso = $meta['num_incasso'] ?? $row->num_incasso ?? $row->riferimento_bancario ?? $row->n_ricevuta ?? $row->n_riferimento ?? null;
|
|
$idCondomino = $meta['id_condomino'] ?? $row->id_condomino ?? $row->cod_cond_gescon ?? null;
|
|
$scalaInterno = trim((string) ($meta['sc_int'] ?? $row->sc_int ?? ''));
|
|
if ($scalaInterno === '' && ! empty($row->pattern_scala) && ! empty($row->pattern_interno)) {
|
|
$scalaInterno = strtoupper(trim((string) $row->pattern_scala)) . '/' . trim((string) $row->pattern_interno);
|
|
}
|
|
|
|
$nomeCondomino = trim((string) ($meta['nome_condomino'] ?? $meta['nominativo'] ?? $row->nome_condomino ?? $row->ordinante ?? $row->pattern_nome_estratto ?? ''));
|
|
$descrizioneAggiuntiva = trim((string) ($meta['descrizione_aggintiva'] ?? $row->descrizione_aggintiva ?? $row->descrizione ?? ''));
|
|
$importo = (float) ($meta['importo'] ?? $row->importo ?? $row->importo_pagato_euro ?? $row->importo_pagato ?? 0);
|
|
$dataRaw = $meta['data_pag'] ?? $row->data_pag ?? $row->data_operazione ?? $row->data_pagamento ?? $row->dt_empag ?? null;
|
|
$data = $this->normalizeDate($dataRaw);
|
|
|
|
$incassoCandidates = $source === 'netgescon.incassi'
|
|
? [[
|
|
'id' => (int) ($row->id ?? 0),
|
|
'score' => 100.0,
|
|
'data' => $data,
|
|
'data_label' => $data ? Carbon::parse($data)->format('d/m/Y') : '—',
|
|
'importo' => $importo,
|
|
'importo_label' => '€ ' . number_format($importo, 2, ',', '.'),
|
|
'label' => $descrizioneAggiuntiva !== '' ? $descrizioneAggiuntiva : null,
|
|
'nome' => $nomeCondomino !== '' ? $nomeCondomino : null,
|
|
'scala_interno' => $scalaInterno !== '' ? $scalaInterno : null,
|
|
'riferimento' => trim((string) (($row->n_ricevuta ?? '') !== '' ? 'Ric. ' . $row->n_ricevuta : ($row->n_riferimento ?? ''))),
|
|
'type' => 'incasso',
|
|
]]
|
|
: $this->findIncassoCandidates($stabileId, $importo, $data, $nomeCondomino, $scalaInterno);
|
|
$movimentoCandidates = $this->findMovimentoCandidates($stabileId, $importo, $data, $nomeCondomino, $scalaInterno);
|
|
|
|
$payload = [
|
|
'source' => $source,
|
|
'stabile_id' => $stabileId,
|
|
'codice_stabile' => $stabile['codice_stabile'] ?? null,
|
|
'legacy_entry' => [
|
|
'incasso_id' => $source === 'netgescon.incassi' ? (int) ($row->id ?? 0) : null,
|
|
'protocollo' => $protocollo,
|
|
'num_incasso' => $numIncasso,
|
|
'data_pag' => $data,
|
|
'id_condomino' => $idCondomino,
|
|
'sc_int' => $scalaInterno !== '' ? $scalaInterno : null,
|
|
'nome_condomino' => $nomeCondomino !== '' ? $nomeCondomino : null,
|
|
'descrizione_aggintiva' => $descrizioneAggiuntiva !== '' ? $descrizioneAggiuntiva : null,
|
|
'importo' => $importo,
|
|
],
|
|
'incasso_candidates' => $incassoCandidates,
|
|
'movimento_candidates' => $movimentoCandidates,
|
|
'candidate_rates' => $this->findRateCandidates(
|
|
(string) ($stabile['codice_stabile'] ?? ''),
|
|
trim((string) $idCondomino) ?: null,
|
|
trim((string) ($row->cond_inquil ?? $meta['cond_inquil'] ?? '')) ?: null,
|
|
$data,
|
|
$this->visibleOrdinaryCompetenceLabels($stabileId),
|
|
20,
|
|
),
|
|
];
|
|
|
|
return [
|
|
'key' => match ($source) {
|
|
'incassi_estratto_conto' => 'dom:',
|
|
'netgescon.incassi' => 'inc:',
|
|
default => 'stg:',
|
|
} . (string) ($row->id ?? $protocollo ?? spl_object_id($row)),
|
|
'source' => $source,
|
|
'protocollo' => $protocollo,
|
|
'num_incasso' => $numIncasso,
|
|
'data_pag' => $data,
|
|
'data_pag_label' => $data ? Carbon::parse($data)->format('d/m/Y') : '—',
|
|
'id_condomino' => $idCondomino,
|
|
'sc_int' => $scalaInterno !== '' ? $scalaInterno : null,
|
|
'nome_condomino' => $nomeCondomino !== '' ? $nomeCondomino : null,
|
|
'descrizione_aggintiva' => $descrizioneAggiuntiva !== '' ? $descrizioneAggiuntiva : null,
|
|
'importo' => $importo,
|
|
'importo_label' => '€ ' . number_format($importo, 2, ',', '.'),
|
|
'incasso_candidates' => $incassoCandidates,
|
|
'movimento_candidates' => $movimentoCandidates,
|
|
'candidate_rates' => $payload['candidate_rates'],
|
|
'ai_payload' => $payload,
|
|
'ai_payload_pretty' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
];
|
|
}
|
|
|
|
private function findRateCandidates(string $codiceStabile, ?string $legacyCondId, ?string $condInq, ?string $paymentDate, array $allowedGestioni = [], int $limit = 30): array
|
|
{
|
|
if ($codiceStabile === '' || $legacyCondId === null || ! Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) {
|
|
return [];
|
|
}
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('rate_emissioni_dettaglio')
|
|
->where('cod_stabile', $codiceStabile)
|
|
->where('cod_cond', $legacyCondId);
|
|
|
|
if ($condInq !== null && $condInq !== '') {
|
|
$query->where('cond_inq', strtoupper($condInq));
|
|
}
|
|
|
|
if ($paymentDate) {
|
|
$end = Carbon::parse($paymentDate)->copy()->addDays(45)->format('Y-m-d');
|
|
$start = Carbon::parse($paymentDate)->copy()->subMonths(18)->format('Y-m-d');
|
|
$query->where(function ($builder) use ($start, $end): void {
|
|
$builder->whereBetween('data_emissione', [$start, $end])
|
|
->orWhereNull('data_emissione');
|
|
});
|
|
|
|
$allowedGestioni = $allowedGestioni !== [] ? array_values(array_unique(array_filter($allowedGestioni))) : $this->nearbyLegacyGestioneLabels($paymentDate);
|
|
$query->where(function ($builder) use ($allowedGestioni): void {
|
|
foreach ($allowedGestioni as $label) {
|
|
$builder->orWhere('anno_gestione', $label);
|
|
}
|
|
$builder
|
|
->orWhereNull('anno_gestione');
|
|
});
|
|
} elseif ($allowedGestioni !== []) {
|
|
$allowedGestioni = array_values(array_unique(array_filter($allowedGestioni)));
|
|
$query->where(function ($builder) use ($allowedGestioni): void {
|
|
foreach ($allowedGestioni as $label) {
|
|
$builder->orWhere('anno_gestione', $label);
|
|
}
|
|
$builder->orWhereNull('anno_gestione');
|
|
});
|
|
}
|
|
|
|
$query->where(function ($builder): void {
|
|
$builder->where('residuo_emesso', '!=', 0)
|
|
->orWhere(function ($inner): void {
|
|
$inner->whereNull('residuo_emesso')
|
|
->where('importo_dovuto_euro', '!=', 0);
|
|
});
|
|
});
|
|
|
|
return $query
|
|
->orderBy('data_emissione')
|
|
->orderBy('numero_ricevuta')
|
|
->limit(max(1, min($limit, 100)))
|
|
->get([
|
|
'id',
|
|
'anno_gestione',
|
|
'cod_cond',
|
|
'cond_inq',
|
|
'data_emissione',
|
|
'descrizione',
|
|
'numero_ricevuta',
|
|
'numero_mese',
|
|
'importo_dovuto_euro',
|
|
'gia_pagato',
|
|
'residuo_emesso',
|
|
'raggruppamento',
|
|
])
|
|
->map(function (object $row): array {
|
|
$importo = (float) ($row->residuo_emesso ?? $row->importo_dovuto_euro ?? 0);
|
|
if (abs($importo) < 0.0001) {
|
|
$importo = (float) ($row->importo_dovuto_euro ?? 0);
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'gestione' => $row->anno_gestione,
|
|
'cod_cond' => $row->cod_cond,
|
|
'ci' => $row->cond_inq,
|
|
'data_emissione' => $this->normalizeDate($row->data_emissione ?? null),
|
|
'descrizione' => trim((string) ($row->descrizione ?? '')) ?: null,
|
|
'ricevuta' => $row->numero_ricevuta,
|
|
'mese' => $row->numero_mese,
|
|
'importo' => $importo,
|
|
'importo_originario' => (float) ($row->importo_dovuto_euro ?? 0),
|
|
'gia_pagato' => (float) ($row->gia_pagato ?? 0),
|
|
'residuo' => $row->residuo_emesso !== null ? (float) $row->residuo_emesso : null,
|
|
'raggruppamento' => $row->raggruppamento,
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function parseBankDescription(string $description): array
|
|
{
|
|
$scala = null;
|
|
$interno = null;
|
|
if (preg_match('/\bsc\.?\s*([A-Z])\s*(?:\/|-|\s)?\s*int\.?\s*(\d+[A-Z]?)/i', $description, $matches)) {
|
|
$scala = strtoupper($matches[1]);
|
|
$interno = strtoupper($matches[2]);
|
|
}
|
|
|
|
$nominativo = null;
|
|
if (preg_match('/A VOSTRO FAVORE\s+(.+?)\s+Data Regolamento:/i', $description, $matches)) {
|
|
$nominativo = ucwords(strtolower(trim($matches[1])));
|
|
}
|
|
if (preg_match('/\b(BARONE\s+MICHELE)\b/i', $description, $matches)) {
|
|
$nominativo = ucwords(strtolower($matches[1]));
|
|
}
|
|
|
|
$ordinanteIban = null;
|
|
if (preg_match('/Coord\.Ordinante:\s*(.+?)(?:\s+Banca Ordinante:|\s+Cro:|\s+Note:|\s+Id\.Operazione:|$)/i', $description, $matches)) {
|
|
$ordinanteIban = $this->normalizeIbanValue($matches[1]);
|
|
}
|
|
|
|
$bancaOrdinante = null;
|
|
$bancaBic = null;
|
|
if (preg_match('/Banca Ordinante:\s*(.+?)(?:\s+Cro:|\s+Note:|\s+Id\.Operazione:|$)/i', $description, $matches)) {
|
|
$bancaOrdinante = trim($matches[1]);
|
|
if (preg_match('/([A-Z0-9]{8,11})\s*$/', $bancaOrdinante, $bicMatches)) {
|
|
$bancaBic = strtoupper($bicMatches[1]);
|
|
}
|
|
}
|
|
|
|
$cro = null;
|
|
if (preg_match('/Cro:\s*([^\s]+)(?:\s+Note:|\s+Id\.Operazione:|$)/i', $description, $matches)) {
|
|
$cro = trim($matches[1]);
|
|
}
|
|
|
|
$ordinanteNome = $nominativo;
|
|
$fingerprint = $this->buildBankReferenceFingerprint($ordinanteIban, $bancaOrdinante, $bancaBic, $ordinanteNome);
|
|
|
|
return [
|
|
'scala' => $scala,
|
|
'interno' => $interno,
|
|
'nominativo' => $nominativo,
|
|
'ordinante_nome' => $ordinanteNome,
|
|
'ordinante_iban' => $ordinanteIban,
|
|
'banca_ordinante' => $bancaOrdinante,
|
|
'banca_bic' => $bancaBic,
|
|
'cro' => $cro,
|
|
'bank_reference_fingerprint' => $fingerprint,
|
|
];
|
|
}
|
|
|
|
private function nearbyLegacyGestioneLabels(string $paymentDate): array
|
|
{
|
|
$date = Carbon::parse($paymentDate);
|
|
$year = (int) $date->format('Y');
|
|
$month = (int) $date->format('m');
|
|
$currentStart = $month >= 10 ? $year : $year - 1;
|
|
|
|
return [
|
|
$this->legacyGestioneLabel($currentStart - 1),
|
|
$this->legacyGestioneLabel($currentStart),
|
|
];
|
|
}
|
|
|
|
private function legacyGestioneLabel(int $startYear): string
|
|
{
|
|
return $startYear . '/' . substr((string) ($startYear + 1), -2);
|
|
}
|
|
|
|
private function extractLegacyGestioneLabel(string $value): ?string
|
|
{
|
|
if (preg_match('/\b((?:19|20)\d{2}\/\d{2})\b/', $value, $matches)) {
|
|
return $matches[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function resolveLegacyCondominByMovement(int $stabileId, string $codiceStabile, array $parsed): array
|
|
{
|
|
$matchedReference = $this->matchSoggettoByBankReference($stabileId, $parsed);
|
|
if ($matchedReference !== []) {
|
|
return $matchedReference;
|
|
}
|
|
|
|
if ($codiceStabile === '' || ! Schema::connection('gescon_import')->hasTable('condomin')) {
|
|
return [];
|
|
}
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('condomin')
|
|
->where('cod_stabile', $codiceStabile);
|
|
|
|
if (! empty($parsed['scala']) && ! empty($parsed['interno'])) {
|
|
$query->where('scala', $parsed['scala'])->where('interno', $parsed['interno']);
|
|
} elseif (! empty($parsed['nominativo'])) {
|
|
$condominCols = Schema::connection('gescon_import')->getColumnListing('condomin');
|
|
$concatFields = [];
|
|
foreach (['nom_cond', 'proprietario_denominazione', 'presso', 'e_mail_condomino'] as $f) {
|
|
if (in_array($f, $condominCols, true)) {
|
|
$concatFields[] = "COALESCE({$f}, '')";
|
|
}
|
|
}
|
|
$concatSql = $concatFields !== [] ? "LOWER(CONCAT_WS(' ', " . implode(', ', $concatFields) . "))" : "''";
|
|
|
|
$tokens = $this->normalizeSearchTokens((string) $parsed['nominativo']);
|
|
$query->where(function ($builder) use ($tokens, $concatSql): void {
|
|
foreach ($tokens as $token) {
|
|
$builder->whereRaw(
|
|
$concatSql . " like ?",
|
|
['%' . $token . '%']
|
|
);
|
|
}
|
|
});
|
|
} else {
|
|
return [];
|
|
}
|
|
|
|
$condominCols = Schema::connection('gescon_import')->getColumnListing('condomin');
|
|
$fields = ['cod_cond', 'id_cond', 'scala', 'interno'];
|
|
foreach (['legacy_id_cond', 'nom_cond', 'proprietario_denominazione'] as $f) {
|
|
if (in_array($f, $condominCols, true)) {
|
|
$fields[] = $f;
|
|
}
|
|
}
|
|
|
|
$row = $query
|
|
->orderByDesc('legacy_year')
|
|
->orderByDesc('id')
|
|
->first($fields);
|
|
|
|
if (! $row) {
|
|
return [];
|
|
}
|
|
|
|
$nome = null;
|
|
if (in_array('nom_cond', $condominCols, true) && ! empty($row->nom_cond)) {
|
|
$nome = $row->nom_cond;
|
|
} elseif (in_array('proprietario_denominazione', $condominCols, true) && ! empty($row->proprietario_denominazione)) {
|
|
$nome = $row->proprietario_denominazione;
|
|
}
|
|
|
|
return [
|
|
'legacy_cond_id' => trim((string) ($row->id_cond ?? (in_array('legacy_id_cond', $condominCols, true) ? ($row->legacy_id_cond ?? '') : '') ?? $row->cod_cond ?? '')),
|
|
'cod_cond' => trim((string) ($row->cod_cond ?? '')),
|
|
'scala' => trim((string) ($row->scala ?? '')) ?: null,
|
|
'interno' => trim((string) ($row->interno ?? '')) ?: null,
|
|
'nome' => trim((string) $nome) ?: null,
|
|
'matched_from_reference' => false,
|
|
];
|
|
}
|
|
|
|
private function matchSoggettoByBankReference(int $stabileId, array $parsed): array
|
|
{
|
|
if ($stabileId <= 0 || ! Schema::hasTable('soggetti_riferimenti_bancari')) {
|
|
return [];
|
|
}
|
|
|
|
$iban = $this->normalizeIbanValue($parsed['ordinante_iban'] ?? null);
|
|
$fingerprint = trim((string) ($parsed['bank_reference_fingerprint'] ?? ''));
|
|
|
|
if ($iban === null && $fingerprint === '') {
|
|
return [];
|
|
}
|
|
|
|
$query = DB::table('soggetti_riferimenti_bancari as srb')
|
|
->leftJoin('soggetti as s', 's.id', '=', 'srb.soggetto_id')
|
|
->where('srb.stabile_id', $stabileId)
|
|
->where('srb.attivo', true)
|
|
->orderByDesc('srb.ultimo_movimento_data')
|
|
->orderByDesc('srb.id');
|
|
|
|
$query->where(function ($builder) use ($iban, $fingerprint): void {
|
|
if ($iban !== null) {
|
|
$builder->orWhere('srb.ordinante_iban', $iban);
|
|
}
|
|
|
|
if ($fingerprint !== '') {
|
|
$builder->orWhere('srb.bank_reference_fingerprint', $fingerprint);
|
|
}
|
|
});
|
|
|
|
$row = $query->first([
|
|
'srb.legacy_cond_id',
|
|
'srb.soggetto_id',
|
|
'srb.unita_id',
|
|
'srb.ordinante_nome',
|
|
'srb.bank_reference_fingerprint',
|
|
's.nome as soggetto_nome',
|
|
's.cognome as soggetto_cognome',
|
|
's.ragione_sociale as soggetto_rs',
|
|
]);
|
|
|
|
if (! $row) {
|
|
return [];
|
|
}
|
|
|
|
$name = trim((string) (($row->soggetto_rs ?? '') ?: trim((string) (($row->soggetto_nome ?? '') . ' ' . ($row->soggetto_cognome ?? '')))));
|
|
if ($name === '') {
|
|
$name = trim((string) ($row->ordinante_nome ?? ''));
|
|
}
|
|
|
|
return [
|
|
'legacy_cond_id' => trim((string) ($row->legacy_cond_id ?? '')) ?: null,
|
|
'soggetto_id' => is_numeric($row->soggetto_id ?? null) ? (int) $row->soggetto_id : null,
|
|
'unita_id' => is_numeric($row->unita_id ?? null) ? (int) $row->unita_id : null,
|
|
'nome' => $name !== '' ? $name : null,
|
|
'matched_from_reference' => true,
|
|
];
|
|
}
|
|
|
|
private function normalizeIbanValue(mixed $value): ?string
|
|
{
|
|
$normalized = strtoupper(preg_replace('/\s+/', '', trim((string) ($value ?? ''))) ?? '');
|
|
|
|
return $normalized !== '' ? $normalized : null;
|
|
}
|
|
|
|
private function buildBankReferenceFingerprint(?string $ordinanteIban, ?string $bankLabel, ?string $bic, ?string $holderName): ?string
|
|
{
|
|
$parts = array_values(array_filter([
|
|
$this->normalizeIbanValue($ordinanteIban),
|
|
trim((string) ($bankLabel ?? '')) !== '' ? strtoupper(trim((string) $bankLabel)) : null,
|
|
trim((string) ($bic ?? '')) !== '' ? strtoupper(trim((string) $bic)) : null,
|
|
trim((string) ($holderName ?? '')) !== '' ? strtoupper(trim((string) $holderName)) : null,
|
|
]));
|
|
|
|
if ($parts === []) {
|
|
return null;
|
|
}
|
|
|
|
return hash('sha256', implode('|', $parts));
|
|
}
|
|
|
|
private function normalizeSearchTokens(string $value): array
|
|
{
|
|
$value = mb_strtolower(trim($value));
|
|
$value = preg_replace('/\b(avv|avv\.|dott|dott\.|ing|ing\.)\b/u', '', $value) ?? $value;
|
|
$parts = preg_split('/[^a-z0-9]+/u', $value) ?: [];
|
|
|
|
return array_values(array_filter(array_unique($parts), fn(string $part): bool => mb_strlen($part) >= 3));
|
|
}
|
|
|
|
private function findIncassoCandidates(int $stabileId, float $importo, ?string $data, string $nomeCondomino, string $scalaInterno): array
|
|
{
|
|
if (! Schema::hasTable('incassi')) {
|
|
return [];
|
|
}
|
|
|
|
$query = DB::table('incassi')
|
|
->where('condominio_id', $stabileId)
|
|
->whereBetween('importo_pagato_euro', [$importo - 1, $importo + 1])
|
|
->orderByDesc('id')
|
|
->limit(30)
|
|
->get([
|
|
'id',
|
|
'importo_pagato_euro',
|
|
'data_pagamento',
|
|
'dt_empag',
|
|
'descrizione',
|
|
'n_ricevuta',
|
|
'n_riferimento',
|
|
'metadati_gescon',
|
|
]);
|
|
|
|
return $this->scoreAndNormalizeCandidates($query->all(), $importo, $data, $nomeCondomino, $scalaInterno, 'incasso');
|
|
}
|
|
|
|
private function findMovimentoCandidates(int $stabileId, float $importo, ?string $data, string $nomeCondomino, string $scalaInterno): array
|
|
{
|
|
$operative = $this->findOperativeBankMovementCandidates($stabileId, $importo, $data, $nomeCondomino, $scalaInterno);
|
|
if ($operative !== []) {
|
|
return $operative;
|
|
}
|
|
|
|
if (! Schema::hasTable('movimenti_bancari') || ! Schema::hasTable('gestioni_contabili')) {
|
|
return [];
|
|
}
|
|
|
|
$gestioniIds = DB::table('gestioni_contabili')
|
|
->where('stabile_id', $stabileId)
|
|
->pluck('id')
|
|
->map(fn($id) => (int) $id)
|
|
->all();
|
|
|
|
if ($gestioniIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$query = DB::table('movimenti_bancari')
|
|
->whereIn('gestione_id', $gestioniIds)
|
|
->where('tipo_movimento', 'entrata')
|
|
->whereBetween('importo', [$importo - 1, $importo + 1])
|
|
->orderByDesc('data_operazione')
|
|
->orderByDesc('id')
|
|
->limit(30)
|
|
->get([
|
|
'id',
|
|
'importo',
|
|
'data_operazione',
|
|
'descrizione_completa',
|
|
'controparte_nome',
|
|
'pattern_scala',
|
|
'pattern_interno',
|
|
'stato_riconciliazione',
|
|
]);
|
|
|
|
return $this->scoreAndNormalizeCandidates($query->all(), $importo, $data, $nomeCondomino, $scalaInterno, 'movimento');
|
|
}
|
|
|
|
private function findOperativeBankMovementCandidates(int $stabileId, float $importo, ?string $data, string $nomeCondomino, string $scalaInterno): array
|
|
{
|
|
if (! Schema::hasTable('contabilita_movimenti_banca')) {
|
|
return [];
|
|
}
|
|
|
|
$query = DB::table('contabilita_movimenti_banca')
|
|
->where('stabile_id', $stabileId)
|
|
->where('importo', '>', 0)
|
|
->whereBetween('importo', [$importo - 1, $importo + 1])
|
|
->orderByDesc('data')
|
|
->orderByDesc('id')
|
|
->limit(30)
|
|
->get([
|
|
'id',
|
|
'importo',
|
|
'data',
|
|
'descrizione',
|
|
'descrizione_estesa',
|
|
'registrazione_id',
|
|
'match_data',
|
|
]);
|
|
|
|
return $this->scoreAndNormalizeCandidates($query->all(), $importo, $data, $nomeCondomino, $scalaInterno, 'movimento_operativo');
|
|
}
|
|
|
|
private function scoreAndNormalizeCandidates(array $rows, float $targetImporto, ?string $targetData, string $targetNome, string $targetScalaInterno, string $type): array
|
|
{
|
|
$normalized = [];
|
|
$targetNomeLower = mb_strtolower(trim($targetNome));
|
|
$targetScalaInternoLower = mb_strtolower(trim($targetScalaInterno));
|
|
|
|
foreach ($rows as $row) {
|
|
$dateValue = $type === 'incasso'
|
|
? $this->normalizeDate($row->data_pagamento ?? $row->dt_empag ?? null)
|
|
: $this->normalizeDate($row->data_operazione ?? $row->data ?? null);
|
|
$importo = (float) ($type === 'incasso' ? ($row->importo_pagato_euro ?? 0) : ($row->importo ?? 0));
|
|
$label = $type === 'incasso'
|
|
? trim((string) ($row->descrizione ?? ''))
|
|
: trim((string) ($row->descrizione_completa ?? $row->descrizione_estesa ?? $row->descrizione ?? ''));
|
|
$name = $type === 'movimento'
|
|
? trim((string) ($row->controparte_nome ?? ''))
|
|
: ($type === 'movimento_operativo' ? trim((string) ($row->descrizione_estesa ?? $row->descrizione ?? ''))
|
|
: trim((string) ($this->decodeArray($row->metadati_gescon ?? null)['nominativo'] ?? '')));
|
|
$scalaInterno = $type === 'movimento'
|
|
? trim((string) (($row->pattern_scala ?? '') !== '' && ($row->pattern_interno ?? '') !== ''
|
|
? strtoupper(trim((string) $row->pattern_scala)) . '/' . trim((string) $row->pattern_interno)
|
|
: ''))
|
|
: '';
|
|
|
|
$score = 0;
|
|
$score += max(0, 40 - (abs($targetImporto - $importo) * 40));
|
|
|
|
if ($targetData && $dateValue) {
|
|
$days = abs(Carbon::parse($targetData)->diffInDays(Carbon::parse($dateValue)));
|
|
$score += max(0, 30 - min($days, 30));
|
|
}
|
|
|
|
if ($targetNomeLower !== '' && $name !== '' && str_contains(mb_strtolower($name), $targetNomeLower)) {
|
|
$score += 20;
|
|
}
|
|
|
|
if ($targetScalaInternoLower !== '' && $scalaInterno !== '' && mb_strtolower($scalaInterno) === $targetScalaInternoLower) {
|
|
$score += 10;
|
|
}
|
|
|
|
$normalized[] = [
|
|
'id' => (int) $row->id,
|
|
'score' => round($score, 2),
|
|
'data' => $dateValue,
|
|
'data_label' => $dateValue ? Carbon::parse($dateValue)->format('d/m/Y') : '—',
|
|
'importo' => $importo,
|
|
'importo_label' => '€ ' . number_format($importo, 2, ',', '.'),
|
|
'label' => $label !== '' ? $label : null,
|
|
'nome' => $name !== '' ? $name : null,
|
|
'scala_interno' => $scalaInterno !== '' ? $scalaInterno : null,
|
|
'riferimento' => $type === 'incasso'
|
|
? trim((string) (($row->n_ricevuta ?? '') !== '' ? 'Ric. ' . $row->n_ricevuta : ($row->n_riferimento ?? '')))
|
|
: trim((string) ($row->stato_riconciliazione ?? (($row->registrazione_id ?? null) ? 'registrato' : 'da_verificare'))),
|
|
'type' => $type,
|
|
];
|
|
}
|
|
|
|
usort($normalized, fn(array $a, array $b): int => $b['score'] <=> $a['score']);
|
|
|
|
return array_slice($normalized, 0, 3);
|
|
}
|
|
|
|
private function buildExportText(array $meta, array $records): string
|
|
{
|
|
$lines = [
|
|
'# HUB incassi read-only',
|
|
'stabile_id: ' . ($meta['stabile_id'] ?? '—'),
|
|
'codice_stabile: ' . ($meta['codice_stabile'] ?? '—'),
|
|
'fonte: ' . ($meta['source_label'] ?? '—'),
|
|
'righe: ' . (string) ($meta['records_count'] ?? 0),
|
|
'',
|
|
];
|
|
|
|
foreach ($records as $index => $record) {
|
|
$lines[] = '## Riga ' . ($index + 1);
|
|
$lines[] = $record['ai_payload_pretty'];
|
|
$lines[] = '';
|
|
}
|
|
|
|
return trim(implode("\n", $lines));
|
|
}
|
|
|
|
private function decodeArray(mixed $value): array
|
|
{
|
|
if (is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (! is_string($value) || trim($value) === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($value, true);
|
|
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
private function loadPersistentExtraordinaryRows(string $codiceStabile, string $legacyCondId, int $limit, array $extraordinaryContext, array $allowedGestioni = []): \Illuminate\Support\Collection
|
|
{
|
|
if (! Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) {
|
|
return collect();
|
|
}
|
|
|
|
$relevantWorks = array_values(array_filter($extraordinaryContext['works'] ?? [], function (array $work): bool {
|
|
$text = (string) ($work['search_text'] ?? '');
|
|
return str_contains($text, 'atto aer')
|
|
|| str_contains($text, 'asc')
|
|
|| str_contains($text, 'facciat')
|
|
|| str_contains($text, 'corrido');
|
|
}));
|
|
|
|
$gestioni = array_values(array_unique(array_filter(array_map(
|
|
fn(array $work): string => trim((string) ($work['gestione_label'] ?? '')),
|
|
$relevantWorks,
|
|
))));
|
|
|
|
$query = DB::connection('gescon_import')
|
|
->table('rate_emissioni_dettaglio')
|
|
->where('cod_stabile', $codiceStabile)
|
|
->where('cod_cond', $legacyCondId)
|
|
->where(function ($builder) use ($gestioni): void {
|
|
$builder->whereNotNull('numero_straordinaria')
|
|
->orWhereIn('numero_emissione', function ($sub): void {
|
|
$sub->select('numero_emissione')
|
|
->from('rate_emissioni')
|
|
->where(function ($q): void {
|
|
$q->where('descrizione', 'like', '%capitolat%')
|
|
->orWhere('descrizione', 'like', '%metr%')
|
|
->orWhere('descrizione', 'like', '%straord%')
|
|
->orWhere('descrizione', 'like', '%facciat%')
|
|
->orWhere('descrizione', 'like', '%detraz%')
|
|
->orWhere('descrizione', 'like', '%atto aer%')
|
|
->orWhere('descrizione', 'like', '%corrido%')
|
|
->orWhere('descrizione', 'like', '%asc%');
|
|
});
|
|
});
|
|
|
|
if ($gestioni !== []) {
|
|
$builder->orWhereIn('anno_gestione', $gestioni);
|
|
}
|
|
})
|
|
->orderBy('data_emissione')
|
|
->orderBy('numero_ricevuta')
|
|
->limit(max(1, min($limit, 300)));
|
|
|
|
if ($allowedGestioni !== []) {
|
|
$query->where(function ($builder) use ($allowedGestioni): void {
|
|
foreach ($allowedGestioni as $label) {
|
|
$builder->orWhere('anno_gestione', $label);
|
|
}
|
|
$builder->orWhereNull('anno_gestione');
|
|
});
|
|
}
|
|
|
|
return $query
|
|
->get([
|
|
'id',
|
|
'numero_emissione',
|
|
'anno_emissione',
|
|
'anno_gestione',
|
|
'cod_cond',
|
|
'cond_inq',
|
|
'data_emissione',
|
|
'descrizione',
|
|
'numero_ricevuta',
|
|
'numero_mese',
|
|
'importo_dovuto_euro',
|
|
'gia_pagato',
|
|
'residuo_emesso',
|
|
'raggruppamento',
|
|
'numero_straordinaria',
|
|
'legacy_year',
|
|
])
|
|
->map(function (object $row): array {
|
|
$importoOriginario = (float) ($row->importo_dovuto_euro ?? 0);
|
|
$residuo = $row->residuo_emesso !== null ? (float) $row->residuo_emesso : $importoOriginario;
|
|
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'numero_emissione' => is_numeric($row->numero_emissione ?? null) ? (int) $row->numero_emissione : null,
|
|
'anno_emissione' => trim((string) ($row->anno_emissione ?? '')) ?: null,
|
|
'gestione' => $row->anno_gestione,
|
|
'cod_cond' => $row->cod_cond,
|
|
'ci' => $row->cond_inq,
|
|
'data_emissione' => $this->normalizeDate($row->data_emissione ?? null),
|
|
'descrizione' => trim((string) ($row->descrizione ?? '')) ?: null,
|
|
'ricevuta' => $row->numero_ricevuta,
|
|
'mese' => $row->numero_mese,
|
|
'importo_originario' => $importoOriginario,
|
|
'gia_pagato' => (float) ($row->gia_pagato ?? 0),
|
|
'residuo' => $residuo,
|
|
'raggruppamento' => $row->raggruppamento,
|
|
'numero_straordinaria' => is_numeric($row->numero_straordinaria ?? null) ? (int) $row->numero_straordinaria : null,
|
|
'legacy_year' => trim((string) ($row->legacy_year ?? '')) ?: null,
|
|
];
|
|
})
|
|
->values();
|
|
}
|
|
|
|
private function loadEmissionDescriptions(string $codiceStabile, array $numeroEmissioni): array
|
|
{
|
|
$numeroEmissioni = array_values(array_unique(array_filter(array_map('intval', $numeroEmissioni), fn(int $value): bool => $value > 0)));
|
|
if ($codiceStabile === '' || $numeroEmissioni === [] || ! Schema::connection('gescon_import')->hasTable('rate_emissioni')) {
|
|
return [];
|
|
}
|
|
|
|
return DB::connection('gescon_import')
|
|
->table('rate_emissioni')
|
|
->where('cod_stabile', $codiceStabile)
|
|
->whereIn('numero_emissione', $numeroEmissioni)
|
|
->get(['numero_emissione', 'descrizione'])
|
|
->mapWithKeys(function (object $row): array {
|
|
return [
|
|
(int) $row->numero_emissione => trim((string) ($row->descrizione ?? '')),
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function buildExtraordinaryContext(int $stabileId, string $codiceStabile): array
|
|
{
|
|
$titles = [];
|
|
$works = [];
|
|
|
|
if ($stabileId > 0 && Schema::hasTable('lavori_straordinari') && Schema::hasTable('gestioni_contabili')) {
|
|
$workRows = DB::table('lavori_straordinari as ls')
|
|
->join('gestioni_contabili as gc', 'gc.id', '=', 'ls.gestione_id')
|
|
->where('gc.stabile_id', $stabileId)
|
|
->where('gc.tipo_gestione', 'straordinaria')
|
|
->orderBy('gc.anno_gestione')
|
|
->orderBy('gc.numero_straordinaria')
|
|
->orderBy('ls.id')
|
|
->get([
|
|
'ls.id',
|
|
'ls.legacy_id',
|
|
'ls.descrizione',
|
|
'gc.anno_gestione',
|
|
'gc.numero_straordinaria',
|
|
'gc.denominazione',
|
|
]);
|
|
|
|
$titles = array_merge(
|
|
$titles,
|
|
$workRows
|
|
->pluck('descrizione')
|
|
->filter()
|
|
->map(fn($value): string => trim((string) $value))
|
|
->all()
|
|
);
|
|
|
|
$works = array_merge($works, $workRows->map(function (object $row): array {
|
|
$gestioneYear = is_numeric($row->anno_gestione ?? null) ? (int) $row->anno_gestione : null;
|
|
return [
|
|
'id' => (int) $row->id,
|
|
'legacy_id' => is_numeric($row->legacy_id ?? null) ? (int) $row->legacy_id : null,
|
|
'descrizione' => trim((string) ($row->descrizione ?? '')),
|
|
'gestione_year' => $gestioneYear,
|
|
'gestione_label' => $gestioneYear ? $this->legacyGestioneLabel($gestioneYear) : trim((string) ($row->denominazione ?? '')),
|
|
'numero_straordinaria' => is_numeric($row->numero_straordinaria ?? null) ? (int) $row->numero_straordinaria : null,
|
|
'search_text' => mb_strtolower(trim((string) ($row->descrizione ?? ''))),
|
|
'tokens' => $this->extractClassificationTokens((string) ($row->descrizione ?? '')),
|
|
];
|
|
})->all());
|
|
}
|
|
|
|
if ($codiceStabile !== '' && Schema::connection('gescon_import')->hasTable('straordinarie')) {
|
|
$titles = array_merge(
|
|
$titles,
|
|
DB::connection('gescon_import')
|
|
->table('straordinarie')
|
|
->where('cod_stabile', $codiceStabile)
|
|
->orderBy('id')
|
|
->get(['descriz_prev_cons', 'descriz_ricev', 'descriz_ccp'])
|
|
->flatMap(function (object $row): array {
|
|
return [
|
|
trim((string) ($row->descriz_prev_cons ?? '')),
|
|
trim((string) ($row->descriz_ricev ?? '')),
|
|
trim((string) ($row->descriz_ccp ?? '')),
|
|
];
|
|
})
|
|
->filter()
|
|
->all()
|
|
);
|
|
}
|
|
|
|
$titles = array_values(array_unique(array_filter($titles)));
|
|
$keywords = [];
|
|
foreach ($titles as $title) {
|
|
$keywords = array_merge($keywords, $this->extractClassificationTokens($title));
|
|
}
|
|
|
|
$keywords = array_values(array_unique(array_merge($keywords, [
|
|
'straord',
|
|
'capitolat',
|
|
'metric',
|
|
'facciat',
|
|
'detraz',
|
|
'corrido',
|
|
'asc',
|
|
'aer',
|
|
])));
|
|
|
|
return [
|
|
'titles' => $titles,
|
|
'keywords' => $keywords,
|
|
'works' => $works,
|
|
];
|
|
}
|
|
|
|
private function classifyLedgerRow(array $row, array $emissionLabels, array $extraordinaryContext): array
|
|
{
|
|
$emissionLabel = null;
|
|
$numeroEmissione = (int) ($row['numero_emissione'] ?? 0);
|
|
if ($numeroEmissione > 0) {
|
|
$emissionLabel = trim((string) ($emissionLabels[$numeroEmissione] ?? '')) ?: null;
|
|
}
|
|
|
|
$texts = array_filter([
|
|
trim((string) ($row['descrizione'] ?? '')),
|
|
trim((string) ($emissionLabel ?? '')),
|
|
]);
|
|
|
|
$normalizedText = implode(' ', array_map(fn(string $value): string => mb_strtolower($value), $texts));
|
|
$matchedWork = $this->matchExtraordinaryWork($row, $emissionLabel, $extraordinaryContext);
|
|
$isExtraordinary = is_numeric($row['numero_straordinaria'] ?? null) && (int) $row['numero_straordinaria'] > 0;
|
|
|
|
if (! $isExtraordinary) {
|
|
foreach (($extraordinaryContext['keywords'] ?? []) as $keyword) {
|
|
if ($keyword !== '' && str_contains($normalizedText, $keyword)) {
|
|
$isExtraordinary = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! $isExtraordinary && is_array($matchedWork)) {
|
|
$isExtraordinary = true;
|
|
}
|
|
|
|
$row['emission_label'] = $emissionLabel;
|
|
$row['bucket'] = $isExtraordinary ? 'extraordinary' : 'ordinary';
|
|
|
|
$rowDescription = trim((string) ($row['descrizione'] ?? ''));
|
|
|
|
if ($isExtraordinary) {
|
|
if (is_array($matchedWork)) {
|
|
$row['extraordinary_work_label'] = $matchedWork['descrizione'];
|
|
$row['extraordinary_work_key'] = $matchedWork['work_key'];
|
|
$row['extraordinary_work_gestione'] = $matchedWork['gestione_label'];
|
|
$row['extraordinary_work_numero'] = $matchedWork['numero_straordinaria'];
|
|
}
|
|
}
|
|
|
|
$row['display_description'] = $row['extraordinary_work_label'] ?? ($rowDescription !== '' ? $rowDescription : $emissionLabel);
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function matchExtraordinaryWork(array $row, ?string $emissionLabel, array $extraordinaryContext): ?array
|
|
{
|
|
$works = array_values(array_filter($extraordinaryContext['works'] ?? [], fn(array $work): bool => ($work['descrizione'] ?? '') !== ''));
|
|
if ($works === []) {
|
|
return null;
|
|
}
|
|
|
|
$texts = array_filter([
|
|
trim((string) ($row['descrizione'] ?? '')),
|
|
trim((string) ($emissionLabel ?? '')),
|
|
]);
|
|
$normalizedText = mb_strtolower(implode(' ', $texts));
|
|
$gestioneLabel = trim((string) ($row['gestione'] ?? ''));
|
|
$rowDate = is_string($row['data_emissione'] ?? null) ? $row['data_emissione'] : null;
|
|
|
|
$best = null;
|
|
$bestScore = 0;
|
|
|
|
foreach ($works as $work) {
|
|
$score = 0;
|
|
$workTokens = array_values(array_filter($work['tokens'] ?? []));
|
|
$workText = (string) ($work['search_text'] ?? '');
|
|
|
|
if ($gestioneLabel !== '' && $gestioneLabel === ($work['gestione_label'] ?? '')) {
|
|
$score += 30;
|
|
}
|
|
|
|
foreach ($workTokens as $token) {
|
|
if ($token !== '' && str_contains($normalizedText, $token)) {
|
|
$score += 20;
|
|
}
|
|
}
|
|
|
|
if (str_contains($normalizedText, 'capitolat') || str_contains($normalizedText, 'metric')) {
|
|
if (str_contains($workText, 'facciat')) {
|
|
$score += 40;
|
|
}
|
|
if (str_contains($workText, 'asc')) {
|
|
$score += 10;
|
|
}
|
|
}
|
|
|
|
if ($rowDate !== null) {
|
|
if ($rowDate >= '2024-07-01' && $rowDate <= '2024-08-31' && (str_contains($workText, 'atto aer') || str_contains($workText, 'aer')) && (str_contains($normalizedText, '1/2 rata') || str_contains($normalizedText, '2/2 rata'))) {
|
|
$score += 50;
|
|
}
|
|
if ($rowDate >= '2025-03-01' && $rowDate <= '2025-03-31') {
|
|
if (str_contains($workText, 'asc') && ! str_contains($normalizedText, 'capitolat')) {
|
|
$score += 35;
|
|
}
|
|
if (str_contains($workText, 'facciat') && str_contains($normalizedText, 'capitolat')) {
|
|
$score += 25;
|
|
}
|
|
}
|
|
if ($rowDate >= '2026-01-01' && str_contains($workText, 'corrido')) {
|
|
$score += 45;
|
|
}
|
|
}
|
|
|
|
if ($score > $bestScore) {
|
|
$bestScore = $score;
|
|
$best = $work;
|
|
}
|
|
}
|
|
|
|
if (! is_array($best) || $bestScore < 45) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'work_key' => ($best['gestione_label'] ?? 'S') . '#' . ($best['numero_straordinaria'] ?? '0') . ':' . ($best['id'] ?? '0'),
|
|
'descrizione' => $best['descrizione'],
|
|
'gestione_label' => $best['gestione_label'] ?? null,
|
|
'numero_straordinaria' => $best['numero_straordinaria'] ?? null,
|
|
];
|
|
}
|
|
|
|
private function summarizeRowsByBucket(array $dueOrdinary, array $dueExtraordinary): array
|
|
{
|
|
return [
|
|
['label' => 'Ordinarie', 'bucket' => 'ordinary', 'rows' => count($dueOrdinary), 'total' => array_reduce($dueOrdinary, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0)],
|
|
['label' => 'Straordinarie', 'bucket' => 'extraordinary', 'rows' => count($dueExtraordinary), 'total' => array_reduce($dueExtraordinary, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0)],
|
|
['label' => 'Totale generale dovuto', 'bucket' => 'all', 'rows' => count($dueOrdinary) + count($dueExtraordinary), 'total' => array_reduce($dueOrdinary, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0) + array_reduce($dueExtraordinary, fn(float $carry, array $row): float => $carry + (float) ($row['residuo'] ?? 0), 0.0)],
|
|
];
|
|
}
|
|
|
|
private function summarizeRowsByGestione(array $rows): array
|
|
{
|
|
$summary = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$label = trim((string) ($row['gestione'] ?? 'Senza gestione')) ?: 'Senza gestione';
|
|
if (! isset($summary[$label])) {
|
|
$summary[$label] = [
|
|
'gestione' => $label,
|
|
'ordinary_total' => 0.0,
|
|
'extraordinary_total' => 0.0,
|
|
'general_total' => 0.0,
|
|
];
|
|
}
|
|
|
|
$amount = (float) ($row['residuo'] ?? 0);
|
|
$summary[$label]['general_total'] += $amount;
|
|
if (($row['bucket'] ?? 'ordinary') === 'extraordinary') {
|
|
$summary[$label]['extraordinary_total'] += $amount;
|
|
} else {
|
|
$summary[$label]['ordinary_total'] += $amount;
|
|
}
|
|
}
|
|
|
|
uasort($summary, fn(array $a, array $b): int => strcmp((string) $a['gestione'], (string) $b['gestione']));
|
|
|
|
return array_values($summary);
|
|
}
|
|
|
|
private function summarizeExtraordinaryByWork(array $rows): array
|
|
{
|
|
$summary = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$key = trim((string) ($row['extraordinary_work_key'] ?? ''));
|
|
$label = trim((string) ($row['extraordinary_work_label'] ?? $row['display_description'] ?? $row['descrizione'] ?? 'Straordinaria')) ?: 'Straordinaria';
|
|
$gestione = trim((string) ($row['extraordinary_work_gestione'] ?? $row['gestione'] ?? '—')) ?: '—';
|
|
|
|
if ($key === '') {
|
|
$key = $gestione . ':' . $label;
|
|
}
|
|
|
|
if (! isset($summary[$key])) {
|
|
$summary[$key] = [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'gestione' => $gestione,
|
|
'rows' => 0,
|
|
'total' => 0.0,
|
|
];
|
|
}
|
|
|
|
$summary[$key]['rows']++;
|
|
$summary[$key]['total'] += (float) ($row['residuo'] ?? 0);
|
|
}
|
|
|
|
usort($summary, fn(array $a, array $b): int => strcmp($a['gestione'] . $a['label'], $b['gestione'] . $b['label']));
|
|
|
|
return array_values($summary);
|
|
}
|
|
|
|
private function detectMissingExtraordinaryWorks(array $rows, array $extraordinaryContext): array
|
|
{
|
|
$matchedKeys = array_values(array_unique(array_filter(array_map(
|
|
fn(array $row): string => trim((string) ($row['extraordinary_work_key'] ?? '')),
|
|
$rows,
|
|
))));
|
|
|
|
$rowGestioni = array_values(array_unique(array_filter(array_map(
|
|
fn(array $row): string => trim((string) ($row['gestione'] ?? '')),
|
|
$rows,
|
|
))));
|
|
|
|
$works = array_values(array_filter($extraordinaryContext['works'] ?? [], function (array $work): bool {
|
|
$text = (string) ($work['search_text'] ?? '');
|
|
return str_contains($text, 'atto aer')
|
|
|| str_contains($text, 'asc')
|
|
|| str_contains($text, 'facciat')
|
|
|| str_contains($text, 'corrido');
|
|
}));
|
|
|
|
if ($rowGestioni !== []) {
|
|
$works = array_values(array_filter($works, fn(array $work): bool => in_array(trim((string) ($work['gestione_label'] ?? '')), $rowGestioni, true)));
|
|
}
|
|
|
|
$missing = [];
|
|
foreach ($works as $work) {
|
|
$key = ($work['gestione_label'] ?? 'S') . '#' . ($work['numero_straordinaria'] ?? '0') . ':' . ($work['id'] ?? '0');
|
|
if (in_array($key, $matchedKeys, true)) {
|
|
continue;
|
|
}
|
|
|
|
$missing[] = [
|
|
'label' => $work['descrizione'],
|
|
'gestione' => $work['gestione_label'] ?? '—',
|
|
'numero_straordinaria' => $work['numero_straordinaria'] ?? null,
|
|
'message' => 'Lavoro straordinario presente nelle gestioni ma senza riga residua materializzata per questo C/I: controllare import di addebiti/rate.',
|
|
];
|
|
}
|
|
|
|
return $missing;
|
|
}
|
|
|
|
private function extractClassificationTokens(string $value): array
|
|
{
|
|
$normalized = mb_strtolower($value);
|
|
$normalized = preg_replace('/[^a-z0-9]+/u', ' ', $normalized) ?? '';
|
|
$parts = preg_split('/\s+/', trim($normalized)) ?: [];
|
|
|
|
$stopwords = [
|
|
'anno', 'anni', 'lavori', 'lavoro', 'rata', 'rate', 'della', 'delle', 'degli', 'dello', 'dell', 'del', 'dei', 'di', 'da', 'per', 'con', 'nel', 'nella', 'sc', 'scala', 'lato', 'quote', 'est', 'ovest', 'sud', 'nord', 'parrocchia', 'messa', 'sicurezza', 'ripristino',
|
|
];
|
|
|
|
return array_values(array_filter(array_unique(array_map(
|
|
static fn(string $part): string => mb_substr($part, 0, 8),
|
|
$parts,
|
|
)), static fn(string $part): bool => mb_strlen($part) >= 4 && ! in_array($part, $stopwords, true)));
|
|
}
|
|
|
|
private function normalizeDate(mixed $value): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return Carbon::parse((string) $value)->format('Y-m-d');
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|