Files
ArchiTools/src/modules/parcel-sync/SKILLS.md
T
AI Assistant 88754250a8 docs: update CLAUDE.md + SKILLS.md with ePay integration, performance fixes
- ParcelSync version 0.6.0 with ePay CF extract ordering
- ANCPI ePay in Current Integrations table
- Static WORKSPACE_TO_COUNTY mapping documented
- GisUat geometry select optimization documented
- Feature count cache (5-min TTL) documented
- ePay endpoint gotchas, auth flow, order flow
- Cleaned outdated info, focused on actionable gotchas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:57:42 +02:00

13 KiB

ParcelSync / eTerra GIS — Skills & Context

Single source of truth for AI assistants working on ParcelSync, eTerra, or ANCPI ePay.


Module Overview

ParcelSync connects to Romania's eTerra / ANCPI cadastral system to fetch, store, enrich, and export parcel data. GIS data lives in PostGIS via Prisma ORM. CF extract ordering uses ANCPI ePay (separate auth, separate API).

Key paths:

Area Path
Module root src/modules/parcel-sync/
Main component components/parcel-sync-module.tsx (~4100 lines)
Services services/ (eterra-client, sync, enrich, epay-*, etc.)
API routes — eTerra src/app/api/eterra/ (20+ routes)
API routes — ePay src/app/api/ancpi/ (7 routes)
Types types.ts
DB models prisma/schema.prismaGisFeature, GisSyncRun, GisUat, CfExtract
Layer catalog services/eterra-layers.ts — 23 layers, 4 categories
Static UAT list public/uat.json (~3000 entries, SIRUTA + name)
County mapping src/app/api/eterra/session/county-refresh.tsWORKSPACE_TO_COUNTY (static, verified)

Architecture

eTerra API Client (services/eterra-client.ts, ~1000 lines)

  • Auth: form-post login, JSESSIONID cookie jar (axios-cookiejar-support)
  • Session cache: keyed by credential hash, 9-min TTL, auto-relogin on 401/redirect
  • Two API surfaces:
    • ArcGIS REST (/api/map/rest/{endpoint}/layer/{name}/query) — spatial queries, geometry
    • Application API (/api/immovable/..., /api/immApps/..., /api/adm/nomen/...) — business data
  • Pagination: maxRecordCount=1000, fallback page sizes (500, 200)
  • Key methods: listLayer(), fetchAllLayer(), fetchAllLayerByWhere(), countLayer(), searchImmovableByIdentifier(), fetchDocumentationData(), fetchImmovableParcelDetails(), fetchImmovableListByAdminUnit(), fetchCounties(), fetchAdminUnitsByCounty(nomenPk)

Session Management (services/session-store.ts)

Global singleton, stores username + password in memory, tracks active job IDs (blocks disconnect while running).

Health Check (services/eterra-health.ts)

Pings eterra.ancpi.ro every 3min, detects maintenance by keywords in HTML, blocks login when down, UI shows amber state.

County Resolution

Primary method: Static WORKSPACE_TO_COUNTY mapping in county-refresh.ts — 42 entries, maps WORKSPACE_ID to county name. Populated on first eTerra login via LIMITE_UAT layer query.

Legacy method: fetchCounties() exists in eterra-client but the nomenclature API (/api/adm/nomen/COUNTY/list) is unreliable (404s intermittently). The PATCH /api/eterra/uats route still uses it as fallback. Prefer the static mapping.


Database Models

GisUat

siruta         String   @id      — SIRUTA code (NOT eTerra nomenPk!)
name           String
county         String?
workspacePk    Int?              — eTerra county WORKSPACE_ID
geometry       Json?             — LIMITE_UAT boundary polygon (EPSG:3844)
areaValue      Float?
lastUpdatedDtm String?

PERFORMANCE CRITICAL: geometry column stores huge polygon data. ALWAYS use select to exclude it in list queries. The GET /api/eterra/uats route uses select: { siruta: true, name: true, county: true, workspacePk: true } — never findMany() without select.

Feature counts cache: The groupBy query for per-UAT feature counts is expensive (~25s). The UATs route caches it in a global with 5-min TTL, returning stale data while refreshing in background.

GisFeature

id             String    @id @default(uuid())
layerId        String    — "TERENURI_ACTIVE", "CLADIRI_ACTIVE", etc.
siruta         String
objectId       Int       — eTerra OBJECTID (negative = no-geometry record)
cadastralRef   String?
areaValue      Float?
isActive       Boolean
attributes     Json      — ArcGIS attributes (WORKSPACE_ID, ADMIN_UNIT_ID, etc.)
geometry       Json?     — EsriGeometry { rings: number[][][] }
geometrySource String?   — "NO_GEOMETRY" for parcels without GIS geometry
enrichment     Json?     — NR_CAD, NR_CF, PROPRIETARI, INTRAVILAN, etc.
enrichedAt     DateTime?
@@unique([layerId, objectId])

