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:
@@ -59,6 +59,7 @@ export async function GET(req: Request) {
|
||||
minioPath: true,
|
||||
documentDate: true,
|
||||
completedAt: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -71,7 +72,9 @@ export async function GET(req: Request) {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i]!;
|
||||
const extract = extractMap.get(id);
|
||||
if (!extract?.minioPath) continue;
|
||||
// Skip rows without a file, and "review" rows (PDF present but the
|
||||
// CF↔doc match is unverified — must not land in a "valid extracts" zip).
|
||||
if (!extract?.minioPath || extract.status === "review") continue;
|
||||
|
||||
const dateForName = extract.documentDate ?? extract.completedAt ?? new Date();
|
||||
const d = new Date(dateForName);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
import { recoverBatch } from "@/modules/parcel-sync/services/epay-queue";
|
||||
import { recoverBatch, getQueueStatus } from "@/modules/parcel-sync/services/epay-queue";
|
||||
import { requireCfAccess } from "@/core/auth/cf-access";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@@ -85,6 +85,22 @@ export async function GET(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Cross-guard against the live batch queue: while a batch is processing,
|
||||
// its rows sit at (orderId:null, status in cart/ordering/...) — exactly the
|
||||
// orphan window the WHERE below matches. Recovering then would re-stamp a
|
||||
// live batch's rows with THIS order's id (wrong PDF, status corruption).
|
||||
// A genuinely-crashed batch leaves __epayQueueProcessing=false (reset on
|
||||
// restart), so blocking here only defers against an actively-running queue.
|
||||
if (getQueueStatus().processing) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"O comandă ePay este în curs de procesare. Așteaptă finalizarea înainte de recuperare.",
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
// Candidate rows: anything already tagged with this order that isn't
|
||||
// terminal, PLUS recent orphaned rows (orderId:null) in a recoverable
|
||||
// state — the operator asserts they belong to this order.
|
||||
|
||||
Reference in New Issue
Block a user