From 4b7a7e5d9c8af37a877d46661120a11c5333cefe Mon Sep 17 00:00:00 2001 From: siddharthd Date: Sun, 26 Jul 2026 15:57:03 +1000 Subject: [PATCH] docs: design proposal for shared expenses, settlement and the shared loan Three problems that look separate are one: the app records money moving, and separately records who owes whom, and the two never meet. Documents what is broken with evidence - two half-built settlement models, settlements existing twice unlinked, and Sonu's $37,980 of loan contributions sitting unrecognised as generic transfers - then proposes settlement contexts, payments as transactions rather than a side table, and loan co-ownership. Nothing built. Five open questions, two of which are decisions about the arrangement rather than the software. --- docs/shared-expenses-design.md | 191 +++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/shared-expenses-design.md diff --git a/docs/shared-expenses-design.md b/docs/shared-expenses-design.md new file mode 100644 index 0000000..1f59380 --- /dev/null +++ b/docs/shared-expenses-design.md @@ -0,0 +1,191 @@ +# Shared expenses, settlement, and the shared loan — design proposal + +Status: **proposal, nothing built**. Written 2026-07-26 for review. + +## Why this exists + +Three questions have no answer in the current model: + +1. Which splits does a settlement payment settle? +2. Is the Europe trip settled, separately from the ongoing household tab? +3. Whose expense is a $2,500 loan repayment when Sonu funds part of it? + +They look like three problems. They are one: **the app records money moving, and +separately records who owes whom, and the two never meet.** + +--- + +## What is actually broken + +### Two settlement models, neither finished + +| Model | Where | State | +|---|---|---| +| Running tab | `split_payments` (from, to, amount, date) | **in use** — 8 payments, $37,881.10 | +| Per-split flag | `transaction_splits.settled` | **never used** — all 673 splits are `false` | + +They are honoured inconsistently: + +- `getParticipantBalances` (the shared page) ignores `settled` entirely +- `/api/participants/[id]/balance` filters on `settled = false` +- `getTripAnalytics` reports settled/unsettled **from the unused flag** + +The third is a live bug. Every trip shows 100% unsettled forever, even though +Molina has paid $20,782.79 against $19,556.07 of splits and is square. + +### Settlements exist twice, unlinked + +Four of the eight recorded payments match an offset-account credit exactly: + +| Payment date | From | Amount | Offset transaction | +|---|---|---:|---| +| 2026-02-02 | Molina | 7,500.00 | `Transfer from - MEGHALEE BOSE mummy Pa…` | +| 2026-04-12 | Sonu | 3,779.33 | `Transfer from - MEGHALEE BOSE transfer` | +| 2026-04-21 | Molina | 1,685.24 | `Transfer from - MEGHALEE BOSE mummy split` | +| 2026-05-16 | Sonu | 4,794.06 | `Transfer from - MEGHALEE BOSE transfer` | + +The same money is a `split_payments` row *and* a `transactions` row. +`split_payments.linked_transaction_id` exists but only 1 of 8 rows uses it. So a +settlement is bookkeeping that happens to resemble a bank credit, rather than +being that credit. + +### The shared loan is invisible + +Sonu's contributions are already in the ledger and unrecognised: + +| Pattern in offset credits | Rows | Total | Meaning | +|---|---:|---:|---| +| `…emi` | 39 | $37,980.24 | Sonu's loan contribution | +| `…mummy…` | 6 | $29,721.24 | Molina's money, forwarded by Sonu | +| other Meghalee | 15 | $71,130.27 | Sonu's own settlements | + +All are categorised `transfers` — correct for spend purposes, but it means a +loan contribution and an expense settlement are indistinguishable. + +Meanwhile the loan itself, over the 12 imported months: + +| | | +|---|---:| +| Principal repaid (`investment`, excluded from spend) | $63,500.00 | +| Interest charged (`loan_interest`, the only part counted as spend) | $16,523.64 | + +At roughly $25,000/year of `emi` against ~$80,000 of annual repayments, Sonu +funds about **31%** — of both the equity being built and the interest being paid. +Today 100% of the interest counts as your spend and 100% of the equity as yours. + +--- + +## The core problem + +The model conflates two different things: + +- **Money movement** — a credit landed in the offset account +- **Obligation** — someone owed someone else, and now owes less + +A settlement is both. A loan contribution is both. Right now movement lives in +`transactions` and obligation lives in `transaction_splits` / `split_payments`, +with nothing joining them. That is why a payment cannot say what it settles: it +was never attached to anything in the first place. + +--- + +## Proposed model + +### 1. Settlement contexts + +Splits belong to something that is settled **as a unit**. Payments name which +unit they settle. Balance is computed per context, not globally. + +| Context | Splits | Settled by | State | +|---|---|---|---| +| Household (default) | ongoing | periodic payments | running tab | +| Europe 2026 | trip-bound | lump sum | closeable | +| Pre-2026 (SplitMyExpenses) | historical | settled elsewhere | **born closed** | + +A closed context still contributes to analytics — you see your true share — but +contributes nothing to what anyone owes. + +This answers all three opening questions, and it dissolves the `splitFrom` date +cutoff: pre-2026 splits can be applied retroactively **because they are born +into a closed context**, so they fix the analytics without creating debt. No date +guard needed, no risk of resurrecting settled obligations. + +Mechanically: `settlement_contexts` table; `transaction_splits.context_id`; +`split_payments.context_id`. `transaction_splits.settled` becomes derived +("is my context closed?") or is dropped. + +### 2. Payments are transactions, not a side table + +A settlement is the offset-account credit. `split_payments` becomes a thin +attribution layer over a real transaction rather than a parallel record of it: + +- Populate `linked_transaction_id` on all existing payments where a match exists +- On ingestion, an incoming credit that looks like a settlement is *proposed* as + one for confirmation, rather than silently becoming `transfers` +- A payment with no matching transaction (cash, or an account not imported) + stays as a manual row — the model must tolerate that + +### 3. The shared loan + +The loan needs a co-ownership share separate from expense splitting, because it +is not a periodic shared expense — it is a jointly funded asset. + +- `emi` credits are recognised as **contributions**, not generic transfers +- Contribution share drives how `loan_interest` is attributed to spend +- Equity (principal) accrues per participant + +**This is the piece I am least sure about** — see open questions. + +--- + +## Migration path + +1. **Fix the trip settlement bug first** — make `getTripAnalytics` and + `getParticipantBalances` agree. Low risk: all splits are currently unsettled, + so honouring the flag changes nothing today. +2. Add contexts; put every existing split in "Household"; every payment likewise. +3. Backfill `linked_transaction_id` for the four exact matches; flag the other + four for manual linking. +4. Create the "Pre-2026" closed context. Apply household split rules to + pre-cutoff transactions into it — fixes ~$97,627 of the trailing 12 months + currently shown as 100% yours. +5. Loan contributions and equity — last, and only after the questions below. + +--- + +## Open questions + +1. **Should loan interest be split?** If Sonu funds 31% of repayments, is 31% of + the $16,523.64 interest her expense — or is the loan simply yours with her + contributing, and the interest all yours? This is a decision about the + arrangement, not a technical one. + +2. **Does equity need tracking per person?** If Sonu accrues a share of the + principal, that is a balance-sheet item the app has no concept of. It may + belong in the future net-worth view rather than here. + +3. **Is the contribution share fixed or derived?** Derived from actual `emi` + payments it fluctuates every fortnight. Fixed, it needs stating and + maintaining. Derived is more honest; fixed is more stable for analytics. + +4. **Attribution of forwarded payments.** `mummy` in the description reliably + marks Molina's money in all six known cases, but it is a description match on + a free-text field. Acceptable as a *suggestion* requiring confirmation; not as + an automatic rule. + +5. **Retroactive split ratios.** Applying today's household rules to 2025 + assumes the arrangement has not changed. The SplitMyExpenses CSVs could give + real historical shares, but transactions were sometimes combined, so matching + is imperfect. Recommendation: use today's ratios, accept the approximation — + the goal is a truer analytics picture, not a restated ledger. + +--- + +## What I would not do + +- **Do not** restate history from the SplitMyExpenses CSVs. The combining problem + makes exact reconciliation impossible, and the value is low: those balances are + settled and will not change. +- **Do not** make the loan a shared *expense*. It is a funded asset. Modelling it + as a recurring split would put $2,500 a fortnight of principal into spend, + which is the error migration 0014 was written to prevent.