Files
finance-app/docs/shared-expenses-design.md
T
siddharthd 4b7a7e5d9c
ci / lint-test (push) Successful in 35s
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.
2026-07-26 15:57:03 +10:00

7.9 KiB

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.