tenantId = $tenantId; $this->stabileId = $stabileId; $this->baseDir = $baseDir; if ($baseDir) { $this->loadYearMap($baseDir); } } private function loadYearMap(string $baseDir): void { $generaleMdb = rtrim($baseDir, '/') . '/generale_stabile.mdb'; if (!is_file($generaleMdb)) { return; } try { $output = shell_exec(sprintf( 'echo "SELECT id_anno, anno_o FROM anni" | mdb-sql %s 2>/dev/null', escapeshellarg($generaleMdb) )); if ($output) { $lines = explode("\n", $output); foreach ($lines as $line) { if (str_contains($line, '|')) { $parts = explode('|', $line); $id = (int) trim($parts[1] ?? ''); $yr = (int) trim($parts[2] ?? ''); if ($id > 0 && $yr >= 2000 && $yr <= 2100) { $dir = str_pad((string)$id, 4, '0', STR_PAD_LEFT); $this->directoryToYearMap[$dir] = $yr; } } } } } catch (\Throwable $e) { Log::warning("Could not load year map from general MDB: " . $e->getMessage()); } } /** * Import completo multi-anno da directory Gescon */ public function importFromGesconDirectory(string $baseDir): array { Log::info("Starting multi-year Gescon import", ['tenant' => $this->tenantId, 'dir' => $baseDir]); // 1. Scansiona directory per anni $years = $this->scanGesconYears($baseDir); // 2. Crea gestioni contabili per ogni anno $this->createGestioni($years); // 3. Import dati per ogni gestione $results = []; foreach ($years as $year => $paths) { $results[$year] = $this->importGestioneYear($year, $paths); } // 4. Crea bilanci di apertura per continuità $this->createBilanciApertura(); return [ 'success' => true, 'gestioni_create' => count($this->gestioni), 'years_imported' => array_keys($results), 'details' => $results, ]; } /** * Scansiona directory Gescon per identificare anni */ private function scanGesconYears(string $baseDir): array { $years = []; // Pattern directory: /Gescon/2024/singolo_anno.mdb, /Gescon/2025/singolo_anno.mdb $gesconDirs = glob($baseDir . '/*/'); foreach ($gesconDirs as $dir) { $year = basename(rtrim($dir, '/')); if (is_numeric($year) && strlen($year) == 4) { $paths = [ 'singolo_anno' => $dir . 'singolo_anno.mdb', 'generale_stabile' => $dir . 'generale_stabile.mdb', ]; // Verifica esistenza file if (file_exists($paths['singolo_anno'])) { $years[$year] = $paths; } } } return $years; } /** * Crea gestioni contabili per tutti gli anni scansionati */ public function createGestioni(array $years): void { foreach ($years as $year => $paths) { $this->initGestioniForYear($year, $paths); } } /** * Inizializza le gestioni contabili per un singolo anno */ public function initGestioniForYear(string $year, array $paths): void { $calendarYear = $this->directoryToYearMap[$year] ?? (int) $year; $mdbPath = $paths['singolo_anno']; $gestioniData = $this->scanGestioniFromMdb($mdbPath, $calendarYear); foreach ($gestioniData as $data) { $this->createOrUpdateGestione($data, $year); } } /** * Scansiona gestioni direttamente dal file MDB */ private function scanGestioniFromMdb(string $mdbPath, int $year): array { $gestioni = []; // Gestione Ordinaria (sempre presente) $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'ordinaria', 'denominazione' => "Gestione Ordinaria {$year}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'protocollo_prefix' => "O{$year}", ]; // Legge le colonne necessarie da Operazioni $operazioni = $this->queryMdb($mdbPath, "SELECT Gestione, n_stra FROM Operazioni"); $hasRiscaldamento = false; $straordinarie = []; foreach ($operazioni as $op) { $g = strtoupper(trim($op['Gestione'] ?? '')); if ($g === 'R') { $hasRiscaldamento = true; } elseif ($g === 'S') { $nStra = (int) ($op['n_stra'] ?? 0); if ($nStra > 0) { $straordinarie[$nStra] = true; } } } if ($hasRiscaldamento) { $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'riscaldamento', 'denominazione' => "Gestione Riscaldamento {$year}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'protocollo_prefix' => "R{$year}", ]; } ksort($straordinarie); foreach (array_keys($straordinarie) as $numero) { $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'straordinaria', 'numero_straordinaria' => $numero, 'denominazione' => "Gestione Straordinaria {$year} - {$numero}", 'data_inizio' => "{$year}-01-01", 'data_fine' => "{$year}-12-31", 'protocollo_prefix' => "S{$year}-{$numero}", ]; } return $gestioni; } /** * Crea o aggiorna gestione */ private function createOrUpdateGestione(array $gestioneData, ?string $yearKey = null): GestioneContabile { $gestione = GestioneContabile::updateOrCreate([ 'tenant_id' => $this->tenantId, 'stabile_id' => $this->stabileId, 'anno_gestione' => $gestioneData['anno_gestione'], 'tipo_gestione' => $gestioneData['tipo_gestione'], 'numero_straordinaria' => $gestioneData['numero_straordinaria'] ?? null, ], [ 'denominazione' => $gestioneData['denominazione'], 'data_inizio' => $gestioneData['data_inizio'], 'data_fine' => $gestioneData['data_fine'], 'stato' => 'aperta', 'protocollo_prefix' => $gestioneData['protocollo_prefix'], ]); // Cache per lookup veloce $year = $yearKey ?? $gestioneData['anno_gestione']; $tipo = $gestioneData['tipo_gestione']; if ($tipo === 'straordinaria') { $key = "straordinaria_{$gestioneData['numero_straordinaria']}"; } else { $key = $tipo; } $this->gestioni[$year][$key] = $gestione; return $gestione; } /** * Import operazioni per gestione specifica */ private function importOperazioniForGestione(GestioneContabile $gestione, string $mdbPath): int { // Filtro per tipo gestione $whereClause = $this->buildGestioneWhereClause($gestione); $operazioni = $this->queryMdb($mdbPath, " SELECT * FROM Operazioni WHERE {$whereClause} "); usort($operazioni, function($a, $b) { $idA = (int) ($a['id_operaz'] ?? 0); $idB = (int) ($b['id_operaz'] ?? 0); return $idA <=> $idB; }); $imported = 0; foreach ($operazioni as $op) { try { $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? ''); $protocollo = $this->generateProtocollo($gestione->id, $op); $competClean = empty($op['compet']) ? 'C' : trim($op['compet']); $natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']); $nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra']; $opId = $op['id_operaz'] ?? $op['id'] ?? null; $dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null; $importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0; $codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? ''; $codBen = $op['cod_ben'] ?? ''; $isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata'; $amount = (float) $importo; $dareVal = (float) ($op['importo_spese'] ?? 0); $avereVal = (float) ($op['importo_entrate'] ?? 0); if ($dareVal == 0 && $avereVal == 0 && $amount != 0) { if ($isEntrata) { $avereVal = $amount; } else { $dareVal = $amount; } } $contoBancarioId = $this->resolveContoBancarioId($mdbPath, $op['cod_cassa'] ?? null); DB::table('operazioni_contabili')->insert([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestione->id, 'legacy_id' => $opId, 'descrizione' => $op['note'] ?? $op['descrizione'] ?? $op['natura'] ?? 'Operazione', 'data_operazione' => $this->parseGesconDate($dtSpe), 'compet' => $competClean, 'natura2' => $natura2Clean, 'n_stra' => $nStraClean, 'protocollo_numero' => $protocollo['numero'], 'protocollo_completo' => $protocollo['completo'], 'voce_spesa_snapshot' => json_encode($voceSnapshot), 'dare' => $dareVal, 'avere' => $avereVal, 'conto_contabile' => $codVoce, 'conto_bancario_id' => $contoBancarioId, 'stato_operazione' => 'confermata', 'created_at' => now(), 'updated_at' => now(), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing operazione for gestione", [ 'gestione_id' => $gestione->id, 'operazione' => $op, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi per gestione */ private function importIncassiForGestione(GestioneContabile $gestione, string $mdbPath): int { $whereClause = $this->buildGestioneWhereClause($gestione); $incassi = $this->queryMdb($mdbPath, " SELECT * FROM incassi WHERE {$whereClause} ORDER BY data_inc "); $imported = 0; foreach ($incassi as $inc) { try { // Import incasso... $imported++; } catch (\Exception $e) { Log::error("Error importing incasso", [ 'gestione_id' => $gestione->id, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import ritenute per gestione */ private function importRitenuteFOrGestione(GestioneContabile $gestione, string $mdbPath): int { $ritenute = $this->queryMdb($mdbPath, " SELECT n.*, o.data_oper, o.euro as imponibile_operazione FROM Nettovers_RDA n LEFT JOIN operazioni o ON n.Rif_RDA = o.n_operazione WHERE n.Rif_RDA IS NOT NULL AND {$this->buildGestioneJoinClause($gestione, 'o')} ORDER BY n.Rif_RDA "); $imported = 0; foreach ($ritenute as $ra) { try { RegistroRitenuteAcconto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestione->id, 'numero_progressivo' => $ra['Rif_RDA'], 'data_competenza' => $this->parseGesconDate($ra['data_oper']), 'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0), 'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20), 'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0), 'rif_rda' => $ra['Rif_RDA'], 'stato_versamento' => 'da_versare', 'metadati_gescon' => json_encode($ra), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing ritenuta", [ 'gestione_id' => $gestione->id, 'ritenuta' => $ra, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi estratto conto per gestione */ private function importIncassiEstrattoContoForGestione(GestioneContabile $gestione, string $mdbPath): int { $incassiEc = $this->queryMdb($mdbPath, " SELECT * FROM Inc_da_ec ORDER BY data_oper "); $imported = 0; foreach ($incassiEc as $inc) { try { IncassoEstrattoConto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestione->id, 'id_gescon' => $inc['id'] ?? null, 'data_operazione' => $this->parseGesconDate($inc['data_oper']), 'data_valuta' => $this->parseGesconDate($inc['data_valuta']), 'importo' => (float) ($inc['importo'] ?? 0), 'descrizione' => $inc['descrizione'] ?? '', 'stato' => 'da_riconciliare', 'metadati_gescon' => json_encode($inc), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing incasso EC", [ 'gestione_id' => $gestione->id, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Costruisce WHERE clause per filtrare per gestione */ private function buildGestioneWhereClause(GestioneContabile $gestione): string { switch ($gestione->tipo_gestione) { case 'ordinaria': return "(gestione = 'O' OR gestione = 'o' OR gestione IS NULL OR gestione = '')"; case 'riscaldamento': return "(gestione = 'R' OR gestione = 'r')"; case 'straordinaria': $numero = $gestione->numero_straordinaria; return "(gestione = 'S' OR gestione = 's') AND N_STRA = {$numero}"; default: return "1=1"; } } /** * Costruisce JOIN clause per filtrare per gestione */ private function buildGestioneJoinClause(GestioneContabile $gestione, string $alias = ''): string { $prefix = $alias ? "{$alias}." : ''; switch ($gestione->tipo_gestione) { case 'ordinaria': return "({$prefix}gestione = 'O' OR {$prefix}gestione = 'o' OR {$prefix}gestione IS NULL OR {$prefix}gestione = '')"; case 'riscaldamento': return "({$prefix}gestione = 'R' OR {$prefix}gestione = 'r')"; case 'straordinaria': $numero = $gestione->numero_straordinaria; return "({$prefix}gestione = 'S' OR {$prefix}gestione = 's') AND {$prefix}N_STRA = {$numero}"; default: return "1=1"; } } /** * Import dati per una specifica gestione/anno */ public function importGestioneYear(string $year, array $paths): array { if (! isset($this->gestioni[$year])) { $this->initGestioniForYear($year, $paths); } $results = [ 'operazioni' => 0, 'incassi' => 0, 'ritenute' => 0, 'rate_emesse' => 0, 'incassi_ec' => 0, ]; // 1. Import operazioni contabili $results['operazioni'] = $this->importOperazioni($year, $paths['singolo_anno']); // 2. Import incassi $results['incassi'] = $this->importIncassi($year, $paths['singolo_anno']); // 3. Import ritenute d'acconto $results['ritenute'] = $this->importRitenuteAcconto($year, $paths['singolo_anno']); // 4. Import rate emesse (da generale_stabile.mdb) if (file_exists($paths['generale_stabile'])) { $results['rate_emesse'] = $this->importRateEmesse($year, $paths['generale_stabile']); } // 5. Import incassi estratto conto if (file_exists($paths['generale_stabile'])) { $results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['generale_stabile']); } return $results; } /** * Import operazioni con gestione COMPET e NATURA2 */ private function importOperazioni(string $year, string $mdbPath): int { $imported = 0; // Query MDB per operazioni $operazioni = $this->queryMdb($mdbPath, " SELECT * FROM Operazioni "); usort($operazioni, function($a, $b) { $idA = (int) ($a['id_operaz'] ?? 0); $idB = (int) ($b['id_operaz'] ?? 0); return $idA <=> $idB; }); foreach ($operazioni as $op) { try { // Determina gestione $gestioneId = $this->determineGestioneId($year, $op); // Genera protocollo $protocollo = $this->generateProtocollo($gestioneId, $op); // Snapshot voce spesa $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? ''); $competClean = empty($op['compet']) ? 'C' : trim($op['compet']); $natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']); $nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra']; $opId = $op['id_operaz'] ?? $op['id'] ?? null; $dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null; $importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0; $codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? ''; $codBen = $op['cod_ben'] ?? ''; $isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata'; $amount = (float) $importo; $dareVal = (float) ($op['importo_spese'] ?? 0); $avereVal = (float) ($op['importo_entrate'] ?? 0); if ($dareVal == 0 && $avereVal == 0 && $amount != 0) { if ($isEntrata) { $avereVal = $amount; } else { $dareVal = $amount; } } $contoBancarioId = $this->resolveContoBancarioId($mdbPath, $op['cod_cassa'] ?? null); DB::table('operazioni_contabili')->insert([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'legacy_id' => $opId, 'descrizione' => $op['note'] ?? $op['descrizione'] ?? $op['natura'] ?? 'Operazione', 'data_operazione' => $this->parseGesconDate($dtSpe), 'compet' => $competClean, 'natura2' => $natura2Clean, 'n_stra' => $nStraClean, 'protocollo_numero' => $protocollo['numero'], 'protocollo_completo' => $protocollo['completo'], 'voce_spesa_snapshot' => json_encode($voceSnapshot), 'dare' => $dareVal, 'avere' => $avereVal, 'conto_contabile' => $codVoce, 'conto_bancario_id' => $contoBancarioId, 'stato_operazione' => 'confermata', 'created_at' => now(), 'updated_at' => now(), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing operazione", [ 'year' => $year, 'operazione' => $op, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi da incassi */ private function importIncassi(string $year, string $mdbPath): int { $imported = 0; $incassi = $this->queryMdb($mdbPath, " SELECT * FROM incassi "); usort($incassi, function($a, $b) { $dateA = $a['dt_empag'] ?? ''; $dateB = $b['dt_empag'] ?? ''; return strcmp($dateA, $dateB); }); foreach ($incassi as $inc) { try { $gestioneId = $this->gestioni[$year]['ordinaria']->id; Incasso::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'condominio_id' => $this->gestioni[$year]['ordinaria']->stabile_id ?? 1, 'conto_bancario_id' => $this->resolveContoBancarioId($mdbPath, $inc['cod_cassa'] ?? null), 'id_incasso_gescon' => $inc['ID_incasso'] ?? null, 'cod_cond_gescon' => $inc['cod_cond'] ?? null, 'cond_inquil' => $inc['cond_inquil'] ?? null, 'n_riferimento' => $inc['n_riferimento'] ?? null, 'anno_rif' => (int) ($inc['anno_rif'] ?? $year), 'n_ricevuta' => $inc['n_ricevuta'] ?? null, 'anno_ricev' => $inc['anno_ricev'] ?? null, 'n_mese' => $inc['n_mese'] ?? null, 'o_r_s' => $inc['o_r_s'] ?? null, 'importo_pagato' => (float) ($inc['importo_pagato'] ?? 0), 'importo_pagato_euro' => (float) ($inc['importo_pagato_euro'] ?? 0), 'dt_empag' => $this->parseGesconDate($inc['dt_empag'] ?? null), 'descrizione' => $inc['descrizione'] ?? null, 'cod_cassa_gescon' => $inc['cod_cassa'] ?? null, 'metadati_gescon' => json_encode($inc), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing incasso", [ 'year' => $year, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import ritenute d'acconto da Nettovers_RDA */ private function importRitenuteAcconto(string $year, string $mdbPath): int { $imported = 0; if (!$this->mdbTableExists($mdbPath, 'Nettovers_RDA')) { return 0; } // Query per ritenute $ritenute = $this->queryMdb($mdbPath, " SELECT n.*, o.dt_spe, o.importo_euro as imponibile_operazione FROM Nettovers_RDA n LEFT JOIN Operazioni o ON n.Rif_RDA = o.id_operaz WHERE n.Rif_RDA IS NOT NULL ORDER BY n.Rif_RDA "); foreach ($ritenute as $ra) { try { $gestioneId = $this->gestioni[$year]['ordinaria']->id; RegistroRitenuteAcconto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'numero_progressivo' => $ra['Rif_RDA'], 'data_competenza' => $this->parseGesconDate($ra['dt_spe'] ?? null), 'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0), 'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20), 'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0), 'rif_rda' => $ra['Rif_RDA'], 'stato_versamento' => 'da_versare', // Default 'metadati_gescon' => json_encode($ra), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing ritenuta", [ 'year' => $year, 'ritenuta' => $ra, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import incassi estratto conto da Inc_da_ec */ private function importIncassiEstrattoConto(string $year, string $mdbPath): int { $imported = 0; $incassiEc = $this->queryMdb($mdbPath, " SELECT * FROM Inc_da_ec "); usort($incassiEc, function($a, $b) { $protoA = (int) ($a['protocollo'] ?? 0); $protoB = (int) ($b['protocollo'] ?? 0); return $protoA <=> $protoB; }); foreach ($incassiEc as $inc) { try { $gestioneId = $this->gestioni[$year]['ordinaria']->id; IncassoEstrattoConto::create([ 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, 'id_gescon' => $inc['protocollo'] ?? null, 'data_operazione' => $this->parseGesconDate($inc['Data_pag'] ?? null), 'data_valuta' => $this->parseGesconDate($inc['Data_pag'] ?? null), 'importo' => (float) ($inc['Importo'] ?? 0), 'descrizione' => trim(($inc['Descrizione_Aggiuntiva'] ?? '') . ' ' . ($inc['Nome_condomino'] ?? '')), 'ordinante' => $inc['Nome_condomino'] ?? null, 'riferimento_bancario' => $inc['Num_incasso'] ?? null, 'file_origine' => $inc['Nome_file_pdf'] ?? null, 'stato' => 'da_riconciliare', 'metadati_gescon' => json_encode($inc), ]); $imported++; } catch (\Exception $e) { Log::error("Error importing incasso EC", [ 'year' => $year, 'incasso' => $inc, 'error' => $e->getMessage(), ]); } } return $imported; } /** * Import rate emesse da generale_stabile.mdb */ private function importRateEmesse(string $year, string $mdbPath): int { $imported = 0; // Unifica emes_det, emes_det_2, emes_gen $tables = ['emes_det', 'emes_det_2', 'emes_gen']; foreach ($tables as $table) { if (!$this->mdbTableExists($mdbPath, $table)) { continue; } $rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table}"); usort($rate, function($a, $b) { $dateA = $a['data_em'] ?? ''; $dateB = $b['data_em'] ?? ''; return strcmp($dateA, $dateB); }); foreach ($rate as $rata) { try { // Logica import rate... $imported++; } catch (\Exception $e) { Log::error("Error importing rata", ['table' => $table, 'rata' => $rata, 'error' => $e->getMessage()]); } } } return $imported; } /** * Determina gestione ID basata su dati operazione */ private function determineGestioneId(string $year, array $operazione): int { $gestione = $operazione['gestione'] ?? 'O'; $nStra = (int) ($operazione['N_STRA'] ?? 0); switch (strtoupper($gestione)) { case 'R': return $this->gestioni[$year]['riscaldamento']->id; case 'S': // Crea gestione straordinaria se non esiste return $this->getOrCreateStraordinaria($year, $nStra); default: return $this->gestioni[$year]['ordinaria']->id; } } /** * Ottiene o crea gestione straordinaria */ private function getOrCreateStraordinaria(string $year, int $nStra): int { $key = "straordinaria_{$nStra}"; $calendarYear = $this->directoryToYearMap[$year] ?? (int) $year; if (! isset($this->gestioni[$year][$key])) { $this->gestioni[$year][$key] = GestioneContabile::create([ 'tenant_id' => $this->tenantId, 'stabile_id' => $this->stabileId, 'anno_gestione' => $calendarYear, 'tipo_gestione' => 'straordinaria', 'numero_straordinaria' => $nStra, 'denominazione' => "Gestione Straordinaria {$calendarYear} - {$nStra}", 'data_inizio' => "{$calendarYear}-01-01", 'data_fine' => "{$calendarYear}-12-31", 'stato' => 'aperta', 'protocollo_prefix' => "S{$calendarYear}-{$nStra}", ]); } return $this->gestioni[$year][$key]->id; } /** * Query diretta su file MDB */ private function queryMdb(string $mdbPath, string $query): array { $query = preg_replace('/\s+/', ' ', trim($query)); // Usa mdb-tools per query diretta $command = sprintf( 'echo %s | mdb-sql -P -F -d %s %s', escapeshellarg($query), escapeshellarg("\t"), escapeshellarg($mdbPath) ); $output = shell_exec($command); // Parser output MDB in array associativo return $this->parseMdbOutput($output); } private function mdbTableExists(string $mdbPath, string $tableName): bool { if (!file_exists($mdbPath)) { return false; } $output = shell_exec("mdb-tables -1 " . escapeshellarg($mdbPath) . " 2>/dev/null"); if (!$output) { return false; } $tables = array_map('strtolower', array_map('trim', explode("\n", trim($output)))); return in_array(strtolower($tableName), $tables, true); } private function resolveContoBancarioId(string $mdbPath, ?string $codCassa): int { $stabileCode = '0021'; // Default fallback if (preg_match('/legacy\/([^\/]+)/', str_replace('\\', '/', $mdbPath), $matches)) { $stabileCode = $matches[1]; } $cod = strtoupper(trim($codCassa ?? '')); if (empty($cod)) { $cod = 'CON'; // Default fallback to cash cassa } // Try to match by code: stabileCode-cod $conto = DB::table('conti_bancari') ->where('tenant_id', $this->tenantId) ->where('codice', "{$stabileCode}-{$cod}") ->first(); if ($conto) { return (int) $conto->id; } // Try fallback to any account for this stable prefix $conto = DB::table('conti_bancari') ->where('tenant_id', $this->tenantId) ->where('codice', 'like', "{$stabileCode}-%") ->first(); if ($conto) { return (int) $conto->id; } // Final fallback to 1 return 1; } /** * Parser output mdb-sql in array */ private function parseMdbOutput(string $output): array { $lines = explode("\n", trim($output)); if (empty($lines)) { return []; } $headers = array_map('trim', explode("\t", array_shift($lines))); $headerCount = count($headers); $data = []; $currentValues = []; foreach ($lines as $line) { if ($line === '' && empty($currentValues)) { continue; } $values = explode("\t", $line); if (empty($currentValues)) { $currentValues = $values; } else { $lastIdx = count($currentValues) - 1; $currentValues[$lastIdx] .= "\n" . array_shift($values); $currentValues = array_merge($currentValues, $values); } if (count($currentValues) >= $headerCount) { $rowValues = array_slice($currentValues, 0, $headerCount); $leftover = array_slice($currentValues, $headerCount); try { $data[] = array_combine($headers, $rowValues); } catch (\Throwable $e) { // Ignora o logga errori per evitare crash } $currentValues = $leftover; } } return $data; } /** * Genera protocollo per operazione */ private function generateProtocollo(int $gestioneId, array $operazione): array { $gestione = GestioneContabile::find($gestioneId); $numero = (isset($operazione['n_spe']) && is_numeric($operazione['n_spe']) && (int) $operazione['n_spe'] > 0) ? (int) $operazione['n_spe'] : (DB::table('operazioni_contabili')->where('gestione_id', $gestioneId)->max('protocollo_numero') ?? 0) + 1; return [ 'numero' => $numero, 'completo' => ($gestione->protocollo_prefix ?? 'O') . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT), ]; } /** * Snapshot voce spesa */ private function getVoceSpesaSnapshot(string $mdbPath, string $codVoce): array { if (empty($codVoce)) { return []; } $voci = $this->queryMdb($mdbPath, " SELECT * FROM voci_spesa WHERE cod_voce = '{$codVoce}' "); return $voci[0] ?? []; } /** * Parse data Gescon */ private function parseGesconDate($dateValue): ?string { if (empty($dateValue)) { return null; } // Prova vari formati date Gescon/Access $formats = ['Y-m-d', 'd/m/Y', 'm/d/Y', 'Y-m-d H:i:s']; foreach ($formats as $format) { $date = \DateTime::createFromFormat($format, $dateValue); if ($date !== false) { return $date->format('Y-m-d'); } } return null; } }