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