From f7f7c59d17cf260b2243abc66e44e09f3375ad1b Mon Sep 17 00:00:00 2001 From: Claude VM Date: Thu, 4 Jun 2026 18:44:17 +0300 Subject: [PATCH] =?UTF-8?q?fix(epay):=20paginate=20ShowOrderDetails=20?= =?UTF-8?q?=E2=80=94=20orders=20>5=20items=20only=20exposed=20first=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ePay paginates order documents 5/page (&navDir=, '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) --- .../parcel-sync/services/epay-client.ts | 101 ++++++++++++------ 1 file changed, 70 insertions(+), 31 deletions(-) diff --git a/src/modules/parcel-sync/services/epay-client.ts b/src/modules/parcel-sync/services/epay-client.ts index 560d9ec..cd010df 100644 --- a/src/modules/parcel-sync/services/epay-client.ts +++ b/src/modules/parcel-sync/services/epay-client.ts @@ -559,23 +559,15 @@ export class EpayClient { /* ── Order Status & Polling ────────────────────────────────── */ - async getOrderStatus(orderId: string): Promise { - 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(); - + /** + * 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++) { - const cf = cfNumbers[i]; - const doc = allSolutii[i]; - if (cf && doc) { - documentsByCadastral.set(cf, doc); + return { cfNumbers, solutii }; + } + + async getOrderStatus(orderId: string): Promise { + 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(); + const seenDocIds = new Set(); + + 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 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") {