fix(accounting): refund-reversal-aware deferred-revenue true-up for partially-refunded orders (scjz.68)#352
Conversation
…artially-refunded orders (scjz.68) The Group-B daily-batch deferred-revenue true-up (scjz.41) excluded PARTIALLY_REFUNDED orders: a refund of unshipped lines posts an UNEARNED_REV_REVERSAL that debits the unearned account OUTSIDE the shipment-recognition running total, and `remainingDeferred` only subtracted prior shipment recognition — so truing up would re-recognize (and over-debit unearned revenue for) value the refund already reversed. The exclusion in turn permanently stranded the goods remainder + shipping share of fully-refunded-net orders in deferral. New pure, unit-tested helpers (lib/domain/accounting/deferred-trueup.ts): - sumPostedUnearnedReversal: the unearned-account debit across an order's posted UNEARNED_REV_REVERSAL syncs (ignores the allocation-reversal line, which debits inventory). Mirrors refund-service's extractPayloadAmount. remainingDeferred now subtracts it, so the unearned liability nets to exactly zero over the order's life and the true-up can never over-recognize — applied to ALL orders, not just partially-refunded ones. - isFullyShippedNetOfRefunds: gates the PARTIALLY_REFUNDED true-up to orders whose every shippable line is covered by dispatched (SHIPPED) shipments plus allocation-source (unshipped) refund qty. Combines component quantities before taking line coverage (min-of-sums) so kits are exact, not stranded. Returns to NOT true up returns of shipped units (they don't reduce the ship obligation). - batchContainsFinalUnjournaledShipment: holds the true-up until this batch holds the order's final dispatched-but-unjournaled shipment, so a XERO_DAILY_BATCH_LIMIT window split can't recognize a later shipment's revenue early. Applied to the terminal-status path too (strictly safer; converges as journaling shrinks the set). Wired identically into the Xero daily-sync, the QuickBooks daily-sync (cross-port), and the daily-batch preview (so the preview matches what posts — scjz.69). type-check + eslint + check:all + 189 accounting tests green; 14 new helper tests. Live-GL: needs finance review of the refund-vs-deferral treatment and Xero/QBO sandbox validation before merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve posting (scjz.68/.69) Codex adversarial review: the preview loaded every unjournaled shipment unbounded, so the new batchContainsFinalUnjournaledShipment guard always saw an order's full shipment set and marked its true-up final — while live Xero slices Group B with XERO_DAILY_BATCH_LIMIT (take: batchLimit + 1 → takeDailyBatchWindow) and would defer that true-up to a later run. The preview therefore overstated the next batch's revenue. Apply the same window to the preview query so the guard sees the same slice and the preview matches what posts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Seeds a PARTIALLY_REFUNDED order that is fully shipped net of refunds (2 shipped + 1 refunded-unshipped of 3 ordered, with a posted UNEARNED_REV_REVERSAL) and one that is not (1 shipped + 1 refunded of 3), runs the real runDailyBatchSync against an isolated DB, and asserts the staged Group B recognition: - fully-shipped-net-of-refunds trues up to deferredBase - reversal (7.00), reversal-aware (not the naive 10.00) and above the proportional slice (6.67); - not-fully-shipped recognises proportional only (3.33), no true-up. Run: DATABASE_URL=...onetwo3d_ims_e2e NODE_OPTIONS='--import tsx' node scripts/repro-scjz68.ts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Isolated-DB daily-batch validation ✅Ran the real
Still gated on: finance review of the refund-vs-deferral treatment (a live Xero/QBO tenant run would be the final belt-and-braces, but the GL decision logic is now validated end-to-end against a real Postgres batch). |
Live Xero Demo Company validation ✅Deployed this branch to staging (
After the run: temporarily-enabled sync flags restored to scjz.68 is now validated at unit + isolated-DB + live Demo Company levels. Remaining gate: finance sign-off on the refund-vs-deferral treatment. |
… Xero Demo) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
The Group-B daily-batch deferred-revenue true-up (scjz.41) excluded
PARTIALLY_REFUNDEDorders. A refund of an order's unshipped lines posts anUNEARNED_REV_REVERSALthat debits the unearned account outside the shipment-recognition running total, whileremainingDeferredonly subtracted prior shipment recognition — so truing up would re-recognize (and over-debit unearned revenue for) value the refund already reversed. The exclusion in turn permanently stranded the goods remainder + shipping share of orders that were fully shipped net of refunds.This is the dedicated rigorous implementation captured after PR #347 (closed) — coverage-based, not the
£0.05rounding-scale heuristic that didn't converge.Approach — pure, unit-tested helpers (
lib/domain/accounting/deferred-trueup.ts)sumPostedUnearnedReversal— the unearned-account debit across an order's posted (PENDING/PROCESSING/SYNCED)UNEARNED_REV_REVERSALsyncs, ignoring the allocation-reversal line (which debits inventory). Mirrors refund-service'sextractPayloadAmount.remainingDeferrednow subtracts it, so over the order's life the unearned liability nets to exactly zero and the true-up can never over-recognize. Applied to all orders, not just partially-refunded ones.isFullyShippedNetOfRefunds— gates thePARTIALLY_REFUNDEDtrue-up to orders whose every shippable line is covered by dispatched (SHIPPED) shipments plus allocation-source (unshipped) refund qty. Combines component quantities before taking line coverage (min-of-sums) so kits are exact, not stranded. Returns of shipped units don't reduce the ship obligation, so shipment-source refund entries are excluded.batchContainsFinalUnjournaledShipment— holds the true-up until this batch holds the order's final dispatched-but-unjournaled shipment, so aXERO_DAILY_BATCH_LIMITwindow split can't recognize a later shipment's revenue early. Applied to the terminal-status path too (strictly safer; converges as journaling shrinks the unjournaled set).Wired identically into the Xero daily-sync, the QuickBooks daily-sync (cross-port), and the Xero daily-batch preview — the preview applies the same
takeDailyBatchWindowso it matches what posts (scjz.69).Tests / gates
type-check+eslint+check:all+ the full 189-test accounting suite all green.onetwo3d-ims-qn8a); round 2 clean.Remaining (merge gates)
onetwo3d-ims-4wuu.Closes
onetwo3d-ims-scjz.68(code complete). Follow-up:onetwo3d-ims-qn8a.🤖 Generated with Claude Code