fix(ancpi): parse CF numbers and solutii separately, zip by position

The nested JSON in ePay HTML breaks [^}]* regex. New approach:
find all CF.stringValues independently, find all solutii independently,
then zip them by position (they appear in same order in HTML).
This correctly maps CF number → document for batch orders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-23 10:06:20 +02:00
parent a826f45b24
commit 7d30e28fdc
+25 -34
View File
@@ -552,50 +552,28 @@ export class EpayClient {
.replace(/&#x21B;/g, "ț")
.replace(/&#x219;/g, "ș");
// Parse metadateCereri blocks to match CF → solutii (document)
// Each metadateCerere has: metadate.CF.stringValues[0] + solutii[{idDocument,...}]
// Pattern: find each block with both CF and solutii
const metadataPattern =
/"metadate"\s*:\s*\{[^}]*"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\][^]*?"solutii"\s*:\s*\[(\{[^[\]]*\})\]/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.
let metaMatch;
while ((metaMatch = metadataPattern.exec(decoded)) !== null) {
const cfNumber = metaMatch[1] ?? "";
try {
const solutii = JSON.parse(`[${metaMatch[2]}]`);
for (const s of solutii) {
if (s?.idDocument && s?.idTipDocument === null) {
const doc: EpaySolutionDoc = {
idDocument: s.idDocument,
idTipDocument: null,
nume: s.nume ?? "",
numar: s.numar ?? null,
serie: s.serie ?? null,
dataDocument: s.dataDocument ?? "",
contentType: s.contentType ?? "application/pdf",
linkDownload: s.linkDownload ?? "",
downloadValabil: s.downloadValabil ?? true,
valabilNelimitat: s.valabilNelimitat ?? true,
zileValabilitateDownload: s.zileValabilitateDownload ?? -1,
transactionId: s.transactionId ?? 0,
};
documents.push(doc);
if (cfNumber) documentsByCadastral.set(cfNumber, doc);
}
}
} catch { /* parse failed */ }
// 1. Find all CF numbers: "CF":{"name":"CF",...,"stringValues":["345295"]}
const cfPattern = /"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\]/g;
const cfNumbers: string[] = [];
let cfMatch;
while ((cfMatch = cfPattern.exec(decoded)) !== null) {
cfNumbers.push(cfMatch[1] ?? "");
}
// Fallback: simpler solutii pattern (without CF matching)
if (documents.length === 0) {
// 2. Find all solutii blocks (CF extracts only, idTipDocument=null)
const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
const allSolutii: 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) {
documents.push({
allSolutii.push({
idDocument: s.idDocument,
idTipDocument: null,
nume: s.nume ?? "",
@@ -613,7 +591,20 @@ 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);
}
}
console.log(
`[epay] Order ${orderId}: CFs=[${cfNumbers.join(",")}], docs=${allSolutii.length}, matched=${documentsByCadastral.size}`,
);
if (documents.length === 0 && status === "Finalizata") {
console.warn(`[epay] Order ${orderId}: Finalizata but no documents found`);