You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(payments): stop double-subtracting refunds from payout balances (#511)
`computeOwedBalances` excluded refunded sales from `grossOwed` and then
subtracted their scaled share again as `clawback`, so a school's balance
was understated by the refund. The second subtraction was never needed:
`alreadyPaid` is the all-time payout sum, so it still holds whatever was
paid out for a sale that later flipped to `refunded`. Dropping the sale
from `grossOwed` recovers that overpayment on its own.
`netOwed` is now `max(grossOwed - alreadyPaid, 0)` — one representation,
one subtraction. The issue's worked example (1000 successful + 100
refunded at 80%, 80 paid) goes from 640 to the correct 720.
`clawback` survives as a reporting-only figure and is gated on
plausibility: a refund counts only when a payout in the same currency
settled up to a point at or after the sale, so refunds that were never
paid out no longer reduce anything or appear as recoverable. The dates
come from `transactions.transaction_date` (that table has no
`created_at`) and from `payouts.period_end ?? paid_at ?? created_at` —
`period_end` is already the exact "covered through" the issue asked for,
so no migration is needed.
The four `#498` unit tests encoded the old behavior as intended and are
rewritten with asymmetric amounts, so the `Math.max(…, 0)` floor can no
longer mask a sign error. Adds coverage for undated refunds, undated
payouts, cross-currency payouts, latest-payout-wins, and mixed ISO
offset formats. 26 tests in the file, 313 across the suite.
The `/platform/payouts` clawback banner and column are reworded, since
that number is no longer a deduction the reader should apply themselves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012nmbfroRHjSNrkGbRqTozs
/** Sum of manual payouts already recorded as paid in this currency. */
73
115
alreadyPaid: number
74
-
/** Sum over refunded transactions of amount × (that transaction's snapshotted split, or the tenant's current split) — money already paid out for sales that later got refunded (issue #498). */
116
+
/**
117
+
* REPORTING ONLY — not a term in `netOwed` (issue #511). Sum, over refunded
118
+
* transactions a payout plausibly covered, of amount × (that transaction's
119
+
* snapshotted split, or the tenant's current split): how much of what was
120
+
* already paid to this school was for sales that have since been refunded.
121
+
* `alreadyPaid` already accounts for it, so it must not be recovered again.
122
+
*/
75
123
clawback: number
76
-
/** max(grossOwed - alreadyPaid - clawback, 0) — what's currently owed in this currency. */
124
+
/** max(grossOwed - alreadyPaid, 0) — what's currently owed in this currency. */
77
125
netOwed: number
78
126
/** Per-provider breakdown of grossCollected in this currency. */
79
127
byProvider: Record<string,number>
@@ -111,29 +159,66 @@ export function computeOwedBalances(
0 commit comments