getPendingReconciliations filtered neither the manual side nor the candidate statement side on superseded_by_id. After the 2026-10-03 wallet cleanup the queue still showed duplicate half 7037 (superseded under 7038) beside its twin, with the same Digidirect statement line offered to both -- accepting it on the hidden row would have spent the line and stranded the real one. The candidate side could likewise offer one of the 31 superseded ANZ rows. findAutoMatches() always had both filters. First tests for this query; both fail against the old SQL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user