analytics: your share of what someone else paid is your spend
ci / lint-test (push) Successful in 1m25s
ci / lint-test (push) Successful in 1m25s
Every spend analytic gated on `OWNER_SCOPE = $1` and scaled by `mySplitOf` *within* that gate, so ownership was a precondition for an expense being yours. Half a grocery shop Sonu paid for counted as zero — in monthly, daily, merchants, subscriptions, fees and the budget page. 167 rows / $3,210.91 across Jan-Jul 2026, worst in April (+$1,354.83, the Europe trips), while getParticipantBalances booked the matching debt correctly. The app could say you owed her for a shop while insisting you had not spent anything on it. New MY_SPEND_SCOPE(): owner = me OR I hold a split. OWNER_SCOPE stays on the things that measure an *account* rather than a person — the income and investment lines, and the statement-level fee rollup. myShare had to change with it, and widening the gate alone would have been worse than the bug: its `100 - everyone else` fallback is the payer's remainder, so on someone else's unsplit row it returns 100 and moves their whole bill onto you. It now branches on ownership — my row resolves as before; their row takes an explicit split row only, absent meaning 0. That 0 is what makes the wider gate safe. my_share_percent is deliberately not read on someone else's row: one unscoped column, writable by anyone who can see the row, so "my" can only mean the owner's. All 402 rows carrying one today are owner-side. MY_SHARE_PCT mirrors myShare for the transactions list, which has no viewer-scoped ts join; a test asserts the two agree across seven fixture shapes. No historical restatement — every non-owner split is 2026-dated, and the 1,266 pre-2026 SplitMyExpenses splits are all on rows you own. Also fixes a latent failure in the NATIVE_CURRENCY test, which inserted a statement relying on participant id 1 existing (owner_id is NOT NULL DEFAULT 1 with an FK) and only passed when a sibling file had left one behind. It now owns its fixture. 15 new integration tests; 189 integration + 130 unit green.
This commit is contained in:
@@ -760,6 +760,50 @@ rather than hand-rolling them. Two failure modes they exist to prevent:
|
||||
Use the `EXCLUDE_NON_SPEND` fragment: a bare `category NOT IN (...)` evaluates
|
||||
to NULL for uncategorised rows and drops them from totals.
|
||||
|
||||
### Spend is gated on MY_SPEND_SCOPE, not OWNER_SCOPE (2026-08-15)
|
||||
|
||||
**Ownership is not a precondition for an expense being yours.** Every spend
|
||||
analytic used to gate on `OWNER_SCOPE = $1`, with `mySplitOf` scaling *within*
|
||||
that gate — so your half of a shop Sonu paid for counted as **zero**, in monthly,
|
||||
daily, merchants, subscriptions, fees and the budget page. 167 rows / **$3,210.91**
|
||||
across Jan–Jul 2026, worst in April (+$1,354.83, the Europe trips). Meanwhile
|
||||
`getParticipantBalances` booked the matching debt correctly, so the app could say
|
||||
you owed her for a shop while insisting you had not spent anything on it.
|
||||
|
||||
Use `MY_SPEND_SCOPE()` — `owner = me OR I hold a split` — for anything measuring
|
||||
what *I* spent. `OWNER_SCOPE` is still right for anything measuring an **account**:
|
||||
the income and investment lines in `/analytics/monthly`, and the statement-level
|
||||
fee rollup in `/analytics/fees` (which reads `statements`, where splits are
|
||||
meaningless).
|
||||
|
||||
**`myShare` had to change with it, and widening the gate alone would have been
|
||||
worse than the bug.** Its old third fallback, `100 - everyone else`, is the
|
||||
*payer's* remainder — on someone else's unsplit row it returns 100 and would have
|
||||
moved their entire bill onto you. It now branches on ownership:
|
||||
|
||||
- **My row** — unchanged: explicit split, then `my_share_percent`, then the
|
||||
remainder.
|
||||
- **Their row** — an explicit split row only; absent means **0**.
|
||||
|
||||
`my_share_percent` is deliberately not consulted on someone else's row. It is one
|
||||
unscoped column on `transaction_overrides` writable by anyone who can see the row,
|
||||
so "my" can only mean the owner's. All 402 rows carrying one today are owner-side.
|
||||
|
||||
That 0 is what makes the wider gate safe: admitting a row can never add more than
|
||||
the share actually held. Tested both ways in
|
||||
`src/__tests__/integration/analytics-sql.test.ts` — the discriminating cases are
|
||||
all ones where you hold **no** split on someone else's row, because that is the
|
||||
only place old and new disagree.
|
||||
|
||||
`MY_SHARE_PCT` in `queries.ts` mirrors `myShare` in subselect form for the
|
||||
transactions list (which has no viewer-scoped `ts` join). A test asserts the two
|
||||
agree row for row across seven fixture shapes — keep it that way, since the list
|
||||
is the drill-down for these totals.
|
||||
|
||||
**No historical restatement:** every non-owner split is 2026-dated. The 1,266
|
||||
pre-2026 SplitMyExpenses splits are all on rows you own, so nothing before the
|
||||
cutover moves.
|
||||
|
||||
### Statement types
|
||||
|
||||
`statements.statement_type` is constrained to `credit_card | transaction |
|
||||
|
||||
Reference in New Issue
Block a user