Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 77 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
| ZenitalCatalog | |
0.00% |
0 / 77 |
|
0.00% |
0 / 5 |
240 | |
0.00% |
0 / 1 |
| sucursalesMap | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| regionMap | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| forget | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| loadSucursalesMap | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
56 | |||
| loadRegionMap | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
30 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services; |
| 4 | |
| 5 | use App\Models\TblFinanceRegions; |
| 6 | use App\Models\TblFinanceSedes; |
| 7 | use Illuminate\Support\Facades\DB; |
| 8 | use Illuminate\Support\Facades\Log; |
| 9 | |
| 10 | /** |
| 11 | * Single source of truth for Zenital DWH catalog lookups |
| 12 | * (dim_sucursal, dim_region) needed by Finance read/write paths. |
| 13 | * |
| 14 | * FIRE-1148: replaces two pairs of duplicate private methods that lived in |
| 15 | * FinanceController and SendFinanceReport — same query, same alias map, same |
| 16 | * SCD-2 dedup. Caches via ResultCache in the 'finance' domain so each cold |
| 17 | * request used to pay 200–500 ms over the WAN to dim_sucursal + dim_region; |
| 18 | * after this it's a Redis hit (< 1 ms) for the TTL window. |
| 19 | * |
| 20 | * Callers that mutate tbl_finance_sedes / tbl_finance_regions must call |
| 21 | * `forget()` (or `ResultCache::forgetDomain('finance')`) so the new entries |
| 22 | * are visible immediately — see FinanceController::ensureSedesExist. |
| 23 | */ |
| 24 | final class ZenitalCatalog |
| 25 | { |
| 26 | /** Cache TTL in seconds — 1h matches FIRE-1148's stated TTL. */ |
| 27 | private const TTL = 3600; |
| 28 | |
| 29 | private const DOMAIN = 'finance'; |
| 30 | |
| 31 | private const KEY_SUCURSALES = 'finance.zenital.sucursales_map'; |
| 32 | |
| 33 | private const KEY_REGIONS = 'finance.zenital.region_map'; |
| 34 | |
| 35 | /** |
| 36 | * Runtime sk_sucursal → local sede metadata. |
| 37 | * |
| 38 | * @return array<int, array{nombre: string, company_id: int, sede_id: int}> |
| 39 | */ |
| 40 | public function sucursalesMap(): array |
| 41 | { |
| 42 | return ResultCache::rememberKey(self::DOMAIN, self::KEY_SUCURSALES, self::TTL, fn (): array => $this->loadSucursalesMap()); |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * Runtime sk_region (Zenital) → region_id (Titan) lookup. |
| 47 | * |
| 48 | * @return array<int, int> |
| 49 | */ |
| 50 | public function regionMap(): array |
| 51 | { |
| 52 | return ResultCache::rememberKey(self::DOMAIN, self::KEY_REGIONS, self::TTL, fn (): array => $this->loadRegionMap()); |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Invalidate both maps. Call after inserting/updating tbl_finance_sedes |
| 57 | * or tbl_finance_regions rows so the new entries surface immediately. |
| 58 | */ |
| 59 | public function forget(): void |
| 60 | { |
| 61 | ResultCache::forgetDomain(self::DOMAIN); |
| 62 | } |
| 63 | |
| 64 | private function loadSucursalesMap(): array |
| 65 | { |
| 66 | $localByName = TblFinanceSedes::where('is_active', 1) |
| 67 | ->whereNotNull('company_id') |
| 68 | ->get() |
| 69 | ->keyBy(fn ($s) => mb_strtolower(trim((string) $s->name))); |
| 70 | |
| 71 | if ($localByName->isEmpty()) { |
| 72 | return []; |
| 73 | } |
| 74 | |
| 75 | // Zenital → Titan name aliases. Añadir aquí cuando el DWH use una |
| 76 | // etiqueta distinta de la de tbl_finance_sedes. |
| 77 | $aliases = [ |
| 78 | 'jomar alcarreña' => 'alcarrena', |
| 79 | ]; |
| 80 | |
| 81 | try { |
| 82 | putenv('PGCONNECT_TIMEOUT='.env('ZENITAL_DB_CONNECT_TIMEOUT', 5)); |
| 83 | DB::connection('zenital')->statement( |
| 84 | 'SET statement_timeout = '.(int) env('ZENITAL_DB_STATEMENT_TIMEOUT_MS', 15000) |
| 85 | ); |
| 86 | // cod_sucursal is the business key (sk_sucursal is the SCD-2 |
| 87 | // surrogate). When the DWH carries multiple versions of the |
| 88 | // same sucursal, every version shares the same cod_sucursal; |
| 89 | // we need to know which version we already kept. Ordering DESC |
| 90 | // ensures the latest sk_sucursal wins per cod. |
| 91 | $rows = DB::connection('zenital') |
| 92 | ->table('dim_sucursal') |
| 93 | ->select('sk_sucursal', 'cod_sucursal', 'sucursal') |
| 94 | ->where('sk_sucursal', '>', 0) |
| 95 | ->orderByDesc('sk_sucursal') |
| 96 | ->get(); |
| 97 | } catch (\Throwable $e) { |
| 98 | Log::channel('third-party')->warning('ZenitalCatalog::sucursalesMap — lookup failed: '.$e->getMessage()); |
| 99 | |
| 100 | return []; |
| 101 | } |
| 102 | |
| 103 | // SCD-2 dedup: when dim_sucursal has multiple rows with the same |
| 104 | // cod_sucursal (different sk_sucursal versions of the same business, |
| 105 | // e.g. STG had cod=14 with sk=149 "Alcarrena" + sk=150 "Jomar |
| 106 | // Alcarreña" both pointing at the same invoices), keep only one sk |
| 107 | // per cod. Pre-fix, both versions got name-mapped to the same local |
| 108 | // sede and the Zenital actuals query summed BOTH versions of every |
| 109 | // invoice — La Mancha doubled from 2,7 M€ to 5,4 M€ on the YTD email. |
| 110 | $map = []; |
| 111 | $seenCods = []; |
| 112 | foreach ($rows as $r) { |
| 113 | $cod = (int) ($r->cod_sucursal ?? 0); |
| 114 | if ($cod > 0) { |
| 115 | if (isset($seenCods[$cod])) { |
| 116 | continue; |
| 117 | } |
| 118 | $seenCods[$cod] = true; |
| 119 | } |
| 120 | |
| 121 | $zenitalKey = mb_strtolower(trim((string) $r->sucursal)); |
| 122 | $titanKey = $aliases[$zenitalKey] ?? $zenitalKey; |
| 123 | $sede = $localByName[$titanKey] ?? null; |
| 124 | if (! $sede) { |
| 125 | continue; |
| 126 | } |
| 127 | $map[(int) $r->sk_sucursal] = [ |
| 128 | 'nombre' => $sede->name, |
| 129 | 'company_id' => (int) $sede->company_id, |
| 130 | 'sede_id' => (int) $sede->id, |
| 131 | ]; |
| 132 | } |
| 133 | |
| 134 | return $map; |
| 135 | } |
| 136 | |
| 137 | private function loadRegionMap(): array |
| 138 | { |
| 139 | $titanRegions = TblFinanceRegions::where('is_active', 1) |
| 140 | ->get() |
| 141 | ->keyBy(fn ($r) => mb_strtolower(trim((string) $r->name))); |
| 142 | |
| 143 | if ($titanRegions->isEmpty()) { |
| 144 | return []; |
| 145 | } |
| 146 | |
| 147 | // Zenital region name (lowercased) → Titan region name (lowercased). |
| 148 | // Sub-entities and accent-only diffs go here. |
| 149 | $aliases = [ |
| 150 | 'cataluña' => 'catalunya', |
| 151 | 'oasys' => 'aragón', |
| 152 | 'crespo' => 'castilla y león', |
| 153 | 'togasa' => 'castilla y león', |
| 154 | 'extinfuego' => 'valencia', |
| 155 | 'almería' => 'andalucía', |
| 156 | ]; |
| 157 | |
| 158 | try { |
| 159 | putenv('PGCONNECT_TIMEOUT='.env('ZENITAL_DB_CONNECT_TIMEOUT', 5)); |
| 160 | DB::connection('zenital')->statement( |
| 161 | 'SET statement_timeout = '.(int) env('ZENITAL_DB_STATEMENT_TIMEOUT_MS', 15000) |
| 162 | ); |
| 163 | $rows = DB::connection('zenital') |
| 164 | ->table('dim_region') |
| 165 | ->select('sk_region', 'region') |
| 166 | ->where('sk_region', '>', 0) |
| 167 | ->get(); |
| 168 | } catch (\Throwable $e) { |
| 169 | Log::channel('third-party')->warning('ZenitalCatalog::regionMap — lookup failed: '.$e->getMessage()); |
| 170 | |
| 171 | return []; |
| 172 | } |
| 173 | |
| 174 | $map = []; |
| 175 | foreach ($rows as $r) { |
| 176 | $key = mb_strtolower(trim((string) $r->region)); |
| 177 | $titanKey = $aliases[$key] ?? $key; |
| 178 | if (! isset($titanRegions[$titanKey])) { |
| 179 | continue; |
| 180 | } |
| 181 | $map[(int) $r->sk_region] = (int) $titanRegions[$titanKey]->id; |
| 182 | } |
| 183 | |
| 184 | return $map; |
| 185 | } |
| 186 | } |