fix(epay): 4 regressions from adversarial review of the hardening diff
Adversarial review (9 agents) of f7f7c59..28c870f found 4 confirmed bugs in the hardening itself; all fixed: 1. Parallel-download index race: two items with the SAME nrCadastral in one batch both scanned MinIO, both computed index 1, the second putObject silently overwrote the first paid extract. Pre-allocate per-cadastral indices sequentially before the parallel block; storeCfExtract takes an explicit index (epay-queue.ts, epay-storage.ts). 2. Metadata-fail orphan charge: on saveMetadata failure the row was popped from cleanup tracking even when deleteCartItem was NOT confirmed, leaving an undeletable metadata-less row in the global cart that submitOrder would check out and charge. Now: pop only on confirmed delete; if unconfirmed, mark cartDirty and ABORT before submit (epay-queue.ts). 3. Recover vs live queue race: the widened recover WHERE (orderId:null + cart/ordering/... states) could scoop a concurrently-processing batch's rows and re-stamp them with the wrong orderId. Block recover while getQueueStatus().processing (recover/route.ts). 4. 'review' status leaked as 'done' in the geoportal CF-order modal (minioPath short-circuit) — handed an unverified PDF as a finished extract. Check review/failed BEFORE the minioPath fallback (cf-order-modal.tsx). Plus 2 nits: download-zip excludes 'review' rows server-side; retry button surfaces recover errors/results instead of swallowing them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -328,17 +328,35 @@ export function EpayTab() {
|
||||
* order, no new charge. For rows that failed at the download/poll
|
||||
* stage (the order exists at ANCPI but we never stored the PDF). -- */
|
||||
const [retryingId, setRetryingId] = useState<string | null>(null);
|
||||
const [retryNotice, setRetryNotice] = useState<string | null>(null);
|
||||
const handleRetryDownload = async (order: CfExtractRecord) => {
|
||||
setRetryingId(order.id);
|
||||
setRetryNotice(null);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/ancpi/recover?extractId=${encodeURIComponent(order.id)}`,
|
||||
);
|
||||
// 409 → the row has no orderId yet (never reached ANCPI); nothing to
|
||||
// recover by row. Other errors surface on the next refresh.
|
||||
await res.json().catch(() => ({}));
|
||||
const data = (await res.json().catch(() => ({}))) as {
|
||||
error?: string;
|
||||
completed?: number;
|
||||
attempted?: number;
|
||||
};
|
||||
if (!res.ok) {
|
||||
// 409 (queue busy / no orderId yet), 404, 500 — tell the user.
|
||||
setRetryNotice(data.error ?? `Reîncercare eșuată (${res.status}).`);
|
||||
} else if ((data.completed ?? 0) > 0) {
|
||||
setRetryNotice(
|
||||
`Recuperat: ${data.completed}/${data.attempted ?? data.completed} extrase.`,
|
||||
);
|
||||
} else {
|
||||
setRetryNotice(
|
||||
"Nimic de recuperat — comanda nu există la ANCPI sau e deja finalizată.",
|
||||
);
|
||||
}
|
||||
void fetchOrders(true);
|
||||
void fetchEpayStatus();
|
||||
} catch {
|
||||
setRetryNotice("Eroare rețea la reîncercare.");
|
||||
} finally {
|
||||
setRetryingId(null);
|
||||
}
|
||||
@@ -911,6 +929,20 @@ export function EpayTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* -- Retry notice ------------------------------------------- */}
|
||||
{retryNotice && (
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-3 py-2 text-xs">
|
||||
<span>{retryNotice}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setRetryNotice(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* -- Active orders indicator -------------------------------- */}
|
||||
{hasActive && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user