diff --git a/src/modules/parcel-sync/services/enrich-service.ts b/src/modules/parcel-sync/services/enrich-service.ts index 35a0bdb..ca38909 100644 --- a/src/modules/parcel-sync/services/enrich-service.ts +++ b/src/modules/parcel-sync/services/enrich-service.ts @@ -170,6 +170,7 @@ export async function enrichFeatures( objectId: true, attributes: true, cadastralRef: true, + enrichedAt: true, }, }); @@ -187,6 +188,56 @@ export async function enrichFeatures( }; } + // Resolve workspace PK from feature attributes or GisUat DB + let resolvedWsPk: number | null = null; + for (const f of terenuri) { + const ws = (f.attributes as Record).WORKSPACE_ID; + if (ws != null) { + const n = Number(ws); + if (Number.isFinite(n) && n > 0) { + resolvedWsPk = n; + break; + } + } + } + if (!resolvedWsPk) { + try { + const row = await prisma.gisUat.findUnique({ + where: { siruta }, + select: { workspacePk: true }, + }); + if (row?.workspacePk && row.workspacePk > 0) + resolvedWsPk = row.workspacePk; + } catch { + /* ignore */ + } + } + if (!resolvedWsPk) { + // Last resort: try ArcGIS layer query for 1 feature + try { + const features = await client.listLayer( + { + id: "TERENURI_ACTIVE", + name: "TERENURI_ACTIVE", + endpoint: "aut", + whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1", + }, + siruta, + { limit: 1, outFields: "WORKSPACE_ID" }, + ); + const wsId = features?.[0]?.attributes?.WORKSPACE_ID; + if (wsId != null) { + const n = Number(wsId); + if (Number.isFinite(n) && n > 0) resolvedWsPk = n; + } + } catch { + /* ignore */ + } + } + // If still null, enrichment will fail gracefully with empty lists + const workspacePkForApi = resolvedWsPk ?? 65; + console.log(`[enrich] siruta=${siruta} workspacePk=${workspacePkForApi}`); + push({ phase: "Pregătire îmbogățire", downloaded: 0, @@ -261,7 +312,7 @@ export async function enrichFeatures( while (listPage < listTotalPages) { const listResponse = await throttled(() => client.fetchImmovableListByAdminUnit( - 65, + workspacePkForApi, siruta, listPage, 200, @@ -299,7 +350,7 @@ export async function enrichFeatures( for (let i = 0; i < immovableIds.length; i += docBatchSize) { const batch = immovableIds.slice(i, i + docBatchSize); const docResponse = await throttled(() => - client.fetchDocumentationData(65, batch), + client.fetchDocumentationData(workspacePkForApi, batch), ); (docResponse?.immovables ?? []).forEach((item: any) => { const idKey = normalizeId(item?.immovablePk); @@ -348,6 +399,20 @@ export async function enrichFeatures( for (let index = 0; index < terenuri.length; index += 1) { const feature = terenuri[index]!; const attrs = feature.attributes as Record; + + // Skip features already enriched (resume after crash/interruption) + if (feature.enrichedAt != null) { + enrichedCount += 1; + if (index % 50 === 0) { + options?.onProgress?.( + index + 1, + terenuri.length, + "Îmbogățire parcele (skip enriched)", + ); + } + continue; + } + const immovableId = attrs.IMMOVABLE_ID ?? ""; const workspaceId = attrs.WORKSPACE_ID ?? ""; const applicationId = (attrs.APPLICATION_ID as number) ?? null; diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts index 12d315f..d9a4c55 100644 --- a/src/modules/parcel-sync/services/no-geom-sync.ts +++ b/src/modules/parcel-sync/services/no-geom-sync.ts @@ -252,77 +252,109 @@ export async function syncNoGeometryParcels( return { imported: 0, skipped: 0, errors: 0, status: "done" }; } - // 4. Import candidates + // 4. Import candidates in batches with retry let imported = 0; let skipped = 0; let errors = 0; const total = candidates.length; + const BATCH_SIZE = 50; + const MAX_RETRIES = 3; - for (let i = 0; i < candidates.length; i++) { - const item = candidates[i]!; - const immPk = Number(item.immovablePk ?? 0); - if (immPk <= 0) { - skipped++; - continue; - } + for ( + let batchStart = 0; + batchStart < candidates.length; + batchStart += BATCH_SIZE + ) { + const batch = candidates.slice(batchStart, batchStart + BATCH_SIZE); + const ops: Array> = []; - const cadRef = String(item.identifierDetails ?? "").trim(); - const areaValue = typeof item.area === "number" ? item.area : null; + for (const item of batch) { + const immPk = Number(item.immovablePk ?? 0); + if (immPk <= 0) { + skipped++; + continue; + } - // Build synthetic attributes to match the eTerra GIS layer format - const attributes: Record = { - OBJECTID: -immPk, // synthetic negative - IMMOVABLE_ID: immPk, - WORKSPACE_ID: item.workspacePk ?? 65, - APPLICATION_ID: item.applicationId ?? null, - NATIONAL_CADASTRAL_REFERENCE: cadRef, - AREA_VALUE: areaValue, - IS_ACTIVE: 1, - ADMIN_UNIT_ID: Number(siruta), - // Metadata from immovable list - PAPER_CAD_NO: item.paperCadNo ?? null, - PAPER_CF_NO: item.paperCfNo ?? null, - PAPER_LB_NO: item.paperLbNo ?? null, - TOP_NO: item.topNo ?? null, - IMMOVABLE_TYPE: item.immovableType ?? "P", - NO_GEOMETRY_SOURCE: "ETERRA_IMMOVABLE_LIST", - }; + const cadRef = String(item.identifierDetails ?? "").trim(); + const areaValue = typeof item.area === "number" ? item.area : null; - try { - await prisma.gisFeature.upsert({ - where: { - layerId_objectId: { - layerId: "TERENURI_ACTIVE", - objectId: -immPk, + const attributes: Record = { + OBJECTID: -immPk, + IMMOVABLE_ID: immPk, + WORKSPACE_ID: item.workspacePk ?? wsPk, + APPLICATION_ID: item.applicationId ?? null, + NATIONAL_CADASTRAL_REFERENCE: cadRef, + AREA_VALUE: areaValue, + IS_ACTIVE: 1, + ADMIN_UNIT_ID: Number(siruta), + PAPER_CAD_NO: item.paperCadNo ?? null, + PAPER_CF_NO: item.paperCfNo ?? null, + PAPER_LB_NO: item.paperLbNo ?? null, + TOP_NO: item.topNo ?? null, + IMMOVABLE_TYPE: item.immovableType ?? "P", + NO_GEOMETRY_SOURCE: "ETERRA_IMMOVABLE_LIST", + }; + + ops.push( + prisma.gisFeature.upsert({ + where: { + layerId_objectId: { + layerId: "TERENURI_ACTIVE", + objectId: -immPk, + }, }, - }, - create: { - layerId: "TERENURI_ACTIVE", - siruta, - objectId: -immPk, - cadastralRef: cadRef || null, - areaValue, - isActive: true, - attributes: attributes as Prisma.InputJsonValue, - geometry: Prisma.JsonNull, - geometrySource: "NO_GEOMETRY", - }, - update: { - cadastralRef: cadRef || null, - areaValue, - attributes: attributes as Prisma.InputJsonValue, - geometrySource: "NO_GEOMETRY", - updatedAt: new Date(), - }, - }); - imported++; - } catch { - errors++; + create: { + layerId: "TERENURI_ACTIVE", + siruta, + objectId: -immPk, + cadastralRef: cadRef || null, + areaValue, + isActive: true, + attributes: attributes as Prisma.InputJsonValue, + geometry: Prisma.JsonNull, + geometrySource: "NO_GEOMETRY", + }, + update: { + cadastralRef: cadRef || null, + areaValue, + attributes: attributes as Prisma.InputJsonValue, + geometrySource: "NO_GEOMETRY", + updatedAt: new Date(), + }, + }), + ); } - if (i % 20 === 0 || i === total - 1) { - options?.onProgress?.(i + 1, total, "Import parcele fără geometrie"); + // Execute batch with retry + if (ops.length > 0) { + let attempt = 0; + while (attempt < MAX_RETRIES) { + try { + await prisma.$transaction(ops); + imported += ops.length; + break; + } catch (err) { + attempt++; + if (attempt >= MAX_RETRIES) { + // Fall back to individual upserts for this batch + for (const op of ops) { + try { + await op; + imported++; + } catch { + errors++; + } + } + } else { + // Wait before retry (exponential backoff) + await new Promise((r) => setTimeout(r, 500 * attempt)); + } + } + } } + + const done = Math.min(batchStart + BATCH_SIZE, total); + options?.onProgress?.(done, total, "Import parcele fără geometrie"); } return { imported, skipped, errors, status: "done" };