Initial SMM panel implementation
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user