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); }); });