const assert = require('node:assert/strict'); const test = require('node:test'); const { closeDbPool, getDb } = require('../src/config/db'); const { chargeBalance, creditBalance, createDepositRequest, reviewDeposit, runReconciliation } = require('../src/services/wallet.service'); const createdUserIds = []; function unique(prefix) { return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`; } async function createUser(overrides = {}) { const db = getDb(); const username = unique('wallet_user'); const [id] = await db('users').insert({ username, email: `${username}@example.com`, password_hash: 'test_hash', balance: '0.0000', role: 'user', status: 'active', ...overrides }); createdUserIds.push(id); return id; } async function readUser(userId) { return getDb()('users').where({ id: userId }).first(); } async function cleanup() { if (createdUserIds.length === 0) { return; } const db = getDb(); await db('password_reset_tokens').whereIn('user_id', createdUserIds).del(); await db('refresh_tokens').whereIn('user_id', createdUserIds).del(); await db('idempotency_keys').whereIn('user_id', createdUserIds).del(); await db('deposits').whereIn('user_id', createdUserIds).orWhereIn('reviewed_by', createdUserIds).del(); await db('transactions').whereIn('user_id', createdUserIds).del(); await db('users').whereIn('id', createdUserIds).del(); } test.after(async () => { await cleanup(); await closeDbPool(); }); test('charge bigger than balance is rejected and leaves balance unchanged', async () => { const userId = await createUser(); await assert.rejects( () => chargeBalance(userId, '1.0000', unique('order_ref'), 'too expensive'), (error) => error.code === 'INSUFFICIENT_BALANCE' ); const user = await readUser(userId); const transactions = await getDb()('transactions').where({ user_id: userId }); assert.equal(user.balance, '0.0000'); assert.equal(transactions.length, 0); }); test('parallel charges on same balance allow only one spend when funds cover one order', async () => { const userId = await createUser(); await creditBalance(userId, '10.0000', unique('seed_ref'), 'seed balance'); const results = await Promise.allSettled([ chargeBalance(userId, '6.0000', unique('order_ref_a'), 'parallel A'), chargeBalance(userId, '6.0000', unique('order_ref_b'), 'parallel B') ]); const fulfilled = results.filter((result) => result.status === 'fulfilled'); const rejected = results.filter((result) => result.status === 'rejected'); const user = await readUser(userId); const orderTransactions = await getDb()('transactions').where({ user_id: userId, type: 'order' }); assert.equal(fulfilled.length, 1); assert.equal(rejected.length, 1); assert.equal(rejected[0].reason.code, 'INSUFFICIENT_BALANCE'); assert.equal(user.balance, '4.0000'); assert.equal(orderTransactions.length, 1); }); test('parallel approval of one deposit credits balance only once', async () => { const adminId = await createUser({ role: 'admin' }); const userId = await createUser(); const deposit = await createDepositRequest(userId, { amount: '15.0000', method: 'paypal', tx_ref: unique('tx_ref') }); const results = await Promise.allSettled([ reviewDeposit(deposit.id, adminId, 'approve', 'approved once'), reviewDeposit(deposit.id, adminId, 'approve', 'approved twice') ]); const fulfilled = results.filter((result) => result.status === 'fulfilled'); const rejected = results.filter((result) => result.status === 'rejected'); const user = await readUser(userId); const depositTransactions = await getDb()('transactions').where({ user_id: userId, type: 'deposit', reference: `deposit:${deposit.id}` }); assert.equal(fulfilled.length, 1); assert.equal(rejected.length, 1); assert.equal(rejected[0].reason.code, 'DEPOSIT_ALREADY_REVIEWED'); assert.equal(user.balance, '15.0000'); assert.equal(depositTransactions.length, 1); }); test('duplicate tx_ref is rejected without touching balance', async () => { const userId = await createUser(); const txRef = unique('duplicate_tx_ref'); await createDepositRequest(userId, { amount: '5.0000', method: 'paypal', tx_ref: txRef }); await assert.rejects( () => createDepositRequest(userId, { amount: '9.0000', method: 'paypal', tx_ref: txRef }), (error) => error.code === 'DUPLICATE_TX_REF' ); const user = await readUser(userId); const deposits = await getDb()('deposits').where({ user_id: userId, tx_ref: txRef }); assert.equal(user.balance, '0.0000'); assert.equal(deposits.length, 1); }); test('deposit with unavailable payment method is rejected before creating a request', async () => { const userId = await createUser(); const txRef = unique('missing_method_tx_ref'); await assert.rejects( () => createDepositRequest(userId, { amount: '5.0000', method: 'missing_method', tx_ref: txRef }), (error) => error.code === 'PAYMENT_METHOD_UNAVAILABLE' ); const deposits = await getDb()('deposits').where({ user_id: userId, tx_ref: txRef }); const user = await readUser(userId); assert.equal(deposits.length, 0); assert.equal(user.balance, '0.0000'); }); test('transaction sum remains equal to user balance after a mixed sequence', async () => { const userId = await createUser(); await creditBalance(userId, '20.0000', unique('credit_ref'), 'first credit'); await chargeBalance(userId, '7.2500', unique('order_ref'), 'order charge'); await creditBalance(userId, '2.1250', unique('refund_ref'), 'partial refund', 'refund'); await chargeBalance(userId, '4.8750', unique('order_ref'), 'second order'); const mismatches = await runReconciliation(); const ownMismatch = mismatches.find((row) => Number(row.userId) === Number(userId)); const [sumRow] = await getDb()('transactions').where({ user_id: userId }).sum({ total: 'amount' }); const user = await readUser(userId); assert.equal(ownMismatch, undefined); assert.equal(sumRow.total, user.balance); assert.equal(user.balance, '10.0000'); });