From d06088fe34f9d1fe1736eeaef4182a8c607b3ab0 Mon Sep 17 00:00:00 2001 From: siddharthd Date: Sun, 26 Jul 2026 16:57:23 +1000 Subject: [PATCH] docs: monthly expense baseline and emergency reserve analysis One-off analysis, nothing built. Realistic baseline $4,140/mo -> $24,800 for six months, against $89,770 already accessible ($81,017 loan redraw + $8,753 offset). Records four corrections the raw data needs before any restatement: misfiled Raiz/Vanguard/moomoo debits counted as spend, `other` credits read as negative spend, `government` conflating ATO with rates/rego, and `fees` being mostly annual. CLAUDE.md gains two traps found while doing it: partial split coverage inside a category is usually correct rather than a gap (only shared utilities and subscriptions are split), and the loan repayment is voluntarily above contracted ($2,500 vs $1,190.54 per fortnight) with the difference recoverable via redraw. --- CLAUDE.md | 31 ++++++ docs/expense-baseline.md | 211 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 docs/expense-baseline.md diff --git a/CLAUDE.md b/CLAUDE.md index f18d2cb..001101b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,6 +135,19 @@ populated on only one. And Sonu's loan contributions (`…emi` in the offset account, 39 rows, $37,980.24) are categorised `transfers`, indistinguishable from ordinary internal transfers. +**Partial split coverage inside a category is usually correct, not a gap.** Only +*shared* items are split. `utilities` sits at 69% yours because Globird, OVO, GWW +and home telecoms are split while Telstra, Vodafone, Optus and JB Hi-Fi Mobile +are personal. `subscriptions` is 91% because Uber One, Amazon Prime and OnePass +are shared while Claude, OpenAI, Anthropic, OpenRouter, You.com, LinkedIn, Xero, +Billdu, Spotify and Patreon are not. `fees` and `charity` are 100% yours and +correct. Check the merchants before concluding a rule was never applied — a +category-level ratio that "looks wrong" usually is not. + +Splits exist in this app from **2026-01-09** only; earlier splits lived in +SplitMyExpenses. So a trailing-12-month per-person series splices six months of +gross onto six months of net. Use Feb–Jun 2026 for anything per-person. + ### The shared loan The loan is a **separate ledger**, not a shared expense and not a settlement @@ -152,6 +165,19 @@ months $63,500 of cash left and debt fell $44,127.36, and the $16,523.64 difference bought nothing. Excluding it would leave the balance sheet unable to reconcile cash out against equity gained. +**The repayment is voluntarily above contracted, and the gap is the largest +flexible cost in the whole picture.** Contracted is $1,190.54/fortnight +($2,579.50/mo annualised); the actual direct debit is $2,500.00/fortnight +($5,416.67/mo). That is $2,837.17/mo of overpayment, and it is *not* sunk — it +shows up as `statements.redraw_available`, which grew $62,387.17 → $81,017.42 +across the two most recent loan statements. Sonu returned to $1,250/fortnight in +July 2026 after the reduced $750 period during her leave. + +Treat the repayment as two figures whenever asking "what does this cost me": +the contracted floor and the actual. `scheduled_repayment` holds the actual +($2,500), not the contracted minimum — the contracted figure is not in the DB at +all. See `docs/expense-baseline.md`. + ### Import Date (`created_at`) `transactions.created_at` is the import timestamp (DB default `now()`). In the transactions and shared views, the "Imported" column shows: @@ -349,6 +375,11 @@ See `README.md` → **Known Gaps / TODOs** for full details. - **Shared expenses redesign** — `docs/shared-expenses-design.md`. Phase 0 done; Phases 1–4 unbuilt. Deliberately paused to live with the current behaviour before committing to a model designed in one session. +- **Expense baseline / emergency reserve** — `docs/expense-baseline.md`. One-off + analysis, nothing built. Records four data corrections the raw numbers need + (misfiled Raiz/super/brokerage debits, `other` credits read as negative spend, + `government` conflating ATO with rates/rego, `fees` being mostly annual) and + why only Feb–Jun 2026 is trustworthy for per-person figures. - **11 statements fail the balance assertion**, ~$4,177 unexplained. Predates this work. One ANZ statement is off by exactly $0.50, traced to a misread digit in fee rows ($5.00 vs $5.50). diff --git a/docs/expense-baseline.md b/docs/expense-baseline.md new file mode 100644 index 0000000..d6ca88e --- /dev/null +++ b/docs/expense-baseline.md @@ -0,0 +1,211 @@ +# Monthly expense baseline and emergency reserve + +Analysis run 2026-07-26. **One-off analysis, not a feature** — nothing in the app +computes these numbers. Read "Reproducing this" before trusting a restated figure. + +The question: how much should be held in reserve to cover 6–12 months of expenses? + +## Answer + +| Scenario | $/mo | 6 months | 12 months | +|---|---:|---:|---:| +| Survival — contracted loan repayment, essentials only | 3,040 | 18,250 | 36,500 | +| **Realistic — contracted loan, + dining and charity** | **4,140** | **24,800** | **49,600** | +| Status quo — keep overpaying the loan, normal life ex-travel | 5,557 | 33,500 | 67,000 | + +Use the middle row. The survival row assumes dining is cut to zero and stays +there, which is not a plan anyone executes for six months. + +Against that, liquidity already available (statements 133 and 131, 2026-06-30): + +| | | +|---|---:| +| Loan redraw | 81,017.42 | +| Offset balance | 8,753.00 | +| **Accessible** | **89,770.42** | + +That is 3.6× the six-month target and 1.8× the twelve-month one. Redraw grew +$62,387.17 → $81,017.42 across the last two loan statements, matching the +overpayment rate — the money spent killing the loan faster is still reachable. + +**Caveat on counting redraw as the reserve.** It is available at AMP's +discretion, and lenders reduce or freeze it exactly when a borrower looks +distressed — which is when it would be needed. The rate also moved 5.54% → 6.29% +between the two statements, so redrawn funds cost more than they did. Hold some +genuine cash; it does not need to be $50,000. + +## This is *your* outgoings, not household spend + +The app only sees accounts that get imported. Sonu's own spending on the +household is invisible to it. Grocery spend reads as ~$300/mo gross on 4–10 +transactions, which is implausible for a household and is partly explained by her +paying from her own account. + +For "how much reserve do **I** need" that blind spot does not matter — your own +outgoings is the correct measure. Do not relabel these figures as household +totals; they are not, and they would be wrong by an unknown amount. + +## The loan has two floors + +This is the largest single lever and the reason there are three scenarios. + +| | $/fortnight | $/mo annualised | Your 50% | +|---|---:|---:|---:| +| Contracted minimum | 1,190.54 | 2,579.50 | **1,290** | +| Actual direct debit | 2,500.00 | 5,416.67 | **2,708** | +| Voluntary overpayment | 1,309.46 | 2,837.17 | 1,419 | + +Dropping to contracted cuts your loan cost by $1,418/mo. Sonu's obligation is a +fixed 50% of the repayment (see CLAUDE.md → "The shared loan"), so it falls with +it. Her rate returned to $1,250/fortnight in July 2026 after the reduced $750 +period during her leave. + +## Baseline composition + +Built from **Feb–Jun 2026** — the months where split data is trustworthy — with +annual items annualised over 12 rather than divided by the 5-month sample. + +| Essential | $/mo | Note | +|---|---:|---| +| Loan (contracted, your 50%) | 1,290 | 2,708 at the current actual rate | +| Transport | 326 | | +| Insurance | 280 | annualised; your 55% | +| Utilities | 278 | shared energy/water + personal mobile | +| Subscriptions | 204 | shared household + personal/AI | +| Groceries | 200 | see the blind-spot note above | +| Card + package fees | 175 | annualised — see below | +| Health | 167 | | +| Rates + rego | 122 | annualised, your share | +| **Essential** | **3,042** | | +| + dining 675, charity 422 | 4,139 | charity is a Smith Family sponsorship commitment | +| + typical shopping | ~4,800 | median 657, **not** the 1,709 mean | + +**Travel is excluded throughout.** At $3,153/mo of your share even post-cutover it +would roughly double every figure, and it is the first thing that stops. + +## Four corrections the raw data needed + +Any restatement that skips these will be wrong. None are fixed in the data yet. + +**Micro-investing counted as spend.** Raiz ($9,406 / 27 rows), Vanguard Super +($500) and moomoo ($300) sit in `other` as debits — $10,206/yr, ~$850/mo of +phantom spend. These belong in `investment`, which is already excluded. + +**Incoming money counted as negative spend.** 17 rows in `other` typed `credit` +($5,622 in the window). `SPEND_SIGNED` negates credits so refunds cancel +purchases, but these are not refunds — they are money arriving. June 2025 shows +*minus* $7,814 of total spend because two Wise credits of ~$16.7k each landed in +`other`. + +**`government` is two unrelated things.** $25,554 of ATO payments (one annual +bill, routed through Zen B2B and RewardPay to earn points) versus $2,054 council +rates and $875 rego. Tax is not a monthly living cost and falls with income +anyway; rates and rego are non-negotiable. Splitting them moves this line from +$2,411/mo to $244/mo. + +**`fees` is mostly annual.** Of $2,599 post-cutover, $1,750 is an annual card fee +and $349 a loan package annual fee. Recurring is ~$175/mo annualised, not the +$520/mo the 5-month mean implies. + +## Splits: what is trustworthy and what is not + +Splits exist in this app from **2026-01-09** only; before that they were tracked +in SplitMyExpenses. So a trailing-12-month per-person series splices six months +of *gross* onto six months of *net* and is not a series at all. Use Feb–Jun 2026. + +**Partial split coverage within a category is usually correct, not a gap.** This +was misdiagnosed once during the analysis. Verified composition: + +| Category | Your share | Split | Unsplit | +|---|---:|---|---| +| utilities | 69% | Globird, OVO, GWW, home telecoms | Telstra, Vodafone, Optus, JB Hi-Fi Mobile | +| subscriptions | 91% | Uber One, Amazon Prime, OnePass | Claude, OpenAI, Anthropic, OpenRouter, You.com, LinkedIn, Xero, Billdu, Spotify, Patreon | +| fees | 100% | — | credit card fees are personal | +| charity | 100% | — | personal commitment | + +Only *shared* utilities and subscriptions are split. Mobile bills, AI +subscriptions and card fees are personal and correctly sit at 100%. Do not +"fix" these ratios. + +## Known weak spots + +- **`other` is $424–773/mo of your share and unclassified.** The single biggest + lever on accuracy. Clearing it with the rule preview + (`GET /api/rules/[id]/matches`) improves every other view at the same time. +- **Five months is a short sample**, and it contains the Europe trip and the ATO + bill. Both are excluded, but they crowd out the ordinary months. The + SplitMyExpenses CSVs would stretch it to 18 months — worth having, not + blocking, since only an *aggregate ratio per category* is needed, not row + matching, so the combined-transaction problem does not bite. +- **`/api/analytics/monthly` does not filter `reconciled_with_id IS NOT NULL`.** + 48 rows are double-counted app-wide. Small, but real, and unrelated to this + analysis. The queries below do filter it. + +## Reproducing this + +Working queries are not checked in; they were run ad hoc against +`postgres-personal`. The shape that matters: + +```sql +-- Per-category monthly distribution, your share, Feb-Jun 2026. +-- Mirrors src/lib/analytics-sql.ts, plus the corrections above. +WITH s AS ( + SELECT to_char(t.transaction_date,'YYYY-MM') m, + COALESCE(o.category_override, t.category, 'other') cat, + SUM((CASE WHEN t.transaction_type IN ('refund','credit') THEN -1 ELSE 1 END) + * (CASE WHEN t.interest_amount IS NOT NULL + THEN t.interest_amount ELSE COALESCE(t.amount_aud, t.amount) END) + * COALESCE(ts.share_percent, o.my_share_percent, + 100 - COALESCE((SELECT SUM(x.share_percent) FROM transaction_splits x + WHERE x.transaction_id = t.id + AND x.participant_id <> 1), 0)) / 100)::numeric(12,2) amt + FROM transactions t + LEFT JOIN transaction_overrides o ON o.transaction_id = t.id + LEFT JOIN transaction_splits ts ON ts.transaction_id = t.id AND ts.participant_id = 1 + LEFT JOIN statements st ON st.id = t.statement_id + WHERE COALESCE(t.owner_id, st.owner_id) = 1 + AND t.reconciled_with_id IS NULL -- see weak spots + AND ((t.transaction_type IN ('debit','fee','interest') + OR t.interest_amount IS NOT NULL) + OR t.transaction_type IN ('refund','credit')) + AND COALESCE(o.category_override, t.category, 'other') + NOT IN ('transfers','investment','income') + -- correction 1: misfiled investments + AND COALESCE(t.merchant_normalized, t.merchant_name, '') + !~* '^(Raiz|Vanguard Super|moomoo)' + -- correction 2: incoming money miscategorised as 'other' credits + AND NOT (COALESCE(o.category_override, t.category, 'other') = 'other' + AND t.transaction_type = 'credit') + AND t.transaction_date >= '2026-02-01' AND t.transaction_date < '2026-07-01' + GROUP BY 1, 2 +) +SELECT cat, count(*) mths, round(avg(amt),0) mean, + round(percentile_cont(0.5) WITHIN GROUP (ORDER BY amt)::numeric, 0) median +FROM s GROUP BY cat ORDER BY mean DESC; +``` + +Use the **median** for anything recurring monthly and the **annualised total** for +lumpy-but-certain items (insurance, rates, rego, annual card fees). Means are +badly skewed here — shopping is 1,709 mean against 657 median. + +Corrections 3 and 4 (`government`, `fees`) are not expressible as filters; they +need the category split by merchant, done by hand above. + +## If this becomes a feature + +The blocker is `other`, not the split model — the split data held up under +scrutiny. Sequence: + +1. Recategorise Raiz / Vanguard Super / moomoo to `investment` (27 rows, + unambiguous) via the rule preview. +2. Split `government` so rates and rego separate from ATO. Probably a new + category or a tag; `government` conflates a recurring obligation with an + annual tax bill. +3. Work `other` down with the rule preview. +4. Then a baseline panel on Insights, which already carries the + Regular/Occasional split this analysis is a stricter version of + (`REGULAR_CATEGORIES` in `src/lib/categories.ts`). + +A **liquidity vs baseline** view would be genuinely new rather than a restatement: +`statements.redraw_available`, offset closing balance, and the +contracted-vs-actual repayment gap are all in the schema and nothing reads them.