docs: design proposal for shared expenses, settlement and the shared loan
ci / lint-test (push) Successful in 35s
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:
@@ -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.
|
||||
Reference in New Issue
Block a user