- 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>
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.prisma — GisFeature, 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.ts — WORKSPACE_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
- ArcGIS REST (
- 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
- nomenPk is NOT SIRUTA: eTerra's internal nomenclature PKs differ from SIRUTA codes. Never assume they're the same.
- 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_COUNTYmapping is the authoritative source. - ePay county IDs = WORKSPACE_IDs, ePay UAT IDs = SIRUTA codes: Zero discovery calls needed for ordering — use
GisUat.workspacePk+sirutadirectly. - GisUat.geometry is huge: Always
selectonly needed fields. Forgetting this turns a 50ms query into 5+ seconds. - Feature counts are cached: 5-min TTL global cache in UATs route. Returns stale data while refreshing.
- Session TTL: eTerra expires after ~10min, client has 9-min cache + auto-relogin.
- Maintenance windows: eTerra goes down regularly. Health check detects and blocks.
- ArcGIS maxRecordCount=1000: Always paginate. Fallback: 1000 -> 500 -> 200.
- Admin field auto-discovery: Different layers use different SIRUTA field names. Use
findAdminField()/buildWhere(). - LIMITE_UAT uses endpoint "all": No workspace context needed (unlike other layers using "aut").
- No-geometry parcels:
objectId = -immovablePkto avoid collision with real ArcGIS OBJECTIDs. - Spring Boot Page responses: Some APIs return
{content: [...]}instead of arrays. UseunwrapArray(). - Geometry in EPSG:3844: Reproject to EPSG:4326 for GeoPackage export.
- Owner tree parsing: Documentation API returns tree (C->A->I->P). Check
nodeStatus === -1up 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.workspacePkas countyId,sirutaas uatId - ePay also uses alphabetical county indices 0-41 for some endpoints (mapped via
epay-counties.ts)
Auth Flow (OpenAM)
- POST
https://oassl.ancpi.ro/openam/UI/LoginwithIDToken1={user}&IDToken2={pass}(form-urlencoded) - Response sets
AMAuthCookie(NOTiPlanetDirectoryPro) - Navigate to
http://epay.ancpi.ro:80/epay/LogIn.action(HTTP, not HTTPS!) for JSESSIONID - Session TTL: ~1 hour
Order Flow (per batch)
- AddToCartOrWishListFromPost.action x N -> basketRowIds
- EpayJsonInterceptor.action x N ->
reqType=saveProductMetadataForBasketItem+productMetadataJSON - EditCartSubmit.action ->
goToCheckout=true(ONE submit for ALL items) - CheckoutConfirmationSubmit.action -> confirms order
- Poll ShowOrderDetails.action?orderId=... -> parse documents from HTML-encoded JSON
- DownloadFile.action?typeD=4&id=... -> PDF download
ePay Endpoint Gotchas
- EpayJsonInterceptor uses form-urlencoded (NOT JSON):
reqType=nomenclatorUAT&countyId=127 - saveProductMetadataForBasketItem uses multipart/form-data (form-data npm package)
- CF/CAD values use
stringValues[0](array!), notstringValue - Document IDs are HTML-encoded in ShowOrderDetails:
"idDocument":47301767-> must decode before parsing - DownloadFile sends Content-Type: application/pdf in the REQUEST (not just response)
- EditCartSubmit returns 200 (not redirect) — Angular app does client-side redirect to CheckoutConfirmation
- SearchEstate needs
identificator/judet/uat(notidentifier/countyId/uatId), plus internal IDs - MinIO metadata must be ASCII — strip diacritics (
FlorestinotFlorești) - fetchCounties() is unreliable — the nomenclature API 404s intermittently. Use static
WORKSPACE_TO_COUNTYmapping instead.
Dedup Protection
- Queue level: batch key = sorted cadastral numbers, 60s dedup window
- API level: optional
noncefield, 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.