docs: design proposal for shared expenses, settlement and the shared loan
ci / lint-test (push) Successful in 35s

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.
This commit is contained in:
2026-07-26 15:57:03 +10:00
parent 3778bfe836
commit 4b7a7e5d9c
+191
View File
@@ -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.