Commit Graph
2 Commits
Author SHA1 Message Date
siddharthd 22e4a1ead0 fix(splits): make every split account for 100%
ci / lint-test (push) Successful in 1m39s
A 50/50 arrangement was stored as a single row saying "Sonu 50%". The
arithmetic was never wrong — `myShare` resolves the payer's share as
`100 - SUM(everyone else)`, so balances and per-user spend were correct
throughout. It was still a bug, because a ledger is read as well as
computed: on screen that row is a 50% share against a blank, which looks
like half the money is unallocated and is indistinguishable from a split
somebody abandoned half-finished.

It also leaked. `getSharedTransactions` filters by participant with an
EXISTS on an explicit split row, so filtering the Shared view by the
payer silently dropped every transaction where their share was only ever
implied.

Four write paths could produce it, three of them unguarded:

  - the Slack nudge's share button, which inserted one row
  - `POST /api/transactions`, where the add form shows an amber total
    under 100 but saves anyway — this is how Lawn Mowing and Hedge
    Pruning were stored
  - `applyRuleActions`, where ten of the fourteen live split rules name
    only the other person

`completeSplit` is now the single place that writes the remainder, and
every one of those paths ends in it. The remainder goes to the
transaction's *owner*, never to "me": the owner's row on their own
transaction is excluded from both halves of the balance query, so it
cannot create, enlarge or discharge a debt, whereas a row for me on
someone else's transaction is a real obligation. That distinction is
what makes this safe to apply to existing data.

Also fixes the order panel's "Shared 50/50" toggle, which was inert in
both directions: it posted a lone 50% row to share (rejected — must
total 100%) and an empty array to un-share (rejected — array required),
because no way to clear a split existed. DELETE on the splits route is
that way.

Backfill: 7 rows, verified against a row-level dump diff — 2549 -> 2556
rows, none removed, none modified — and participant balances byte
identical before and after (Molina 19556.07, Sonu 20913.35). Every split
in the database now totals 100%.

Not done: a database-level constraint. Enforcing the sum needs a
deferred constraint trigger, and the rule path commits its DELETE and
INSERT as separate statements, so the trigger would reject the
intermediate state. Making it work means wrapping every write path in a
transaction, which is a larger change than the defect warrants.
2026-07-29 10:23:06 +10:00
siddharthd 9f90d8726f feat(rules): apply_split rules with run history and revert
- POST /api/rules/apply — run all enabled rules against unmatched transactions
- POST /api/rules/apply/:id — apply a single rule by id
- DELETE /api/rules/apply/:id — revert a rule run (remove applied splits)
- Rules page: show run history with revert button, apply individual rules
2026-03-14 20:06:19 +11:00