diff --git a/src/app/api/ancpi/test/route.ts b/src/app/api/ancpi/test/route.ts index 7a7b86a..ef5fcf2 100644 --- a/src/app/api/ancpi/test/route.ts +++ b/src/app/api/ancpi/test/route.ts @@ -77,50 +77,43 @@ export async function GET(req: Request) { } // ── Step: search ── - // Test EditCartItemJson.action — the real endpoint for configuring cart items + // Test UAT lookup (JSON) + SearchEstate (with basketId) if (step === "search") { const client = await EpayClient.create(username, password); - const BASE = process.env.ANCPI_BASE_URL || "https://epay.ancpi.ro/epay"; + const countyIdx = resolveEpayCountyIndex("Cluj")!; + const results: Record = { countyIdx }; - // Add to cart + // 1. Get UAT list (JSON body now) + const uatList = await client.getUatList(countyIdx); + results["uatCount"] = uatList.length; + results["uatFirst5"] = uatList.slice(0, 5); + + // Find our test UATs + const normalize = (s: string) => + s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase(); + const clujNapoca = uatList.find((u) => normalize(u.value).includes("CLUJ-NAPOCA") || normalize(u.value).includes("CLUJNAPOCA")); + const feleacu = uatList.find((u) => normalize(u.value).includes("FELEACU")); + const floresti = uatList.find((u) => normalize(u.value).includes("FLORESTI")); + results["uatMatches"] = { clujNapoca, feleacu, floresti }; + + // 2. Add to cart + SearchEstate with basketId const basketRowId = await client.addToCart(14200); + results["basketRowId"] = basketRowId; - const results: Record = { basketRowId }; - - // Test 1: Set county via EditCartItemJson to get UAT list - try { - const body1 = new URLSearchParams(); - body1.set("basketId", String(basketRowId)); - body1.set("metadate.judet", "13"); // CLUJ - - const resp1 = await client.postRaw( - `${BASE}/EditCartItemJson.action`, - body1.toString(), - ); - results["setCounty_type"] = typeof resp1; - results["setCounty_len"] = typeof resp1 === "string" ? resp1.length : JSON.stringify(resp1).length; - results["setCounty_sample"] = (typeof resp1 === "string" ? resp1 : JSON.stringify(resp1)).slice(0, 1000); - } catch (e) { - results["setCounty_error"] = (e as Error).message; + if (clujNapoca) { + results["search_345295"] = await client + .searchEstate("345295", countyIdx, clujNapoca.id, basketRowId) + .catch((e: Error) => ({ error: e.message })); } - - // Test 2: Try SearchEstate via the Angular way (maybe it's inside EditCartItemJson) - try { - const body2 = new URLSearchParams(); - body2.set("basketId", String(basketRowId)); - body2.set("identifier", "345295"); - body2.set("countyId", "13"); - - const resp2 = await client.postRaw( - `${BASE}/SearchEstate.action`, - body2.toString(), - { "X-Requested-With": "XMLHttpRequest", Accept: "application/json" }, - ); - results["searchAjax_type"] = typeof resp2; - results["searchAjax_len"] = typeof resp2 === "string" ? resp2.length : JSON.stringify(resp2).length; - results["searchAjax_sample"] = (typeof resp2 === "string" ? resp2 : JSON.stringify(resp2)).slice(0, 1000); - } catch (e) { - results["searchAjax_error"] = (e as Error).message; + if (feleacu) { + results["search_63565"] = await client + .searchEstate("63565", countyIdx, feleacu.id, basketRowId) + .catch((e: Error) => ({ error: e.message })); + } + if (floresti) { + results["search_88089"] = await client + .searchEstate("88089", countyIdx, floresti.id, basketRowId) + .catch((e: Error) => ({ error: e.message })); } return NextResponse.json({ step: "search", results }); @@ -128,7 +121,6 @@ export async function GET(req: Request) { // ── Step: order ── (USES 3 CREDITS!) if (step === "order") { - // Ensure session exists if (!getEpayCredentials()) { createEpaySession(username, password, 0); } @@ -158,6 +150,7 @@ export async function GET(req: Request) { if (!clujNapoca || !feleacu || !floresti) { return NextResponse.json({ error: "Nu s-au găsit UAT-urile.", + uatCount: uatList.length, clujNapoca, feleacu, floresti, }); } @@ -168,21 +161,21 @@ export async function GET(req: Request) { judetIndex: countyIdx, judetName: "CLUJ", uatId: clujNapoca.id, - uatName: "Cluj-Napoca", + uatName: clujNapoca.value, }, { nrCadastral: "63565", judetIndex: countyIdx, judetName: "CLUJ", uatId: feleacu.id, - uatName: "Feleacu", + uatName: feleacu.value, }, { nrCadastral: "88089", judetIndex: countyIdx, judetName: "CLUJ", uatId: floresti.id, - uatName: "Florești", + uatName: floresti.value, }, ]; @@ -195,11 +188,12 @@ export async function GET(req: Request) { return NextResponse.json({ step: "order", credits, - message: `Enqueued ${ids.length} orders. Queue processing started.`, + message: `Enqueued ${ids.length} orders. Processing sequentially...`, orderIds: ids, parcels: parcels.map((p, i) => ({ nrCadastral: p.nrCadastral, uatName: p.uatName, + uatId: p.uatId, extractId: ids[i], })), }); diff --git a/src/modules/parcel-sync/services/epay-client.ts b/src/modules/parcel-sync/services/epay-client.ts index 80d50a3..eb3a935 100644 --- a/src/modules/parcel-sync/services/epay-client.ts +++ b/src/modules/parcel-sync/services/epay-client.ts @@ -353,18 +353,23 @@ export class EpayClient { /* ── Estate Search ─────────────────────────────────────────── */ + /** + * Search estate on ePay. Requires basketRowId in context. + * countyIdx = ePay county index (0-41), uatId = real ePay UAT ID (NOT index). + */ async searchEstate( identifier: string, countyIdx: number, - uatId?: number, + uatId: number, + basketRowId: number, ): Promise { return this.retryOnAuthFail(async () => { + // Must include basketId for SearchEstate to return JSON const body = new URLSearchParams(); body.set("identifier", identifier); body.set("countyId", String(countyIdx)); - if (uatId != null && uatId >= 0) { - body.set("uatId", String(uatId)); - } + body.set("uatId", String(uatId)); + body.set("basketId", String(basketRowId)); const response = await this.client.post( `${BASE_URL}/SearchEstate.action`, @@ -372,7 +377,6 @@ export class EpayClient { { headers: { "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json, text/plain, */*", "X-Requested-With": "XMLHttpRequest", }, timeout: DEFAULT_TIMEOUT_MS, @@ -380,90 +384,133 @@ export class EpayClient { ); const data = response.data; - - // Log raw response for debugging - const rawStr = typeof data === "string" ? data : JSON.stringify(data); - console.log( - `[epay] SearchEstate(${identifier}, county=${countyIdx}, uat=${uatId ?? "none"}):`, - `type=${typeof data}, len=${rawStr?.length ?? 0}, sample=${rawStr?.slice(0, 300)}`, - ); - if (Array.isArray(data)) return data as EpaySearchResult[]; if (typeof data === "string") { - // Try JSON parse const trimmed = data.trim(); - if (trimmed.startsWith("[") || trimmed.startsWith("{")) { + if (trimmed.startsWith("[")) { try { - const parsed = JSON.parse(trimmed); - if (Array.isArray(parsed)) return parsed as EpaySearchResult[]; - // Wrapped in object? - if (parsed?.results) return parsed.results as EpaySearchResult[]; - } catch { - // Not JSON - } + return JSON.parse(trimmed) as EpaySearchResult[]; + } catch { /* not JSON */ } } } + console.warn( + `[epay] SearchEstate(${identifier}): unexpected response`, + String(typeof data === "string" ? data : JSON.stringify(data)).slice(0, 200), + ); return []; }); } /* ── UAT Lookup ────────────────────────────────────────────── */ + /** + * Get UAT list for a county. Uses JSON Content-Type (not form-urlencoded). + */ async getUatList(countyIdx: number): Promise { return this.retryOnAuthFail(async () => { - // ePay uses EpayJsonInterceptor for dynamic dropdowns - // Try the interceptor first - const body = new URLSearchParams(); - body.set("actionType", "getUAT"); - body.set("countyIndex", String(countyIdx)); - + // EpayJsonInterceptor requires JSON body, not form-urlencoded const response = await this.client.post( `${BASE_URL}/EpayJsonInterceptor.action`, - body.toString(), + JSON.stringify({ judet: countyIdx }), { - headers: { "Content-Type": "application/x-www-form-urlencoded" }, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, timeout: DEFAULT_TIMEOUT_MS, }, ); const data = response.data; - // Response: { jsonResult: "[{\"id\":155546,\"value\":\"Balint\"}, ...]" } - if (data?.jsonResult) { + // Response: { jsonResult: "[{\"id\":55473,\"value\":\"Aghiresu\"}, ...]" } + if (data?.jsonResult && typeof data.jsonResult === "string") { try { const parsed = JSON.parse(data.jsonResult); if (Array.isArray(parsed)) return parsed as EpayUatEntry[]; - } catch { - // Parse failed - } + } catch { /* parse failed */ } } - // Direct array response if (Array.isArray(data)) return data as EpayUatEntry[]; console.warn( - `[epay] getUatList(${countyIdx}) returned unexpected:`, + `[epay] getUatList(${countyIdx}):`, JSON.stringify(data).slice(0, 200), ); return []; }); } + /* ── Configure Cart Item (save metadata via JSON) ──────────── */ + + /** + * Save cart item metadata via EditCartItemJson. Uses JSON body + * with bigDecimalValue/stringValue structure. + */ + async configureCartItem( + basketRowId: number, + countyIdx: number, + uatId: number, + nrCF: string, + nrCadastral: string, + solicitantId: string, + ): Promise { + await this.retryOnAuthFail(async () => { + const payload = { + basketId: basketRowId, + metadate: { + judet: { bigDecimalValue: countyIdx }, + uat: { bigDecimalValue: uatId }, + CF: { stringValue: nrCF }, + CAD: { stringValue: nrCadastral }, + metodeLivrare: { + stringValue: "Electronic", + stringValues: ["Electronic"], + }, + differentSolicitant: { stringValue: solicitantId }, + }, + }; + + const response = await this.client.post( + `${BASE_URL}/EditCartItemJson.action`, + JSON.stringify(payload), + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + timeout: DEFAULT_TIMEOUT_MS, + }, + ); + + console.log( + `[epay] ConfigureCartItem(${basketRowId}): status=${response.status}`, + ); + }); + } + /* ── Order Submission ──────────────────────────────────────── */ + /** + * Submit order after configuring cart item via EditCartItemJson. + */ async submitOrder(metadata: OrderMetadata): Promise { return this.retryOnAuthFail(async () => { + // First configure the cart item via JSON API + await this.configureCartItem( + metadata.basketRowId, + metadata.judetIndex, + metadata.uatId, + metadata.nrCF, + metadata.nrCadastral, + metadata.solicitantId, + ); + + // Then submit checkout via form (EditCartSubmit still uses form encoding) const body = new URLSearchParams(); body.set("goToCheckout", "true"); - body.set("basketItems[0].basketId", String(metadata.basketRowId)); - body.set("basketItems[0].metadate.judet", String(metadata.judetIndex)); - body.set("basketItems[0].metadate.uat", String(metadata.uatId)); - body.set("basketItems[0].metadate.CF", metadata.nrCF); - body.set("basketItems[0].metadate.CAD", metadata.nrCadastral); - body.set("basketItems[0].metadate.metodeLivrare", "Electronic"); - body.set("basketItems[0].solicitant", metadata.solicitantId); const response = await this.client.post( `${BASE_URL}/EditCartSubmit.action`, diff --git a/src/modules/parcel-sync/services/epay-queue.ts b/src/modules/parcel-sync/services/epay-queue.ts index 148f8ff..c0581f0 100644 --- a/src/modules/parcel-sync/services/epay-queue.ts +++ b/src/modules/parcel-sync/services/epay-queue.ts @@ -175,33 +175,11 @@ async function processItem(item: QueueItem): Promise { const basketRowId = await client.addToCart(input.prodId ?? 14200); await updateStatus(extractId, "cart", { basketRowId }); - // Step 3: Search estate on ePay - await updateStatus(extractId, "searching"); - const results = await client.searchEstate( - input.nrCadastral, - input.judetIndex, - input.uatId, - ); - - if (results.length === 0) { - await updateStatus(extractId, "failed", { - errorMessage: `Imobilul ${input.nrCadastral} nu a fost găsit în baza ANCPI.`, - }); - return; - } - - const estate = results[0]!; - await updateStatus(extractId, "searching", { - immovableId: estate.immovableId, - immovableType: estate.immovableTypeCode, - measuredArea: estate.measureadArea, - legalArea: estate.legalArea, - address: estate.address, - }); - - // Step 4: Submit order + // Step 3: Configure + submit order + // Skip SearchEstate — we already have the data from eTerra. + // configureCartItem (via EditCartItemJson) + submitOrder (via EditCartSubmit) await updateStatus(extractId, "ordering"); - const nrCF = input.nrCF ?? estate.electronicIdentifier ?? input.nrCadastral; + const nrCF = input.nrCF ?? input.nrCadastral; const orderId = await client.submitOrder({ basketRowId, judetIndex: input.judetIndex,