From f9a2f6f82a17e6a1f8ee5ef24de7c9835ac3d035 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sun, 22 Mar 2026 22:29:56 +0200 Subject: [PATCH] fix(parcel-sync): use LIMITE_UAT + fetchNomenByPk for county data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchCounties() returns 404 — endpoint doesn't exist on eTerra. New approach: query LIMITE_UAT layer for all features (no geometry) to discover SIRUTA + WORKSPACE_ID per UAT, then resolve each unique WORKSPACE_ID to county name via fetchNomenByPk(). Fallback: resolve county for UATs that already have workspacePk in DB. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/eterra/session/county-refresh.ts | 348 +++++++++++-------- 1 file changed, 197 insertions(+), 151 deletions(-) diff --git a/src/app/api/eterra/session/county-refresh.ts b/src/app/api/eterra/session/county-refresh.ts index cd489d9..ccb522f 100644 --- a/src/app/api/eterra/session/county-refresh.ts +++ b/src/app/api/eterra/session/county-refresh.ts @@ -1,25 +1,22 @@ /** - * County refresh — populates GisUat.county from eTerra nomenclature. + * County refresh — populates GisUat.county from eTerra. * * Called with an already-authenticated EterraClient (fire-and-forget * after login), so there's no session expiry risk. * * Strategy: - * 1. fetchCounties() → build countyMap (nomenPk → county name) - * 2. Phase 1: UATs with workspacePk → instant lookup - * 3. Phase 2: enumerate counties → fetchAdminUnitsByCounty() → match by name + * 1. Query LIMITE_UAT layer for all features (no geometry) to get + * SIRUTA + WORKSPACE_ID (or other county field) per UAT + * 2. For each unique WORKSPACE_ID, call fetchNomenByPk() to get county name + * 3. Batch-update GisUat.county + workspacePk + * + * Fallback: for UATs that already have workspacePk in DB but no county, + * resolve county name via fetchNomenByPk() directly. */ import { prisma } from "@/core/storage/prisma"; import type { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; - -function normalizeName(s: string): string { - return s - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .toUpperCase() - .trim(); -} +import { findLayerById } from "@/modules/parcel-sync/services/eterra-layers"; function titleCase(s: string): string { return s @@ -30,43 +27,13 @@ function titleCase(s: string): string { // eslint-disable-next-line @typescript-eslint/no-explicit-any function extractName(entry: any): string { if (!entry || typeof entry !== "object") return ""; - for (const key of ["name", "nomenName", "label", "denumire", "NAME"]) { + for (const key of ["name", "nomenName", "label", "denumire"]) { const val = entry[key]; if (typeof val === "string" && val.trim()) return val.trim(); } return ""; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function extractCode(entry: any): string { - if (!entry || typeof entry !== "object") return ""; - for (const key of [ - "code", - "sirutaCode", - "siruta", - "externalCode", - "cod", - ]) { - const val = entry[key]; - if (val != null) { - const s = String(val).trim(); - if (s && /^\d+$/.test(s)) return s; - } - } - return ""; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function unwrapArray(data: any): any[] { - if (Array.isArray(data)) return data; - if (data && typeof data === "object") { - if (Array.isArray(data.content)) return data.content; - if (Array.isArray(data.data)) return data.data; - if (Array.isArray(data.items)) return data.items; - } - return []; -} - export async function refreshCountyData(client: EterraClient): Promise { // Check if refresh is needed const [total, withCounty] = await Promise.all([ @@ -86,133 +53,212 @@ export async function refreshCountyData(client: EterraClient): Promise { `[county-refresh] Starting: ${withCounty}/${total} have county.`, ); - // 1. Fetch counties - const rawCounties = await client.fetchCounties(); - const counties = unwrapArray(rawCounties); - const countyMap = new Map(); - for (const c of counties) { - const pk = Number(c?.nomenPk ?? 0); - const name = extractName(c); - if (pk > 0 && name) countyMap.set(pk, titleCase(name)); - } - - if (countyMap.size === 0) { - console.error( - "[county-refresh] fetchCounties returned 0 counties. Raw sample:", - JSON.stringify(rawCounties).slice(0, 500), - ); - return; - } - - console.log(`[county-refresh] Got ${countyMap.size} counties.`); - - // 2. Load all UATs needing county - const allUats = await prisma.gisUat.findMany({ - where: { county: null }, - select: { siruta: true, name: true, workspacePk: true }, - }); - - // Phase 1: UATs with workspacePk - let phase1 = 0; - const needsCounty: Array<{ siruta: string; name: string }> = []; - - for (const uat of allUats) { - if (uat.workspacePk && uat.workspacePk > 0) { - const county = countyMap.get(uat.workspacePk); - if (county) { - await prisma.gisUat.update({ - where: { siruta: uat.siruta }, - data: { county }, - }); - phase1++; - continue; - } - } - needsCounty.push({ siruta: uat.siruta, name: uat.name }); - } - - console.log( - `[county-refresh] Phase 1: ${phase1} via workspacePk. ${needsCounty.length} remaining.`, - ); - - // Phase 2: enumerate counties → match by code or name - const nameToSirutas = new Map(); - const sirutaSet = new Set(); - for (const u of needsCounty) { - sirutaSet.add(u.siruta); - const key = normalizeName(u.name); - const arr = nameToSirutas.get(key); - if (arr) arr.push(u.siruta); - else nameToSirutas.set(key, [u.siruta]); - } - - let phase2 = 0; - let codeMatches = 0; - let nameMatches = 0; - const matched = new Set(); - - for (const [countyPk, countyName] of countyMap) { - if (matched.size >= needsCounty.length) break; + // ── Strategy 1: LIMITE_UAT layer ────────────────────────────── + // Query all features (no geometry) to get SIRUTA + WORKSPACE_ID per UAT + const limiteUat = findLayerById("LIMITE_UAT"); + let layerUpdated = 0; + if (limiteUat) { try { - const rawUats = await client.fetchAdminUnitsByCounty(countyPk); - const uats = unwrapArray(rawUats); + // Discover available fields + const fields = await client.getLayerFieldNames(limiteUat); + const upperFields = fields.map((f) => f.toUpperCase()); - // Log first response for debugging - if (phase2 === 0 && codeMatches === 0 && nameMatches === 0 && uats.length > 0) { - console.log( - `[county-refresh] Sample from ${countyName} (${uats.length} UATs):`, - JSON.stringify(uats[0]).slice(0, 500), - ); + console.log(`[county-refresh] LIMITE_UAT fields: ${fields.join(", ")}`); + + // Find admin field (SIRUTA) + const adminCandidates = [ + "ADMIN_UNIT_ID", + "SIRUTA", + "UAT_ID", + "SIRUTA_UAT", + ]; + let adminField: string | null = null; + for (const key of adminCandidates) { + const idx = upperFields.indexOf(key); + if (idx >= 0) { + adminField = fields[idx] ?? null; + break; + } } - for (const uat of uats) { - // Strategy A: match by code - const code = extractCode(uat); - if (code && sirutaSet.has(code) && !matched.has(code)) { - matched.add(code); - await prisma.gisUat.update({ - where: { siruta: code }, - data: { county: countyName, workspacePk: countyPk }, - }); - phase2++; - codeMatches++; - continue; + // Find workspace/county field + const wsCandidates = [ + "WORKSPACE_ID", + "COUNTY_ID", + "JUDET_ID", + "JUDET", + "COUNTY", + "COUNTY_NAME", + "JUDET_NAME", + "JUDET_SIRUTA", + ]; + let wsField: string | null = null; + for (const key of wsCandidates) { + const idx = upperFields.indexOf(key); + if (idx >= 0) { + wsField = fields[idx] ?? null; + break; + } + } + + // Also check for name fields that might contain county + const nameCandidates = ["NAME", "NUME", "UAT_NAME", "DENUMIRE"]; + let nameField: string | null = null; + for (const key of nameCandidates) { + const idx = upperFields.indexOf(key); + if (idx >= 0) { + nameField = fields[idx] ?? null; + break; + } + } + + if (adminField) { + // Fetch all features — attributes only + const outFields = [adminField, wsField, nameField] + .filter(Boolean) + .join(","); + + const features = await client.fetchAllLayerByWhere( + limiteUat, + "1=1", + { + outFields: outFields || "*", + returnGeometry: false, + pageSize: 1000, + }, + ); + + console.log( + `[county-refresh] LIMITE_UAT: ${features.length} features. ` + + `adminField=${adminField}, wsField=${wsField}, nameField=${nameField}`, + ); + + // Log sample for debugging + if (features.length > 0) { + console.log( + `[county-refresh] Sample feature:`, + JSON.stringify(features[0]?.attributes).slice(0, 500), + ); } - // Strategy B: match by name - const eterraName = extractName(uat); - if (!eterraName) continue; - const key = normalizeName(eterraName); - const sirutas = nameToSirutas.get(key); - if (!sirutas || sirutas.length === 0) continue; + // Collect unique WORKSPACE_IDs to resolve county names + const wsToSirutas = new Map(); + const sirutaToWs = new Map(); - const siruta = sirutas.find((s) => !matched.has(s)); - if (!siruta) continue; + for (const f of features) { + const attrs = f.attributes; + const sirutaVal = String(attrs[adminField] ?? "") + .trim() + .replace(/\.0$/, ""); - matched.add(siruta); - await prisma.gisUat.update({ - where: { siruta }, - data: { county: countyName, workspacePk: countyPk }, - }); - phase2++; - nameMatches++; + if (!sirutaVal) continue; - if (sirutas.every((s) => matched.has(s))) { - nameToSirutas.delete(key); + if (wsField) { + const wsPk = Number(attrs[wsField] ?? 0); + if (Number.isFinite(wsPk) && wsPk > 0) { + sirutaToWs.set(sirutaVal, wsPk); + const arr = wsToSirutas.get(wsPk); + if (arr) arr.push(sirutaVal); + else wsToSirutas.set(wsPk, [sirutaVal]); + } + } + } + + // Resolve county names via fetchNomenByPk for each unique workspace + if (wsToSirutas.size > 0) { + console.log( + `[county-refresh] Resolving ${wsToSirutas.size} unique workspaces...`, + ); + + const wsToCounty = new Map(); + for (const wsPk of wsToSirutas.keys()) { + try { + const nomen = await client.fetchNomenByPk(wsPk); + const name = extractName(nomen); + if (name) wsToCounty.set(wsPk, titleCase(name)); + } catch { + // Skip failed lookups + } + } + + console.log( + `[county-refresh] Resolved ${wsToCounty.size} county names.`, + ); + + // Batch update + for (const [wsPk, sirutas] of wsToSirutas) { + const county = wsToCounty.get(wsPk); + if (!county) continue; + + for (let i = 0; i < sirutas.length; i += 100) { + const batch = sirutas.slice(i, i + 100); + const result = await prisma.gisUat.updateMany({ + where: { siruta: { in: batch }, county: null }, + data: { county, workspacePk: wsPk }, + }); + layerUpdated += result.count; + } + } } } } catch (err) { console.warn( - `[county-refresh] County ${countyName} failed:`, + `[county-refresh] LIMITE_UAT approach failed:`, err instanceof Error ? err.message : err, ); } } console.log( - `[county-refresh] Done: ${phase1 + phase2} updated ` + - `(phase1=${phase1}, phase2=${phase2}: ${codeMatches} code, ${nameMatches} name). ` + - `Unmatched: ${needsCounty.length - phase2}.`, + `[county-refresh] LIMITE_UAT phase: ${layerUpdated} updated.`, + ); + + // ── Strategy 2: Resolve via existing workspacePk in DB ──────── + // For UATs that already have workspacePk but no county + let dbUpdated = 0; + + const uatsWithWs = await prisma.gisUat.findMany({ + where: { county: null, workspacePk: { not: null } }, + select: { siruta: true, workspacePk: true }, + }); + + if (uatsWithWs.length > 0) { + // Get unique workspacePks + const uniqueWs = new Set( + uatsWithWs + .map((u) => u.workspacePk) + .filter((pk): pk is number => pk != null && pk > 0), + ); + + const wsToCounty = new Map(); + for (const wsPk of uniqueWs) { + if (wsToCounty.has(wsPk)) continue; + try { + const nomen = await client.fetchNomenByPk(wsPk); + const name = extractName(nomen); + if (name) wsToCounty.set(wsPk, titleCase(name)); + } catch { + // Skip + } + } + + for (const uat of uatsWithWs) { + if (!uat.workspacePk) continue; + const county = wsToCounty.get(uat.workspacePk); + if (!county) continue; + + await prisma.gisUat.update({ + where: { siruta: uat.siruta }, + data: { county }, + }); + dbUpdated++; + } + } + + const totalUpdated = layerUpdated + dbUpdated; + console.log( + `[county-refresh] Done: ${totalUpdated} updated ` + + `(layer=${layerUpdated}, db=${dbUpdated}).`, ); }