ci / lint-test (push) Successful in 35s
The loan is a separate ledger, not a settlement context: a contribution must never be able to settle a dinner. The share is fixed at 50%, not derived from actual payments. During Sonu's leave the obligation did not change, only the payment did - a percentage-of-actual model would silently redefine her share as 30% and make the shortfall vanish. So the model needs an expected schedule alongside actual contributions, with the difference as a tracked receivable. Currently $4,000.00 over Jul 2025 - Jun 2026. On interest: recorded the mechanics (it is debited to the loan and repaid as part of the balance - the reconciliation is exact) alongside the counter-argument that $16,523.64 left and bought nothing, which is what an expense is. Recommends keeping it as spend with a fixed/discretionary grouping to address the real concern, but flags it as a judgement call rather than settling it.
245 lines
9.9 KiB
Markdown
245 lines
9.9 KiB
Markdown
# 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 — a separate ledger
|
||
|
||
Not a settlement context. The loan is a jointly funded asset with its own
|
||
obligation, and mixing it with expense settlement would let a contribution
|
||
accidentally settle a dinner.
|
||
|
||
- `emi` credits are recognised as **contributions**, not generic transfers
|
||
- A **contribution schedule** states what is owed per period (50% of the
|
||
repayment), independent of what was actually paid
|
||
- The running difference is a **receivable** — currently $4,000.00
|
||
|
||
The schedule matters: during Sonu's leave the obligation did not change, only the
|
||
payment did. A percentage-of-actual model would silently redefine her share as
|
||
30% and make the shortfall disappear.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## Decisions taken (2026-07-26)
|
||
|
||
### The loan is separate from shared expenses
|
||
|
||
Different obligations, different rhythms, different nature: one funds an asset,
|
||
the other funds consumption. They do not share a settlement context and a
|
||
contribution is never a settlement.
|
||
|
||
### The share is 50/50, fixed — with the shortfall tracked
|
||
|
||
Not derived from actual payments, which fluctuate. Sonu's obligation is half the
|
||
repayment; the difference between obligation and actual is a **receivable**, and
|
||
it is the interesting number.
|
||
|
||
Over 2025-07-01 → 2026-06-30:
|
||
|
||
| | |
|
||
|---|---:|
|
||
| Repayments | $63,500.00 |
|
||
| Sonu's 50% obligation | $31,750.00 |
|
||
| Actually contributed (26 payments) | $27,750.00 |
|
||
| **Shortfall** | **$4,000.00** |
|
||
|
||
She never missed a fortnight; the rate changed:
|
||
|
||
| Rate | Payments | Period |
|
||
|---|---:|---|
|
||
| $1,250 | 15 | Aug 2025 – Feb 2026 (the correct 50%) |
|
||
| $1,000 | 3 | Jul 2025 (pre-adjustment) |
|
||
| $750 | 8 | Mar – Jun 2026 (leave) |
|
||
|
||
So the model needs a **contribution schedule** (expected per period) alongside
|
||
actual contributions, with the running difference as a tracked balance. A flat
|
||
percentage cannot express "obligation unchanged, payment temporarily reduced,
|
||
difference owed".
|
||
|
||
### Interest: recommended as expense, pending final call
|
||
|
||
The mechanics are as described — interest is debited to the loan and repayments
|
||
pay down the combined balance. Reconciles exactly:
|
||
|
||
134: 31,000.00 − 8,553.27 = 22,446.73 = balance reduction
|
||
133: 32,500.00 − 7,970.37 − 2,849.00 = 21,680.63 = balance reduction
|
||
|
||
But mechanics are not the same as economics. Over 12 months $63,500 of cash left
|
||
and debt fell by $44,127.36. The $16,523.64 difference bought nothing and is not
|
||
recoverable — that is an expense by definition. Excluding it leaves the balance
|
||
sheet unable to reconcile cash out against equity gained, and understates annual
|
||
cost by ~10%.
|
||
|
||
The legitimate concern is that interest is **non-discretionary**. The answer to
|
||
that is a fixed-commitments grouping alongside rent, insurance and utilities —
|
||
a presentation change, not an exclusion.
|
||
|
||
**Recommendation: keep `loan_interest` as spend, add a fixed/discretionary
|
||
split.** Flagged rather than settled: it is a judgement about what "spend" means
|
||
in your own reporting.
|
||
|
||
## Open questions
|
||
|
||
1. **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. Probably
|
||
belongs in the net-worth view rather than here.
|
||
|
||
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.
|