Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lib/domain/sales/refund-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
takeFromSnapshotEntries,
type CostLayerSnapshotEntry,
} from '@/lib/cost-layer-snapshots'
import { addMoney, roundQuantity, subtractMoney, toDecimal, type DecimalInput } from '@/lib/domain/math/decimal'

Check warning on line 14 in lib/domain/sales/refund-service.ts

View workflow job for this annotation

GitHub Actions / validate

'addMoney' is defined but never used
import { getSalesOrderReference } from '@/lib/sales-order-display'
import { validateRefundSalesOrderStatusUpdate } from '@/lib/domain/workflows/action-guards'
import { isFullRefundAmount } from '@/lib/domain/sales/refund-thresholds'
Expand Down Expand Up @@ -1601,8 +1601,21 @@

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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block refunds while prior accounting is still staging

This guard only checks accountingRetryRequired, which remains false for a just-created refund until the later post-transaction stageRefundAccountingReversals call either succeeds or catches and marks the row for retry. Because the advisory/order locks are released before that staging work runs, a second refund started in that window sees the prior refund but passes this check; if the first staging then fails before writing cost-layer snapshots, the original qty-vs-cost-basis divergence can still occur. Consider marking the refund as accounting-pending before releasing the create transaction, or otherwise blocking on in-progress/missing prior refund cost snapshots as well as retry-required rows.

Useful? React with 👍 / 👎.

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
Expand Down
Loading