fix(ancpi): correct PDF-to-parcel matching + UAT search priority
Critical fix: batch order documents are now matched by CF number from parsed metadateCereri (documentsByCadastral), not by index. Prevents PDF content mismatch when ePay returns docs in different order. UAT search: name matches shown first, county-only matches after. Typing "cluj" now shows CLUJ-NAPOCA before county "Cluj" matches. Cleaned MinIO + DB of incorrectly mapped old test data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -606,16 +606,24 @@ export function ParcelSyncModule() {
|
|||||||
}
|
}
|
||||||
const isDigit = /^\d+$/.test(raw);
|
const isDigit = /^\d+$/.test(raw);
|
||||||
const query = normalizeText(raw);
|
const query = normalizeText(raw);
|
||||||
const results = uatData
|
// Filter and sort: UAT name matches first, then county-only matches
|
||||||
.filter((item) => {
|
const nameMatches: typeof uatData = [];
|
||||||
if (isDigit) return item.siruta.startsWith(raw);
|
const countyOnlyMatches: typeof uatData = [];
|
||||||
// Match UAT name or county name
|
|
||||||
if (normalizeText(item.name).includes(query)) return true;
|
for (const item of uatData) {
|
||||||
if (item.county && normalizeText(item.county).includes(query))
|
if (isDigit) {
|
||||||
return true;
|
if (item.siruta.startsWith(raw)) nameMatches.push(item);
|
||||||
return false;
|
} else {
|
||||||
})
|
const nameMatch = normalizeText(item.name).includes(query);
|
||||||
.slice(0, 12);
|
const countyMatch =
|
||||||
|
item.county && normalizeText(item.county).includes(query);
|
||||||
|
if (nameMatch) nameMatches.push(item);
|
||||||
|
else if (countyMatch) countyOnlyMatches.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UAT name matches first (priority), then county-only matches
|
||||||
|
const results = [...nameMatches, ...countyOnlyMatches].slice(0, 12);
|
||||||
setUatResults(results);
|
setUatResults(results);
|
||||||
}, [uatQuery, uatData]);
|
}, [uatQuery, uatData]);
|
||||||
|
|
||||||
|
|||||||
@@ -540,6 +540,8 @@ export class EpayClient {
|
|||||||
else if (html.includes("Plata refuzata")) status = "Plata refuzata";
|
else if (html.includes("Plata refuzata")) status = "Plata refuzata";
|
||||||
|
|
||||||
const documents: EpaySolutionDoc[] = [];
|
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
|
||||||
@@ -550,17 +552,20 @@ export class EpayClient {
|
|||||||
.replace(/ț/g, "ț")
|
.replace(/ț/g, "ț")
|
||||||
.replace(/ș/g, "ș");
|
.replace(/ș/g, "ș");
|
||||||
|
|
||||||
// Parse solutii JSON from decoded HTML
|
// Parse metadateCereri blocks to match CF → solutii (document)
|
||||||
// Format: "solutii":[{"idDocument":47301767,"nome":"Extras_Informare_65346.pdf",...}]
|
// Each metadateCerere has: metadate.CF.stringValues[0] + solutii[{idDocument,...}]
|
||||||
const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
|
// Pattern: find each block with both CF and solutii
|
||||||
let solutiiMatch;
|
const metadataPattern =
|
||||||
while ((solutiiMatch = solutiiPattern.exec(decoded)) !== null) {
|
/"metadate"\s*:\s*\{[^}]*"CF"\s*:\s*\{[^}]*"stringValues"\s*:\s*\["(\d+)"\][^]*?"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
|
||||||
|
|
||||||
|
let metaMatch;
|
||||||
|
while ((metaMatch = metadataPattern.exec(decoded)) !== null) {
|
||||||
|
const cfNumber = metaMatch[1] ?? "";
|
||||||
try {
|
try {
|
||||||
const arr = JSON.parse(`[${solutiiMatch[1]}]`);
|
const solutii = JSON.parse(`[${metaMatch[2]}]`);
|
||||||
for (const s of arr) {
|
for (const s of solutii) {
|
||||||
if (s?.idDocument && s?.idTipDocument === null) {
|
if (s?.idDocument && s?.idTipDocument === null) {
|
||||||
// idTipDocument=null means it's a soluție (CF extract), not a chitanță (idTipDocument=1)
|
const doc: EpaySolutionDoc = {
|
||||||
documents.push({
|
|
||||||
idDocument: s.idDocument,
|
idDocument: s.idDocument,
|
||||||
idTipDocument: null,
|
idTipDocument: null,
|
||||||
nume: s.nume ?? "",
|
nume: s.nume ?? "",
|
||||||
@@ -573,31 +578,40 @@ export class EpayClient {
|
|||||||
valabilNelimitat: s.valabilNelimitat ?? true,
|
valabilNelimitat: s.valabilNelimitat ?? true,
|
||||||
zileValabilitateDownload: s.zileValabilitateDownload ?? -1,
|
zileValabilitateDownload: s.zileValabilitateDownload ?? -1,
|
||||||
transactionId: s.transactionId ?? 0,
|
transactionId: s.transactionId ?? 0,
|
||||||
});
|
};
|
||||||
|
documents.push(doc);
|
||||||
|
if (cfNumber) documentsByCadastral.set(cfNumber, doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { /* parse failed */ }
|
} catch { /* parse failed */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: extract idDocument directly from decoded HTML
|
// Fallback: simpler solutii pattern (without CF matching)
|
||||||
if (documents.length === 0) {
|
if (documents.length === 0) {
|
||||||
const idPattern = /"idDocument"\s*:\s*(\d+)\s*,\s*"idTipDocument"\s*:\s*null\s*,\s*"nume"\s*:\s*"([^"]+)"\s*,.*?"dataDocument"\s*:\s*"([^"]+)"/g;
|
const solutiiPattern = /"solutii"\s*:\s*\[(\{[^[\]]*\})\]/g;
|
||||||
let idMatch;
|
let solutiiMatch;
|
||||||
while ((idMatch = idPattern.exec(decoded)) !== null) {
|
while ((solutiiMatch = solutiiPattern.exec(decoded)) !== null) {
|
||||||
documents.push({
|
try {
|
||||||
idDocument: parseInt(idMatch[1] ?? "0", 10),
|
const arr = JSON.parse(`[${solutiiMatch[1]}]`);
|
||||||
idTipDocument: null,
|
for (const s of arr) {
|
||||||
nume: idMatch[2] ?? "",
|
if (s?.idDocument && s?.idTipDocument === null) {
|
||||||
numar: null,
|
documents.push({
|
||||||
serie: null,
|
idDocument: s.idDocument,
|
||||||
dataDocument: idMatch[3] ?? "",
|
idTipDocument: null,
|
||||||
contentType: "application/pdf",
|
nume: s.nume ?? "",
|
||||||
linkDownload: "",
|
numar: s.numar ?? null,
|
||||||
downloadValabil: true,
|
serie: s.serie ?? null,
|
||||||
valabilNelimitat: true,
|
dataDocument: s.dataDocument ?? "",
|
||||||
zileValabilitateDownload: -1,
|
contentType: s.contentType ?? "application/pdf",
|
||||||
transactionId: 0,
|
linkDownload: s.linkDownload ?? "",
|
||||||
});
|
downloadValabil: s.downloadValabil ?? true,
|
||||||
|
valabilNelimitat: s.valabilNelimitat ?? true,
|
||||||
|
zileValabilitateDownload: s.zileValabilitateDownload ?? -1,
|
||||||
|
transactionId: s.transactionId ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* parse failed */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +619,11 @@ export class EpayClient {
|
|||||||
console.warn(`[epay] Order ${orderId}: Finalizata but no documents found`);
|
console.warn(`[epay] Order ${orderId}: Finalizata but no documents found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { orderId, status, documents };
|
console.log(
|
||||||
|
`[epay] Order ${orderId}: ${documents.length} docs, ${documentsByCadastral.size} matched by CF`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { orderId, status, documents, documentsByCadastral };
|
||||||
}
|
}
|
||||||
|
|
||||||
async pollUntilComplete(
|
async pollUntilComplete(
|
||||||
|
|||||||
@@ -355,10 +355,8 @@ async function processBatch(
|
|||||||
`[epay-queue] Order ${orderId}: ${downloadableDocs.length} documents for ${validItems.length} items`,
|
`[epay-queue] Order ${orderId}: ${downloadableDocs.length} documents for ${validItems.length} items`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try to match documents to items by cadastral number in filename
|
// Match documents to items by CF number (from documentsByCadastral)
|
||||||
// ePay filenames look like "Extras_Informare_65297.pdf" (the number is an internal ID, not nrCadastral)
|
// This is the CORRECT way — ePay returns docs in its own order, not ours
|
||||||
// If we have exactly as many docs as items, assign in order.
|
|
||||||
// Otherwise, download all and try best-effort matching.
|
|
||||||
|
|
||||||
if (downloadableDocs.length === 0) {
|
if (downloadableDocs.length === 0) {
|
||||||
for (const item of validItems) {
|
for (const item of validItems) {
|
||||||
@@ -370,12 +368,23 @@ async function processBatch(
|
|||||||
return orderId;
|
return orderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple matching strategy:
|
|
||||||
// If docs.length === items.length, assign 1:1 in order
|
|
||||||
// Otherwise, assign first doc to first item, etc., and leftover items get "failed"
|
|
||||||
for (let i = 0; i < validItems.length; i++) {
|
for (let i = 0; i < validItems.length; i++) {
|
||||||
const item = validItems[i]!;
|
const item = validItems[i]!;
|
||||||
const doc = downloadableDocs[i];
|
const nrCF = item.input.nrCF ?? item.input.nrCadastral;
|
||||||
|
|
||||||
|
// Try CF-based matching first (correct for batch orders)
|
||||||
|
let doc = finalStatus.documentsByCadastral.get(nrCF);
|
||||||
|
// Also try nrCadastral if different from nrCF
|
||||||
|
if (!doc && item.input.nrCadastral !== nrCF) {
|
||||||
|
doc = finalStatus.documentsByCadastral.get(item.input.nrCadastral);
|
||||||
|
}
|
||||||
|
// Last resort: fall back to index matching
|
||||||
|
if (!doc) {
|
||||||
|
doc = downloadableDocs[i];
|
||||||
|
console.warn(
|
||||||
|
`[epay-queue] Could not match by CF for ${item.input.nrCadastral}, using index ${i}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
await updateStatus(item.extractId, "failed", {
|
await updateStatus(item.extractId, "failed", {
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export type EpayOrderStatus = {
|
|||||||
orderId: string;
|
orderId: string;
|
||||||
status: string; // "Receptionata" | "In curs de procesare" | "Finalizata" | "Anulata"
|
status: string; // "Receptionata" | "In curs de procesare" | "Finalizata" | "Anulata"
|
||||||
documents: EpaySolutionDoc[];
|
documents: EpaySolutionDoc[];
|
||||||
|
/** CF number → document mapping (for correct batch assignment) */
|
||||||
|
documentsByCadastral: Map<string, EpaySolutionDoc>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Domain Types ────────────────────────────────────────────────── */
|
/* ── Domain Types ────────────────────────────────────────────────── */
|
||||||
|
|||||||
Reference in New Issue
Block a user