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 */
}
}
// If still null, enrichment will fail gracefully with empty lists
const workspacePkForApi = resolvedWsPk ?? 65;
console.log(`[enrich] siruta=${siruta} workspacePk=${workspacePkForApi}`);
if (!resolvedWsPk) {
console.warn(
`[enrich] siruta=${siruta}: workspace nu s-a rezolvat, folosesc fallback PK=${workspacePkForApi}`,
);
} else {
console.log(`[enrich] siruta=${siruta} workspacePk=${workspacePkForApi}`);
}
push({
phase: "Pregătire îmbogățire",
@@ -334,6 +339,18 @@ export async function enrichFeatures(
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 ──
push({ phase: "Descărcare documentații CF" });
const docByImmovable = new Map<string, any>();
@@ -392,22 +409,41 @@ export async function enrichFeatures(
const attrs = feature.attributes as Record<string, unknown>;
// Skip features with complete enrichment (resume after crash/interruption).
// Re-enrich if enrichment schema is incomplete (e.g., missing PROPRIETARI_VECHI
// added in a later version).
// Re-enrich if: schema incomplete, values are all "-" (empty), or older than 30 days.
if (feature.enrichedAt != null) {
const enrichJson = feature.enrichment as Record<string, unknown> | null;
const isComplete =
// Structural check: all 7 core fields must exist
const coreFields = [
"NR_CAD",
"NR_CF",
"PROPRIETARI",
"PROPRIETARI_VECHI",
"ADRESA",
"CATEGORIE_FOLOSINTA",
"HAS_BUILDING",
];
const structurallyComplete =
enrichJson != null &&
[
"NR_CAD",
"NR_CF",
"PROPRIETARI",
"PROPRIETARI_VECHI",
"ADRESA",
"CATEGORIE_FOLOSINTA",
"HAS_BUILDING",
].every((k) => k in enrichJson && enrichJson[k] !== undefined);
if (isComplete) {
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;
if (index % 50 === 0) {
options?.onProgress?.(
@@ -418,9 +454,12 @@ export async function enrichFeatures(
}
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 workspaceId = attrs.WORKSPACE_ID ?? "";
const applicationId = (attrs.APPLICATION_ID as number) ?? null;
@@ -474,13 +513,17 @@ export async function enrichFeatures(
const folKey = `${workspaceId}:${immovableId}:${appId}`;
let fol = folCache.get(folKey);
if (!fol) {
fol = await throttled(() =>
client.fetchParcelFolosinte(
workspaceId as string | number,
immovableId as string | number,
appId,
),
);
try {
fol = await throttled(() =>
client.fetchParcelFolosinte(
workspaceId as string | number,
immovableId as string | number,
appId,
),
);
} catch {
fol = [];
}
folCache.set(folKey, fol);
}
if (fol && fol.length > 0) {
@@ -603,6 +646,16 @@ export async function enrichFeatures(
});
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) {
push({
phase: "Îmbogățire parcele",