Files
senaro_dealer_api/scripts/api-check.js
T
2026-10-01 14:04:56 +05:30

487 lines
18 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const root = path.join(__dirname, '..');
process.chdir(root);
const dbPath = path.join(root, 'data', 'api-check.db');
for (const file of [dbPath, `${dbPath}-journal`, `${dbPath}-wal`, `${dbPath}-shm`]) {
if (fs.existsSync(file)) fs.unlinkSync(file);
}
process.env.DATABASE_URL = 'file:../data/api-check.db';
process.env.JWT_SECRET = 'api-check-secret';
process.env.TEXTLK_API_TOKEN = '';
process.env.SEED_ADMIN_NAME = 'Super Admin';
process.env.SEED_ADMIN_PHONE = '94770000001';
execSync('npx prisma db push --skip-generate --accept-data-loss', {
cwd: root,
stdio: 'inherit',
env: process.env,
});
const bcrypt = require('bcryptjs');
const app = require('../src/app');
const prisma = require('../src/db');
const { ageBadge } = require('../src/lib/stockRules');
function assert(condition, message) {
if (!condition) throw new Error(message);
}
async function request(port, method, urlPath, { token, body } = {}) {
const response = await fetch(`http://127.0.0.1:${port}${urlPath}`, {
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
let json = null;
if (text) {
try {
json = JSON.parse(text);
} catch {
json = { raw: text };
}
}
return { status: response.status, json };
}
function daysAgo(days) {
return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
}
async function run(port) {
assert(ageBadge(29) === 'green', '29 days is green');
assert(ageBadge(30) === 'yellow', '30 days is yellow');
assert(ageBadge(45) === 'yellow', '45 days is yellow');
assert(ageBadge(46) === 'red', '46 days is red');
const unknown = await request(port, 'POST', '/api/auth/dealer-login', {
body: { phone: '94770000099' },
});
assert(unknown.status === 404, `unknown phone should be 404, got ${unknown.status}`);
const adminHash = await bcrypt.hash('admin-pass-123', 10);
await prisma.user.create({
data: {
name: 'Super Admin',
phone: '94770000001',
role: 'superAdmin',
passwordHash: adminHash,
},
});
const badPassword = await request(port, 'POST', '/api/auth/staff-login', {
body: { phone: '94770000001', password: 'wrong-password' },
});
assert(badPassword.status === 401, 'wrong password should be rejected');
const verified = await request(port, 'POST', '/api/auth/staff-login', {
body: { phone: '94770000001', password: 'admin-pass-123' },
});
assert(verified.status === 200 && verified.json.token, 'staff login failed');
const adminToken = verified.json.token;
const me = await request(port, 'GET', '/api/auth/me', { token: adminToken });
assert(me.status === 200 && me.json.role === 'superAdmin', '/me failed');
const missing = await request(port, 'GET', '/api/auth/me');
assert(missing.status === 401, 'missing token should be 401');
const dealerMissingPhone = await request(port, 'POST', '/api/admin/dealers', {
token: adminToken,
body: { name: 'No Phone', code: 'NONE' },
});
assert(dealerMissingPhone.status === 400, 'dealer without a phone should fail');
const dealerA = await request(port, 'POST', '/api/admin/dealers', {
token: adminToken,
body: { name: 'Kandy Motors', code: 'KDY', district: 'Kandy', phone: '94772222222' },
});
const dealerB = await request(port, 'POST', '/api/admin/dealers', {
token: adminToken,
body: { name: 'Galle Motors', code: 'GAL', district: 'Galle', phone: '94774444444' },
});
const dealerC = await request(port, 'POST', '/api/admin/dealers', {
token: adminToken,
body: { name: 'Jaffna Motors', code: 'JAF', district: 'Jaffna', phone: '94773333333' },
});
assert(dealerA.status === 201 && dealerB.status === 201 && dealerC.status === 201, 'dealer create failed');
const kandyLogin = await prisma.user.findUnique({ where: { phone: '94772222222' } });
assert(
kandyLogin && kandyLogin.role === 'dealer' && kandyLogin.dealerId === dealerA.json.data.id,
'creating a dealer should create its login'
);
const rep = await request(port, 'POST', '/api/admin/sales-reps', {
token: adminToken,
body: {
name: 'Nimal Perera',
phone: '94771111111',
password: 'rep-pass-123',
dealerIds: [dealerA.json.data.id, dealerB.json.data.id],
},
});
assert(rep.status === 201, `sales rep create failed ${rep.status}`);
async function staffLogin(phone, password) {
const result = await request(port, 'POST', '/api/auth/staff-login', {
body: { phone, password },
});
assert(result.status === 200 && result.json.token, `staff login failed for ${phone}`);
return result.json.token;
}
async function approveDealer(phone, token) {
const started = await request(port, 'POST', '/api/auth/dealer-login', { body: { phone } });
assert(started.status === 201, `dealer login failed for ${phone}: ${started.status}`);
const approved = await request(
port,
'POST',
`/api/auth/login-requests/${started.json.requestId}/approve`,
{ token }
);
assert(approved.status === 200, `approve failed for ${phone}: ${approved.status}`);
const ready = await request(
port,
'GET',
`/api/auth/dealer-login/${started.json.requestId}?secret=${encodeURIComponent(started.json.secret)}`
);
assert(ready.status === 200 && ready.json.token, `dealer token missing for ${phone}`);
return ready.json.token;
}
const repToken = await staffLogin('94771111111', 'rep-pass-123');
const dealerStaff = await request(port, 'POST', '/api/auth/staff-login', {
body: { phone: '94772222222', password: 'rep-pass-123' },
});
assert(dealerStaff.status === 401, 'dealer should not use staff login');
const repDealer = await request(port, 'POST', '/api/admin/dealers', {
token: repToken,
body: { name: 'Matara Motors', code: 'MTR', district: 'Matara', phone: '0715555555' },
});
assert(repDealer.status === 201, `sales rep dealer create failed ${repDealer.status}`);
const repDealers = await request(port, 'GET', '/api/dealers', { token: repToken });
assert(
repDealers.status === 200 && repDealers.json.data.some((item) => item.code === 'MTR'),
'sales rep should see the dealer they created'
);
const outsideRequest = await request(port, 'POST', '/api/auth/dealer-login', {
body: { phone: '94773333333' },
});
assert(outsideRequest.status === 201, 'outside dealer login failed');
const repQueue = await request(port, 'GET', '/api/auth/login-requests', { token: repToken });
assert(
repQueue.status === 200 &&
!repQueue.json.data.some((item) => item.id === outsideRequest.json.requestId),
'sales rep should not see an unassigned dealer login'
);
const repDenied = await request(
port,
'POST',
`/api/auth/login-requests/${outsideRequest.json.requestId}/approve`,
{ token: repToken }
);
assert(repDenied.status === 403, 'sales rep should not approve an unassigned dealer');
const dealerToken = await approveDealer('94772222222', adminToken);
const ownStock = await request(port, 'POST', '/api/stocks', {
token: dealerToken,
body: {
chassisNumber: 'CH-OWN',
engineNumber: 'EN-OWN',
model: 'gn125',
color: 'Red',
currentDealerId: dealerA.json.data.id,
},
});
assert(
ownStock.status === 201 && ownStock.json.data.currentDealerId === dealerA.json.data.id,
`dealer should add stock to their own dealership ${ownStock.status}`
);
const forbiddenStock = await request(port, 'POST', '/api/stocks', {
token: dealerToken,
body: {
chassisNumber: 'NOPE1',
engineNumber: 'ENG-NOPE',
model: 'gn125',
color: 'Red',
currentDealerId: dealerB.json.data.id,
},
});
assert(forbiddenStock.status === 403, 'dealer should not add stock to another dealership');
async function addStock(chassis, engine, days, dealerId) {
const created = await request(port, 'POST', '/api/stocks', {
token: adminToken,
body: {
chassisNumber: chassis,
engineNumber: engine,
model: 'gn125',
color: 'Black',
currentDealerId: dealerId,
dateReceived: daysAgo(days),
},
});
assert(created.status === 201, `stock create failed for ${chassis}: ${created.status} ${JSON.stringify(created.json)}`);
return created.json.data;
}
const green = await addStock('CH-GREEN', 'EN-GREEN', 10, dealerA.json.data.id);
const yellow = await addStock('CH-YELLOW', 'EN-YELLOW', 35, dealerA.json.data.id);
const red = await addStock('CH-RED', 'EN-RED', 50, dealerA.json.data.id);
assert(green.badge === 'green' && green.ageInDays === 10, `green badge mismatch ${JSON.stringify(green)}`);
assert(yellow.badge === 'yellow' && yellow.ageInDays === 35, 'yellow badge mismatch');
assert(red.badge === 'red' && red.ageInDays === 50, 'red badge mismatch');
assert(green.stageLogDates.received, 'received stage was not logged');
const skip = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: adminToken,
body: { stage: 'plateReceived' },
});
assert(skip.status === 400, 'skipping stages should fail');
const ordered = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: { stage: 'orderedByCustomer' },
});
assert(ordered.status === 200, 'dealer stage update failed');
const docs = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: { stage: 'documentsProcessed' },
});
assert(docs.status === 200, 'documents stage failed');
const payMissing = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: { stage: 'paymentApproved' },
});
assert(payMissing.status === 400, 'payment without type should fail');
const paid = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: {
stage: 'paymentApproved',
paymentType: 'lease',
leaseProviderType: 'thirdPartyFinance',
financeCompanyName: 'Sampath Finance',
},
});
assert(paid.status === 200 && paid.json.data.paymentType === 'lease', 'lease payment failed');
assert(paid.json.data.stageLogDates.paymentApproved, 'payment stage log missing');
for (const stage of ['orderedByCustomer', 'documentsProcessed']) {
const stepped = await request(port, 'PATCH', '/api/stocks/CH-RED/stage', {
token: dealerToken,
body: { stage },
});
assert(stepped.status === 200, `CH-RED stage ${stage} failed`);
}
const bothMissingName = await request(port, 'PATCH', '/api/stocks/CH-RED/stage', {
token: dealerToken,
body: { stage: 'paymentApproved', paymentType: 'lease', leaseProviderType: 'both' },
});
assert(bothMissingName.status === 400, 'both lease options should require a finance company');
const bothPaid = await request(port, 'PATCH', '/api/stocks/CH-RED/stage', {
token: dealerToken,
body: {
stage: 'paymentApproved',
paymentType: 'lease',
leaseProviderType: 'both',
financeCompanyName: 'GCI Finance',
},
});
assert(
bothPaid.status === 200 &&
bothPaid.json.data.leaseProviderType === 'both' &&
bothPaid.json.data.financeCompanyName === 'GCI Finance',
'lease with both GCI and finance failed'
);
const registered = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: { stage: 'rmvRegistration' },
});
assert(registered.status === 200, 'rmv stage failed');
const plateMissing = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: { stage: 'plateReceived' },
});
assert(plateMissing.status === 400, 'plate received without a number should fail');
const plated = await request(port, 'PATCH', '/api/stocks/CH-GREEN/stage', {
token: dealerToken,
body: { stage: 'plateReceived', plateNumber: 'WP ABC-1234' },
});
assert(
plated.status === 200 && plated.json.data.plateNumber === 'WP ABC-1234',
'saving the plate number failed'
);
const outside = await request(port, 'POST', '/api/stocks/CH-YELLOW/transfer', {
token: repToken,
body: { dealerId: dealerC.json.data.id },
});
assert(outside.status === 403, 'rep should not transfer outside assignment');
const moved = await request(port, 'POST', '/api/stocks/CH-YELLOW/transfer', {
token: repToken,
body: { dealerId: dealerB.json.data.id },
});
assert(moved.status === 200 && moved.json.data.currentDealerId === dealerB.json.data.id, 'transfer failed');
const eventBad = await request(port, 'POST', '/api/events', {
token: dealerToken,
body: {
dealerId: dealerA.json.data.id,
planType: 'monthlySpecial',
title: 'April push',
description: 'Showroom weekend',
expense: 15000,
imageUrls: [],
},
});
assert(eventBad.status === 400, 'monthly plan without month should fail');
const eventOk = await request(port, 'POST', '/api/events', {
token: dealerToken,
body: {
dealerId: dealerA.json.data.id,
planType: 'monthlySpecial',
month: 4,
year: 2026,
title: 'April push',
description: 'Showroom weekend',
expense: 15000,
imageUrls: ['https://example.com/poster.jpg'],
},
});
assert(eventOk.status === 201, 'event create failed');
const otherDealerEvent = await request(port, 'POST', '/api/events', {
token: dealerToken,
body: {
dealerId: dealerB.json.data.id,
planType: 'general',
title: 'Nope',
description: 'Wrong dealer',
expense: 1,
imageUrls: [],
},
});
assert(otherDealerEvent.status === 403, 'dealer should not post events for another dealer');
const pointForbidden = await request(port, 'POST', '/api/service-points', {
token: dealerToken,
body: { name: 'Hidden', district: 'Ratnapura' },
});
assert(pointForbidden.status === 403, 'dealer should not add service points');
const point = await request(port, 'POST', '/api/service-points', {
token: repToken,
body: {
name: 'Ratnapura Cycle Repair',
district: 'Ratnapura',
address: 'Main street',
notes: 'engine repair',
},
});
assert(point.status === 201, 'service point create failed');
const found = await request(
port,
'GET',
'/api/service-points?district=Ratnapura&search=repair',
{ token: dealerToken }
);
assert(
found.status === 200 && found.json.data.some((item) => item.name === 'Ratnapura Cycle Repair'),
'service point search failed'
);
const reassigned = await request(port, 'PATCH', '/api/admin/assign-dealer', {
token: adminToken,
body: { salesRepId: rep.json.data.id, dealerId: dealerC.json.data.id, assigned: true },
});
assert(
reassigned.status === 200 && reassigned.json.data.assignedDealerIds.includes(dealerC.json.data.id),
'assign dealer failed'
);
const dealerD = await request(port, 'POST', '/api/admin/dealers', {
token: adminToken,
body: { name: 'Delete Motors', code: 'DEL', district: 'Colombo', phone: '94776666666' },
});
assert(dealerD.status === 201, 'dealer for delete test failed');
await addStock('CH-DEL', 'EN-DEL', 1, dealerD.json.data.id);
const transferred = await request(port, 'POST', '/api/stocks/CH-DEL/transfer', {
token: adminToken,
body: { dealerId: dealerA.json.data.id },
});
assert(transferred.status === 200, 'transfer before delete failed');
const dealerDToken = await approveDealer('94776666666', adminToken);
const doomedEvent = await request(port, 'POST', '/api/events', {
token: dealerDToken,
body: {
dealerId: dealerD.json.data.id,
planType: 'general',
title: 'Going away',
description: 'Removed with the dealer',
expense: 10,
imageUrls: [],
},
});
assert(doomedEvent.status === 201, 'event before delete failed');
const repDelete = await request(port, 'DELETE', `/api/admin/dealers/${dealerD.json.data.id}`, {
token: repToken,
});
assert(repDelete.status === 403, 'sales rep should not delete a dealer');
const removed = await request(port, 'DELETE', `/api/admin/dealers/${dealerD.json.data.id}`, {
token: adminToken,
});
assert(removed.status === 200, `dealer delete failed ${removed.status} ${JSON.stringify(removed.json)}`);
const dealerGone = await prisma.dealer.findUnique({ where: { id: dealerD.json.data.id } });
const loginGone = await prisma.user.findUnique({ where: { phone: '94776666666' } });
const eventGone = await prisma.event.findUnique({ where: { id: doomedEvent.json.data.id } });
const stockKept = await prisma.stock.findUnique({ where: { chassisNumber: 'CH-DEL' } });
assert(!dealerGone && !loginGone && !eventGone, 'dealer, login, and plans should be removed');
assert(stockKept && stockKept.currentDealerId === dealerA.json.data.id, 'stock already transferred should stay');
const missingDelete = await request(port, 'DELETE', `/api/admin/dealers/${dealerD.json.data.id}`, {
token: adminToken,
});
assert(missingDelete.status === 404, 'deleting a missing dealer should be 404');
}
async function main() {
const server = await new Promise((resolve) => {
const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
});
try {
await run(server.address().port);
console.log('API checks passed');
} finally {
await new Promise((resolve) => server.close(resolve));
await prisma.$disconnect();
for (const file of [dbPath, `${dbPath}-journal`, `${dbPath}-wal`, `${dbPath}-shm`]) {
if (fs.existsSync(file)) fs.unlinkSync(file);
}
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});