Initial SMM panel implementation
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const { createApp } = require('../src/app');
|
||||
const { closeDbPool, getDb } = require('../src/config/db');
|
||||
const { validateEnv } = require('../src/config/env');
|
||||
|
||||
const createdUserIds = [];
|
||||
const createdServiceIds = [];
|
||||
const createdOrderIds = [];
|
||||
const createdPaymentMethodIds = [];
|
||||
|
||||
function unique(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function listen(app) {
|
||||
return new Promise((resolve) => {
|
||||
const server = app.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
baseUrl: `http://127.0.0.1:${server.address().port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function request(baseUrl, path, options = {}) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
const body = await response.json().catch(() => ({}));
|
||||
|
||||
return { response, body };
|
||||
}
|
||||
|
||||
async function createAccount(baseUrl, prefix) {
|
||||
const account = {
|
||||
username: unique(prefix),
|
||||
email: `${unique(prefix)}@example.com`,
|
||||
password: 'StrongPass123'
|
||||
};
|
||||
const registered = await request(baseUrl, '/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(account)
|
||||
});
|
||||
|
||||
assert.equal(registered.response.status, 201);
|
||||
createdUserIds.push(registered.body.user.id);
|
||||
|
||||
return {
|
||||
...account,
|
||||
id: registered.body.user.id
|
||||
};
|
||||
}
|
||||
|
||||
async function createDbAccount(prefix, overrides = {}) {
|
||||
const db = getDb();
|
||||
const password = 'StrongPass123';
|
||||
const account = {
|
||||
username: unique(prefix),
|
||||
email: `${unique(prefix)}@example.com`,
|
||||
password
|
||||
};
|
||||
const [id] = await db('users').insert({
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
password_hash: await bcrypt.hash(password, 12),
|
||||
balance: '0.0000',
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
...overrides
|
||||
});
|
||||
|
||||
createdUserIds.push(id);
|
||||
|
||||
return {
|
||||
...account,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
async function login(baseUrl, account) {
|
||||
const loggedIn = await request(baseUrl, '/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: account.email,
|
||||
password: account.password
|
||||
})
|
||||
});
|
||||
|
||||
assert.equal(loggedIn.response.status, 200);
|
||||
return loggedIn.body.accessToken;
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
const db = getDb();
|
||||
|
||||
if (createdOrderIds.length > 0) {
|
||||
await db('orders').whereIn('id', createdOrderIds).del();
|
||||
}
|
||||
|
||||
if (createdServiceIds.length > 0) {
|
||||
await db('services').whereIn('id', createdServiceIds).del();
|
||||
}
|
||||
|
||||
if (createdPaymentMethodIds.length > 0) {
|
||||
await db('payment_methods').whereIn('id', createdPaymentMethodIds).del();
|
||||
}
|
||||
|
||||
if (createdUserIds.length > 0) {
|
||||
await db('password_reset_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('refresh_tokens').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();
|
||||
}
|
||||
|
||||
await closeDbPool();
|
||||
});
|
||||
|
||||
test('admin users and stats work, while normal users receive 403', async () => {
|
||||
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
||||
const api = await listen(createApp());
|
||||
|
||||
try {
|
||||
const db = getDb();
|
||||
const admin = await createAccount(api.baseUrl, 'admin_test_admin');
|
||||
const user = await createAccount(api.baseUrl, 'admin_test_user');
|
||||
|
||||
await db('users').where({ id: admin.id }).update({ role: 'admin' });
|
||||
|
||||
const [serviceId] = await db('services').insert({
|
||||
provider_service_id: unique('admin_provider_service'),
|
||||
name: 'Admin Test Service',
|
||||
category: 'Admin',
|
||||
provider_price: '4.0000',
|
||||
markup_percent: '25.0000',
|
||||
sell_price: '5.0000',
|
||||
min: 100,
|
||||
max: 10000,
|
||||
is_active: true
|
||||
});
|
||||
createdServiceIds.push(serviceId);
|
||||
|
||||
const [orderId] = await db('orders').insert({
|
||||
user_id: user.id,
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/admin-order',
|
||||
quantity: 1000,
|
||||
charge: '5.0000',
|
||||
provider_order_id: unique('admin_provider_order'),
|
||||
status: 'completed'
|
||||
});
|
||||
createdOrderIds.push(orderId);
|
||||
|
||||
const adminToken = await login(api.baseUrl, admin);
|
||||
const userToken = await login(api.baseUrl, user);
|
||||
|
||||
const users = await request(api.baseUrl, '/admin/users', {
|
||||
headers: { authorization: `Bearer ${adminToken}` }
|
||||
});
|
||||
assert.equal(users.response.status, 200);
|
||||
assert.equal(users.body.users.some((row) => Number(row.id) === Number(user.id)), true);
|
||||
|
||||
const stats = await request(api.baseUrl, '/admin/stats', {
|
||||
headers: { authorization: `Bearer ${adminToken}` }
|
||||
});
|
||||
assert.equal(stats.response.status, 200);
|
||||
assert.equal(stats.body.stats.gross_sales, '5.0000');
|
||||
assert.equal(stats.body.stats.provider_cost, '4.0000');
|
||||
assert.equal(stats.body.stats.profit, '1.0000');
|
||||
|
||||
const adjusted = await request(api.baseUrl, `/admin/users/${user.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
body: JSON.stringify({
|
||||
amount: '3.0000',
|
||||
note: 'Admin test credit'
|
||||
})
|
||||
});
|
||||
assert.equal(adjusted.response.status, 200);
|
||||
assert.equal(adjusted.body.user.balance, '3.0000');
|
||||
assert.equal(adjusted.body.wallet.balance, '3.0000');
|
||||
|
||||
const banned = await request(api.baseUrl, `/admin/users/${user.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
body: JSON.stringify({ status: 'banned' })
|
||||
});
|
||||
assert.equal(banned.response.status, 200);
|
||||
assert.equal(banned.body.user.status, 'banned');
|
||||
|
||||
const forbiddenChecks = [
|
||||
request(api.baseUrl, '/admin/users', { headers: { authorization: `Bearer ${userToken}` } }),
|
||||
request(api.baseUrl, '/admin/deposits', { headers: { authorization: `Bearer ${userToken}` } }),
|
||||
request(api.baseUrl, '/admin/orders', { headers: { authorization: `Bearer ${userToken}` } }),
|
||||
request(api.baseUrl, '/admin/services', { headers: { authorization: `Bearer ${userToken}` } }),
|
||||
request(api.baseUrl, '/admin/payment-methods', { headers: { authorization: `Bearer ${userToken}` } }),
|
||||
request(api.baseUrl, '/admin/stats', { headers: { authorization: `Bearer ${userToken}` } }),
|
||||
request(api.baseUrl, `/admin/users/${user.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${userToken}` },
|
||||
body: JSON.stringify({ status: 'active' })
|
||||
}),
|
||||
request(api.baseUrl, '/admin/services/sync', {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${userToken}` },
|
||||
body: JSON.stringify({})
|
||||
}),
|
||||
request(api.baseUrl, '/admin/deposits/1', {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${userToken}` },
|
||||
body: JSON.stringify({ action: 'reject' })
|
||||
}),
|
||||
request(api.baseUrl, `/admin/services/${serviceId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${userToken}` },
|
||||
body: JSON.stringify({ is_active: false })
|
||||
}),
|
||||
request(api.baseUrl, '/admin/payment-methods', {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${userToken}` },
|
||||
body: JSON.stringify({
|
||||
code: unique('normal_forbidden'),
|
||||
label_ar: 'Forbidden',
|
||||
instructions: 'Forbidden payment method'
|
||||
})
|
||||
})
|
||||
];
|
||||
const forbiddenResponses = await Promise.all(forbiddenChecks);
|
||||
|
||||
assert.equal(forbiddenResponses.every((result) => result.response.status === 403), true);
|
||||
} finally {
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('admin can manage payment methods and active list hides disabled methods', async () => {
|
||||
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
||||
const api = await listen(createApp());
|
||||
|
||||
try {
|
||||
const db = getDb();
|
||||
const admin = await createAccount(api.baseUrl, 'payment_admin');
|
||||
const user = await createAccount(api.baseUrl, 'payment_user');
|
||||
|
||||
await db('users').where({ id: admin.id }).update({ role: 'admin' });
|
||||
|
||||
const adminToken = await login(api.baseUrl, admin);
|
||||
const userToken = await login(api.baseUrl, user);
|
||||
const code = unique('payment_method').replace(/[^a-z0-9_]/g, '_').slice(0, 30);
|
||||
|
||||
const created = await request(api.baseUrl, '/admin/payment-methods', {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
label_ar: 'اختبار دفع',
|
||||
instructions: 'تعليمات قناة الدفع الاختبارية',
|
||||
destination: 'test@example.com',
|
||||
sort_order: 999
|
||||
})
|
||||
});
|
||||
assert.equal(created.response.status, 201);
|
||||
createdPaymentMethodIds.push(created.body.paymentMethod.id);
|
||||
|
||||
const active = await request(api.baseUrl, '/payment-methods', {
|
||||
headers: { authorization: `Bearer ${userToken}` }
|
||||
});
|
||||
assert.equal(active.response.status, 200);
|
||||
assert.equal(active.body.paymentMethods.some((method) => method.code === code), true);
|
||||
|
||||
const disabled = await request(api.baseUrl, `/admin/payment-methods/${created.body.paymentMethod.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
body: JSON.stringify({ is_active: false })
|
||||
});
|
||||
assert.equal(disabled.response.status, 200);
|
||||
assert.equal(disabled.body.paymentMethod.is_active, 0);
|
||||
|
||||
const hidden = await request(api.baseUrl, '/payment-methods', {
|
||||
headers: { authorization: `Bearer ${userToken}` }
|
||||
});
|
||||
assert.equal(hidden.response.status, 200);
|
||||
assert.equal(hidden.body.paymentMethods.some((method) => method.code === code), false);
|
||||
} finally {
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('admin user update refuses self-ban and blocks rate-limited sensitive routes', async () => {
|
||||
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
||||
const api = await listen(createApp());
|
||||
|
||||
try {
|
||||
const admin = await createDbAccount('security_admin', { role: 'admin' });
|
||||
const user = await createDbAccount('security_user');
|
||||
|
||||
const adminToken = await login(api.baseUrl, admin);
|
||||
const userToken = await login(api.baseUrl, user);
|
||||
|
||||
const selfBan = await request(api.baseUrl, `/admin/users/${admin.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { authorization: `Bearer ${adminToken}` },
|
||||
body: JSON.stringify({ status: 'banned' })
|
||||
});
|
||||
assert.equal(selfBan.response.status, 400);
|
||||
assert.equal(selfBan.body.error.code, 'ADMIN_SELF_BAN_FORBIDDEN');
|
||||
|
||||
const forgotResponses = [];
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
forgotResponses.push(await request(api.baseUrl, '/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: `missing_${index}@example.com` })
|
||||
}));
|
||||
}
|
||||
assert.equal(forgotResponses.slice(0, 5).every((result) => result.response.status === 200), true);
|
||||
assert.equal(forgotResponses[5].response.status, 429);
|
||||
|
||||
const depositResponses = [];
|
||||
for (let index = 0; index < 11; index += 1) {
|
||||
depositResponses.push(await request(api.baseUrl, '/deposits', {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${userToken}` },
|
||||
body: JSON.stringify({
|
||||
amount: '1.0000',
|
||||
method: 'paypal',
|
||||
tx_ref: unique(`rate_limited_deposit_${index}`)
|
||||
})
|
||||
}));
|
||||
}
|
||||
assert.equal(depositResponses.slice(0, 10).every((result) => result.response.status === 201), true);
|
||||
assert.equal(depositResponses[10].response.status, 429);
|
||||
} finally {
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('production environment requires CORS_ORIGIN', () => {
|
||||
const previous = {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
PROVIDER_API_URL: process.env.PROVIDER_API_URL,
|
||||
PROVIDER_API_KEY: process.env.PROVIDER_API_KEY,
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN
|
||||
};
|
||||
|
||||
try {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.DATABASE_URL = 'mysql://smm:smm_password@127.0.0.1:3306/smm';
|
||||
process.env.JWT_SECRET = 'a'.repeat(32);
|
||||
process.env.PROVIDER_API_URL = 'https://provider.real/api/v2';
|
||||
process.env.PROVIDER_API_KEY = 'real_provider_key';
|
||||
delete process.env.CORS_ORIGIN;
|
||||
|
||||
assert.throws(
|
||||
() => validateEnv(),
|
||||
/Missing required environment variables: CORS_ORIGIN/
|
||||
);
|
||||
|
||||
process.env.CORS_ORIGIN = 'https://panel.example.com';
|
||||
assert.doesNotThrow(() => validateEnv());
|
||||
} finally {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const test = require('node:test');
|
||||
|
||||
const { createApp } = require('../src/app');
|
||||
const { closeDbPool, getDb } = require('../src/config/db');
|
||||
|
||||
const createdUserIds = [];
|
||||
const createdServiceIds = [];
|
||||
let providerMock;
|
||||
|
||||
function unique(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function startFailingProviderMock() {
|
||||
const server = http.createServer(async (_req, res) => {
|
||||
res.setHeader('content-type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'forced provider failure' }));
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${server.address().port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listen(app) {
|
||||
return new Promise((resolve) => {
|
||||
const server = app.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
baseUrl: `http://127.0.0.1:${server.address().port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function request(baseUrl, path, options = {}) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
const body = await response.json().catch(() => ({}));
|
||||
|
||||
return { response, body };
|
||||
}
|
||||
|
||||
async function register(baseUrl, prefix) {
|
||||
const account = {
|
||||
username: unique(prefix),
|
||||
email: `${unique(prefix)}@example.com`,
|
||||
password: 'StrongPass123'
|
||||
};
|
||||
const result = await request(baseUrl, '/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(account)
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 201);
|
||||
createdUserIds.push(result.body.user.id);
|
||||
|
||||
return {
|
||||
...account,
|
||||
id: result.body.user.id
|
||||
};
|
||||
}
|
||||
|
||||
async function login(baseUrl, account) {
|
||||
const result = await request(baseUrl, '/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: account.email,
|
||||
password: account.password
|
||||
})
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 200);
|
||||
return result.body.accessToken;
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
providerMock = await startFailingProviderMock();
|
||||
process.env.PROVIDER_API_URL = providerMock.url;
|
||||
process.env.PROVIDER_API_KEY = 'mock-key';
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
const db = getDb();
|
||||
|
||||
if (createdUserIds.length > 0) {
|
||||
await db('orders').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('refresh_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('password_reset_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('users').whereIn('id', createdUserIds).del();
|
||||
}
|
||||
|
||||
if (createdServiceIds.length > 0) {
|
||||
await db('services').whereIn('id', createdServiceIds).del();
|
||||
}
|
||||
|
||||
await closeDbPool();
|
||||
await providerMock.close();
|
||||
});
|
||||
|
||||
test('full flow refunds wallet when provider order fails', async () => {
|
||||
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_secret';
|
||||
const api = await listen(createApp());
|
||||
|
||||
try {
|
||||
const db = getDb();
|
||||
const user = await register(api.baseUrl, 'e2e_user');
|
||||
const admin = await register(api.baseUrl, 'e2e_admin');
|
||||
await db('users').where({ id: admin.id }).update({ role: 'admin' });
|
||||
|
||||
const userToken = await login(api.baseUrl, user);
|
||||
const adminToken = await login(api.baseUrl, admin);
|
||||
const userAuth = { authorization: `Bearer ${userToken}` };
|
||||
const adminAuth = { authorization: `Bearer ${adminToken}` };
|
||||
|
||||
const deposit = await request(api.baseUrl, '/deposits', {
|
||||
method: 'POST',
|
||||
headers: userAuth,
|
||||
body: JSON.stringify({
|
||||
amount: '10.0000',
|
||||
method: 'paypal',
|
||||
tx_ref: unique('e2e_tx')
|
||||
})
|
||||
});
|
||||
assert.equal(deposit.response.status, 201);
|
||||
|
||||
const approved = await request(api.baseUrl, `/admin/deposits/${deposit.body.deposit.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: adminAuth,
|
||||
body: JSON.stringify({ action: 'approve' })
|
||||
});
|
||||
assert.equal(approved.response.status, 200);
|
||||
|
||||
const [serviceId] = await db('services').insert({
|
||||
provider_service_id: unique('e2e_provider_service'),
|
||||
name: 'E2E Failure Service',
|
||||
category: 'E2E',
|
||||
provider_price: '2.0000',
|
||||
markup_percent: '25.0000',
|
||||
sell_price: '2.5000',
|
||||
min: 100,
|
||||
max: 10000,
|
||||
is_active: true
|
||||
});
|
||||
createdServiceIds.push(serviceId);
|
||||
|
||||
const failedOrder = await request(api.baseUrl, '/orders', {
|
||||
method: 'POST',
|
||||
headers: userAuth,
|
||||
body: JSON.stringify({
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/e2e-failure',
|
||||
quantity: 1000
|
||||
})
|
||||
});
|
||||
assert.equal(failedOrder.response.status, 502);
|
||||
|
||||
const me = await request(api.baseUrl, '/me', { headers: userAuth });
|
||||
const orders = await request(api.baseUrl, '/orders', { headers: userAuth });
|
||||
const transactions = await request(api.baseUrl, '/transactions', { headers: userAuth });
|
||||
|
||||
assert.equal(me.body.user.balance, '10.0000');
|
||||
assert.equal(orders.body.orders.some((order) => order.status === 'failed' && order.charge === '2.5000'), true);
|
||||
assert.equal(transactions.body.transactions.some((transaction) => transaction.type === 'refund' && transaction.amount === '2.5000'), true);
|
||||
} finally {
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const test = require('node:test');
|
||||
|
||||
const { closeDbPool, getDb } = require('../src/config/db');
|
||||
const { syncOrderStatuses } = require('../src/services/orders.service');
|
||||
|
||||
const createdUserIds = [];
|
||||
const createdServiceIds = [];
|
||||
let providerMock;
|
||||
const statuses = new Map();
|
||||
|
||||
function unique(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function startProviderMock() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const chunks = [];
|
||||
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const body = new URLSearchParams(Buffer.concat(chunks).toString());
|
||||
const orderId = body.get('order');
|
||||
const payload = statuses.get(orderId) || { status: 'Processing', remains: 10 };
|
||||
|
||||
res.setHeader('content-type', 'application/json');
|
||||
|
||||
if (body.get('action') !== 'status') {
|
||||
res.end(JSON.stringify({ error: 'unsupported action' }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.end(JSON.stringify(payload));
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${server.address().port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createUser(balance = '0.0000') {
|
||||
const username = unique('status_user');
|
||||
const [id] = await getDb()('users').insert({
|
||||
username,
|
||||
email: `${username}@example.com`,
|
||||
password_hash: 'hash',
|
||||
balance
|
||||
});
|
||||
|
||||
createdUserIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function createService() {
|
||||
const [id] = await getDb()('services').insert({
|
||||
provider_service_id: unique('status_provider_service'),
|
||||
name: 'Status Test Service',
|
||||
category: 'Status',
|
||||
provider_price: '2.0000',
|
||||
markup_percent: '25.0000',
|
||||
sell_price: '2.5000',
|
||||
min: 10,
|
||||
max: 10000,
|
||||
is_active: true
|
||||
});
|
||||
|
||||
createdServiceIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function createTrackedOrder(userId, serviceId, providerOrderId, status = 'processing') {
|
||||
const [id] = await getDb()('orders').insert({
|
||||
user_id: userId,
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/status-order',
|
||||
quantity: 1000,
|
||||
charge: '2.5000',
|
||||
provider_order_id: providerOrderId,
|
||||
status
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
providerMock = await startProviderMock();
|
||||
process.env.PROVIDER_API_URL = providerMock.url;
|
||||
process.env.PROVIDER_API_KEY = 'mock-key';
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
const db = getDb();
|
||||
|
||||
if (createdUserIds.length > 0) {
|
||||
await db('orders').whereIn('user_id', createdUserIds).del();
|
||||
await db('transactions').whereIn('user_id', createdUserIds).del();
|
||||
await db('users').whereIn('id', createdUserIds).del();
|
||||
}
|
||||
|
||||
if (createdServiceIds.length > 0) {
|
||||
await db('services').whereIn('id', createdServiceIds).del();
|
||||
}
|
||||
|
||||
await closeDbPool();
|
||||
await providerMock.close();
|
||||
});
|
||||
|
||||
test('syncOrderStatuses updates completed orders with remains and provider charge', async () => {
|
||||
const userId = await createUser();
|
||||
const serviceId = await createService();
|
||||
const providerOrderId = unique('provider_completed');
|
||||
const orderId = await createTrackedOrder(userId, serviceId, providerOrderId);
|
||||
|
||||
statuses.set(providerOrderId, {
|
||||
status: 'Completed',
|
||||
charge: '1.7500',
|
||||
start_count: 100,
|
||||
remains: 0
|
||||
});
|
||||
|
||||
const result = await syncOrderStatuses();
|
||||
const order = await getDb()('orders').where({ id: orderId }).first();
|
||||
|
||||
assert.equal(result.updated >= 1, true);
|
||||
assert.equal(order.status, 'completed');
|
||||
assert.equal(order.start_count, 100);
|
||||
assert.equal(order.remains, 0);
|
||||
assert.equal(order.provider_charge, '1.7500');
|
||||
assert.ok(order.last_status_check_at);
|
||||
});
|
||||
|
||||
test('partial status is recorded without automatic wallet refund', async () => {
|
||||
const userId = await createUser('7.5000');
|
||||
const serviceId = await createService();
|
||||
const providerOrderId = unique('provider_partial');
|
||||
const orderId = await createTrackedOrder(userId, serviceId, providerOrderId);
|
||||
|
||||
statuses.set(providerOrderId, {
|
||||
status: 'Partial',
|
||||
charge: '1.0000',
|
||||
start_count: 55,
|
||||
remains: 400
|
||||
});
|
||||
|
||||
await syncOrderStatuses();
|
||||
|
||||
const order = await getDb()('orders').where({ id: orderId }).first();
|
||||
const user = await getDb()('users').where({ id: userId }).first();
|
||||
const refunds = await getDb()('transactions').where({ user_id: userId, type: 'refund' });
|
||||
|
||||
assert.equal(order.status, 'partial');
|
||||
assert.equal(order.remains, 400);
|
||||
assert.equal(user.balance, '7.5000');
|
||||
assert.equal(refunds.length, 0);
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const test = require('node:test');
|
||||
|
||||
const { closeDbPool, getDb } = require('../src/config/db');
|
||||
const {
|
||||
calculateOrderCharge,
|
||||
createOrder,
|
||||
getUserOrder,
|
||||
listUserOrders
|
||||
} = require('../src/services/orders.service');
|
||||
const { creditBalance } = require('../src/services/wallet.service');
|
||||
|
||||
const createdUserIds = [];
|
||||
const createdServiceIds = [];
|
||||
let providerCalls = [];
|
||||
let providerMode = 'success';
|
||||
let providerMock;
|
||||
|
||||
function unique(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function startProviderMock() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const chunks = [];
|
||||
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const body = new URLSearchParams(Buffer.concat(chunks).toString());
|
||||
providerCalls.push(Object.fromEntries(body.entries()));
|
||||
res.setHeader('content-type', 'application/json');
|
||||
|
||||
if (body.get('action') !== 'add') {
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'unsupported action' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (providerMode === 'failure') {
|
||||
res.end(JSON.stringify({ error: 'provider rejected order' }));
|
||||
return;
|
||||
}
|
||||
|
||||
res.end(JSON.stringify({ order: `provider-${Date.now()}` }));
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${server.address().port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const db = getDb();
|
||||
const username = unique('order_user');
|
||||
const [id] = await db('users').insert({
|
||||
username,
|
||||
email: `${username}@example.com`,
|
||||
password_hash: 'hash',
|
||||
balance: '0.0000'
|
||||
});
|
||||
|
||||
createdUserIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function createService(overrides = {}) {
|
||||
const [id] = await getDb()('services').insert({
|
||||
provider_service_id: unique('provider_service'),
|
||||
name: 'Instagram Followers',
|
||||
category: 'Instagram',
|
||||
provider_price: '4.0000',
|
||||
markup_percent: '25.0000',
|
||||
sell_price: '5.0000',
|
||||
min: 100,
|
||||
max: 10000,
|
||||
is_active: true,
|
||||
...overrides
|
||||
});
|
||||
|
||||
createdServiceIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
async function readUser(userId) {
|
||||
return getDb()('users').where({ id: userId }).first();
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
providerMock = await startProviderMock();
|
||||
process.env.PROVIDER_API_URL = providerMock.url;
|
||||
process.env.PROVIDER_API_KEY = 'mock-key';
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
const db = getDb();
|
||||
|
||||
if (createdUserIds.length > 0) {
|
||||
await db('orders').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('password_reset_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('refresh_tokens').whereIn('user_id', createdUserIds).del();
|
||||
await db('users').whereIn('id', createdUserIds).del();
|
||||
}
|
||||
|
||||
if (createdServiceIds.length > 0) {
|
||||
await db('services').whereIn('id', createdServiceIds).del();
|
||||
}
|
||||
|
||||
await closeDbPool();
|
||||
await providerMock.close();
|
||||
});
|
||||
|
||||
test('calculateOrderCharge uses server sell_price per 1000', () => {
|
||||
assert.equal(calculateOrderCharge('5.0000', 1000), '5.0000');
|
||||
assert.equal(calculateOrderCharge('5.0000', 250), '1.2500');
|
||||
assert.equal(calculateOrderCharge('1.3333', 3), '0.0040');
|
||||
});
|
||||
|
||||
test('successful order charges wallet, creates order, and sends provider order', async () => {
|
||||
providerMode = 'success';
|
||||
providerCalls = [];
|
||||
const userId = await createUser();
|
||||
const serviceId = await createService({ provider_service_id: 'provider-success-test' });
|
||||
|
||||
await creditBalance(userId, '20.0000', unique('seed_order_success'), 'seed balance');
|
||||
|
||||
const order = await createOrder(userId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/post/1',
|
||||
quantity: 1500
|
||||
});
|
||||
const user = await readUser(userId);
|
||||
const transactions = await getDb()('transactions').where({ user_id: userId, type: 'order', reference: `order:${order.id}` });
|
||||
|
||||
assert.equal(order.status, 'processing');
|
||||
assert.equal(order.charge, '7.5000');
|
||||
assert.equal(user.balance, '12.5000');
|
||||
assert.equal(transactions.length, 1);
|
||||
assert.equal(providerCalls.length, 1);
|
||||
assert.equal(providerCalls[0].service, 'provider-success-test');
|
||||
assert.equal(providerCalls[0].quantity, '1500');
|
||||
});
|
||||
|
||||
test('failed provider order refunds wallet and marks order failed', async () => {
|
||||
providerMode = 'failure';
|
||||
providerCalls = [];
|
||||
const userId = await createUser();
|
||||
const serviceId = await createService({ provider_service_id: 'provider-failure-test' });
|
||||
|
||||
await creditBalance(userId, '10.0000', unique('seed_order_failure'), 'seed balance');
|
||||
|
||||
await assert.rejects(
|
||||
() => createOrder(userId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/post/2',
|
||||
quantity: 1000
|
||||
}),
|
||||
(error) => error.code === 'PROVIDER_ERROR'
|
||||
);
|
||||
|
||||
const user = await readUser(userId);
|
||||
const [order] = await getDb()('orders').where({ user_id: userId, service_id: serviceId });
|
||||
const refund = await getDb()('transactions').where({ user_id: userId, type: 'refund', reference: `refund:order:${order.id}` }).first();
|
||||
|
||||
assert.equal(order.status, 'failed');
|
||||
assert.equal(user.balance, '10.0000');
|
||||
assert.equal(refund.amount, '5.0000');
|
||||
});
|
||||
|
||||
test('users can list and read only their own orders', async () => {
|
||||
providerMode = 'success';
|
||||
const ownerId = await createUser();
|
||||
const otherUserId = await createUser();
|
||||
const serviceId = await createService({ provider_service_id: 'provider-visibility-test' });
|
||||
|
||||
await creditBalance(ownerId, '10.0000', unique('seed_owner'), 'seed owner');
|
||||
await creditBalance(otherUserId, '10.0000', unique('seed_other'), 'seed other');
|
||||
|
||||
const ownerOrder = await createOrder(ownerId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/owner',
|
||||
quantity: 1000
|
||||
});
|
||||
await createOrder(otherUserId, {
|
||||
service_id: serviceId,
|
||||
link: 'https://example.com/other',
|
||||
quantity: 1000
|
||||
});
|
||||
|
||||
const ownerOrders = await listUserOrders(ownerId);
|
||||
|
||||
assert.equal(ownerOrders.some((order) => order.id === ownerOrder.id), true);
|
||||
assert.equal(ownerOrders.every((order) => Number(order.user_id) === Number(ownerId)), true);
|
||||
await assert.rejects(
|
||||
() => getUserOrder(otherUserId, ownerOrder.id),
|
||||
(error) => error.code === 'ORDER_NOT_FOUND'
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const test = require('node:test');
|
||||
|
||||
const { closeDbPool, getDb } = require('../src/config/db');
|
||||
const {
|
||||
calculateSellPrice,
|
||||
listActiveServices,
|
||||
listAdminServices,
|
||||
syncServicesFromProvider,
|
||||
updateService
|
||||
} = require('../src/services/services.service');
|
||||
|
||||
const providerIds = [];
|
||||
|
||||
function startProviderMock() {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const chunks = [];
|
||||
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const body = new URLSearchParams(Buffer.concat(chunks).toString());
|
||||
const action = body.get('action');
|
||||
|
||||
res.setHeader('content-type', 'application/json');
|
||||
|
||||
if (action === 'services') {
|
||||
res.end(JSON.stringify([
|
||||
{
|
||||
service: 'svc-test-1',
|
||||
name: 'Instagram Followers',
|
||||
category: 'Instagram',
|
||||
rate: '4.0000',
|
||||
min: 100,
|
||||
max: 10000
|
||||
},
|
||||
{
|
||||
service: 'svc-test-2',
|
||||
name: 'TikTok Views',
|
||||
category: 'TikTok',
|
||||
rate: '1.2000',
|
||||
min: 1000,
|
||||
max: 500000
|
||||
}
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 400;
|
||||
res.end(JSON.stringify({ error: 'unknown action' }));
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const address = server.address();
|
||||
resolve({
|
||||
url: `http://127.0.0.1:${address.port}`,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
if (providerIds.length > 0) {
|
||||
await getDb()('services').whereIn('provider_service_id', providerIds).del();
|
||||
}
|
||||
|
||||
await closeDbPool();
|
||||
});
|
||||
|
||||
test('calculateSellPrice applies markup and keeps four decimals', () => {
|
||||
assert.equal(calculateSellPrice('4.0000', '25'), '5.0000');
|
||||
assert.equal(calculateSellPrice('1.2000', '12.5'), '1.3500');
|
||||
});
|
||||
|
||||
test('syncServicesFromProvider stores services and public listing hides provider price', async () => {
|
||||
const mock = await startProviderMock();
|
||||
const oldUrl = process.env.PROVIDER_API_URL;
|
||||
const oldKey = process.env.PROVIDER_API_KEY;
|
||||
|
||||
process.env.PROVIDER_API_URL = mock.url;
|
||||
process.env.PROVIDER_API_KEY = 'mock-key';
|
||||
|
||||
try {
|
||||
const result = await syncServicesFromProvider('25');
|
||||
assert.equal(result.synced, 2);
|
||||
providerIds.push('svc-test-1', 'svc-test-2');
|
||||
|
||||
const adminServices = await listAdminServices();
|
||||
const firstAdmin = adminServices.find((service) => service.provider_service_id === 'svc-test-1');
|
||||
assert.equal(firstAdmin.provider_price, '4.0000');
|
||||
assert.equal(firstAdmin.markup_percent, '25.0000');
|
||||
assert.equal(firstAdmin.sell_price, '5.0000');
|
||||
|
||||
const publicServices = await listActiveServices();
|
||||
const firstPublic = publicServices.find((service) => service.provider_service_id === 'svc-test-1');
|
||||
assert.equal(firstPublic.sell_price, '5.0000');
|
||||
assert.equal(Object.hasOwn(firstPublic, 'provider_price'), false);
|
||||
assert.equal(Object.hasOwn(firstPublic, 'markup_percent'), false);
|
||||
} finally {
|
||||
process.env.PROVIDER_API_URL = oldUrl;
|
||||
process.env.PROVIDER_API_KEY = oldKey;
|
||||
await mock.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('updateService can adjust markup, sell price, and activation', async () => {
|
||||
let service = await getDb()('services').where({ provider_service_id: 'svc-test-2' }).first();
|
||||
|
||||
if (!service) {
|
||||
const [id] = await getDb()('services').insert({
|
||||
provider_service_id: 'svc-test-2',
|
||||
name: 'TikTok Views',
|
||||
category: 'TikTok',
|
||||
provider_price: '1.2000',
|
||||
markup_percent: '25.0000',
|
||||
sell_price: '1.5000',
|
||||
min: 1000,
|
||||
max: 500000,
|
||||
is_active: true
|
||||
});
|
||||
providerIds.push('svc-test-2');
|
||||
service = await getDb()('services').where({ id }).first();
|
||||
}
|
||||
|
||||
const updated = await updateService(
|
||||
service.id,
|
||||
{
|
||||
markup_percent: '50',
|
||||
is_active: false
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(updated.sell_price, '1.8000');
|
||||
assert.equal(Boolean(updated.is_active), false);
|
||||
|
||||
const publicServices = await listActiveServices();
|
||||
assert.equal(publicServices.some((service) => service.provider_service_id === 'svc-test-2'), false);
|
||||
});
|
||||
@@ -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