# 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 - **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: `"idDocument":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.