Files
finance-app/src/__tests__/integration/reconcile-queue.test.ts
T
siddharthd 069034951e
ci / lint-test (push) Successful in 1m4s
reconcile queue: offer a foreign row's exact native twin regardless of provisional AUD
Wallet INR taps were priced at a provisional 0.0180 AUD/INR; CommBank charged
~0.0149, so every rupee twin sat ~20% outside the picker's +-5% band. The
auto-matcher pairs on the native figure but refuses ambiguity (INR 112 twice in
three days, INR 175 three times), which left those rows offered nothing at all.
2026-10-11 00:35:50 +11:00

87 lines
4.5 KiB
TypeScript

import { describe, it, expect, beforeAll, beforeEach } from "vitest";
import { Pool } from "pg";
import { createPool, resetDB, seedParticipants, insertTransaction } from "./helpers";
import { getPendingReconciliations } from "../../lib/queries";
/**
* The reconcile queue must not offer what is already resolved.
*
* A superseded row is hidden from every figure (EXCLUDE_RECONCILED_SOURCE), so
* listing it is not merely noise: on 2026-10-03 the queue showed the duplicate
* wallet half 7037, superseded under its twin 7038, as a second "Digidirect"
* with the same statement line offered as its match. Accepting it there would
* have spent that line on a hidden row and left the real one unmatched forever.
* findAutoMatches() always had both filters; this query had neither.
*/
let pool: Pool;
let ownerId: number;
let acctSeq = 0;
async function statementLine(amount: number, date: string, description: string) {
const st = await pool.query(
`INSERT INTO statements (filename, account_number, bank_name, billing_start_date, billing_end_date, owner_id)
VALUES ('q.pdf', $3, 'Test Bank', $1::date - 20, $1::date + 10, $2) RETURNING id`,
[date, ownerId, `5555-0000-0000-${String(1000 + acctSeq++)}`]
);
const t = await pool.query(
`INSERT INTO transactions (statement_id, transaction_date, description, amount, transaction_type, owner_id)
VALUES ($1, $2, $3, $4, 'debit', $5) RETURNING id`,
[st.rows[0].id, date, description, amount, ownerId]
);
return t.rows[0].id as number;
}
beforeAll(() => { pool = createPool(); });
beforeEach(async () => {
await resetDB(pool);
({ ownerId } = await seedParticipants(pool));
});
describe("getPendingReconciliations", () => {
it("does not list a superseded manual row", async () => {
const survivor = await insertTransaction(pool, ownerId, { description: "DIGIDIRECT", amount: 1180.05, transaction_date: "2026-08-25" });
const hidden = await insertTransaction(pool, ownerId, { description: "Digidirect", amount: 1180.05, transaction_date: "2026-08-25" });
await pool.query(`UPDATE transactions SET superseded_by_id = $1 WHERE id = $2`, [survivor, hidden]);
const line = await statementLine(1180.05, "2026-08-25", "SP DIGIDIRECT STRATHFIELD");
const queue = await getPendingReconciliations(ownerId);
const ids = queue.map((r) => r.id);
expect(ids).toContain(survivor);
expect(ids).not.toContain(hidden);
// The survivor is still offered its line.
const row = queue.find((r) => r.id === survivor)!;
expect(row.matches.map((m) => m.id)).toContain(line);
});
it("does not offer a superseded statement row as a match", async () => {
const manual = await insertTransaction(pool, ownerId, { description: "Grocer", amount: 42.0, transaction_date: "2026-08-10" });
const live = await statementLine(42.0, "2026-08-10", "GROCER");
const dupe = await statementLine(42.0, "2026-08-10", "GROCER");
await pool.query(`UPDATE transactions SET superseded_by_id = $1 WHERE id = $2`, [live, dupe]);
const [row] = (await getPendingReconciliations(ownerId)).filter((r) => r.id === manual);
const offered = row.matches.map((m) => m.id);
expect(offered).toContain(live);
expect(offered).not.toContain(dupe);
});
it("offers the exact native twin of a foreign row even when its provisional AUD is far off", async () => {
// Live shape, 2026-10-09: wallet INR 112 at a provisional A$2.01; CommBank
// charged A$1.68 -- 16% under, outside the +-5% band.
const manual = await insertTransaction(pool, ownerId, { description: "SWIGGY LIMITED", amount: 2.01, transaction_date: "2026-10-04" });
await pool.query(`UPDATE transactions SET foreign_currency_code = 'INR', foreign_currency_amount = 112 WHERE id = $1`, [manual]);
const twin = await statementLine(1.68, "2026-10-04", "Swiggy Limited Bengaluru");
const otherRupees = await statementLine(1.68, "2026-10-04", "Swiggy Limited Bengaluru");
await pool.query(`UPDATE transactions SET foreign_currency_code = 'INR', foreign_currency_amount = 112 WHERE id = $1`, [twin]);
await pool.query(`UPDATE transactions SET foreign_currency_code = 'INR', foreign_currency_amount = 113 WHERE id = $1`, [otherRupees]);
const [row] = (await getPendingReconciliations(ownerId)).filter((r) => r.id === manual);
const offered = row.matches.map((m) => m.id);
expect(offered).toContain(twin);
// A different native figure at the same AUD is not admitted by this path.
expect(offered).not.toContain(otherRupees);
});
});