test(intake): the second review pass, and CLAUDE.md for the rules it set
ci / lint-test (push) Successful in 45s
ci / lint-test (push) Successful in 45s
Six of the nine fixes have a test that fails against the previous tree: the metadata row's currency, the Create-path hold, two concurrent batches booking once, mrkdwn escaping including plain_text, the escaped currency, and candidate-row currency. Three do not, and the reason is the same each time — the window needs a crash or a lost race that a test cannot force. Asserted instead: a stale metadataId falls back to the live row rather than reporting a phantom write; two writes racing to create a metadata row both resolve to one; the create path records a baseline at all. All three passed before as well.
This commit is contained in:
@@ -250,6 +250,35 @@ describe("PUT /api/transactions/[id]/line-items (C2)", () => {
|
||||
expect(second.previous_line_items).toEqual(items);
|
||||
});
|
||||
|
||||
it("a pinned row that no longer exists falls back to the live one, rather than reporting a phantom write", async () => {
|
||||
const txn = await seedOrder({ merchant: "Burgertory (Werribee)", date: "2026-08-19", amount: 22.4 });
|
||||
const live = (await pool.query(`SELECT id FROM expense_metadata WHERE transaction_id = $1`, [txn])).rows[0].id as number;
|
||||
const { replaceLineItems } = await import("../../lib/line-items");
|
||||
const r = await replaceLineItems(txn, items, { source: "intake", metadataId: live + 999 });
|
||||
expect(r.metadata_id).toBe(live);
|
||||
expect(await lineItemsOf(txn)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("two writes racing to create the metadata row both resolve to the same one", async () => {
|
||||
const txn = await seedOrder({ merchant: "Coles", date: "2026-08-18", amount: 85.74, withMeta: false });
|
||||
const both = await Promise.all([
|
||||
put(txn, { line_items: items, source: "intake", replace: true }),
|
||||
put(txn, { line_items: items, source: "intake", replace: true }),
|
||||
]);
|
||||
expect(both.map((r) => r.status)).toEqual([200, 200]);
|
||||
expect((await pool.query(`SELECT count(*)::int AS n FROM expense_metadata WHERE transaction_id = $1`, [txn])).rows[0].n).toBe(1);
|
||||
});
|
||||
|
||||
it("the create path records an undo baseline too, before it writes", async () => {
|
||||
const txn = await seedOrder({ merchant: "Coles", date: "2026-08-18", amount: 85.74, withMeta: false });
|
||||
const id = uuid();
|
||||
await pool.query(`INSERT INTO intake_batches (id, source, file_sha256, status) VALUES ($1::uuid, 'other', '{}', 'received')`, [id]);
|
||||
await put(txn, { line_items: items, source: "intake", batch_id: id, currency: "AUD", replace: true });
|
||||
// [] rather than NULL: there was nothing there, and "nothing" is the state
|
||||
// undo has to be able to restore.
|
||||
expect((await pool.query(`SELECT previous_line_items FROM intake_batches WHERE id = $1`, [id])).rows[0].previous_line_items).toEqual([]);
|
||||
});
|
||||
|
||||
it("the panel reads the row the write landed on, whichever order the rows were inserted in", async () => {
|
||||
const t = await seedOrder({ merchant: "UBER *EATS", date: "2026-08-19", amount: 22.4, withMeta: false });
|
||||
// Inserted first: the delivery receipt pointing at the statement line.
|
||||
@@ -670,6 +699,42 @@ describe("Slack taps (C5)", () => {
|
||||
const t = (await pool.query(`SELECT amount::text, amount_aud, foreign_currency_amount::text, foreign_currency_code FROM transactions`)).rows[0];
|
||||
expect(t).toEqual({ amount: "22.40", amount_aud: null, foreign_currency_amount: "22.40", foreign_currency_code: "USD" });
|
||||
expect(lastSlackUpdate().text).toMatch(/USD 22\.40/);
|
||||
// OrderDetails prefers expense_metadata.currency, so a hard-coded 'AUD'
|
||||
// here printed "$" on a USD order's receipt panel.
|
||||
expect((await pool.query(`SELECT currency FROM expense_metadata`)).rows[0].currency).toBe("USD");
|
||||
});
|
||||
|
||||
it("the hold applies to the Create tap's re-match, not only to the upload path", async () => {
|
||||
modelReply = { ...burgertory, subtotal: null, items: burgertory.items.map((i) => ({ ...i, unit_price: null, line_total: null })) };
|
||||
const id = uuid();
|
||||
const body = await (await intakePOST(multipart({ source: "uber_eats", batch_id: id }, [pdf]))).json();
|
||||
expect(body.status).toBe("candidates");
|
||||
|
||||
// Booked WITH its items between the card and the tap. The re-match walked
|
||||
// past the guard routeFood applies, so the same upload destroyed the same
|
||||
// receipt depending on which button reached it.
|
||||
const txn = await seedOrder({ merchant: "Burgertory (Werribee)", date: "2026-08-19", amount: 22.4 });
|
||||
const already = [{ qty: 1, description: "the itemised receipt already there", amount: 22.4 }];
|
||||
await pool.query(`UPDATE expense_metadata SET line_items = $2::jsonb WHERE transaction_id = $1`, [txn, JSON.stringify(already)]);
|
||||
|
||||
await slackPOST(slackPress("intake_create", { batch_id: id }));
|
||||
expect(await lineItemsOf(txn)).toEqual(already);
|
||||
expect(await batch(id)).toMatchObject({ status: "held" });
|
||||
expect((await pool.query(`SELECT count(*)::int AS n FROM transactions`)).rows[0].n).toBe(1);
|
||||
});
|
||||
|
||||
it("two DIFFERENT batches for one purchase, tapped together, book it once", async () => {
|
||||
const a = uuid();
|
||||
const b = uuid();
|
||||
await intakePOST(multipart({ source: "uber_eats", batch_id: a }, [pdf]));
|
||||
await intakePOST(multipart({ source: "uber_eats", batch_id: b }, [{ name: "shot.png", type: "image/png", bytes: Buffer.from("shot") }]));
|
||||
// Their source_refs differ, so ON CONFLICT cannot save this; both taps
|
||||
// clear their match and duplicate checks before either inserts.
|
||||
await Promise.all([slackPOST(slackPress("intake_create", { batch_id: a })), slackPOST(slackPress("intake_create", { batch_id: b }))]);
|
||||
const rows = (await pool.query(`SELECT id FROM transactions`)).rows;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect((await batch(a)).transaction_id).toBe(rows[0].id);
|
||||
expect((await batch(b)).transaction_id).toBe(rows[0].id);
|
||||
});
|
||||
|
||||
it("unknown shared_by falls back to the default owner — never a NULL owner", async () => {
|
||||
|
||||
Reference in New Issue
Block a user