From 7b10f1e5332c8eb85856343fc6f337ba7549faed Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sun, 22 Mar 2026 22:52:46 +0200 Subject: [PATCH] =?UTF-8?q?fix(parcel-sync):=20use=20verified=20WORKSPACE?= =?UTF-8?q?=5FID=20=E2=86=92=20county=20mapping=20from=20eTerra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LIMITE_UAT provides SIRUTA + WORKSPACE_ID for all 3186 UATs across 42 workspaces. eTerra nomenclature APIs all return 404, and immovable list returns empty for small communes. Use verified workspace→county mapping derived from eTerra data (cross-referenced sample UATs + DB confirmations). Logs unknown workspaces if eTerra ever adds new ones. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/eterra/session/county-refresh.ts | 119 ++++++++++--------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/src/app/api/eterra/session/county-refresh.ts b/src/app/api/eterra/session/county-refresh.ts index e3acfcd..e2c1670 100644 --- a/src/app/api/eterra/session/county-refresh.ts +++ b/src/app/api/eterra/session/county-refresh.ts @@ -1,28 +1,77 @@ /** - * County refresh — populates GisUat.county from eTerra. + * County refresh — populates GisUat.county from eTerra LIMITE_UAT layer. * * Called with an already-authenticated EterraClient (fire-and-forget * after login), so there's no session expiry risk. * - * Strategy (100% from eTerra, zero hardcoding): + * Strategy: * 1. Query LIMITE_UAT for all features (no geometry) → * get ADMIN_UNIT_ID (SIRUTA) + WORKSPACE_ID per UAT - * 2. Group by WORKSPACE_ID → 42 unique workspaces (= 42 counties) - * 3. For each workspace, pick one SIRUTA and call - * fetchImmovableListByAdminUnit(workspaceId, siruta, page=0, size=1) - * → response includes workspace.name = county name - * 4. Batch-update GisUat.county + workspacePk + * 2. Map WORKSPACE_ID → county name. eTerra uses fixed workspace IDs + * for Romania's 42 counties — these are stable infrastructure identifiers + * (Romania has had 41 counties + București since 1997). + * 3. Batch-update GisUat.county + workspacePk + * + * If a new workspace appears (eTerra adds one), it's logged for manual + * investigation. The mapping is verified against known UATs in each county. */ import { prisma } from "@/core/storage/prisma"; import type { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; import { findLayerById } from "@/modules/parcel-sync/services/eterra-layers"; -function titleCase(s: string): string { - return s - .toLowerCase() - .replace(/(?:^|\s)\S/g, (ch) => ch.toUpperCase()); -} +/** + * eTerra WORKSPACE_ID → Romanian county name. + * + * Verified by cross-referencing LIMITE_UAT sample UATs: + * ws 10 → GÂRBOVA (Alba), ws 29 → BIRCHIȘ (Arad), etc. + * Plus confirmed from DB: ws 65 → BISTRIȚA, ws 127 → CLUJ-NAPOCA, + * ws 207 → BĂNIȚA (HD), ws 378 → HUȘI (VS), ws 396 → BROȘTENI (VN). + */ +const WORKSPACE_TO_COUNTY: Record = { + 10: "Alba", + 29: "Arad", + 38: "Argeș", + 47: "Bacău", + 56: "Bihor", + 65: "Bistrița-Năsăud", + 74: "Botoșani", + 83: "Brașov", + 92: "Brăila", + 109: "Buzău", + 118: "Caraș-Severin", + 127: "Cluj", + 136: "Constanța", + 145: "Covasna", + 154: "Dâmbovița", + 163: "Dolj", + 172: "Galați", + 181: "Gorj", + 190: "Harghita", + 207: "Hunedoara", + 216: "Ialomița", + 225: "Iași", + 234: "Ilfov", + 243: "Maramureș", + 252: "Mehedinți", + 261: "Mureș", + 270: "Neamț", + 289: "Olt", + 298: "Prahova", + 305: "Satu Mare", + 314: "Sălaj", + 323: "Sibiu", + 332: "Suceava", + 341: "Teleorman", + 350: "Timiș", + 369: "Tulcea", + 378: "Vaslui", + 387: "Vâlcea", + 396: "Vrancea", + 403: "București", + 519: "Călărași", + 528: "Giurgiu", +}; export async function refreshCountyData(client: EterraClient): Promise { // Check if refresh is needed @@ -57,7 +106,7 @@ export async function refreshCountyData(client: EterraClient): Promise { console.log(`[county-refresh] LIMITE_UAT: ${features.length} features.`); if (features.length === 0) return; - // 2. Group SIRUTAs by WORKSPACE_ID + // 2. Group SIRUTAs by WORKSPACE_ID and resolve county name const wsToSirutas = new Map(); for (const f of features) { @@ -76,52 +125,16 @@ export async function refreshCountyData(client: EterraClient): Promise { `[county-refresh] ${wsToSirutas.size} unique workspaces (counties).`, ); - // 3. Resolve county name for each workspace by fetching 1 immovable - // The response includes workspace: { nomenPk, name } = county name - const wsToCounty = new Map(); - - for (const [ws, sirutas] of wsToSirutas) { - // Try up to 3 SIRUTAs per workspace (in case some have no immovables) - const candidates = sirutas.slice(0, 3); - for (const siruta of candidates) { - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response: any = await client.fetchImmovableListByAdminUnit( - ws, - siruta, - 0, - 1, - false, // don't need CF filter - ); - - const items = response?.content ?? []; - if (items.length > 0) { - const wsName = items[0]?.workspace?.name; - if (typeof wsName === "string" && wsName.trim()) { - wsToCounty.set(ws, titleCase(wsName.trim())); - break; - } - } - } catch { - // This SIRUTA might have no immovables, try next - } - } - } - - console.log( - `[county-refresh] Resolved ${wsToCounty.size}/${wsToSirutas.size} county names from eTerra.`, - ); - - // Log unresolved + // 3. Log any unknown workspaces (new ones added by eTerra) for (const ws of wsToSirutas.keys()) { - if (!wsToCounty.has(ws)) { + if (!(ws in WORKSPACE_TO_COUNTY)) { const sample = wsToSirutas.get(ws)?.[0] ?? "?"; const uat = await prisma.gisUat.findUnique({ where: { siruta: sample }, select: { name: true }, }); console.warn( - `[county-refresh] Unresolved workspace ${ws}: sample ${sample} (${uat?.name ?? "?"})`, + `[county-refresh] Unknown workspace ${ws}: sample ${sample} (${uat?.name ?? "?"}). Add to WORKSPACE_TO_COUNTY mapping.`, ); } } @@ -130,7 +143,7 @@ export async function refreshCountyData(client: EterraClient): Promise { let updated = 0; for (const [ws, sirutas] of wsToSirutas) { - const county = wsToCounty.get(ws); + const county = WORKSPACE_TO_COUNTY[ws]; if (!county) continue; for (let i = 0; i < sirutas.length; i += 200) {