434 lines
18 KiB
PHP
434 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Contabilita;
|
|
|
|
use App\Models\DatiBancari;
|
|
use App\Models\Stabile;
|
|
use App\Models\UnitaImmobiliare;
|
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
|
use App\Services\Gescon\GesconEstrattoContoService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class GermanicoBancaReconciliationService
|
|
{
|
|
public const STABILE_ID = 19;
|
|
public const COD_STABILE = '0016';
|
|
public const IBAN_GERMANICO = 'IT48W0312403203000000233378';
|
|
public const DEFAULT_BANCA_DIR = '/home/michele/netgescon-day0-backup/Miki-Bug-workspace/Banca/germanico 96';
|
|
|
|
public function __construct(
|
|
protected ExcelMovimentiParser $parser,
|
|
protected GesconEstrattoContoService $gesconService
|
|
) {}
|
|
|
|
/**
|
|
* Assicura l'esistenza del conto bancario CCB per lo Stabile Germanico 96.
|
|
*/
|
|
public function ensureContoBancario(int $stabileId = self::STABILE_ID): DatiBancari
|
|
{
|
|
$conto = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where(function ($q) {
|
|
$q->where('iban', self::IBAN_GERMANICO)
|
|
->orWhere('legacy_cod_cassa', 'CCB')
|
|
->orWhere('numero_conto', '233378');
|
|
})
|
|
->first();
|
|
|
|
if ($conto) {
|
|
return $conto;
|
|
}
|
|
|
|
return DatiBancari::create([
|
|
'stabile_id' => $stabileId,
|
|
'tipo_conto' => 'corrente',
|
|
'denominazione_banca' => 'Banca del Fucino',
|
|
'numero_conto' => '233378',
|
|
'legacy_cod_cassa' => 'CCB',
|
|
'iban' => self::IBAN_GERMANICO,
|
|
'abi' => '03124',
|
|
'cab' => '03203',
|
|
'cin' => 'W',
|
|
'intestazione_conto' => 'CONDOMINIO VIA GERMANICO 96',
|
|
'data_saldo_iniziale' => '2022-10-01',
|
|
'saldo_iniziale' => 3433.49,
|
|
'valuta' => 'EUR',
|
|
'stato_conto' => 'attivo',
|
|
'is_nostro_conto' => true,
|
|
'note' => '[GESCON Germanico 96 CCB]',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Esegue sincronizzazione completa: importazione dai file .xls e riconciliazione contabile.
|
|
*/
|
|
public function syncAndReconcileAll(?string $dir = null, int $stabileId = self::STABILE_ID): array
|
|
{
|
|
$importRes = $this->importFromFiles($dir, $stabileId);
|
|
$recRes = $this->reconcile($stabileId);
|
|
|
|
return array_merge($importRes, $recRes);
|
|
}
|
|
|
|
/**
|
|
* Importa tutti i file .xls della banca nella tabella contabilita_movimenti_banca.
|
|
*
|
|
* @return array{total_files: int, raw_rows: int, imported: int, duplicates: int}
|
|
*/
|
|
public function importFromFiles(?string $dir = null, int $stabileId = self::STABILE_ID): array
|
|
{
|
|
$dir = $dir ?: self::DEFAULT_BANCA_DIR;
|
|
if (!is_dir($dir)) {
|
|
return ['total_files' => 0, 'raw_rows' => 0, 'imported' => 0, 'duplicates' => 0, 'error' => "Directory {$dir} non trovata"];
|
|
}
|
|
|
|
$conto = $this->ensureContoBancario($stabileId);
|
|
$files = glob($dir . '/*.xls');
|
|
sort($files);
|
|
|
|
$totalFiles = count($files);
|
|
$rawRows = 0;
|
|
$imported = 0;
|
|
$duplicates = 0;
|
|
|
|
foreach ($files as $filePath) {
|
|
$fileName = basename($filePath);
|
|
try {
|
|
$parsed = $this->parser->parseXlsx($filePath);
|
|
} catch (\Throwable $e) {
|
|
Log::warning("Errore nel parsing del file bancario {$fileName}: " . $e->getMessage());
|
|
continue;
|
|
}
|
|
|
|
foreach ($parsed['rows'] as $r) {
|
|
$rawRows++;
|
|
$data = $r['data'] instanceof Carbon ? $r['data'] : Carbon::parse($r['data']);
|
|
$valuta = !empty($r['valuta']) ? ($r['valuta'] instanceof Carbon ? $r['valuta'] : Carbon::parse($r['valuta'])) : null;
|
|
$importo = round((float) $r['importo'], 2);
|
|
$descrizione = trim((string) ($r['descrizione'] ?? ''));
|
|
|
|
// Calcolo hash univoco di riga
|
|
$hashBase = implode('|', [
|
|
$stabileId,
|
|
$data->format('Y-m-d'),
|
|
$valuta ? $valuta->format('Y-m-d') : '',
|
|
number_format($importo, 2, '.', ''),
|
|
$descrizione,
|
|
]);
|
|
$rowHash = hash('sha256', $hashBase);
|
|
|
|
$exists = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('row_hash', $rowHash)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
$duplicates++;
|
|
continue;
|
|
}
|
|
|
|
// Estrazione dati strutturati dalla descrizione bancaria
|
|
$mittente = null;
|
|
$beneficiario = null;
|
|
$cro = null;
|
|
$note = null;
|
|
$tipoOperazione = 'altro';
|
|
|
|
if ($importo > 0) {
|
|
$tipoOperazione = 'incasso';
|
|
if (preg_match('/BONIFICO A VOSTRO FAVORE\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+?)\s+(?:Data|Coord|Banca|Cro|Note|Id)/i', $descrizione, $m)) {
|
|
$mittente = trim($m[1]);
|
|
} elseif (preg_match('/BONIFICO.*?FAVORE\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+)/i', $descrizione, $m)) {
|
|
$mittente = trim($m[1]);
|
|
} elseif (preg_match('/VERSAMENTO CONTANTE\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+)/i', $descrizione, $m)) {
|
|
$mittente = trim($m[1]);
|
|
}
|
|
} else {
|
|
$tipoOperazione = 'spesa';
|
|
if (preg_match('/ADDEBITO BONIFICO DA HOME BANKING\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+?)\s+(?:Bonifico|Data|Coord|Banca|Cro|Note|Id)/i', $descrizione, $m)) {
|
|
$beneficiario = trim($m[1]);
|
|
} elseif (preg_match('/ADDEBITO DIRETTO CORE RCUR.*?Prg\.Car\.:\s*\d+\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+?)\s+-\s+/i', $descrizione, $m)) {
|
|
$beneficiario = trim($m[1]);
|
|
} elseif (preg_match('/PAGAMENTO POS.*?C\/O\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+)/i', $descrizione, $m)) {
|
|
$beneficiario = trim($m[1]);
|
|
} elseif (str_contains($descrizione, 'COMM.')) {
|
|
$beneficiario = 'Banca del Fucino (Commissioni)';
|
|
}
|
|
}
|
|
|
|
if (preg_match('/Cro:\s*([A-Za-z0-9]+)/i', $descrizione, $m)) {
|
|
$cro = trim($m[1]);
|
|
}
|
|
if (preg_match('/Note:\s*(.+?)(?:Id\.Operazione|$)/i', $descrizione, $m)) {
|
|
$note = trim($m[1]);
|
|
}
|
|
|
|
MovimentoBanca::create([
|
|
'stabile_id' => $stabileId,
|
|
'conto_id' => $conto->id,
|
|
'iban' => self::IBAN_GERMANICO,
|
|
'data' => $data,
|
|
'valuta' => $valuta,
|
|
'descrizione' => $descrizione,
|
|
'descrizione_estesa' => $note ?: $descrizione,
|
|
'importo' => $importo,
|
|
'causale' => $r['causale'] ?? null,
|
|
'tipo_operazione' => $tipoOperazione,
|
|
'mittente' => $mittente,
|
|
'beneficiario' => $beneficiario,
|
|
'rif_disposizione' => $cro,
|
|
'banca_nome' => 'Banca del Fucino',
|
|
'source_file' => $fileName,
|
|
'raw_line' => $r['raw_line'] ?? null,
|
|
'row_hash' => $rowHash,
|
|
'stato_riconciliazione' => 'da_riconciliare',
|
|
]);
|
|
|
|
$imported++;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'total_files' => $totalFiles,
|
|
'raw_rows' => $rawRows,
|
|
'imported' => $imported,
|
|
'duplicates' => $duplicates,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Riconcilia tutti i movimenti bancari con gli Incassi Gescon (Inc_da_ec e incassi)
|
|
* e con le Spese (Operazioni).
|
|
*
|
|
* @return array{total_movimenti: int, riconciliati_incassi: int, riconciliati_spese: int, totale_riconciliati: int, percentuale: float}
|
|
*/
|
|
public function reconcile(int $stabileId = self::STABILE_ID): array
|
|
{
|
|
$genMdb = "/mnt/gescon-archives/gescon/" . self::COD_STABILE . "/generale_stabile.mdb";
|
|
$incDaEc = $this->gesconService->runMdbExport($genMdb, 'Inc_da_ec');
|
|
|
|
// Mappa delle unità per scala e interno e legacy_cond_id
|
|
$unitaList = UnitaImmobiliare::query()->where('stabile_id', $stabileId)->get();
|
|
$unitaByScInt = [];
|
|
$unitaByLegacyId = [];
|
|
foreach ($unitaList as $u) {
|
|
$key = strtoupper(trim((string)$u->scala)) . '|' . trim((string)$u->interno);
|
|
$unitaByScInt[$key] = $u;
|
|
if (!empty($u->legacy_cond_id)) {
|
|
$unitaByLegacyId[trim((string)$u->legacy_cond_id)] = $u;
|
|
}
|
|
}
|
|
|
|
// Movimenti bancari dello stabile
|
|
$movimenti = MovimentoBanca::query()->where('stabile_id', $stabileId)->get();
|
|
$totalMovimenti = $movimenti->count();
|
|
$riconciliatiIncassi = 0;
|
|
$riconciliatiSpese = 0;
|
|
|
|
// Indicizzazione Inc_da_ec per importo
|
|
$ecByAmount = [];
|
|
foreach ($incDaEc as $ec) {
|
|
$amt = number_format(round((float)($ec['Importo'] ?? 0), 2), 2, '.', '');
|
|
$ecByAmount[$amt][] = $ec;
|
|
}
|
|
|
|
// Spese consolidate Gescon dagli archivi annuali (0914, 0913, 0912, 0909)
|
|
$speseArchivio = [];
|
|
foreach (['0914', '0913', '0912', '0909', '0005', '0004'] as $yrDir) {
|
|
$singoloMdb = "/mnt/gescon-archives/gescon/" . self::COD_STABILE . "/{$yrDir}/singolo_anno.mdb";
|
|
if (!file_exists($singoloMdb)) continue;
|
|
$ops = $this->gesconService->runMdbExport($singoloMdb, 'Operazioni');
|
|
foreach ($ops as $op) {
|
|
$amt = round((float)($op['importo_euro'] ?? 0), 2);
|
|
if ($amt > 0) {
|
|
$dt = !empty($op['dt_spe']) ? date('Y-m-d', strtotime($op['dt_spe'])) : null;
|
|
$speseArchivio[] = [
|
|
'anno_dir' => $yrDir,
|
|
'id_operaz' => $op['id_operaz'] ?? null,
|
|
'data' => $dt,
|
|
'importo' => $amt,
|
|
'beneficiario' => trim((string)($op['benef'] ?? '')),
|
|
'cod_forn' => $op['cod_for'] ?? null,
|
|
'num_fat' => $op['num_fat'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($movimenti as $mov) {
|
|
$importo = (float) $mov->importo;
|
|
$dataMov = $mov->data ? $mov->data->format('Y-m-d') : null;
|
|
$desc = strtoupper($mov->descrizione);
|
|
|
|
// 1. Riconciliazione INCASSI (importo > 0)
|
|
if ($importo > 0) {
|
|
$amtKey = number_format($importo, 2, '.', '');
|
|
$candidates = $ecByAmount[$amtKey] ?? [];
|
|
$bestMatch = null;
|
|
|
|
foreach ($candidates as $ec) {
|
|
$ecDateRaw = $ec['Data_pag'] ?? '';
|
|
$ecDate = $ecDateRaw ? date('Y-m-d', strtotime($ecDateRaw)) : null;
|
|
|
|
// Match esatto o per data ravvicinata (+/- 14 giorni) o per nome
|
|
$nomeCond = strtoupper(trim((string)($ec['Nome_condomino'] ?? '')));
|
|
$cognomeTokens = array_filter(explode(' ', $nomeCond));
|
|
$nameMatched = false;
|
|
foreach ($cognomeTokens as $tok) {
|
|
if (strlen($tok) >= 4 && str_contains($desc, $tok)) {
|
|
$nameMatched = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$daysDiff = ($dataMov && $ecDate) ? abs(strtotime($dataMov) - strtotime($ecDate)) / 86400 : 999;
|
|
|
|
if ($nameMatched || $daysDiff <= 14) {
|
|
$bestMatch = $ec;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Match speciale per bonifici cumulativi noti (es. Barone Michele 23/12/2025 € 960,79)
|
|
if (!$bestMatch && abs($importo - 960.79) < 0.01 && str_contains($desc, 'BARONE')) {
|
|
foreach ($candidates as $ec) {
|
|
if (str_contains(strtoupper($ec['Nome_condomino'] ?? ''), 'BARONE')) {
|
|
$bestMatch = $ec;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($bestMatch) {
|
|
$scInt = trim((string)($bestMatch['sc_int'] ?? ''));
|
|
$scIntParts = explode('/', $scInt);
|
|
$scala = trim($scIntParts[0] ?? '');
|
|
$interno = trim($scIntParts[1] ?? '');
|
|
|
|
$unita = null;
|
|
if ($scala !== '' && $interno !== '') {
|
|
$unita = $unitaByScInt[strtoupper($scala) . '|' . $interno] ?? null;
|
|
}
|
|
if (!$unita && !empty($bestMatch['id_condomino'])) {
|
|
$unita = $unitaByLegacyId[trim((string)$bestMatch['id_condomino'])] ?? null;
|
|
}
|
|
|
|
$pdfFile = trim((string)($bestMatch['Nome_file_pdf'] ?? ''));
|
|
|
|
$matchData = [
|
|
'tipo' => 'incasso',
|
|
'protocollo' => $bestMatch['protocollo'] ?? null,
|
|
'data_pagamento' => $bestMatch['Data_pag'] ?? null,
|
|
'num_incasso' => $bestMatch['Num_incasso'] ?? null,
|
|
'anno_incasso' => $bestMatch['Anno_incasso'] ?? null,
|
|
'id_condomino' => $bestMatch['id_condomino'] ?? null,
|
|
'sc_int' => $scInt,
|
|
'nome_condomino' => $bestMatch['Nome_condomino'] ?? null,
|
|
'pdf_ricevuta' => $pdfFile,
|
|
'pdf_url' => $pdfFile ? "/admin/gescon-inc-ec-pdf/0016/{$pdfFile}" : null,
|
|
];
|
|
|
|
$mov->update([
|
|
'stato_riconciliazione' => 'riconciliato',
|
|
'unita_immobiliare_id' => $unita?->id,
|
|
'mittente' => $bestMatch['Nome_condomino'] ?? $mov->mittente,
|
|
'match_data' => $matchData,
|
|
]);
|
|
|
|
$riconciliatiIncassi++;
|
|
}
|
|
}
|
|
|
|
// 2. Riconciliazione SPESE (importo < 0)
|
|
if ($importo < 0) {
|
|
$absAmt = abs($importo);
|
|
$bestSpesa = null;
|
|
|
|
foreach ($speseArchivio as $sp) {
|
|
if (abs($sp['importo'] - $absAmt) < 0.01) {
|
|
$daysDiff = ($dataMov && $sp['data']) ? abs(strtotime($dataMov) - strtotime($sp['data'])) / 86400 : 999;
|
|
$benefUpper = strtoupper($sp['beneficiario']);
|
|
$benefTokens = array_filter(explode(' ', $benefUpper));
|
|
|
|
$tokenMatch = false;
|
|
foreach ($benefTokens as $tok) {
|
|
if (strlen($tok) >= 4 && str_contains($desc, $tok)) {
|
|
$tokenMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($tokenMatch || $daysDiff <= 14) {
|
|
$bestSpesa = $sp;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Match per commissioni bancarie o addebiti specifici
|
|
if (!$bestSpesa && (str_contains($desc, 'COMM.') || str_contains($desc, 'CANONE HB') || str_contains($desc, 'COMPETENZE'))) {
|
|
$bestSpesa = [
|
|
'id_operaz' => null,
|
|
'beneficiario' => 'Banca del Fucino - Spese e Commissioni',
|
|
'num_fat' => null,
|
|
'data' => $dataMov,
|
|
'importo' => $absAmt,
|
|
];
|
|
}
|
|
|
|
if ($bestSpesa) {
|
|
$matchData = [
|
|
'tipo' => 'spesa',
|
|
'id_operaz' => $bestSpesa['id_operaz'] ?? null,
|
|
'beneficiario' => $bestSpesa['beneficiario'],
|
|
'num_fat' => $bestSpesa['num_fat'] ?? null,
|
|
'data_spesa' => $bestSpesa['data'] ?? null,
|
|
];
|
|
|
|
$mov->update([
|
|
'stato_riconciliazione' => 'riconciliato',
|
|
'beneficiario' => $bestSpesa['beneficiario'],
|
|
'match_data' => $matchData,
|
|
]);
|
|
|
|
$riconciliatiSpese++;
|
|
}
|
|
}
|
|
}
|
|
|
|
$totaleRiconciliati = $riconciliatiIncassi + $riconciliatiSpese;
|
|
$percentuale = $totalMovimenti > 0 ? round(($totaleRiconciliati / $totalMovimenti) * 100, 2) : 0.0;
|
|
|
|
return [
|
|
'total_movimenti' => $totalMovimenti,
|
|
'riconciliati_incassi' => $riconciliatiIncassi,
|
|
'riconciliati_spese' => $riconciliatiSpese,
|
|
'totale_riconciliati' => $totaleRiconciliati,
|
|
'percentuale' => $percentuale,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Restituisce i movimenti bancari riconciliati per una specifica unità immobiliare.
|
|
*/
|
|
public function getMovimentiForUnita(int $unitaId): \Illuminate\Database\Eloquent\Collection
|
|
{
|
|
return MovimentoBanca::query()
|
|
->where('unita_immobiliare_id', $unitaId)
|
|
->orderByDesc('data')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Trova il movimento bancario collegato ad una specifica ricevuta PDF in Inc_da_ec.
|
|
*/
|
|
public function getBankMovementForPdf(string $pdfName): ?MovimentoBanca
|
|
{
|
|
return MovimentoBanca::query()
|
|
->where('match_data->pdf_ricevuta', $pdfName)
|
|
->first();
|
|
}
|
|
}
|