EditCartSubmit returns 200 (not redirect) — Angular does client-side
redirect to CheckoutConfirmationSubmit.action. Added this step to
actually confirm the order before looking for the orderId.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
submitOrder now captures the previous orderId BEFORE submitting, then
searches for a NEW orderId that isn't in the knownOrderIds set. Queue
passes knownOrderIds between sequential items to prevent duplicate
orderId assignment (unique constraint violation).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Angular uses doPostAsFormMultipart — the save endpoint requires
multipart/form-data, not application/x-www-form-urlencoded.
Install form-data package and restore multipart upload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
form-data package not installed — crashes at runtime. Use
URLSearchParams instead (the Angular source uses doPostAsForm
which is form-urlencoded, so this should work too).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zero discovery calls needed! ePay internal county IDs are identical
to eTerra WORKSPACE_IDs (CLUJ=127, ALBA=10, etc.) and ePay UAT IDs
are SIRUTA codes (Cluj-Napoca=54975, Florești=57706). Queue now
uses workspacePk + siruta directly from GisUat DB.
Flow: AddToCart → saveMetadata → EditCartSubmit → Poll+Download.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SearchEstate might need active cart and/or different headers.
Add X-Requested-With: XMLHttpRequest, make uatId optional, log raw
response (type, length, sample), and add-to-cart before searching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OpenAM requires an initial GET to set session cookies before the
credentials POST. Without it, POST returns 500 and only sets
AMAuthCookie (intermediate) instead of iPlanetDirectoryPro (final SSO).
Then navigate to ePay goto URL to establish JSESSIONID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OpenAM goto URL is http://epay.ancpi.ro:80 (HTTP, not HTTPS).
Try multiple URL variants to establish JSESSIONID after OpenAM auth.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ANCPI's OpenAM uses AMAuthCookie instead of iPlanetDirectoryPro.
Accept AMAuthCookie, iPlanetDirectoryPro, or JSESSIONID as valid
session indicators. Navigate to ePay after auth to establish JSESSIONID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Let axios follow all redirects (maxRedirects=10) so cookie jar captures
iPlanetDirectoryPro from the chain. Explicitly navigate to ePay after
login to ensure JSESSIONID. Log all cookies for debugging. Last resort:
verify login by checking if credit info is visible on ePay page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OpenAM requires module=SelfRegistration and goto= redirect URL.
Also handle 302 manually to capture iPlanetDirectoryPro cookie,
then follow redirect to ePay for JSESSIONID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Foundation (Phase 1):
- CfExtract Prisma model with version tracking, expiry, MinIO path
- epay-types.ts: all ePay API response types
- epay-counties.ts: WORKSPACE_ID → ePay county index mapping (42 counties)
- epay-storage.ts: MinIO helpers (bucket, naming, upload, download)
- docker-compose.yml: ANCPI env vars
ePay Client (Phase 2):
- epay-client.ts: full HTTP client (login, credits, cart, search estate,
submit order, poll status, download PDF) with cookie jar + auto-relogin
- epay-session-store.ts: separate session from eTerra
Queue + API (Phase 3):
- epay-queue.ts: sequential FIFO queue (global cart constraint),
10-step workflow per order with DB status updates at each step
- POST /api/ancpi/session: connect/disconnect
- POST /api/ancpi/order: create single or bulk orders
- GET /api/ancpi/orders: list all extracts
- GET /api/ancpi/credits: live credit balance
- GET /api/ancpi/download: stream PDF from MinIO
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New eterra-health.ts service: pings eTerra periodically (3min),
detects maintenance (503, keywords), tracks consecutive failures
- New /api/eterra/health endpoint for explicit health queries
- Session route blocks login when eTerra is in maintenance (503 response)
- GET /api/eterra/session now includes eterraAvailable/eterraMaintenance
- ConnectionPill shows amber 'Mentenanță' state with AlertTriangle icon
instead of confusing red error when eTerra is down
- Auto-connect skips when maintenance detected, retries when back online
- 30s session poll auto-detects recovery and re-enables auto-connect
- New search mode toggle: Nr. Cadastral / Proprietar
- Owner search queries:
1. Local DB first (enrichment PROPRIETARI/PROPRIETARI_VECHI ILIKE)
2. eTerra API fallback (tries personName/titularName/ownerName filter keys)
- DB search works offline (no eTerra connection needed) — uses enriched data
- New API route: POST /api/eterra/search-owner
- New eterra-client method: searchImmovableByOwnerName()
- Owner results show source badge (DB local / eTerra online)
- Results can be added to saved list and exported as CSV
- Relaxed search tab guard: only requires UAT selection (not eTerra connection)
- Cadastral search still requires eTerra connection (shows hint when offline)
- DEFAULT_TIMEOUT_MS: 40_000 -> 120_000 (1000 features with full geometry
from Feleacu regularly exceed 40s on the eTerra server)
- Add timeoutMs option to syncLayer() for caller override
- syncLayer now passes timeoutMs through to EterraClient.create()
Fixes 'timeout of 40000ms exceeded' on TERENURI_ACTIVE sync.
- DEFAULT_PAGE_SIZE: 2000 -> 1000 (matches eTerra maxRecordCount, avoids
requesting more than the server supports on first try)
- PAGE_SIZE_FALLBACKS: [500, 200] (removed 1000 since it's now the default)
- Add retry-once logic for 'Error performing query operation':
Wait 2s and retry same page before falling to smaller sizes.
These errors are often transient server-side timeouts.
- Longer delay (1s vs 0.5s) between page size fallback attempts
Fixes Feleacu (7951 features) background sync failure.
- eterra-client: detect server maxRecordCount cap in fetchAllLayerByWhere
When server returns exactly 1000 (or other round cap) but we asked for 2000,
recognize this as a server limit, adjust pageSize, and CONTINUE paginating.
Previously: 1000 < 2000 -> break (lost all data beyond page 1).
- no-geom-sync: count layers first, pass total to fetchAllLayer
Belt-and-suspenders: even if cap detection misses, known total prevents
early termination. Also use pageSize 1000 to match typical server cap.
Clădiri count uses countLayer instead of fetching all OBJECTIDs.
- UI: add include-no-geom checkbox in background sync section
Users can toggle it independently of scan status.
Shows '(scanare in curs)' hint when scan is still running.
- fetchImmovableParcelDetails called FIRST (1 call, no applicationId needed)
- app-based fetchParcelFolosinte only as fallback when direct returns nothing
- SOLICITANT skipped entirely (was always '-' for old CF records)
- Remove unused pickApplication helper
- Net savings: ~500+ API calls per UAT enrichment (50-65% reduction)
- copycf/get returns same data as list (no enrichment value, kept as utility)
QUALITY GATE TIGHTENED:
No-geometry import now requires hasLandbook=1 (imobil electronic).
This filters out immovables without carte funciara — they have no
CF data, no owners, no parcel details to extract. For Cosbuc this
reduces useful no-geom from ~1916 to ~468 (only IEs with real data).
Three-tier quality gate:
1. Active (status=1)
2. Has landbook (hasLandbook=1) — is electronic immovable [NEW]
3. Has identification (cadRef/paperLbNo/paperCadNo) OR area
CLEANUP also updated: DB cleanup now removes stale no-geom records
that don't pass the tightened gate (existing non-IE records will be
cleaned on next import run).
NEW API METHODS (eterra-client):
- checkIfIsIE(adminUnitId, paperCadNo, topNo, paperCfNo) → boolean
Calls /api/immovable/checkIfIsIE — verifies IE status per-parcel
Available for future per-item verification if needed
- getCfExtractUrl(immovablePk, workspaceId) → string
Returns URL for /api/cf/landbook/copycf/get/{pk}/{ws}/0/true
Downloads the CF extract as PDF blob (future enrichment)
UI updated: 'Filtrate' label now says 'fara CF/inactive/fara date'
to reflect the new hasLandbook filter.
PROBLEM:
For no-geometry parcels (and many geometry parcels without application
IDs), CATEGORIE_FOLOSINTA was always '-' because:
1. fetchImmAppsByImmovable returned no apps (no applicationId)
2. Without appId, fetchParcelFolosinte was skipped entirely
3. No fallback existed
DISCOVERY (from eTerra UI investigation):
The endpoint /api/immovable/details/parcels/list/{wp}/{pk}/{page}/{size}
returns parcel use categories DIRECTLY — no applicationId needed.
Example: [{useCategory:'arabil', intravilan:'Necunoscut', parcelPk:17753903}]
FIX:
- After the app-based CATEGORIE_FOLOSINTA attempt, if result is still '-',
fall back to fetchImmovableParcelDetails (the direct endpoint)
- formatCategories now handles both API formats:
- App-based: categorieFolosinta + suprafata fields
- Direct: useCategory field (no area — shows category name only)
- When direct endpoint provides area=0, format shows just the category
name without ':0' (e.g. 'arabil; faneata' instead of 'arabil:0; faneata:0')
- Also picks up intravilan from direct endpoint if app-based was empty
- Fixed fetchImmovableParcelDetails default size: 1 → 20 (one immovable
can have multiple parcels, e.g. IE 25332 has 2: arabil + faneata)
- Results are cached in folCache to avoid duplicate requests
LAYER FETCH:
- fetchAllLayerByWhere now falls back through 2000 → 1000 → 500 → 200
instead of just 2000 → 1000 before giving up
- 500ms delay between fallback attempts to let eTerra recover
- Error message now includes the original cause:
'Failed to fetch layer TERENURI_ACTIVE: Session expired (401)'
instead of just 'Failed to fetch layer TERENURI_ACTIVE'
DISPLAY:
- 505 terenuri count no longer green (was emerald-600, now neutral semibold)
It's just a data value, not a status indicator
ROOT CAUSE: The cross-reference between immovable list and GIS layer
produces wildly different matchedCount on each scan (320, 430, 629, 433)
because the eTerra immovable/list API with inscrisCF=-1 returns
inconsistent results across calls. The GIS layer count (505) is stable.
SCAN DISPLAY — now uses only stable numbers:
- Header shows 'Layer GIS: 505 terenuri + X cladiri' (stable ArcGIS count)
- Shows 'Lista imobile: 2.717 (estimat ~2.212 fara geometrie)' using
simple subtraction totalImmovables - remoteGisCount
- Cross-ref matchedCount kept internally for import logic, but NOT shown
as the primary number — eliminates visual instability
- hasNoGeomParcels now uses estimated count (stable)
WORKFLOW PREVIEW — now accurate:
- Step 1: 'Sync GIS — descarca 505 terenuri + X cladiri' (separate counts)
or 'skip (date proaspete in DB)' when fresh
- Step 2 (enrichment): Fixed 'deja imbogatite' bug when DB is empty.
Now correctly computes what WILL be in DB after sync completes:
geoAfterSync + noGeomAfterImport - localDbEnrichedComplete
- Steps 3-4 unchanged
CLADIRI COUNT:
- Scan now also fetches CLADIRI_ACTIVE layer count (lightweight, OBJECTID only)
- New field remoteCladiriCount in NoGeomScanResult
- Displayed in header and workflow step 1
- Non-fatal: if CLADIRI fetch fails, just shows 0
SCAN DISPLAY:
- Use matchedCount (withGeometry) for 'cu geometrie' — ALWAYS adds up
with noGeomCount to equal totalImmovables (ground truth arithmetic)
- Show remoteGisCount separately as 'Layer GIS: N features (se descarca toate)'
- When remoteGisCount != matchedCount, show matching detail with breakdown
(X potrivite + cadRef/ID split) so mismatches are transparent
- Workflow preview step 1 still uses remoteGisCount (correct: all GIS
features get downloaded regardless of matching)
MATCH QUALITY TRACKING:
- New fields: matchedByRef, matchedById in NoGeomScanResult
- Track how many immovables matched by cadastral ref vs by IMMOVABLE_ID
- Console log match quality for server-side debugging
- scannedAt timestamp for audit trail
PIPELINE AUDIT (export report):
- New 'pipeline' section in export_report.json with full trace:
syncedGis, noGeometry (imported/cleaned/skipped), enriched, finalDb
- raport_calitate.txt now has PIPELINE section before quality analysis
showing exactly what happened at each step
- Capture noGeomCleaned + noGeomSkipped in addition to noGeomImported
- UI: scan card now shows remoteGisCount instead of matchedCount (withGeometry)
as the primary 'cu geometrie' number — this is the true GIS layer feature count
- UI: workflow preview step 1 shows remoteGisCount for download count
- UI: mismatch note reworded as secondary detail about cross-reference matching
- Import: automatic cleanup step at start of syncNoGeometryParcels
- Builds valid immovablePk set from fresh list (active + identification/area)
- Deletes stale NO_GEOMETRY records not in the valid set
- Reports cleaned count in result + progress note
- NoGeomSyncResult type: added 'cleaned' field
- Gitignore: temp-db-check.cjs
BUGS FIXED:
- paperCfNo does NOT exist in eTerra API — field is paperLbNo
Renamed withPaperCf → withPaperLb everywhere (type, scan, UI)
- Area fields: only measuredArea and legalArea exist on immovable/list
Removed phantom area/areaValue/suprafata checks from import filter
FILTERING TIGHTENED:
- Quality gate now requires status=1 (active) in eTerra
- Items with status≠1 are filtered out before import
- Quality breakdown adds: withActiveStatus, withLandbook counters
- Import attributes now store MEASURED_AREA, LEGAL_AREA, HAS_LANDBOOK
- workspace.nomenPk used instead of workspacePk for accuracy
ENRICHMENT ROBUSTNESS:
- Area fallback: when AREA_VALUE is missing (no-geom), enrichment
now falls back to listItem.measuredArea/legalArea from immovable list
- Post-enrichment verification: logs 100% coverage or warns about gaps
- EnrichResult type extended with totalFeatures + unenrichedCount
UI UPDATES:
- Quality grid shows 6 stats: cadRef, CF/LB, paperCad, area, active, landbook
- Filter explanation updated: 'inactive sau fără date' instead of old text
- Magic GPKG (terenuri_magic.gpkg) now contains ALL records:
rows with geometry render as polygons, rows without have null geom
but still carry all attribute/enrichment data (QGIS shows them fine)
- Added HAS_GEOMETRY column to Magic GPKG fields (0 or 1)
- GPKG builder now supports includeNullGeometry option: splits features
into spatial-first (creates table), then appends null-geom rows
- Base terenuri.gpkg / cladiri.gpkg unchanged (spatial only)
- CSV still has all records as before
- GeoJsonFeature type now allows null geometry
- Reproject: null geometry guard added
- UI text updated: no longer says 'Nu apar in GPKG'
- NoGeomScanResult now includes: localDbTotal, localDbWithGeom, localDbNoGeom,
localDbEnriched, localSyncFresh (parallel DB queries, fast)
- Scan card shows 'Baza de date locala: X cu geometrie + Y fara + Z imbogatite'
- Workflow preview shows numbered steps with smart estimates:
step 1 shows 'skip (date proaspete)' when sync is fresh
step 2 shows '~N noi de importat' or 'deja importate' for no-geom
step 3 shows '~N de procesat (~M min)' or 'deja imbogatite' for enrichment
- All-geometry card also shows local DB summary
- User can see exactly what will happen before pressing Magic
- scanNoGeometryParcels now fetches TERENURI_ACTIVE features from remote
ArcGIS (lightweight, no geometry) to cross-reference with eTerra immovable list
- Cross-references by both NATIONAL_CADASTRAL_REFERENCE and IMMOVABLE_ID
- Works correctly regardless of whether user has synced to local DB
- Renamed totalInDb -> withGeometry in NoGeomScanResult, UI, and API
- Extended fetchAllLayer() to forward outFields/returnGeometry options
- resolveWorkspacePk chain: explicit param -> GisUat DB -> ArcGIS layer query
- UI passes workspacePk from UAT selection to scan API
- Fixes: FELEACU (Cluj, workspace!=65) returning 0 immovables
- Better messaging: shows X total, Y with geometry, Z without
- Shows warning when 0 immovables found (workspace resolution failed)
- Add geometrySource field to GisFeature (NO_GEOMETRY marker)
- New no-geom-sync service: scan + import parcels missing from GIS layer
- Uses negative immovablePk as objectId to avoid @@unique collision
- New /api/eterra/no-geom-scan endpoint for counting
- Export-bundle: includeNoGeometry flag, imports before enrich
- CSV export: new HAS_GEOMETRY column (0/1)
- GPKG: still geometry-only (unchanged)
- UI: checkbox + scan button on Export tab
- Baza de Date tab: shows no-geometry counts per UAT
- db-summary API: includes noGeomCount per layer
2 bugs:
1. After Magic/base download completes, progress bar stayed stuck at
77% because exportProgress was never updated to 'done' client-side.
Fix: set progress to 'Finalizat' + 100% after successful blob download.
2. syncLayer overwrote the export route's weighted percentages (0-100)
with raw feature counts (50/200), causing progress bar to jump.
Fix: when isSubStep=true, sync writes phase/note/phaseCurrent/phaseTotal
but preserves the parent route's downloaded/total weighted values.
3 bugs fixed:
- syncLayer was called without jobId -> user saw no progress duringSync
- syncLayer set status:'done' prematurely -> client stopped polling before GPKG phase
- syncLayer errors were silently ignored -> confusing 'no features in DB' error
Added isSubStep option to syncLayer: when true, keeps status as 'running'
and doesn't schedule clearProgress. Export routes now pass jobId + isSubStep
so the real sync progress (Descărcare features 50/200) is visible in the UI.
- Rewrite export-bundle to sync-first: check freshness -> sync layers -> enrich (magic) -> build GPKG/CSV from local DB
- Rewrite export-layer-gpkg to sync-first: sync if stale -> export from DB
- Create enrich-service.ts: extracted magic enrichment logic (CF, owners, addresses) with DB storage
- Add enrichment + enrichedAt columns to GisFeature schema
- Update PostGIS views to include enrichment data
- UI: update button labels for sync-first semantics, refresh sync status after exports
- Smart caching: skip sync if data is fresh (168h / 1 week default)
- resolveWorkspace: use listLayer() instead of listLayerByWhere() with
hardcoded field names. Auto-discovers admin field (ADMIN_UNIT_ID/SIRUTA)
from ArcGIS layer metadata via buildWhere().
- resolveWorkspace: persist WORKSPACE_ID to DB on first resolution for
fast subsequent lookups.
- UATs POST: seed from uat.json (correct SIRUTA codes) instead of eTerra
nomenclature API (nomenPk != SIRUTA, county nomenPk != WORKSPACE_ID).
- Remove eTerra nomenclature dependency from UATs endpoint.
- Fix activeJobs Set iteration error on container restart.
- Remove unused enrichedUatsFetched ref.
- Add fetchNomenByPk() to EterraClient single nomen entry lookup
- resolveWorkspace() now tries fast path first: direct nomen lookup for
SIRUTA walk parentNomenPk chain to find COUNTY (1-3 calls vs 42+)
- Falls back to full county scan only if direct lookup fails
- Search route: DB lookup as middle fallback between workspacePk and resolve
- Debug logging to trace workspace resolution on production
- Fix: try all possible UAT identifier fields (nomenPk, siruta, code, pk)
- New /api/eterra/uats endpoint fetches all counties + UATs from eTerra,
caches server-side for 1 hour, returns enriched data with county name
and workspacePk for each UAT
- When eTerra is connected, auto-fetches enriched UAT list (replaces
static uat.json fallback) shows 'FELEACU (57582), CLUJ' format
- UAT autocomplete now searches both UAT name and county name
- Selected UAT stores workspacePk in state, passes it directly to
/api/eterra/search eliminates slow per-search county resolution
- Search route accepts optional workspacePk, falls back to resolveWorkspace()
- Dropdown shows UAT name, SIRUTA code, and county prominently
- Increased autocomplete results from 8 to 12 items
Search tab now uses eTerra application API (same as the web UI):
- POST /api/eterra/search queries /api/immovable/list with exact
identifierDetails filter + /api/documentation/data for full details
- Returns: nr cad, nr CF, CF vechi, nr topo, suprafata, intravilan,
categorii folosinta, adresa, proprietari, solicitant
- Automatic workspace (county) resolution from SIRUTA with cache
- Support for multiple cadastral numbers (comma separated)
UI changes:
- Detail cards instead of flat ArcGIS feature table
- Copy details to clipboard button per parcel
- Add parcels to list + CSV export
- Search list with summary table + CSV download
- No more layer filter or pagination (not needed for app API)
New EterraClient methods:
- searchImmovableByIdentifier (exact cadaster lookup)
- fetchCounties / fetchAdminUnitsByCounty (workspace resolution)