error('Tabella contabilita_fatture_fornitori non trovata.'); return self::FAILURE; } $tenant = (string) $this->argument('tenant'); $stabileId = $this->resolveStabileId(); $stabileCode = trim((string) ($this->option('stabile-code') ?: '')); $anno = $this->option('anno') ? (int) $this->option('anno') : null; $limit = $this->option('limit') ? max(1, (int) $this->option('limit')) : null; $dryRun = (bool) $this->option('dry-run'); if (! $stabileId) { $this->error('Stabile non risolto: passa --stabile-id oppure --stabile-code.'); return self::FAILURE; } [$sourceType, $query] = $this->buildSourceQuery($tenant, $stabileCode, $anno); if (! $query) { $this->error('Nessuna sorgente disponibile: né stg_fatture_gescon né gescon_import.fatture.'); return self::FAILURE; } if ($limit) { $query->limit($limit); } $rows = $query->get(); if ($rows->isEmpty()) { $this->warn('Nessuna fattura da sincronizzare.'); return self::SUCCESS; } $codes = $rows->pluck('cod_fornitore') ->filter(fn($v) => trim((string) $v) !== '') ->map(fn($v) => trim((string) $v)) ->flatMap(function (string $code) { $trim = trim($code); $noZero = ltrim($trim, '0'); $out = [$trim]; if ($noZero !== '' && $noZero !== $trim) { $out[] = $noZero; } return $out; }) ->unique() ->values() ->all(); $fornitoreMap = $this->buildFornitoreMapByLegacyCode($codes); $created = 0; $updated = 0; $skipped = 0; foreach ($rows as $row) { $codForn = trim((string) ($row->cod_fornitore ?? '')); $codFornNoZero = ltrim($codForn, '0'); $fornitoreId = $fornitoreMap[$codForn] ?? ($codFornNoZero !== '' ? ($fornitoreMap[$codFornNoZero] ?? null) : null); if (! $fornitoreId) { $skipped++; continue; } $gestioneTipoRaw = strtoupper(trim((string) ($row->reg_gestione ?? 'O'))); $tipoGestione = match ($gestioneTipoRaw) { 'R' => 'riscaldamento', 'S' => 'straordinaria', default => 'ordinaria', }; $numeroStra = $tipoGestione === 'straordinaria' && is_numeric($row->reg_nstra ?? null) ? (int) $row->reg_nstra : null; $gestioneId = null; if (Schema::hasTable('gestioni_contabili')) { $gestioneQuery = DB::table('gestioni_contabili') ->where('tenant_id', $tenant) ->where('tipo_gestione', $tipoGestione) ->when($stabileId, fn($q) => $q->where('stabile_id', (int) $stabileId)); if ($anno) { $gestioneQuery->where('anno_gestione', $anno); } if ($numeroStra !== null) { $gestioneQuery->where('numero_straordinaria', $numeroStra); } $gestioneId = $gestioneQuery->value('id'); } $dataDocumento = $this->toDate($row->data_fattura ?? null); $numeroDocumento = $this->nullIfEmpty($row->numero_fattura ?? null); $totale = $this->money($row->totale_fattura ?? $row->importo_totale ?? $row->importo_euro ?? 0); if (! $dataDocumento || ! $numeroDocumento) { $skipped++; continue; } $existing = DB::table('contabilita_fatture_fornitori') ->where('stabile_id', (int) $stabileId) ->where('fornitore_id', (int) $fornitoreId) ->where('numero_documento', $numeroDocumento) ->whereDate('data_documento', $dataDocumento) ->first(['id']); $payload = [ 'stabile_id' => $stabileId, 'gestione_id' => $gestioneId, 'fornitore_id' => $fornitoreId, 'data_registrazione' => $dataDocumento, 'data_documento' => $dataDocumento, 'numero_documento' => $numeroDocumento, 'causale_contabile' => 'FATT_FORN', 'descrizione' => $this->nullIfEmpty($row->descrizione_sintetica ?? $row->descrizione ?? $row->descrizione_corpo ?? null), 'imponibile' => $this->money($row->imponibile ?? $row->importo_totale ?? 0), 'iva' => $this->money($row->importo_iva ?? $row->iva ?? 0), 'totale' => $totale, 'data_scadenza' => $this->toDate($row->data_scadenza ?? null), 'data_pagamento' => $this->toDate($row->data_pagamento ?? null), 'ritenuta_importo' => $this->money($row->importo_rda ?? $row->ritenuta ?? 0), 'netto_da_pagare' => $this->money($row->importo_netto ?? $totale), 'stato' => ! empty($row->data_pagamento) ? 'pagato' : 'inserito', 'note' => $this->buildNote($sourceType, $row), 'dati_fornitura' => json_encode([ 'source_table' => $sourceType, 'legacy_id' => $row->legacy_id_fatture ?? null, 'legacy_cod_fornitore' => $codForn !== '' ? $codForn : null, 'legacy_riferimento' => $row->riferimento ?? null, ], JSON_UNESCAPED_UNICODE), 'updated_at' => now(), ]; if (Schema::hasColumn('contabilita_fatture_fornitori', 'fattura_elettronica_id') && Schema::hasTable('fatture_elettroniche')) { $feId = $this->resolveFatturaElettronicaId((int) $stabileId, (int) $fornitoreId, $numeroDocumento, $dataDocumento); if ($feId) { $payload['fattura_elettronica_id'] = $feId; } } if ($dryRun) { if ($existing) { $updated++; } else { $created++; } continue; } if ($existing) { DB::table('contabilita_fatture_fornitori') ->where('id', (int) $existing->id) ->update($payload); $updated++; } else { $payload['created_at'] = now(); DB::table('contabilita_fatture_fornitori')->insert($payload); $created++; } } $mode = $dryRun ? 'SIMULAZIONE' : 'SYNC'; $this->info("{$mode} completata: create={$created}, aggiornate={$updated}, saltate={$skipped}"); return self::SUCCESS; } private function buildSourceQuery(string $tenant, string $stabileCode, ?int $anno): array { $legacyStabileIds = $this->resolveLegacyStabileIdCandidates($stabileCode); $legacyCodiciStabile = $this->resolveLegacyCodiceCandidates($stabileCode); if (Schema::hasTable('stg_fatture_gescon')) { $query = DB::table('stg_fatture_gescon') ->where('tenant_id', $tenant); if ($anno && Schema::hasColumn('stg_fatture_gescon', 'data_fattura')) { $query->whereYear('data_fattura', $anno); } if (! empty($legacyStabileIds) && Schema::hasColumn('stg_fatture_gescon', 'legacy_id_stabile')) { $query->whereIn('legacy_id_stabile', $legacyStabileIds); } if ((clone $query)->exists()) { return ['stg_fatture_gescon', $query]; } } if (Schema::connection('gescon_import')->hasTable('fatture')) { $query = DB::connection('gescon_import')->table('fatture'); if (! empty($legacyCodiciStabile) && Schema::connection('gescon_import')->hasColumn('fatture', 'cod_stabile')) { $query->whereIn('cod_stabile', $legacyCodiciStabile); } if ($anno && Schema::connection('gescon_import')->hasColumn('fatture', 'data_fattura')) { $query->whereYear('data_fattura', $anno); } return ['fatture', $query]; } return [null, null]; } private function resolveLegacyStabileIdCandidates(string $stabileCode): array { $ids = []; $trim = trim($stabileCode); if ($trim !== '' && is_numeric($trim)) { $ids[] = (int) ltrim($trim, '0'); $ids[] = (int) $trim; } if ($trim !== '' && Schema::hasTable('stabili')) { $row = DB::table('stabili') ->where(function ($q) use ($trim) { $q->where('codice_stabile', $trim) ->orWhere('cod_stabile', $trim); }) ->first(['old_id', 'cod_stabile', 'codice_stabile']); foreach ([(string) ($row->old_id ?? ''), (string) ($row->cod_stabile ?? ''), (string) ($row->codice_stabile ?? '')] as $value) { $v = trim($value); if ($v !== '' && is_numeric($v)) { $ids[] = (int) ltrim($v, '0'); $ids[] = (int) $v; } } } $ids = array_values(array_unique(array_filter($ids, fn($v) => is_int($v) && $v > 0))); return $ids; } private function resolveLegacyCodiceCandidates(string $stabileCode): array { $codes = []; $trim = trim($stabileCode); if ($trim !== '') { $codes[] = $trim; $noZero = ltrim($trim, '0'); if ($noZero !== '') { $codes[] = $noZero; if (is_numeric($noZero)) { $codes[] = str_pad((string) ((int) $noZero), 4, '0', STR_PAD_LEFT); } } } if ($trim !== '' && Schema::hasTable('stabili')) { $row = DB::table('stabili') ->where(function ($q) use ($trim) { $q->where('codice_stabile', $trim) ->orWhere('cod_stabile', $trim); }) ->first(['old_id', 'cod_stabile', 'codice_stabile']); foreach ([(string) ($row->old_id ?? ''), (string) ($row->cod_stabile ?? ''), (string) ($row->codice_stabile ?? '')] as $value) { $v = trim($value); if ($v === '') { continue; } $codes[] = $v; $noZero = ltrim($v, '0'); if ($noZero !== '') { $codes[] = $noZero; if (is_numeric($noZero)) { $codes[] = str_pad((string) ((int) $noZero), 4, '0', STR_PAD_LEFT); } } } } $codes = array_values(array_unique(array_filter($codes, fn($v) => is_string($v) && $v !== ''))); return $codes; } private function resolveStabileId(): ?int { $fromOption = $this->option('stabile-id'); if (is_numeric($fromOption)) { return (int) $fromOption; } $code = trim((string) ($this->option('stabile-code') ?: '')); if ($code !== '' && Schema::hasTable('stabili')) { $id = DB::table('stabili')->where('codice_stabile', $code)->value('id'); return is_numeric($id) ? (int) $id : null; } return null; } private function nullIfEmpty($value): ?string { if ($value === null) { return null; } $trim = trim((string) $value); return $trim === '' ? null : $trim; } private function toDate($value): ?string { if ($value === null) { return null; } $raw = trim((string) $value); if ($raw === '') { return null; } if (preg_match('/^\d{4}-\d{2}-\d{2}/', $raw)) { return substr($raw, 0, 10); } if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $raw)) { $dt = \DateTime::createFromFormat('d/m/Y', $raw); return $dt?->format('Y-m-d'); } $ts = @strtotime($raw); return $ts ? date('Y-m-d', $ts) : null; } private function money($value): float { if ($value === null || $value === '') { return 0.0; } $raw = str_replace(["\r", "\n", "\t", ' '], '', (string) $value); if (str_contains($raw, ',') && str_contains($raw, '.')) { $raw = str_replace('.', '', $raw); $raw = str_replace(',', '.', $raw); } elseif (str_contains($raw, ',')) { $raw = str_replace(',', '.', $raw); } return is_numeric($raw) ? (float) $raw : 0.0; } private function buildNote(string $sourceType, object $row): ?string { $parts = [ 'Legacy ' . $sourceType, ]; if (! empty($row->legacy_id_fatture)) { $parts[] = 'ID ' . $row->legacy_id_fatture; } if (! empty($row->riferimento)) { $parts[] = 'Rif ' . $row->riferimento; } return implode(' · ', $parts); } private function resolveFatturaElettronicaId(int $stabileId, int $fornitoreId, string $numeroDocumento, string $dataDocumento): ?int { $numeroNorm = strtolower(trim((string) $numeroDocumento)); if ($stabileId <= 0 || $fornitoreId <= 0 || $numeroNorm === '' || $dataDocumento === '') { return null; } $fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreId); $base = DB::table('fatture_elettroniche') ->where('stabile_id', $stabileId) ->whereIn('fornitore_id', $fornitoreIds) ->whereDate('data_fattura', $dataDocumento) ->whereRaw('LOWER(TRIM(numero_fattura)) = ?', [$numeroNorm]); $strict = (clone $base) ->orderByDesc('id') ->value('id'); if (is_numeric($strict) && (int) $strict > 0) { return (int) $strict; } $rows = (clone $base) ->orderByDesc('id') ->limit(2) ->pluck('id') ->filter(fn($v) => is_numeric($v)) ->map(fn($v) => (int) $v) ->values() ->all(); if (count($rows) === 1) { return $rows[0]; } return null; } /** @param array $codes * @return array */ private function buildFornitoreMapByLegacyCode(array $codes): array { if ($codes === [] || ! Schema::hasTable('fornitori')) { return []; } $map = []; $suppliers = Fornitore::query()->get(['id', 'cod_forn', 'old_id', 'operational_config']); foreach ($suppliers as $supplier) { $directCodes = []; if ((int) ($supplier->cod_forn ?? 0) > 0) { $directCodes[] = (string) (int) $supplier->cod_forn; } if ((int) ($supplier->old_id ?? 0) > 0) { $directCodes[] = (string) (int) $supplier->old_id; } $operationalConfig = is_array($supplier->operational_config ?? null) ? $supplier->operational_config : []; $aliasCodes = array_map( fn($value): string => (string) (int) $value, array_filter((array) ($operationalConfig['legacy_cod_forn_aliases'] ?? []), fn($value): bool => is_numeric($value) && (int) $value > 0) ); $aliasOldIds = array_map( fn($value): string => (string) (int) $value, array_filter((array) ($operationalConfig['legacy_old_id_aliases'] ?? []), fn($value): bool => is_numeric($value) && (int) $value > 0) ); foreach (array_merge($directCodes, $aliasCodes, $aliasOldIds) as $code) { $this->registerFornitoreCodeMap($map, $code, (int) $supplier->id, in_array($code, $directCodes, true)); } } foreach ($codes as $code) { $trim = trim((string) $code); if ($trim === '') { continue; } $noZero = ltrim($trim, '0'); if ($noZero !== '' && isset($map[$noZero]) && ! isset($map[$trim])) { $map[$trim] = $map[$noZero]; } if ($noZero !== '' && isset($map[$trim]) && ! isset($map[$noZero])) { $map[$noZero] = $map[$trim]; } } return $map; } /** @param array $map */ private function registerFornitoreCodeMap(array &$map, string $code, int $supplierId, bool $isDirectCode): void { $trim = trim($code); if ($supplierId <= 0 || $trim === '') { return; } $keys = array_values(array_unique(array_filter([$trim, ltrim($trim, '0')], fn($value): bool => $value !== ''))); foreach ($keys as $key) { if (! isset($map[$key]) || $isDirectCode) { $map[$key] = $supplierId; } } } /** @return array */ private function resolveEquivalentFornitoreIds(int $fornitoreId): array { if ($fornitoreId <= 0 || ! Schema::hasTable('fornitori')) { return [$fornitoreId]; } $supplier = Fornitore::query()->find($fornitoreId, ['id', 'rubrica_id', 'partita_iva', 'codice_fiscale', 'old_id']); if (! $supplier) { return [$fornitoreId]; } $query = Fornitore::query()->where('id', $fornitoreId); $rubricaId = (int) ($supplier->rubrica_id ?? 0); if ($rubricaId > 0) { $query->orWhere('rubrica_id', $rubricaId); } $partitaIva = trim((string) ($supplier->partita_iva ?? '')); if ($partitaIva !== '') { $query->orWhere('partita_iva', $partitaIva); } $codiceFiscale = trim((string) ($supplier->codice_fiscale ?? '')); if ($codiceFiscale !== '') { $query->orWhere('codice_fiscale', $codiceFiscale); } $oldId = (int) ($supplier->old_id ?? 0); if ($oldId > 0) { $query->orWhere('old_id', $oldId); } $ids = $query->pluck('id') ->map(fn($value) => (int) $value) ->filter(fn($value) => $value > 0) ->unique() ->values() ->all(); return $ids !== [] ? $ids : [$fornitoreId]; } }