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:
@@ -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<EpaySearchResult[]> {
|
||||
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<EpayUatEntry[]> {
|
||||
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<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 ──────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Submit order after configuring cart item via EditCartItemJson.
|
||||
*/
|
||||
async submitOrder(metadata: OrderMetadata): Promise<string> {
|
||||
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`,
|
||||
|
||||
@@ -175,33 +175,11 @@ async function processItem(item: QueueItem): Promise<void> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user