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 ────────────────────────────────── */
|
/* ── Order Status & Polling ────────────────────────────────── */
|
||||||
|
|
||||||
async getOrderStatus(orderId: string): Promise<EpayOrderStatus> {
|
/**
|
||||||
const response = await this.client.get(
|
* Parse one ShowOrderDetails page: extract CF numbers + solutii docs and
|
||||||
`${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}`,
|
* zip them by position (each metadateCereri block carries both, in the
|
||||||
{ timeout: DEFAULT_TIMEOUT_MS },
|
* same order WITHIN a page — zipping must therefore happen per page).
|
||||||
);
|
*/
|
||||||
const html = String(response.data ?? "");
|
private parseOrderPage(html: string): {
|
||||||
|
cfNumbers: string[];
|
||||||
let status = "Receptionata";
|
solutii: EpaySolutionDoc[];
|
||||||
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>();
|
|
||||||
|
|
||||||
// Decode HTML entities — ePay embeds JSON with " encoding
|
// Decode HTML entities — ePay embeds JSON with " encoding
|
||||||
const decoded = html
|
const decoded = html
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
@@ -585,10 +577,6 @@ export class EpayClient {
|
|||||||
.replace(/ț/g, "ț")
|
.replace(/ț/g, "ț")
|
||||||
.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"]}
|
// 1. Find all CF numbers: "CF":{"name":"CF",...,"stringValues":["345295"]}
|
||||||
const cfPattern = /"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\]/g;
|
const cfPattern = /"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\]/g;
|
||||||
const cfNumbers: string[] = [];
|
const cfNumbers: string[] = [];
|
||||||
@@ -599,14 +587,14 @@ export class EpayClient {
|
|||||||
|
|
||||||
// 2. Find all solutii blocks (CF extracts only, idTipDocument=null)
|
// 2. Find all solutii blocks (CF extracts only, idTipDocument=null)
|
||||||
const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
|
const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
|
||||||
const allSolutii: EpaySolutionDoc[] = [];
|
const solutii: EpaySolutionDoc[] = [];
|
||||||
let solutiiMatch;
|
let solutiiMatch;
|
||||||
while ((solutiiMatch = solutiiPattern.exec(decoded)) !== null) {
|
while ((solutiiMatch = solutiiPattern.exec(decoded)) !== null) {
|
||||||
try {
|
try {
|
||||||
const arr = JSON.parse(`[${solutiiMatch[1]}]`);
|
const arr = JSON.parse(`[${solutiiMatch[1]}]`);
|
||||||
for (const s of arr) {
|
for (const s of arr) {
|
||||||
if (s?.idDocument && s?.idTipDocument === null) {
|
if (s?.idDocument && s?.idTipDocument === null) {
|
||||||
allSolutii.push({
|
solutii.push({
|
||||||
idDocument: s.idDocument,
|
idDocument: s.idDocument,
|
||||||
idTipDocument: null,
|
idTipDocument: null,
|
||||||
nume: s.nume ?? "",
|
nume: s.nume ?? "",
|
||||||
@@ -625,18 +613,69 @@ export class EpayClient {
|
|||||||
} catch { /* parse failed */ }
|
} catch { /* parse failed */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Zip CF numbers with solutii — they appear in the same order
|
return { cfNumbers, solutii };
|
||||||
documents.push(...allSolutii);
|
}
|
||||||
for (let i = 0; i < Math.min(cfNumbers.length, allSolutii.length); i++) {
|
|
||||||
const cf = cfNumbers[i];
|
async getOrderStatus(orderId: string): Promise<EpayOrderStatus> {
|
||||||
const doc = allSolutii[i];
|
const response = await this.client.get(
|
||||||
if (cf && doc) {
|
`${BASE_URL}/ShowOrderDetails.action?orderId=${orderId}`,
|
||||||
documentsByCadastral.set(cf, doc);
|
{ 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 = 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(
|
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") {
|
if (documents.length === 0 && status === "Finalizata") {
|
||||||
|
|||||||
Reference in New Issue
Block a user