ci / lint-test (push) Successful in 1m4s
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.
87 lines
4.5 KiB
TypeScript
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);
|
|
});
|
|
});
|