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 dealerUndo = await request(port, 'POST', '/api/stocks/CH-RED/stage/undo', { token: dealerToken, }); assert(dealerUndo.status === 403, 'dealer should not undo a stage'); const undonePayment = await request(port, 'POST', '/api/stocks/CH-RED/stage/undo', { token: repToken, }); assert( undonePayment.status === 200 && undonePayment.json.data.currentStage === 'documentsProcessed' && undonePayment.json.data.paymentType == null && undonePayment.json.data.leaseProviderType == null && undonePayment.json.data.financeCompanyName == null && !undonePayment.json.data.stageLogDates.paymentApproved, `undo payment stage failed ${JSON.stringify(undonePayment.json)}` ); const undoneDocs = await request(port, 'POST', '/api/stocks/CH-RED/stage/undo', { token: repToken, }); assert( undoneDocs.status === 200 && undoneDocs.json.data.currentStage === 'orderedByCustomer', 'undo documents stage failed' ); const undoneOrder = await request(port, 'POST', '/api/stocks/CH-RED/stage/undo', { token: repToken, }); assert( undoneOrder.status === 200 && undoneOrder.json.data.currentStage === 'received', 'undo ordered stage failed' ); const undoReceived = await request(port, 'POST', '/api/stocks/CH-RED/stage/undo', { token: repToken, }); assert(undoReceived.status === 400, 'received stage should not be undoable'); const undonePlate = await request(port, 'POST', '/api/stocks/CH-GREEN/stage/undo', { token: adminToken, }); assert( undonePlate.status === 200 && undonePlate.json.data.currentStage === 'rmvRegistration' && undonePlate.json.data.plateNumber == null && !undonePlate.json.data.stageLogDates.plateReceived, `undo plate stage failed ${JSON.stringify(undonePlate.json)}` ); await addStock('CH-OUT', 'EN-OUT', 1, dealerC.json.data.id); const repUndoOutside = await request(port, 'POST', '/api/stocks/CH-OUT/stage/undo', { token: repToken, }); assert(repUndoOutside.status === 403, 'sales rep should not undo stock outside assignment'); const repDeleteOutside = await request(port, 'DELETE', '/api/stocks/CH-OUT', { token: repToken }); assert(repDeleteOutside.status === 403, 'sales rep should not delete stock outside assignment'); const dealerDelete = await request(port, 'DELETE', '/api/stocks/CH-YELLOW', { token: dealerToken }); assert(dealerDelete.status === 403, 'dealer should not delete a bike'); const deleted = await request(port, 'DELETE', '/api/stocks/CH-YELLOW', { token: repToken }); assert(deleted.status === 200, `stock delete failed ${deleted.status} ${JSON.stringify(deleted.json)}`); const stockGone = await prisma.stock.findUnique({ where: { chassisNumber: 'CH-YELLOW' } }); const transferGone = await prisma.stockTransfer.findFirst({ where: { stockId: moved.json.data.id } }); assert(!stockGone && !transferGone, 'deleted bike and its transfers should be gone'); const missingStock = await request(port, 'DELETE', '/api/stocks/CH-YELLOW', { token: adminToken }); assert(missingStock.status === 404, 'deleting a missing bike should be 404'); const missingUndo = await request(port, 'POST', '/api/stocks/NO-SUCH/stage/undo', { token: adminToken, }); assert(missingUndo.status === 404, 'undo of a missing bike should be 404'); 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; });