# ParcelSync / eTerra GIS — Skills & Context > This file is the **single source of truth** for any AI assistant working on > the ParcelSync module, eTerra integration, or GIS features. Read this FIRST > before touching any code in this area. --- ## Module Overview ParcelSync connects to Romania's **eTerra / ANCPI** national cadastral system to fetch, store, enrich, and export parcel (land + building) data. All GIS data lives in a local **PostGIS** database via Prisma ORM. **Key paths:** | Area | Path | |------|------| | Module root | `src/modules/parcel-sync/` | | Components | `src/modules/parcel-sync/components/parcel-sync-module.tsx` (~4100 lines, single file) | | Services | `src/modules/parcel-sync/services/` | | API routes | `src/app/api/eterra/` (20+ route files) | | Types | `src/modules/parcel-sync/types.ts` | | DB models | `prisma/schema.prisma` → `GisFeature`, `GisSyncRun`, `GisUat` | | Layer catalog | `services/eterra-layers.ts` — 23 layers, 4 categories | | Static UAT list | `public/uat.json` (~3000 entries, SIRUTA + name only) | --- ## 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-minute 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()` — ArcGIS layer queries - `countLayer()` — feature count - `getLayerFieldNames()` — discover available fields - `searchImmovableByIdentifier()` — parcel search by cadastral number - `fetchDocumentationData()` — CF, owners (partTwoRegs tree) - `fetchImmovableParcelDetails()` — area, intravilan, use categories - `fetchImmovableListByAdminUnit()` — all immovables in a UAT (paginated, Spring Boot Page format) - `fetchCounties()` — all counties from nomenclature (`/api/adm/nomen/COUNTY/list`) - `fetchAdminUnitsByCounty(nomenPk)` — UATs per county (`/api/adm/nomen/ADMINISTRATIVEUNIT/filterByParent/{pk}`) - `fetchNomenByPk(pk)` — single nomenclature entry ### Session Management (`services/session-store.ts`) - Global singleton (one shared session for the whole app) - Stores username + password in memory only - Tracks active job IDs (blocks disconnect while jobs run) - `getSessionCredentials()` — used by all API routes ### Health Check (`services/eterra-health.ts`) - Pings `eterra.ancpi.ro` every 3 minutes - Detects maintenance by keywords in HTML response - Blocks login attempts when down - UI shows amber "Mentenanță" state --- ## Database Models ### GisUat (UAT registry) ``` siruta String @id — SIRUTA code (NOT eTerra nomenPk!) name String — UAT name (from uat.json) county String? — County name (populated via PATCH /api/eterra/uats) workspacePk Int? — eTerra county workspace ID (= county nomenPk) ``` **CRITICAL**: `eTerra nomenPk ≠ SIRUTA`. The nomenclature API uses `nomenPk` internally, but our DB keys on SIRUTA codes from `uat.json`. Matching is done by name or by resolving WORKSPACE_ID from ArcGIS layer features. ### GisFeature (parcel/building data) ``` id String @id @default(uuid()) layerId String — e.g. "TERENURI_ACTIVE", "CLADIRI_ACTIVE" siruta String — UAT this feature belongs to objectId Int — eTerra OBJECTID (negative = no-geometry record) cadastralRef String? — National cadastral reference areaValue Float? — Area in sqm isActive Boolean attributes Json — All ArcGIS attributes (WORKSPACE_ID, ADMIN_UNIT_ID, etc.) geometry Json? — EsriGeometry { rings: number[][][] } geometrySource String? — "NO_GEOMETRY" for parcels without GIS geometry enrichment Json? — Scraped data: NR_CAD, NR_CF, PROPRIETARI, INTRAVILAN, etc. enrichedAt DateTime? — When enrichment was fetched @@unique([layerId, objectId]) ``` ### GisSyncRun (sync history) ``` siruta, layerId, status, totalRemote, totalLocal, newFeatures, removedFeatures, etc. ``` --- ## Key Flows ### 1. UAT Selection & Autocomplete - On mount: `GET /api/eterra/uats` → loads all UATs from DB (fast, no eTerra) - If DB empty: seeds via `POST /api/eterra/uats` from `public/uat.json` - Client-side filter: diacritics-insensitive, matches SIRUTA prefix or name/county substring - Dropdown shows: **name – county** + local feature count badge + SIRUTA code - On select: sets `siruta` + `workspacePk` state ### 2. County Population (`PATCH /api/eterra/uats`) - **Phase 1**: UATs with `workspacePk` already → instant lookup via `fetchCounties()` map - **Phase 2**: Enumerate all counties → `fetchAdminUnitsByCounty()` per county → match by code then name - Auto-triggered on first eTerra connection when >50% UATs lack county - ~43 API calls total (1 counties + 42 per-county) - Name matching: NFD normalize, strip diacritics, uppercase ### 3. Workspace Resolution - `workspacePk` = eTerra county identifier (= county nomenPk from nomenclature) - Resolved lazily: query 1 feature from TERENURI_ACTIVE → read WORKSPACE_ID attribute - Cached in-memory + persisted to GisUat.workspacePk - Resolution chain: explicit param → GisUat DB → ArcGIS layer query ### 4. Layer Sync (`services/sync-service.ts`) - Background job: fetch all features from an ArcGIS layer for a SIRUTA - Uses UAT boundary geometry (from LIMITE_UAT) as spatial filter for dynamic layers - Stores features in GisFeature with full geometry - Progress tracking via `progress-store.ts` (2s polling) ### 5. Enrichment Pipeline (`services/enrich-service.ts`) - Per-feature: hits eTerra `/api/immovable/list` to extract detailed data - Stored in `GisFeature.enrichment` JSONB: - `NR_CAD`, `NR_CF`, `NR_CF_VECHI`, `NR_TOPO` - `PROPRIETARI`, `PROPRIETARI_VECHI` (semicolon-separated) - `SUPRAFATA`, `INTRAVILAN`, `CATEGORIE_FOLOSINTA` - `HAS_BUILDING`, `BUILD_LEGAL` - `ADRESA`, `SOLICITANT` ### 6. Parcel Search (`POST /api/eterra/search`) - Input: SIRUTA + cadastral number(s) - Resolves workspace, then calls: 1. `searchImmovableByIdentifier()` — find immovable 2. `fetchDocumentationData()` — CF, owners (active/cancelled via nodeStatus tree) 3. `fetchImmovableParcelDetails()` — area, intravilan, use categories 4. `fetchImmAppsByImmovable()` → `fetchParcelFolosinte()` — application data - Returns `ParcelDetail[]` with full property info ### 7. No-Geometry Sync (`services/no-geom-sync.ts`) - Finds eTerra immovables that have NO GIS geometry (no polygon in TERENURI_ACTIVE) - Cross-references immovable list against remote ArcGIS features - Stores as GisFeature with `geometry=null`, `objectId=-immovablePk` - Quality gate: must be active + hasLandbook + has identification/area ### 8. Export - **GeoPackage** (`services/gpkg-export.ts`): EPSG:3844 → EPSG:4326 reprojection - **Export bundle** (`/api/eterra/export-bundle`): ZIP with CSV + GPKG - **Local export** (`/api/eterra/export-local`): from local DB --- ## API Routes Reference | Route | Method | Purpose | |-------|--------|---------| | `/api/eterra/uats` | GET | All UATs from DB (with local feature counts) | | `/api/eterra/uats` | POST | Seed DB from uat.json | | `/api/eterra/uats` | PATCH | Populate county from eTerra nomenclature | | `/api/eterra/login` | POST | Connect to eTerra | | `/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 | --- ## Critical Gotchas 1. **nomenPk ≠ SIRUTA**: eTerra's internal nomenclature PKs are NOT SIRUTA codes. Never assume they're the same. 2. **workspacePk = county nomenPk**: The WORKSPACE_ID from ArcGIS layers equals the county's nomenclature PK from `fetchCounties()`. 3. **Session TTL**: eTerra sessions expire after ~10 minutes. Client has 9-minute cache TTL + auto-relogin. 4. **Maintenance windows**: eTerra goes down regularly. Health check detects this and blocks operations. 5. **ArcGIS maxRecordCount=1000**: Always paginate with `resultOffset`/`resultRecordCount`. Page size fallbacks: 1000 → 500 → 200. 6. **Admin field auto-discovery**: Different layers use different field names for SIRUTA (ADMIN_UNIT_ID, SIRUTA, UAT_ID, etc.). Always use `findAdminField()` / `buildWhere()`. 7. **LIMITE_UAT endpoint is "all"**: Unlike other layers (endpoint "aut"), LIMITE_UAT uses the "all" endpoint — no workspace context needed. 8. **No-geometry parcels use negative objectId**: `objectId = -immovablePk` to avoid collision with real ArcGIS OBJECTIDs. 9. **Spring Boot Page responses**: Some eTerra APIs return `{content: [...], totalPages: N}` instead of flat arrays. Always use `unwrapArray()` helper. 10. **Geometry format**: EsriGeometry `{ rings: number[][][] }` in EPSG:3844 (Stereo70). Reproject to EPSG:4326 for GeoPackage export. 11. **Owner tree parsing**: Documentation API returns owners as a tree (C→A→I→P nodes). Check `nodeStatus === -1` up the parent chain to detect radiated/cancelled owners. 12. **Local UatEntry type**: The component defines its own `UatEntry` type at the top of `parcel-sync-module.tsx` — this shadows the one in `types.ts`. Keep both in sync. --- ## Test UAT **Feleacu** — SIRUTA `57582`, ~30k immovables, ~8k GIS features. Used as the primary test UAT. --- ## Last Updated 2026-03-22 — Added county population via nomenclature API, local feature count in UAT dropdown.