fix(epay): paginate ShowOrderDetails — orders >5 items only exposed first page
ePay paginates order documents 5/page (&navDir=<page>, 'Total items: N'). getOrderStatus only parsed page 1, so a 15-item order surfaced 5 docs: 5 parcels CF-matched correctly, 5 got WRONG PDFs via the index fallback, 5 failed with 'Document lipsă' (2026-06-04, order 10009605). - parseOrderPage(): per-page CF+solutii extraction, zipped per page - getOrderStatus(): walks all pages, dedupes by idDocument, stops on empty page Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -559,23 +559,15 @@ export class EpayClient {
|
||||
|
||||
/* ── Order Status & Polling ────────────────────────────────── */
|
||||
|
||||
async getOrderStatus(orderId: string): Promise<EpayOrderStatus> {
|
||||
const response = await this.client.get(
|
||||
`${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}`,
|
||||
{ timeout: DEFAULT_TIMEOUT_MS },
|
||||
);
|
||||
const html = String(response.data ?? "");
|
||||
|
||||
let status = "Receptionata";
|
||||
if (html.includes("Finalizata")) status = "Finalizata";
|
||||
else if (html.includes("In curs de procesare")) status = "In curs de procesare";
|
||||
else if (html.includes("Anulata")) status = "Anulata";
|
||||
else if (html.includes("Plata refuzata")) status = "Plata refuzata";
|
||||
|
||||
const documents: EpaySolutionDoc[] = [];
|
||||
// Map CF number → document (for correct batch matching)
|
||||
const documentsByCadastral = new Map<string, EpaySolutionDoc>();
|
||||
|
||||
/**
|
||||
* Parse one ShowOrderDetails page: extract CF numbers + solutii docs and
|
||||
* zip them by position (each metadateCereri block carries both, in the
|
||||
* same order WITHIN a page — zipping must therefore happen per page).
|
||||
*/
|
||||
private parseOrderPage(html: string): {
|
||||
cfNumbers: string[];
|
||||
solutii: EpaySolutionDoc[];
|
||||
} {
|
||||
// Decode HTML entities — ePay embeds JSON with " encoding
|
||||
const decoded = html
|
||||
.replace(/"/g, '"')
|
||||
@@ -585,10 +577,6 @@ export class EpayClient {
|
||||
.replace(/ț/g, "ț")
|
||||
.replace(/ș/g, "ș");
|
||||
|
||||
// Strategy: find CF numbers and solutii separately, then match by position.
|
||||
// In the HTML, each metadateCereri block has both CF.stringValues and solutii
|
||||
// in the same order. We find them independently and zip them.
|
||||
|
||||
// 1. Find all CF numbers: "CF":{"name":"CF",...,"stringValues":["345295"]}
|
||||
const cfPattern = /"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\]/g;
|
||||
const cfNumbers: string[] = [];
|
||||
@@ -599,14 +587,14 @@ export class EpayClient {
|
||||
|
||||
// 2. Find all solutii blocks (CF extracts only, idTipDocument=null)
|
||||
const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
|
||||
const allSolutii: EpaySolutionDoc[] = [];
|
||||
const solutii: EpaySolutionDoc[] = [];
|
||||
let solutiiMatch;
|
||||
while ((solutiiMatch = solutiiPattern.exec(decoded)) !== null) {
|
||||
try {
|
||||
const arr = JSON.parse(`[${solutiiMatch[1]}]`);
|
||||
for (const s of arr) {
|
||||
if (s?.idDocument && s?.idTipDocument === null) {
|
||||
allSolutii.push({
|
||||
solutii.push({
|
||||
idDocument: s.idDocument,
|
||||
idTipDocument: null,
|
||||
nume: s.nume ?? "",
|
||||
@@ -625,18 +613,69 @@ export class EpayClient {
|
||||
} catch { /* parse failed */ }
|
||||
}
|
||||
|
||||
// 3. Zip CF numbers with solutii — they appear in the same order
|
||||
documents.push(...allSolutii);
|
||||
for (let i = 0; i < Math.min(cfNumbers.length, allSolutii.length); i++) {
|
||||
return { cfNumbers, solutii };
|
||||
}
|
||||
|
||||
async getOrderStatus(orderId: string): Promise<EpayOrderStatus> {
|
||||
const response = await this.client.get(
|
||||
`${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}`,
|
||||
{ timeout: DEFAULT_TIMEOUT_MS },
|
||||
);
|
||||
const html = String(response.data ?? "");
|
||||
|
||||
let status = "Receptionata";
|
||||
if (html.includes("Finalizata")) status = "Finalizata";
|
||||
else if (html.includes("In curs de procesare")) status = "In curs de procesare";
|
||||
else if (html.includes("Anulata")) status = "Anulata";
|
||||
else if (html.includes("Plata refuzata")) status = "Plata refuzata";
|
||||
|
||||
const documents: EpaySolutionDoc[] = [];
|
||||
// Map CF number → document (for correct batch matching)
|
||||
const documentsByCadastral = new Map<string, EpaySolutionDoc>();
|
||||
const seenDocIds = new Set<number>();
|
||||
|
||||
const collect = (pageHtml: string): number => {
|
||||
const { cfNumbers, solutii } = this.parseOrderPage(pageHtml);
|
||||
let added = 0;
|
||||
for (const doc of solutii) {
|
||||
if (seenDocIds.has(doc.idDocument)) continue;
|
||||
seenDocIds.add(doc.idDocument);
|
||||
documents.push(doc);
|
||||
added++;
|
||||
}
|
||||
for (let i = 0; i < Math.min(cfNumbers.length, solutii.length); i++) {
|
||||
const cf = cfNumbers[i];
|
||||
const doc = allSolutii[i];
|
||||
if (cf && doc) {
|
||||
const doc = solutii[i];
|
||||
if (cf && doc && !documentsByCadastral.has(cf)) {
|
||||
documentsByCadastral.set(cf, doc);
|
||||
}
|
||||
}
|
||||
return added;
|
||||
};
|
||||
|
||||
collect(html);
|
||||
|
||||
// ShowOrderDetails paginates the requests (5/page, "Total items: N",
|
||||
// page selected via &navDir=<page>; page 1 == no param). Without this,
|
||||
// a 15-item batch only ever saw its first 5 documents (2026-06-04,
|
||||
// order 10009605).
|
||||
const totalMatch = html.match(/Total items:\s*(?:<[^>]*>)?\s*(\d+)/i);
|
||||
const totalItems = totalMatch ? parseInt(totalMatch[1] ?? "0", 10) : 0;
|
||||
const perPage = Math.max(documents.length, 1);
|
||||
if (totalItems > documents.length) {
|
||||
const pages = Math.min(Math.ceil(totalItems / perPage), 20);
|
||||
for (let page = 2; page <= pages; page++) {
|
||||
const pageResponse = await this.client.get(
|
||||
`${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}&navDir=${page}`,
|
||||
{ timeout: DEFAULT_TIMEOUT_MS },
|
||||
);
|
||||
const added = collect(String(pageResponse.data ?? ""));
|
||||
if (added === 0) break; // defensive: stop if a page yields nothing new
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[epay] Order ${orderId}: CFs=[${cfNumbers.join(",")}], docs=${allSolutii.length}, matched=${documentsByCadastral.size}`,
|
||||
`[epay] Order ${orderId}: total=${totalItems || "?"}, docs=${documents.length}, matched=${documentsByCadastral.size}`,
|
||||
);
|
||||
|
||||
if (documents.length === 0 && status === "Finalizata") {
|
||||
|
||||
Reference in New Issue
Block a user