feat: enrichment fallback via direct parcel details endpoint

PROBLEM:
For no-geometry parcels (and many geometry parcels without application
IDs), CATEGORIE_FOLOSINTA was always '-' because:
1. fetchImmAppsByImmovable returned no apps (no applicationId)
2. Without appId, fetchParcelFolosinte was skipped entirely
3. No fallback existed

DISCOVERY (from eTerra UI investigation):
The endpoint /api/immovable/details/parcels/list/{wp}/{pk}/{page}/{size}
returns parcel use categories DIRECTLY — no applicationId needed.
Example: [{useCategory:'arabil', intravilan:'Necunoscut', parcelPk:17753903}]

FIX:
- After the app-based CATEGORIE_FOLOSINTA attempt, if result is still '-',
  fall back to fetchImmovableParcelDetails (the direct endpoint)
- formatCategories now handles both API formats:
  - App-based: categorieFolosinta + suprafata fields
  - Direct: useCategory field (no area — shows category name only)
- When direct endpoint provides area=0, format shows just the category
  name without ':0' (e.g. 'arabil; faneata' instead of 'arabil:0; faneata:0')
- Also picks up intravilan from direct endpoint if app-based was empty
- Fixed fetchImmovableParcelDetails default size: 1 → 20 (one immovable
  can have multiple parcels, e.g. IE 25332 has 2: arabil + faneata)
- Results are cached in folCache to avoid duplicate requests
This commit is contained in:
AI Assistant
2026-03-08 00:46:02 +02:00
parent a7c9e8a6cc
commit f09eaaad7c
2 changed files with 43 additions and 6 deletions
@@ -80,13 +80,18 @@ const normalizeIntravilan = (values: string[]) => {
const formatCategories = (entries: any[]) => {
const map = new Map<string, number>();
for (const entry of entries) {
const key = String(entry?.categorieFolosinta ?? "").trim();
// Support both API formats:
// fetchParcelFolosinte (via app): categorieFolosinta + suprafata
// fetchImmovableParcelDetails (direct): useCategory (no area)
const key = String(
entry?.categorieFolosinta ?? entry?.useCategory ?? "",
).trim();
if (!key) continue;
const area = Number(entry?.suprafata ?? 0);
map.set(key, (map.get(key) ?? 0) + (Number.isFinite(area) ? area : 0));
}
return Array.from(map.entries())
.map(([k, a]) => `${k}:${formatNumber(a)}`)
.map(([k, a]) => (a > 0 ? `${k}:${formatNumber(a)}` : k))
.join("; ");
};
@@ -482,6 +487,40 @@ export async function enrichFeatures(
);
categorie = formatCategories(fol);
}
// Fallback: if no application or empty categories, use direct
// parcel details endpoint (doesn't need applicationId).
// Discovered via eTerra UI: /api/immovable/details/parcels/list/{wp}/{pk}/{page}/{size}
// Returns: [{useCategory: "arabil", intravilan: "Necunoscut", ...}]
if (categorie === "-" && immovableId) {
const immPkForDetails =
immovableListById.get(normalizeId(immovableId))?.immovablePk ??
immovableId;
const detKey = `${workspaceId}:${immPkForDetails}:details`;
let details = folCache.get(detKey);
if (!details) {
try {
details = await throttled(() =>
client.fetchImmovableParcelDetails(
workspaceId as string | number,
immPkForDetails as string | number,
),
);
} catch {
details = [];
}
folCache.set(detKey, details);
}
if (details && details.length > 0) {
const detIntravilan = normalizeIntravilan(
details.map((d: any) => d?.intravilan ?? ""),
);
const detCategorie = formatCategories(details);
if (detCategorie && detCategorie !== "-") categorie = detCategorie;
if (detIntravilan && detIntravilan !== "-" && intravilan === "-")
intravilan = detIntravilan;
}
}
}
const cadRefRaw = (attrs.NATIONAL_CADASTRAL_REFERENCE ?? "") as string;
@@ -362,9 +362,7 @@ export class EterraClient {
await sleep(500); // small delay before retry with smaller page
continue;
}
throw new Error(
`Failed to fetch layer ${layer.name}: ${cause}`,
);
throw new Error(`Failed to fetch layer ${layer.name}: ${cause}`);
}
const features = data.features ?? [];
@@ -504,7 +502,7 @@ export class EterraClient {
workspaceId: string | number,
immovableId: string | number,
page = 1,
size = 1,
size = 20,
): Promise<any[]> {
const url = `${BASE_URL}/api/immovable/details/parcels/list/${workspaceId}/${immovableId}/${page}/${size}`;
return this.getRawJson(url);