From d77892c28b8fe154e3859b6f4524d517fa89fc2a Mon Sep 17 00:00:00 2001 From: OneTwo3D IMS Date: Mon, 22 Jun 2026 21:50:17 +0000 Subject: [PATCH] fix(refund): block new refund while a prior refund's accounting is unresolved (scjz.22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A refund whose accounting staging failed (accountingRetryRequired) may not have written its cost-layer snapshot, so its quantity counts toward the refund qty cap while NOT reducing shipment cost availability — a second refund can then be under qty-budget yet over-draw the cost basis and throw spuriously (qty cap and COGS-basis reduction read divergent state). Block a genuinely-new refund while any prior refund on the order has accountingRetryRequired; the operator/sweep must resolve the prior refund's accounting first, restoring a consistent source of truth. Idempotent replays returned earlier. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/domain/sales/refund-service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/domain/sales/refund-service.ts b/lib/domain/sales/refund-service.ts index 67570074..7a6035af 100644 --- a/lib/domain/sales/refund-service.ts +++ b/lib/domain/sales/refund-service.ts @@ -1601,8 +1601,21 @@ export async function createSalesOrderRefund( const existingRefunds = await tx.salesOrderRefund.findMany({ where: { orderId: input.orderId }, - select: { totalBase: true }, + select: { totalBase: true, accountingRetryRequired: true }, }) + // scjz.22: block a NEW refund while a prior refund on this order still has + // unresolved accounting (accountingRetryRequired). A refund whose accounting + // staging failed may not have written its cost-layer snapshot, so its quantity + // counts toward the refund qty cap while NOT reducing shipment cost availability — + // a second refund can then be under qty-budget yet over-draw the cost basis and + // throw spuriously (the refund qty cap and the COGS-basis reduction read divergent + // state). Requiring the prior refund's accounting to be retried first (manually via + // retryRefundAccounting, or automatically by the accounting-sync sweep) keeps the + // two sources consistent. Idempotent replays of an existing refund returned earlier, + // so this only blocks genuinely-new refunds. + if (existingRefunds.some((refund) => refund.accountingRetryRequired)) { + return { error: 'A previous refund on this order has unresolved accounting and must be retried before another refund can be created.' } as const + } const previouslyRefunded = existingRefunds.reduce((sum, refund) => sum + refundBoundaryNumber(refund.totalBase), 0) // audit-M-o2c: cumulative refunded must not exceed the order total, with a // fixed rounding epsilon (not a 0.1% relative slack, which on a large order