fix(analytics): my share is what is left over, not 100%
ci / lint-test (push) Successful in 41s

The split-adjusted spend expression assumed a transaction with no split row for
me was entirely mine. That is wrong when a transaction is allocated fully to
someone else: I paid, they owe all of it, and there is no row for me to match.
Both branches of the CASE missed and the ELSE charged me the full amount.

24 transactions were affected, all travel bookings between 2026-01-09 and
2026-06-26, overstating my spend by $8,579.07 across every analytics view.

Adds myShare/mySplitOf to analytics-sql.ts, which fall back to
100 - (sum of everyone else's shares) instead of 100, and applies them to all
five analytics routes. Centralised for the same reason as EXCLUDE_NON_SPEND:
the expression was duplicated five times and had already drifted.
This commit is contained in:
2026-07-26 10:05:09 +10:00
parent 02495e4173
commit ab00f8c592
6 changed files with 42 additions and 26 deletions
+29
View File
@@ -42,6 +42,35 @@ export const SPEND_BASE = `CASE WHEN t.interest_amount IS NOT NULL THEN t.intere
/** Effective category, honouring overrides. Never NULL. */
export const EFFECTIVE_CATEGORY = `COALESCE(o.category_override, t.category, 'other')`;
/**
* My percentage share of a transaction, 0-100.
*
* Resolution order:
* 1. An explicit `transaction_splits` row for me.
* 2. The `my_share_percent` override.
* 3. Whatever is left after everyone else's shares.
*
* Step 3 is the one that matters. Assuming 100% when no split row exists for me
* is wrong whenever a transaction is allocated entirely to someone else — I paid,
* they owe all of it, and there is no row for me to find. Those rows would
* otherwise land in my spend at full value.
*
* Requires `transaction_splits ts` joined on `ts.participant_id = <participant>`
* and `transaction_overrides o` joined on the transaction.
*/
export const myShare = (participant = "$1") => `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 <> ${participant}
), 0)
)`;
/** `base` scaled to my share. Use for every per-user spend total. */
export const mySplitOf = (base: string, participant = "$1") =>
`((${base}) * ${myShare(participant)} / 100)`;
/**
* Predicate excluding money-movement categories.
* The COALESCE matters: a bare `category NOT IN (...)` evaluates to NULL for