harden(epay): cart-hygiene invariant uses confirmed cart count + add service architecture plan

- cartCount tracks actual cart rows (decrement only on confirmed delete) so a
  failed cleanup delete can't trigger a false dirty-cart abort.
- docs/plans/006: the multi-tenant CF-service architecture (DB-backed
  fulfiller, account pool, catalog dedup, per-tenant credential model,
  reversible flag flip) — the executable next phase. The Phase-F flag flip is
  gated on the orchestrator fulfiller existing (Plan 003 Faza F was wrong).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-06-05 00:06:06 +03:00
parent f49fdb1da0
commit 28c870fb12
6 changed files with 1703 additions and 11 deletions
+18 -11
View File
@@ -308,7 +308,10 @@ async function processBatch(
// leftover row from a previously-crashed batch would be paid for and
// attach the wrong PDF. We track our own basket ids for cleanup, and
// bail the moment ANCPI reports more rows than we put in.
let addedCount = 0;
// cartCount tracks the rows actually in the cart (incremented on add,
// decremented only on a CONFIRMED delete) so the invariant stays correct
// even if a cleanup delete fails.
let cartCount = 0;
for (let idx = 0; idx < items.length; idx++) {
const item = items[idx]!;
const { extractId, input } = item;
@@ -316,14 +319,17 @@ async function processBatch(
await updateStatus(extractId, "cart");
const { basketRowId, numberOfItems, itemIds } =
await client.addToCartDetailed(input.prodId ?? 14200);
cartCount++;
// After N successful adds a clean cart reports exactly N items. More
// than that = pre-existing junk (orphans from a crash). Never submit a
// cart we didn't fully build: wipe everything ANCPI listed and abort —
// the next retry starts clean. No charge happens (we never submit).
if (numberOfItems > addedCount + 1) {
// Right after the add, a clean cart reports exactly cartCount rows.
// More than that = pre-existing junk (orphans from a crash). Never
// submit a cart we didn't fully build: wipe everything ANCPI listed
// and abort — the next retry starts clean. No charge (we never submit).
// (numberOfItems falls back to items.length, so an unexpected response
// shape degrades to "no excess detected" rather than a false abort.)
if (numberOfItems > cartCount) {
console.error(
`[epay-queue] Dirty cart: expected ${addedCount + 1} rows, ANCPI reports ${numberOfItems}. Wiping + aborting batch.`,
`[epay-queue] Dirty cart: expected ${cartCount} rows, ANCPI reports ${numberOfItems}. Wiping + aborting batch.`,
);
const toWipe = itemIds.length
? itemIds
@@ -338,7 +344,6 @@ async function processBatch(
}
ourBasketIdsForCleanup.push(basketRowId);
addedCount++;
item.basketRowId = basketRowId;
await updateStatus(extractId, "cart", { basketRowId });
@@ -380,10 +385,12 @@ async function processBatch(
errorMessage: "Salvarea metadatelor în ePay a eșuat.",
});
// Remove this metadata-less row from the cart so it can't be
// checked out and charged. Drop it from our tracking + batch.
await client.deleteCartItem(basketRowId, idx);
// checked out and charged. Only decrement cartCount if ANCPI
// confirmed the delete — otherwise the row is still there and the
// invariant must keep counting it.
const deleted = await client.deleteCartItem(basketRowId, idx);
if (deleted) cartCount--;
ourBasketIdsForCleanup.pop();
addedCount--;
item.basketRowId = undefined;
}
}