BillTracker/tests/paymentsQuickRoute.test.js

90 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
// X1 — POST /payments/quick must be idempotent under a double-clicked / retried
// "pay" and must apply the balance delta atomically with the INSERT. Before the
// fix, a second identical request created a duplicate payment AND double-applied
// the balance drop.
const test = require('node:test');
const assert = require('node:assert/strict');
const os = require('node:os');
const path = require('node:path');
const fs = require('node:fs');
const dbPath = path.join(os.tmpdir(), `bill-tracker-quickpay-${process.pid}.sqlite`);
process.env.DB_PATH = dbPath;
const { getDb, closeDb } = require('../db/database');
const router = require('../routes/payments');
function handler(method, routePath) {
const layer = router.stack.find(l => l.route?.path === routePath && l.route.methods[method]);
assert.ok(layer, `${method.toUpperCase()} ${routePath} exists`);
return layer.route.stack[layer.route.stack.length - 1].handle;
}
function call(method, routePath, { userId, params = {}, body = {} } = {}) {
const h = handler(method, routePath);
return new Promise((resolve) => {
const req = { params, body, query: {}, user: { id: userId, role: 'user' } };
const res = {
statusCode: 200,
status(c) { this.statusCode = c; return this; },
json(d) { resolve({ status: this.statusCode, data: d }); },
};
h(req, res);
});
}
let userId, billId;
test.before(() => {
const db = getDb();
userId = db.prepare("INSERT INTO users (username, password_hash, role, active) VALUES ('qp-user','x','user',1)").run().lastInsertRowid;
// $100 expected, $1,000 balance so we can observe the balance drop.
billId = db.prepare(
"INSERT INTO bills (user_id, name, due_day, expected_amount, current_balance, minimum_payment, interest_rate, active) VALUES (?, 'Card', 1, 10000, 100000, 5000, 0, 1)",
).run(userId).lastInsertRowid;
});
test.after(() => {
closeDb();
for (const s of ['', '-wal', '-shm']) { try { fs.unlinkSync(dbPath + s); } catch {} }
});
test('first quick pay creates the payment (201) and drops the balance once', async () => {
const { status, data } = await call('post', '/quick', {
userId, body: { bill_id: billId, amount: 100, paid_date: '2026-07-01' },
});
assert.equal(status, 201);
assert.equal(data.amount, 100, 'payment amount in dollars');
const db = getDb();
const count = db.prepare('SELECT COUNT(*) c FROM payments WHERE bill_id = ? AND deleted_at IS NULL').get(billId).c;
assert.equal(count, 1);
const bal = db.prepare('SELECT current_balance FROM bills WHERE id = ?').get(billId).current_balance;
assert.equal(bal, 90000, '1000.00 100.00 = 900.00 (cents)');
});
test('a duplicate quick pay (same bill+date+amount) is idempotent — no second row, balance unchanged', async () => {
const { status, data } = await call('post', '/quick', {
userId, body: { bill_id: billId, amount: 100, paid_date: '2026-07-01' },
});
assert.equal(status, 200, 'returns the existing payment, not a new 201');
assert.equal(data.amount, 100);
const db = getDb();
const count = db.prepare('SELECT COUNT(*) c FROM payments WHERE bill_id = ? AND deleted_at IS NULL').get(billId).c;
assert.equal(count, 1, 'still exactly one payment');
const bal = db.prepare('SELECT current_balance FROM bills WHERE id = ?').get(billId).current_balance;
assert.equal(bal, 90000, 'balance NOT dropped a second time');
});
test('a different amount on the same day is a legitimate new payment', async () => {
const { status } = await call('post', '/quick', {
userId, body: { bill_id: billId, amount: 50, paid_date: '2026-07-01' },
});
assert.equal(status, 201);
const db = getDb();
const count = db.prepare('SELECT COUNT(*) c FROM payments WHERE bill_id = ? AND deleted_at IS NULL').get(billId).c;
assert.equal(count, 2);
const bal = db.prepare('SELECT current_balance FROM bills WHERE id = ?').get(billId).current_balance;
assert.equal(bal, 85000, '900.00 50.00 = 850.00 (cents)');
});