From 369b3d0fef99a9d3cae3ccdbc2577a9b8d8ce04a Mon Sep 17 00:00:00 2001 From: OneTwo3D IMS Date: Sat, 13 Jun 2026 17:00:57 +0000 Subject: [PATCH 1/2] fix(sales): order-to-cash medium gaps (closes audit-M-o2c, audit-M-sales) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cluster of order-to-cash medium findings (47fa + chc2, duplicate clusters): 1. Refund tolerance: the cumulative-refunded check used a 0.1% RELATIVE slack (so a large order had pounds of headroom, and partial refunds could creep over). New pure refundWouldExceedOrderTotal() applies a fixed rounding epsilon (0.011) to the cumulative total instead. 2. Deleting the last payment cleared paidAt but never reverted status. deletePayment now detects when the order became not-fully-paid while in an advanced status (SHIPPED/COMPLETED/DELIVERED/PARTIALLY_REFUNDED) and emits a payment_status_mismatch WARNING; the SO detail shows an amber ' but not paid' chip. 3. applySalesOrderStatusTransition now rejects status edits on ARCHIVED orders (manual + sessionless-cron callers); the WooCommerce force-sync bypass token keeps its escape-hatch semantics. 4. queueSalesInvoiceForOrder idempotency key — VERIFIED already payload-based (accountingPayloadKey(..., payload)); no change needed. 5. confirmSalesOrderShipments double-shipment — VERIFIED already guarded (it nets committed non-PENDING shipment lines out of the allocation before recreating); no change needed. 6. Documented COMPLETED vs DELIVERED semantics + the archived/payment-mismatch behaviour in docs/workflows.md. Shared pure o2c-guards.ts (refundWouldExceedOrderTotal, isPaymentStatusMismatch, PAID_EXPECTED_SALES_STATUSES) reused by both call sites. Tests: 5 guard cases (refund within/at/over total incl. the relative-vs-fixed contrast; mismatch true/false); 105/105 sales suite; type-check + lint clean. Co-Authored-By: Claude Fable 5 --- .../sales/[id]/so-detail-client.tsx | 7 +++ app/actions/sales.ts | 30 ++++++++++++- docs/workflows.md | 14 ++++++ lib/domain/sales/o2c-guards.ts | 43 +++++++++++++++++++ lib/domain/sales/refund-service.ts | 6 ++- tests/domain/sales/o2c-guards.test.ts | 38 ++++++++++++++++ 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 lib/domain/sales/o2c-guards.ts create mode 100644 tests/domain/sales/o2c-guards.test.ts diff --git a/app/(dashboard)/sales/[id]/so-detail-client.tsx b/app/(dashboard)/sales/[id]/so-detail-client.tsx index 232ba6fa..9acf8fa9 100644 --- a/app/(dashboard)/sales/[id]/so-detail-client.tsx +++ b/app/(dashboard)/sales/[id]/so-detail-client.tsx @@ -1007,6 +1007,13 @@ export function SoDetailClient({ order: so, warehouses, currencies, externalOrde )} + {/* audit-M-o2c: an order advanced past payment but no longer fully paid + (e.g. its last payment was deleted) — a status/payment mismatch. */} + {!so.paidAt && ['SHIPPED', 'COMPLETED', 'DELIVERED', 'PARTIALLY_REFUNDED'].includes(so.status) && ( + + {STATUS_LABELS[so.status]} but not paid + + )} {canRefund && ( )} - {/* audit-M-o2c: an order advanced past payment but no longer fully paid - (e.g. its last payment was deleted) — a status/payment mismatch. */} - {!so.paidAt && ['SHIPPED', 'COMPLETED', 'DELIVERED', 'PARTIALLY_REFUNDED'].includes(so.status) && ( - - {STATUS_LABELS[so.status]} but not paid - - )} + {/* audit-M-o2c: the paid→unpaid mismatch from deleting a payment on an + advanced-status order is recorded as a payment_status_mismatch WARNING + activity log (the durable, accurate signal). A read-time chip on + `!paidAt` alone can't tell "shipped on credit, never paid" from + "was paid then unpaid", so it isn't shown here; the existing + Paid / Part. Paid indicators cover the payment state. */} {canRefund && (