fix(parcel-sync): use eTerra nomenclature API for county population
LIMITE_UAT layer lacks WORKSPACE_ID field, so the previous approach failed silently. Now uses fetchCounties() + fetchAdminUnitsByCounty() nomenclature API: Phase 1 fills county for UATs with existing workspacePk, Phase 2 enumerates counties and matches by name. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+108
-114
@@ -3,7 +3,6 @@ import { prisma } from "@/core/storage/prisma";
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
|
||||
import { findLayerById } from "@/modules/parcel-sync/services/eterra-layers";
|
||||
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -158,15 +157,33 @@ export async function POST() {
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* PATCH /api/eterra/uats */
|
||||
/* */
|
||||
/* Populate county names from eTerra. */
|
||||
/* 1. fetchCounties() → build workspacePk → county name map */
|
||||
/* 2. Query LIMITE_UAT layer (all features, no geometry) to get */
|
||||
/* SIRUTA → WORKSPACE_ID mapping for every UAT */
|
||||
/* 3. Batch-update GisUat.county + GisUat.workspacePk */
|
||||
/* Populate county names from eTerra nomenclature API. */
|
||||
/* */
|
||||
/* Strategy (two phases): */
|
||||
/* Phase 1: For UATs that already have workspacePk resolved, */
|
||||
/* use fetchCounties() → countyMap[workspacePk] → instant update. */
|
||||
/* Phase 2: For remaining UATs, enumerate counties → */
|
||||
/* fetchAdminUnitsByCounty() per county → match by name. */
|
||||
/* */
|
||||
/* Requires active eTerra session. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Remove diacritics and lowercase for fuzzy name matching */
|
||||
function normalizeName(s: string): string {
|
||||
return s
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Title-case: "SATU MARE" → "Satu Mare" */
|
||||
function titleCase(s: string): string {
|
||||
return s
|
||||
.toLowerCase()
|
||||
.replace(/(?:^|\s)\S/g, (ch) => ch.toUpperCase());
|
||||
}
|
||||
|
||||
export async function PATCH() {
|
||||
try {
|
||||
// 1. Get eTerra credentials from session
|
||||
@@ -187,19 +204,15 @@ export async function PATCH() {
|
||||
|
||||
const client = await EterraClient.create(username, password);
|
||||
|
||||
// 2. Fetch all counties from eTerra nomenclature → workspacePk → county name
|
||||
// 2. Fetch all counties from eTerra nomenclature
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const counties: any[] = await client.fetchCounties();
|
||||
const countyMap = new Map<number, string>();
|
||||
const countyMap = new Map<number, string>(); // nomenPk → county name
|
||||
for (const c of counties) {
|
||||
const pk = Number(c?.nomenPk ?? 0);
|
||||
const name = String(c?.name ?? "").trim();
|
||||
if (pk > 0 && name) {
|
||||
// Title-case: "CLUJ" → "Cluj", "SATU MARE" → "Satu Mare"
|
||||
const titleCase = name
|
||||
.toLowerCase()
|
||||
.replace(/(?:^|\s)\S/g, (ch) => ch.toUpperCase());
|
||||
countyMap.set(pk, titleCase);
|
||||
countyMap.set(pk, titleCase(name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,121 +225,102 @@ export async function PATCH() {
|
||||
|
||||
console.log(`[uats-patch] Fetched ${countyMap.size} counties from eTerra`);
|
||||
|
||||
// 3. Query LIMITE_UAT layer for all features (no geometry)
|
||||
// to get SIRUTA + WORKSPACE_ID per UAT
|
||||
const limiteUat = findLayerById("LIMITE_UAT");
|
||||
if (!limiteUat) {
|
||||
return NextResponse.json(
|
||||
{ error: "Layer LIMITE_UAT nu este configurat." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
// Discover field names
|
||||
const fields = await client.getLayerFieldNames(limiteUat);
|
||||
const upperFields = fields.map((f) => f.toUpperCase());
|
||||
|
||||
// Find the SIRUTA/admin field
|
||||
const adminFieldCandidates = [
|
||||
"ADMIN_UNIT_ID",
|
||||
"SIRUTA",
|
||||
"UAT_ID",
|
||||
"SIRUTA_UAT",
|
||||
"UAT_SIRUTA",
|
||||
];
|
||||
let adminField: string | null = null;
|
||||
for (const key of adminFieldCandidates) {
|
||||
const idx = upperFields.indexOf(key);
|
||||
if (idx >= 0) {
|
||||
adminField = fields[idx] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the WORKSPACE_ID field
|
||||
const wsFieldCandidates = ["WORKSPACE_ID", "COUNTY_ID", "JUDET_ID"];
|
||||
let wsField: string | null = null;
|
||||
for (const key of wsFieldCandidates) {
|
||||
const idx = upperFields.indexOf(key);
|
||||
if (idx >= 0) {
|
||||
wsField = fields[idx] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!adminField) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `LIMITE_UAT nu are câmp SIRUTA. Câmpuri disponibile: ${fields.join(", ")}`,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch all features — attributes only, no geometry
|
||||
const outFields = [adminField, wsField, "NAME", "NUME", "UAT_NAME"]
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
|
||||
const features = await client.fetchAllLayerByWhere(limiteUat, "1=1", {
|
||||
outFields,
|
||||
returnGeometry: false,
|
||||
pageSize: 1000,
|
||||
// 3. Load all UATs from DB
|
||||
const allUats = await prisma.gisUat.findMany({
|
||||
select: { siruta: true, name: true, county: true, workspacePk: true },
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[uats-patch] Fetched ${features.length} UAT features from LIMITE_UAT`,
|
||||
);
|
||||
// Phase 1: instant fill for UATs that already have workspacePk
|
||||
let phase1Updated = 0;
|
||||
const needsCounty: Array<{ siruta: string; name: string }> = [];
|
||||
|
||||
// 4. Build SIRUTA → { county, workspacePk } map
|
||||
const sirutaCountyMap = new Map<
|
||||
string,
|
||||
{ county: string; workspacePk: number }
|
||||
>();
|
||||
for (const uat of allUats) {
|
||||
if (uat.county) continue; // already has county
|
||||
|
||||
for (const f of features) {
|
||||
const attrs = f.attributes;
|
||||
const sirutaVal = String(attrs[adminField] ?? "")
|
||||
.trim()
|
||||
.replace(/\.0$/, "");
|
||||
const wsPk = Number(attrs[wsField ?? ""] ?? 0);
|
||||
|
||||
if (!sirutaVal || !Number.isFinite(wsPk) || wsPk <= 0) continue;
|
||||
|
||||
const countyName = countyMap.get(wsPk);
|
||||
if (!countyName) continue;
|
||||
|
||||
sirutaCountyMap.set(sirutaVal, { county: countyName, workspacePk: wsPk });
|
||||
if (uat.workspacePk && uat.workspacePk > 0) {
|
||||
const county = countyMap.get(uat.workspacePk);
|
||||
if (county) {
|
||||
await prisma.gisUat.update({
|
||||
where: { siruta: uat.siruta },
|
||||
data: { county },
|
||||
});
|
||||
phase1Updated++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
needsCounty.push({ siruta: uat.siruta, name: uat.name });
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[uats-patch] Mapped ${sirutaCountyMap.size} SIRUTAs to counties`,
|
||||
`[uats-patch] Phase 1: ${phase1Updated} updated via workspacePk. ` +
|
||||
`${needsCounty.length} remaining.`,
|
||||
);
|
||||
|
||||
// 5. Batch-update GisUat records
|
||||
let updated = 0;
|
||||
const CHUNK_SIZE = 100;
|
||||
const entries = Array.from(sirutaCountyMap.entries());
|
||||
|
||||
for (let i = 0; i < entries.length; i += CHUNK_SIZE) {
|
||||
const chunk = entries.slice(i, i + CHUNK_SIZE);
|
||||
const ops = chunk.map(([s, data]) =>
|
||||
prisma.gisUat.updateMany({
|
||||
where: { siruta: s },
|
||||
data: { county: data.county, workspacePk: data.workspacePk },
|
||||
}),
|
||||
);
|
||||
const results = await prisma.$transaction(ops);
|
||||
for (const r of results) updated += r.count;
|
||||
// Phase 2: enumerate UATs per county from nomenclature, match by name
|
||||
// Build lookup: normalized name → list of SIRUTAs (for same-name UATs)
|
||||
const nameToSirutas = new Map<string, string[]>();
|
||||
for (const u of needsCounty) {
|
||||
const key = normalizeName(u.name);
|
||||
const arr = nameToSirutas.get(key);
|
||||
if (arr) arr.push(u.siruta);
|
||||
else nameToSirutas.set(key, [u.siruta]);
|
||||
}
|
||||
|
||||
console.log(`[uats-patch] Updated ${updated} UAT records with county`);
|
||||
let phase2Updated = 0;
|
||||
const matchedSirutas = new Set<string>();
|
||||
|
||||
for (const [countyPk, countyName] of countyMap) {
|
||||
if (nameToSirutas.size === 0) break; // all matched
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const uats: any[] = await client.fetchAdminUnitsByCounty(countyPk);
|
||||
|
||||
for (const uat of uats) {
|
||||
const eterraName = String(uat?.name ?? "").trim();
|
||||
if (!eterraName) continue;
|
||||
|
||||
const key = normalizeName(eterraName);
|
||||
const sirutas = nameToSirutas.get(key);
|
||||
if (!sirutas || sirutas.length === 0) continue;
|
||||
|
||||
// Pick the first unmatched SIRUTA with this name
|
||||
const siruta = sirutas.find((s) => !matchedSirutas.has(s));
|
||||
if (!siruta) continue;
|
||||
|
||||
matchedSirutas.add(siruta);
|
||||
|
||||
await prisma.gisUat.update({
|
||||
where: { siruta },
|
||||
data: { county: countyName, workspacePk: countyPk },
|
||||
});
|
||||
phase2Updated++;
|
||||
|
||||
// If all SIRUTAs for this name matched, remove the key
|
||||
if (sirutas.every((s) => matchedSirutas.has(s))) {
|
||||
nameToSirutas.delete(key);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[uats-patch] Failed to fetch UATs for county ${countyName}:`,
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const totalUpdated = phase1Updated + phase2Updated;
|
||||
console.log(
|
||||
`[uats-patch] Phase 2: ${phase2Updated} updated via name match. ` +
|
||||
`Total: ${totalUpdated}. Unmatched: ${needsCounty.length - phase2Updated}.`,
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
updated,
|
||||
updated: totalUpdated,
|
||||
phase1: phase1Updated,
|
||||
phase2: phase2Updated,
|
||||
totalCounties: countyMap.size,
|
||||
totalMapped: sirutaCountyMap.size,
|
||||
totalFeatures: features.length,
|
||||
unmatched: needsCounty.length - phase2Updated,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Eroare server";
|
||||
|
||||
Reference in New Issue
Block a user