docs: splits total 100%, and why the remainder is the owner's
ci / lint-test (push) Successful in 45s

This commit is contained in:
2026-07-29 10:26:44 +10:00
parent 22e4a1ead0
commit a56e5e2de5
+31
View File
@@ -133,6 +133,37 @@ filter on `settled` — half a 2025 grocery shop was your expense whether or not
the other half was repaid. Filtering it out re-inflates exactly the figures the the other half was repaid. Filtering it out re-inflates exactly the figures the
historical import exists to correct. historical import exists to correct.
**Every split totals 100%, and the payer's row is written down.** `myShare`
resolves the payer's share as `100 - SUM(everyone else)`, so a 50/50 stored as
a lone "Sonu 50%" row still computed correctly — and still read on screen as a
50% share against a blank. `completeSplit` (`src/lib/splits.ts`) is the single
place that materialises the remainder, and every write path ends in it:
`applyRuleActions`, `POST /api/transactions`, the Slack nudge's share button,
and the rule-revert restore. `POST /api/transactions/[id]/splits` needs no call
— it already rejects anything not summing to 100.
The remainder always goes to the transaction's **owner**, never to "me". The
owner's row on their own transaction is excluded from both halves of
`getParticipantBalances` (`ts.participant_id != $1` on transactions I own; the
converse on ones I do not), so writing it cannot create, enlarge or discharge a
debt. A row for *me* on someone else's transaction is a real obligation — never
synthesise one. This is what made the 7-row backfill in `22e4a1e` safe;
balances were byte-identical across it.
There is no database-level constraint on the sum. Enforcing it needs a deferred
constraint trigger, and the rule path commits its DELETE and INSERT as separate
autocommitted statements, so the trigger would reject the intermediate state.
`share_percent` has a CHECK of `> 0 AND <= 100`, so a 0% row cannot be stored —
when the others grow to cover the whole amount, the owner's row is deleted
rather than zeroed.
**Un-sharing needs DELETE, not an empty POST.** The splits route rejects an
empty array ("splits array required"), so `DELETE /api/transactions/[id]/splits`
is the only way to clear. The order panel's "Shared 50/50" toggle was inert in
both directions until `22e4a1e` because it posted a lone 50% row to share and
`[]` to un-share, and the endpoint rejected both.
**Any split write path that deletes-and-recreates must carry `settled` across.** **Any split write path that deletes-and-recreates must carry `settled` across.**
`POST /api/transactions/[id]/splits` did not, and silently converted discharged `POST /api/transactions/[id]/splits` did not, and silently converted discharged
obligations into live debt — $37,233.28 was exposed. Fixed in `6add958`. obligations into live debt — $37,233.28 was exposed. Fixed in `6add958`.