fix(splits): the cutover date, not a boolean, is what gates a balance
ci / lint-test (push) Successful in 46s

Nothing dated before 2026-01-09 can be owed, because carryover transaction 2348
already carries the entire pre-cutover balance as a single figure. ACTIVE_OBLIGATION
now says so directly.

This inverts which mechanism is load-bearing, and that is the point. Until now
the only thing keeping $37,233.28 of paid debt out of the balances was
transaction_splits.settled -- a boolean that any delete-and-recreate write path
resets to false, as the split modal did until commit 6add958. Losing the flag on
a pre-cutover row now costs nothing: the date still excludes it. The flag matters
only on or after the cutover, marking the few settled outside this app.

It also makes splitting history safe to do freely. A split on a 2024 grocery
shop can now describe how the expense was shared -- which is what stops it
inflating spend -- without asserting a debt that was settled years ago. That was
the whole reason not to split pre-2026 expenses, and it no longer applies.

The bound is inclusive because transaction 2348 is itself dated 2026-01-09; an
exclusive one would drop the carryover and with it the entire pre-cutover
balance.

Corrects one live figure: a Woolworths on 2026-01-06 was split 50/50 three days
before the cutover, double-counting $7.55 against the carryover. Sonu
$5,428.08 -> $5,420.53.

The test fixture default moved to 2026-06-15 -- it was 2024-06-15, which is now
pre-cutover and made every balance fixture read zero. That the suite caught this
is the guard working.
This commit is contained in:
2026-07-28 13:52:30 +10:00
parent 6add958132
commit 788219b9fd
3 changed files with 81 additions and 3 deletions
+4 -1
View File
@@ -73,7 +73,10 @@ export async function insertTransaction(
VALUES ($1, NULL, $2, $3, $4, $5, $6, 0) RETURNING id`,
[
ownerId,
overrides.transaction_date ?? "2024-06-15",
// Post-cutover by default: a split on an older transaction never counts
// towards a balance (ACTIVE_OBLIGATION), so a pre-cutover default would
// make every balance fixture silently read zero.
overrides.transaction_date ?? "2026-06-15",
overrides.description ?? "Test transaction",
overrides.amount ?? 100,
overrides.transaction_type ?? "debit",
+55
View File
@@ -666,3 +666,58 @@ describe("superseded duplicates are excluded but kept", () => {
).rejects.toThrow();
});
});
// Nothing before the cutover can be owed: carryover transaction 2348 already
// carries the entire pre-cutover balance as one figure. Splits on older
// transactions exist to describe how an expense was shared -- which keeps it
// out of spend -- without asserting a debt.
describe("the split cutover gates every balance", () => {
it("ignores a split on a transaction before the cutover", async () => {
const { ownerId, otherId } = await seedParticipants(pool);
const txId = await insertTransaction(pool, ownerId, {
amount: 100, transaction_date: "2026-01-08",
});
await pool.query(
`INSERT INTO transaction_splits (transaction_id, participant_id, share_percent) VALUES ($1, $2, 50)`,
[txId, otherId]
);
const balances = await getParticipantBalances(ownerId);
const bob = balances.find((b) => b.id === otherId);
expect(Number(bob?.total_owed ?? 0)).toBeCloseTo(0);
});
// Inclusive: transaction 2348, which carries the whole pre-cutover balance,
// is itself dated 2026-01-09. An exclusive bound would drop it.
it("counts a split dated exactly on the cutover", async () => {
const { ownerId, otherId } = await seedParticipants(pool);
const txId = await insertTransaction(pool, ownerId, {
amount: 100, transaction_date: "2026-01-09",
});
await pool.query(
`INSERT INTO transaction_splits (transaction_id, participant_id, share_percent) VALUES ($1, $2, 50)`,
[txId, otherId]
);
const balances = await getParticipantBalances(ownerId);
const bob = balances.find((b) => b.id === otherId);
expect(Number(bob!.total_owed)).toBeCloseTo(50);
});
// The point of the date guard: it does not depend on `settled` surviving.
it("still ignores a pre-cutover split whose settled flag was lost", async () => {
const { ownerId, otherId } = await seedParticipants(pool);
const txId = await insertTransaction(pool, ownerId, {
amount: 200, transaction_date: "2025-06-01",
});
await pool.query(
`INSERT INTO transaction_splits (transaction_id, participant_id, share_percent, settled)
VALUES ($1, $2, 50, false)`,
[txId, otherId]
);
const balances = await getParticipantBalances(ownerId);
const bob = balances.find((b) => b.id === otherId);
expect(Number(bob?.total_owed ?? 0)).toBeCloseTo(0);
});
});