GisSyncRun

siruta, layerId, status, totalRemote, totalLocal, newFeatures, removedFeatures, etc.

CfExtract (ANCPI ePay orders)

id              String    @id @default(uuid())
orderId         String?            — ePay orderId (shared across batch items, NOT unique)
basketRowId     Int?
nrCadastral     String
nrCF            String?
siruta          String?
judetIndex      Int                — ePay county index (0-41, alphabetical)
judetName       String
uatId           Int                — ePay UAT ID = SIRUTA code
uatName         String
prodId          Int    @default(14200)
solicitantId    String @default("14452")  — Beletage persoana juridica
status          String @default("pending")  — pending|queued|cart|searching|ordering|polling|downloading|completed|failed|cancelled
epayStatus      String?
idDocument      Int?
documentName    String?
documentDate    DateTime?
minioPath       String?            — parcele/{nrCadastral}/{idx}_Extras CF_{nrCadastral} - {DD-MM-YYYY}.pdf
minioIndex      Int?
creditsUsed     Int    @default(1)
immovableId     String?
immovableType   String?
measuredArea    String?
legalArea       String?
address         String?
gisFeatureId    String?
version         Int    @default(1)  — increments on re-order
expiresAt       DateTime?          — 30 days after documentDate
supersededById  String?
requestedBy     String?
errorMessage    String?
pollAttempts    Int    @default(0)

Critical Gotchas

  1. nomenPk is NOT SIRUTA: eTerra's internal nomenclature PKs differ from SIRUTA codes. Never assume they're the same.
  2. WORKSPACE_ID = county ID everywhere: Both eTerra ArcGIS and ePay use the same WORKSPACE_IDs for counties (CLUJ=127, ALBA=10, etc.). The static WORKSPACE_TO_COUNTY mapping is the authoritative source.
  3. ePay county IDs = WORKSPACE_IDs, ePay UAT IDs = SIRUTA codes: Zero discovery calls needed for ordering — use GisUat.workspacePk + siruta directly.
  4. GisUat.geometry is huge: Always select only needed fields. Forgetting this turns a 50ms query into 5+ seconds.
  5. Feature counts are cached: 5-min TTL global cache in UATs route. Returns stale data while refreshing.
  6. Session TTL: eTerra expires after ~10min, client has 9-min cache + auto-relogin.
  7. Maintenance windows: eTerra goes down regularly. Health check detects and blocks.
  8. ArcGIS maxRecordCount=1000: Always paginate. Fallback: 1000 -> 500 -> 200.
  9. Admin field auto-discovery: Different layers use different SIRUTA field names. Use findAdminField() / buildWhere().
  10. LIMITE_UAT uses endpoint "all": No workspace context needed (unlike other layers using "aut").
  11. No-geometry parcels: objectId = -immovablePk to avoid collision with real ArcGIS OBJECTIDs.
  12. Spring Boot Page responses: Some APIs return {content: [...]} instead of arrays. Use unwrapArray().
  13. Geometry in EPSG:3844: Reproject to EPSG:4326 for GeoPackage export.
  14. Owner tree parsing: Documentation API returns tree (C->A->I->P). Check nodeStatus === -1 up parent chain for cancelled owners.

ANCPI ePay — CF Extract Ordering

Overview

Orders CF extracts (Carte Funciara) from ANCPI ePay (epay.ancpi.ro). Separate auth from eTerra. Uses prepaid credits (1 per extract).

Critical ID Mapping

  • ePay county IDs = eTerra WORKSPACE_IDs (CLUJ=127, ALBA=10, etc.)
  • ePay UAT IDs = SIRUTA codes (Cluj-Napoca=54975, Floresti=57706)
  • Zero discovery needed: use GisUat.workspacePk as countyId, siruta as uatId
  • ePay also uses alphabetical county indices 0-41 for some endpoints (mapped via epay-counties.ts)

Auth Flow (OpenAM)

  1. POST https://oassl.ancpi.ro/openam/UI/Login with IDToken1={user}&IDToken2={pass} (form-urlencoded)
  2. Response sets AMAuthCookie (NOT iPlanetDirectoryPro)
  3. Navigate to http://epay.ancpi.ro:80/epay/LogIn.action (HTTP, not HTTPS!) for JSESSIONID
  4. Session TTL: ~1 hour

Order Flow (per batch)

  1. AddToCartOrWishListFromPost.action x N -> basketRowIds
  2. EpayJsonInterceptor.action x N -> reqType=saveProductMetadataForBasketItem + productMetadataJSON
  3. EditCartSubmit.action -> goToCheckout=true (ONE submit for ALL items)
  4. CheckoutConfirmationSubmit.action -> confirms order
  5. Poll ShowOrderDetails.action?orderId=... -> parse documents from HTML-encoded JSON
  6. DownloadFile.action?typeD=4&id=... -> PDF download

