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 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/send-otp', { body: { phone: '94770000099' }, }); assert(unknown.status === 404, `unknown phone should be 404, got ${unknown.status}`); await prisma.user.create({ data: { name: 'Super Admin', phone: '94770000001', role: 'superAdmin' }, }); const sent = await request(port, 'POST', '/api/auth/send-otp', { body: { phone: '94770000001' }, }); assert(sent.status === 200 && sent.json.success === true, 'OTP send failed'); const adminRow = await prisma.user.findUnique({ where: { phone: '94770000001' } }); const wrongCode = adminRow.currentOtp === '000000' ? '111111' : '000000'; const badOtp = await request(port, 'POST', '/api/auth/verify-otp', { body: { phone: '94770000001', otp: wrongCode }, }); assert(badOtp.status === 401, 'wrong OTP should be rejected'); const verified = await request(port, 'POST', '/api/auth/verify-otp', { body: { phone: '94770000001', otp: adminRow.currentOtp }, }); assert(verified.status === 200 && verified.json.token, 'OTP verify 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 dealerA = await request(port, 'POST', '/api/admin/dealers', { token: adminToken, body: { name: 'Kandy Motors', code: 'KDY', district: 'Kandy' }, }); const dealerB = await request(port, 'POST', '/api/admin/dealers', { token: adminToken, body: { name: 'Galle Motors', code: 'GAL', district: 'Galle' }, }); const dealerC = await request(port, 'POST', '/api/admin/dealers', { token: adminToken, body: { name: 'Jaffna Motors', code: 'JAF', district: 'Jaffna' }, }); assert(dealerA.status === 201 && dealerB.status === 201 && dealerC.status === 201, 'dealer create failed'); const rep = await request(port, 'POST', '/api/admin/sales-reps', { token: adminToken, body: { name: 'Nimal Perera', phone: '94771111111', dealerIds: [dealerA.json.data.id, dealerB.json.data.id], }, }); assert(rep.status === 201, `sales rep create failed ${rep.status}`); const dealerUser = await request(port, 'POST', '/api/admin/dealer-users', { token: adminToken, body: { name: 'Kandy Desk', phone: '94772222222', dealerId: dealerA.json.data.id }, }); assert(dealerUser.status === 201, 'dealer user create failed'); async function login(phone) { const otpSend = await request(port, 'POST', '/api/auth/send-otp', { body: { phone } }); assert(otpSend.status === 200, `otp send failed for ${phone}`); const row = await prisma.user.findUnique({ where: { phone } }); const otpVerify = await request(port, 'POST', '/api/auth/verify-otp', { body: { phone, otp: row.currentOtp }, }); assert(otpVerify.status === 200, `otp verify failed for ${phone}`); return otpVerify.json.token; } const repToken = await login('94771111111'); const dealerToken = await login('94772222222'); const forbiddenStock = await request(port, 'POST', '/api/stocks', { token: dealerToken, body: { chassisNumber: 'NOPE1', engineNumber: 'ENG-NOPE', model: 'gn125', color: 'Red', currentDealerId: dealerA.json.data.id, }, }); assert(forbiddenStock.status === 403, 'dealer should not add stock'); 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'); 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' ); } 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; });