fix(parcel-sync): enrichment robustness — 5 fixes for better coverage

1. Completeness check with real values: features with all "-" values
   are now re-enriched instead of being considered "complete"

2. Age-based re-enrichment: features older than 30 days are re-enriched
   on next run (catches eTerra data updates)

3. Per-feature try-catch: one feature failing no longer aborts the
   entire UAT enrichment — logs warning and continues

4. fetchParcelFolosinte wrapped in try-catch: was a hard failure that
   killed the whole enrichment process

5. Workspace resolution logging: warns when immovable list is empty
   (wrong workspace), warns on fallback to PK=65

These fixes should progressively improve enrichment coverage toward
100% with each weekend sync cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-26 22:39:32 +02:00
parent 24b565f5ea
commit 54d9a36686
@@ -226,9 +226,14 @@ export async function enrichFeatures(
/* ignore */ /* ignore */
} }
} }
// If still null, enrichment will fail gracefully with empty lists
const workspacePkForApi = resolvedWsPk ?? 65; const workspacePkForApi = resolvedWsPk ?? 65;
if (!resolvedWsPk) {
console.warn(
`[enrich] siruta=${siruta}: workspace nu s-a rezolvat, folosesc fallback PK=${workspacePkForApi}`,
);
} else {
console.log(`[enrich] siruta=${siruta} workspacePk=${workspacePkForApi}`); console.log(`[enrich] siruta=${siruta} workspacePk=${workspacePkForApi}`);
}
push({ push({
phase: "Pregătire îmbogățire", phase: "Pregătire îmbogățire",
@@ -334,6 +339,18 @@ export async function enrichFeatures(
listPage += 1; listPage += 1;
} }
if (immovableListById.size === 0) {
console.warn(
`[enrich] siruta=${siruta}: lista de imobile e GOALĂ (workspace=${workspacePkForApi}). ` +
`Enrichment va continua dar toate parcelele vor avea date goale. ` +
`Verifică workspace-ul corect pentru acest UAT.`,
);
} else {
console.log(
`[enrich] siruta=${siruta}: ${immovableListById.size} imobile găsite`,
);
}
// ── Fetch documentation/owner data ── // ── Fetch documentation/owner data ──
push({ phase: "Descărcare documentații CF" }); push({ phase: "Descărcare documentații CF" });
const docByImmovable = new Map<string, any>(); const docByImmovable = new Map<string, any>();
@@ -392,13 +409,11 @@ export async function enrichFeatures(
const attrs = feature.attributes as Record<string, unknown>; const attrs = feature.attributes as Record<string, unknown>;
// Skip features with complete enrichment (resume after crash/interruption). // Skip features with complete enrichment (resume after crash/interruption).
// Re-enrich if enrichment schema is incomplete (e.g., missing PROPRIETARI_VECHI // Re-enrich if: schema incomplete, values are all "-" (empty), or older than 30 days.
// added in a later version).
if (feature.enrichedAt != null) { if (feature.enrichedAt != null) {
const enrichJson = feature.enrichment as Record<string, unknown> | null; const enrichJson = feature.enrichment as Record<string, unknown> | null;
const isComplete = // Structural check: all 7 core fields must exist
enrichJson != null && const coreFields = [
[
"NR_CAD", "NR_CAD",
"NR_CF", "NR_CF",
"PROPRIETARI", "PROPRIETARI",
@@ -406,8 +421,29 @@ export async function enrichFeatures(
"ADRESA", "ADRESA",
"CATEGORIE_FOLOSINTA", "CATEGORIE_FOLOSINTA",
"HAS_BUILDING", "HAS_BUILDING",
].every((k) => k in enrichJson && enrichJson[k] !== undefined); ];
if (isComplete) { const structurallyComplete =
enrichJson != null &&
coreFields.every((k) => k in enrichJson && enrichJson[k] !== undefined);
// Value check: at least some fields must have real data (not just "-")
// A feature with ALL text fields === "-" is considered empty and needs re-enrichment
const valueFields = ["NR_CF", "PROPRIETARI", "ADRESA", "CATEGORIE_FOLOSINTA"];
const hasRealValues =
enrichJson != null &&
valueFields.some(
(k) =>
k in enrichJson &&
enrichJson[k] !== undefined &&
enrichJson[k] !== "-" &&
enrichJson[k] !== "",
);
// Age check: re-enrich if older than 30 days (catches eTerra updates)
const ageMs = Date.now() - new Date(feature.enrichedAt).getTime();
const isTooOld = ageMs > 30 * 24 * 60 * 60 * 1000;
if (structurallyComplete && hasRealValues && !isTooOld) {
enrichedCount += 1; enrichedCount += 1;
if (index % 50 === 0) { if (index % 50 === 0) {
options?.onProgress?.( options?.onProgress?.(
@@ -418,9 +454,12 @@ export async function enrichFeatures(
} }
continue; continue;
} }
// Stale enrichment — will be re-enriched below // Incomplete, empty, or stale — will be re-enriched below
} }
// Per-feature try-catch: one feature failing should not abort the whole UAT
try {
const immovableId = attrs.IMMOVABLE_ID ?? ""; const immovableId = attrs.IMMOVABLE_ID ?? "";
const workspaceId = attrs.WORKSPACE_ID ?? ""; const workspaceId = attrs.WORKSPACE_ID ?? "";
const applicationId = (attrs.APPLICATION_ID as number) ?? null; const applicationId = (attrs.APPLICATION_ID as number) ?? null;
@@ -474,6 +513,7 @@ export async function enrichFeatures(
const folKey = `${workspaceId}:${immovableId}:${appId}`; const folKey = `${workspaceId}:${immovableId}:${appId}`;
let fol = folCache.get(folKey); let fol = folCache.get(folKey);
if (!fol) { if (!fol) {
try {
fol = await throttled(() => fol = await throttled(() =>
client.fetchParcelFolosinte( client.fetchParcelFolosinte(
workspaceId as string | number, workspaceId as string | number,
@@ -481,6 +521,9 @@ export async function enrichFeatures(
appId, appId,
), ),
); );
} catch {
fol = [];
}
folCache.set(folKey, fol); folCache.set(folKey, fol);
} }
if (fol && fol.length > 0) { if (fol && fol.length > 0) {
@@ -603,6 +646,16 @@ export async function enrichFeatures(
}); });
enrichedCount += 1; enrichedCount += 1;
} catch (featureErr) {
// Log and continue — don't abort the whole UAT
const cadRef = (attrs.NATIONAL_CADASTRAL_REFERENCE ?? "?") as string;
const msg = featureErr instanceof Error ? featureErr.message : String(featureErr);
console.warn(
`[enrich] Feature ${index + 1}/${terenuri.length} (cad=${cadRef}) failed: ${msg}`,
);
}
if (index % 10 === 0) { if (index % 10 === 0) {
push({ push({
phase: "Îmbogățire parcele", phase: "Îmbogățire parcele",