ePay Endpoint Gotchas

  1. EpayJsonInterceptor uses form-urlencoded (NOT JSON): reqType=nomenclatorUAT&countyId=127
  2. saveProductMetadataForBasketItem uses multipart/form-data (form-data npm package)
  3. CF/CAD values use stringValues[0] (array!), not stringValue
  4. Document IDs are HTML-encoded in ShowOrderDetails: &quot;idDocument&quot;:47301767 -> must decode before parsing
  5. DownloadFile sends Content-Type: application/pdf in the REQUEST (not just response)
  6. EditCartSubmit returns 200 (not redirect) — Angular app does client-side redirect to CheckoutConfirmation
  7. SearchEstate needs identificator/judet/uat (not identifier/countyId/uatId), plus internal IDs
  8. MinIO metadata must be ASCII — strip diacritics (Floresti not Florești)
  9. fetchCounties() is unreliable — the nomenclature API 404s intermittently. Use static WORKSPACE_TO_COUNTY mapping instead.

Dedup Protection

  • Queue level: batch key = sorted cadastral numbers, 60s dedup window
  • API level: optional nonce field, 60s idempotency cache
  • Test endpoint: 30s dedup on hardcoded test parcels

Key Files

File Purpose
services/epay-client.ts HTTP client (login, cart, metadata, submit, poll, download)
services/epay-queue.ts Batch queue with dedup
services/epay-storage.ts MinIO storage helpers
services/epay-counties.ts County index mapping (eTerra county name -> ePay alphabetical index)
services/epay-session-store.ts Session singleton
services/epay-types.ts TypeScript types
components/epay-connect.tsx Connection widget
components/epay-order-button.tsx Per-parcel order button
components/epay-tab.tsx Full "Extrase CF" tab
api/ancpi/session/ Connect/disconnect
api/ancpi/order/ Create batch orders
api/ancpi/orders/ List all extracts
api/ancpi/credits/ Credit balance
api/ancpi/download/ Stream PDF from MinIO
api/ancpi/download-zip/ Batch ZIP download
api/ancpi/test/ Diagnostic endpoint (temporary)

Credentials

  • Env vars: ANCPI_USERNAME, ANCPI_PASSWORD, ANCPI_BASE_URL, ANCPI_LOGIN_URL, ANCPI_DEFAULT_SOLICITANT_ID
  • MinIO bucket: ancpi-documente (env: MINIO_BUCKET_ANCPI)

API Routes Reference

eTerra Routes (/api/eterra/)

Route Method Purpose
/api/eterra/uats GET All UATs from DB (with cached feature counts, excludes geometry)
/api/eterra/uats POST Seed DB from uat.json
/api/eterra/uats PATCH Populate county from eTerra nomenclature (fallback)
/api/eterra/login POST Connect to eTerra (triggers county-refresh on first login)
/api/eterra/session GET/DELETE Session status / disconnect
/api/eterra/health GET eTerra platform health
/api/eterra/search POST Search parcels by cadastral number
/api/eterra/search-owner POST Search by owner name
/api/eterra/layers GET Available layers + field names
/api/eterra/count POST Count features in layer
/api/eterra/sync POST Start layer sync job
/api/eterra/sync-background POST Background sync
/api/eterra/sync-status GET Sync job progress
/api/eterra/progress GET Generic job progress
/api/eterra/features GET Query local features
/api/eterra/db-summary GET Aggregate stats all UATs
/api/eterra/uat-dashboard GET Per-UAT analytics
/api/eterra/export-bundle POST Export ZIP (CSV + GPKG)
/api/eterra/export-layer-gpkg POST Export single layer GPKG
/api/eterra/export-local POST Export from local DB
/api/eterra/no-geom-scan POST Scan for no-geometry parcels
/api/eterra/no-geom-debug POST Debug no-geom data
/api/eterra/setup-postgis POST Initialize PostGIS extensions

ANCPI ePay Routes (/api/ancpi/)

Route Method Purpose
/api/ancpi/session POST/DELETE Connect/disconnect ePay
/api/ancpi/credits GET Credit balance
/api/ancpi/order POST Create batch CF extract order
/api/ancpi/orders GET List all extracts from DB
/api/ancpi/download GET Stream single PDF from MinIO
/api/ancpi/download-zip POST Batch ZIP download
/api/ancpi/test POST Diagnostic test endpoint

Test UAT

Feleacu -- SIRUTA 57582, ~30k immovables, ~8k GIS features.


Last Updated

2026-03-23 -- ANCPI ePay CF extract ordering, performance fixes (GisUat select, feature count cache), static WORKSPACE_TO_COUNTY mapping.