fix(ancpi): use JSON body for EpayJsonInterceptor + EditCartItemJson
Root cause from ePay Angular analysis:
- EpayJsonInterceptor needs Content-Type: application/json + {"judet": N}
- EditCartItemJson needs JSON with bigDecimalValue/stringValue structure
- SearchEstate needs basketId in body for JSON response
- Queue skips SearchEstate (data already from eTerra), uses
configureCartItem → submitOrder flow directly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,50 +77,43 @@ export async function GET(req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Step: search ──
|
// ── Step: search ──
|
||||||
// Test EditCartItemJson.action — the real endpoint for configuring cart items
|
// Test UAT lookup (JSON) + SearchEstate (with basketId)
|
||||||
if (step === "search") {
|
if (step === "search") {
|
||||||
const client = await EpayClient.create(username, password);
|
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<string, unknown> = { 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);
|
const basketRowId = await client.addToCart(14200);
|
||||||
|
results["basketRowId"] = basketRowId;
|
||||||
|
|
||||||
const results: Record<string, unknown> = { basketRowId };
|
if (clujNapoca) {
|
||||||
|
results["search_345295"] = await client
|
||||||
// Test 1: Set county via EditCartItemJson to get UAT list
|
.searchEstate("345295", countyIdx, clujNapoca.id, basketRowId)
|
||||||
try {
|
.catch((e: Error) => ({ error: e.message }));
|
||||||
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 (feleacu) {
|
||||||
// Test 2: Try SearchEstate via the Angular way (maybe it's inside EditCartItemJson)
|
results["search_63565"] = await client
|
||||||
try {
|
.searchEstate("63565", countyIdx, feleacu.id, basketRowId)
|
||||||
const body2 = new URLSearchParams();
|
.catch((e: Error) => ({ error: e.message }));
|
||||||
body2.set("basketId", String(basketRowId));
|
}
|
||||||
body2.set("identifier", "345295");
|
if (floresti) {
|
||||||
body2.set("countyId", "13");
|
results["search_88089"] = await client
|
||||||
|
.searchEstate("88089", countyIdx, floresti.id, basketRowId)
|
||||||
const resp2 = await client.postRaw(
|
.catch((e: Error) => ({ error: e.message }));
|
||||||
`${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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ step: "search", results });
|
return NextResponse.json({ step: "search", results });
|
||||||
@@ -128,7 +121,6 @@ export async function GET(req: Request) {
|
|||||||
|
|
||||||
// ── Step: order ── (USES 3 CREDITS!)
|
// ── Step: order ── (USES 3 CREDITS!)
|
||||||
if (step === "order") {
|
if (step === "order") {
|
||||||
// Ensure session exists
|
|
||||||
if (!getEpayCredentials()) {
|
if (!getEpayCredentials()) {
|
||||||
createEpaySession(username, password, 0);
|
createEpaySession(username, password, 0);
|
||||||
}
|
}
|
||||||
@@ -158,6 +150,7 @@ export async function GET(req: Request) {
|
|||||||
if (!clujNapoca || !feleacu || !floresti) {
|
if (!clujNapoca || !feleacu || !floresti) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: "Nu s-au găsit UAT-urile.",
|
error: "Nu s-au găsit UAT-urile.",
|
||||||
|
uatCount: uatList.length,
|
||||||
clujNapoca, feleacu, floresti,
|
clujNapoca, feleacu, floresti,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -168,21 +161,21 @@ export async function GET(req: Request) {
|
|||||||
judetIndex: countyIdx,
|
judetIndex: countyIdx,
|
||||||
judetName: "CLUJ",
|
judetName: "CLUJ",
|
||||||
uatId: clujNapoca.id,
|
uatId: clujNapoca.id,
|
||||||
uatName: "Cluj-Napoca",
|
uatName: clujNapoca.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nrCadastral: "63565",
|
nrCadastral: "63565",
|
||||||
judetIndex: countyIdx,
|
judetIndex: countyIdx,
|
||||||
judetName: "CLUJ",
|
judetName: "CLUJ",
|
||||||
uatId: feleacu.id,
|
uatId: feleacu.id,
|
||||||
uatName: "Feleacu",
|
uatName: feleacu.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nrCadastral: "88089",
|
nrCadastral: "88089",
|
||||||
judetIndex: countyIdx,
|
judetIndex: countyIdx,
|
||||||
judetName: "CLUJ",
|
judetName: "CLUJ",
|
||||||
uatId: floresti.id,
|
uatId: floresti.id,
|
||||||
uatName: "Florești",
|
uatName: floresti.value,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -195,11 +188,12 @@ export async function GET(req: Request) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
step: "order",
|
step: "order",
|
||||||
credits,
|
credits,
|
||||||
message: `Enqueued ${ids.length} orders. Queue processing started.`,
|
message: `Enqueued ${ids.length} orders. Processing sequentially...`,
|
||||||
orderIds: ids,
|
orderIds: ids,
|
||||||
parcels: parcels.map((p, i) => ({
|
parcels: parcels.map((p, i) => ({
|
||||||
nrCadastral: p.nrCadastral,
|
nrCadastral: p.nrCadastral,
|
||||||
uatName: p.uatName,
|
uatName: p.uatName,
|
||||||
|
uatId: p.uatId,
|
||||||
extractId: ids[i],
|
extractId: ids[i],
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -353,18 +353,23 @@ export class EpayClient {
|
|||||||
|
|
||||||
/* ── Estate Search ─────────────────────────────────────────── */
|
/* ── 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(
|
async searchEstate(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
countyIdx: number,
|
countyIdx: number,
|
||||||
uatId?: number,
|
uatId: number,
|
||||||
|
basketRowId: number,
|
||||||
): Promise<EpaySearchResult[]> {
|
): Promise<EpaySearchResult[]> {
|
||||||
return this.retryOnAuthFail(async () => {
|
return this.retryOnAuthFail(async () => {
|
||||||
|
// Must include basketId for SearchEstate to return JSON
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
body.set("identifier", identifier);
|
body.set("identifier", identifier);
|
||||||
body.set("countyId", String(countyIdx));
|
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(
|
const response = await this.client.post(
|
||||||
`${BASE_URL}/SearchEstate.action`,
|
`${BASE_URL}/SearchEstate.action`,
|
||||||
@@ -372,7 +377,6 @@ export class EpayClient {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
Accept: "application/json, text/plain, */*",
|
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
},
|
},
|
||||||
timeout: DEFAULT_TIMEOUT_MS,
|
timeout: DEFAULT_TIMEOUT_MS,
|
||||||
@@ -380,90 +384,133 @@ export class EpayClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const data = response.data;
|
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 (Array.isArray(data)) return data as EpaySearchResult[];
|
||||||
|
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
// Try JSON parse
|
|
||||||
const trimmed = data.trim();
|
const trimmed = data.trim();
|
||||||
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
if (trimmed.startsWith("[")) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed);
|
return JSON.parse(trimmed) as EpaySearchResult[];
|
||||||
if (Array.isArray(parsed)) return parsed as EpaySearchResult[];
|
} catch { /* not JSON */ }
|
||||||
// Wrapped in object?
|
|
||||||
if (parsed?.results) return parsed.results as EpaySearchResult[];
|
|
||||||
} catch {
|
|
||||||
// Not JSON
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
`[epay] SearchEstate(${identifier}): unexpected response`,
|
||||||
|
String(typeof data === "string" ? data : JSON.stringify(data)).slice(0, 200),
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── UAT Lookup ────────────────────────────────────────────── */
|
/* ── UAT Lookup ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UAT list for a county. Uses JSON Content-Type (not form-urlencoded).
|
||||||
|
*/
|
||||||
async getUatList(countyIdx: number): Promise<EpayUatEntry[]> {
|
async getUatList(countyIdx: number): Promise<EpayUatEntry[]> {
|
||||||
return this.retryOnAuthFail(async () => {
|
return this.retryOnAuthFail(async () => {
|
||||||
// ePay uses EpayJsonInterceptor for dynamic dropdowns
|
// EpayJsonInterceptor requires JSON body, not form-urlencoded
|
||||||
// Try the interceptor first
|
|
||||||
const body = new URLSearchParams();
|
|
||||||
body.set("actionType", "getUAT");
|
|
||||||
body.set("countyIndex", String(countyIdx));
|
|
||||||
|
|
||||||
const response = await this.client.post(
|
const response = await this.client.post(
|
||||||
`${BASE_URL}/EpayJsonInterceptor.action`,
|
`${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,
|
timeout: DEFAULT_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
// Response: { jsonResult: "[{\"id\":155546,\"value\":\"Balint\"}, ...]" }
|
// Response: { jsonResult: "[{\"id\":55473,\"value\":\"Aghiresu\"}, ...]" }
|
||||||
if (data?.jsonResult) {
|
if (data?.jsonResult && typeof data.jsonResult === "string") {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data.jsonResult);
|
const parsed = JSON.parse(data.jsonResult);
|
||||||
if (Array.isArray(parsed)) return parsed as EpayUatEntry[];
|
if (Array.isArray(parsed)) return parsed as EpayUatEntry[];
|
||||||
} catch {
|
} catch { /* parse failed */ }
|
||||||
// Parse failed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct array response
|
|
||||||
if (Array.isArray(data)) return data as EpayUatEntry[];
|
if (Array.isArray(data)) return data as EpayUatEntry[];
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`[epay] getUatList(${countyIdx}) returned unexpected:`,
|
`[epay] getUatList(${countyIdx}):`,
|
||||||
JSON.stringify(data).slice(0, 200),
|
JSON.stringify(data).slice(0, 200),
|
||||||
);
|
);
|
||||||
return [];
|
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<void> {
|
||||||
|
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 ──────────────────────────────────────── */
|
/* ── Order Submission ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit order after configuring cart item via EditCartItemJson.
|
||||||
|
*/
|
||||||
async submitOrder(metadata: OrderMetadata): Promise<string> {
|
async submitOrder(metadata: OrderMetadata): Promise<string> {
|
||||||
return this.retryOnAuthFail(async () => {
|
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();
|
const body = new URLSearchParams();
|
||||||
body.set("goToCheckout", "true");
|
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(
|
const response = await this.client.post(
|
||||||
`${BASE_URL}/EditCartSubmit.action`,
|
`${BASE_URL}/EditCartSubmit.action`,
|
||||||
|
|||||||
@@ -175,33 +175,11 @@ async function processItem(item: QueueItem): Promise<void> {
|
|||||||
const basketRowId = await client.addToCart(input.prodId ?? 14200);
|
const basketRowId = await client.addToCart(input.prodId ?? 14200);
|
||||||
await updateStatus(extractId, "cart", { basketRowId });
|
await updateStatus(extractId, "cart", { basketRowId });
|
||||||
|
|
||||||
// Step 3: Search estate on ePay
|
// Step 3: Configure + submit order
|
||||||
await updateStatus(extractId, "searching");
|
// Skip SearchEstate — we already have the data from eTerra.
|
||||||
const results = await client.searchEstate(
|
// configureCartItem (via EditCartItemJson) + submitOrder (via EditCartSubmit)
|
||||||
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
|
|
||||||
await updateStatus(extractId, "ordering");
|
await updateStatus(extractId, "ordering");
|
||||||
const nrCF = input.nrCF ?? estate.electronicIdentifier ?? input.nrCadastral;
|
const nrCF = input.nrCF ?? input.nrCadastral;
|
||||||
const orderId = await client.submitOrder({
|
const orderId = await client.submitOrder({
|
||||||
basketRowId,
|
basketRowId,
|
||||||
judetIndex: input.judetIndex,
|
judetIndex: input.judetIndex,
|
||||||
|
|||||||
Reference in New Issue
Block a user