diff --git a/CLAUDE.md b/CLAUDE.md index 16824bf..b127057 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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.** `POST /api/transactions/[id]/splits` did not, and silently converted discharged obligations into live debt — $37,233.28 was exposed. Fixed in `6add958`.