// apps/hq/test/dashboard.test.ts import { describe, it, expect, afterAll } from 'vitest' import express from 'express' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createStaff } from '../src/auth' import { apiRouter } from '../src/api' import { createClient } from '../src/repos-clients' import { assignModule, createModule, setPrice, updateClientModule } from '../src/repos-modules' import { createDraft, issueDocument } from '../src/repos-documents' import { createInteraction } from '../src/repos-interactions' import { dashboardView } from '../src/repos-dashboard' import { runDailyScan, type ScanDeps } from '../src/scheduler' const scanDeps: ScanDeps = { gmail: { f: (async () => new Response('{}')) as typeof fetch, clientId: '', clientSecret: '', keyHex: '' }, renderPdf: async () => Buffer.from('%PDF-'), company: () => ({ 'company.name': 'Tecnostac' }), now: () => '2026-07-10T00:00:00Z', } function seeded() { const db = openDb(':memory:'); seedIfEmpty(db) const c = createClient(db, 'u1', { name: 'Acme', stateCode: '32', contacts: [{ name: 'R', email: 'r@acme.in' }] }) const m = createModule(db, 'u1', { code: 'POS', name: 'POS' }) setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-01-01' }) const inv = issueDocument(db, 'u1', createDraft(db, 'u1', { docType: 'INVOICE', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }).id) db.prepare(`UPDATE document SET doc_date='2026-06-01' WHERE id=?`).run(inv.id) const cm = assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) updateClientModule(db, 'u1', cm.id, { nextRenewal: '2026-07-20' }) createInteraction(db, 'u1', { clientId: c.id, typeCode: 'call', onDate: '2026-07-01', followUpOn: '2026-07-09' }) return { db, c, inv } } describe('dashboardView', () => { it('aggregates overdue, renewals, follow-ups and the reminder queue', async () => { const { db, inv } = seeded() await runDailyScan(db, scanDeps, '2026-07-10') // populates the queue const view = dashboardView(db, '2026-07-10') expect(view.overdue.map((o) => o.docId)).toContain(inv.id) expect(view.overdue[0]!.outstandingPaise).toBe(11_800_00) expect(view.renewalsThisMonth).toHaveLength(1) expect(view.followUpsToday).toHaveLength(1) expect(view.queue.length).toBeGreaterThanOrEqual(3) // overdue + renewal + follow_up expect(view.totals.overduePaise).toBe(11_800_00) }) }) describe('GET /api/dashboard + reminder queue routes', () => { const { db } = seeded() createStaff(db, { email: 'e2e@test.in', displayName: 'E2E', role: 'owner', password: 'e2e-password' }) const app = express(); app.use(express.json()); app.locals['db'] = db; app.use('/api', apiRouter(db)) const server = app.listen(0) const base = `http://localhost:${(server.address() as { port: number }).port}/api` let token = '' const call = async (method: string, path: string, body?: unknown) => { const res = await fetch(base + path, { method, headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, ...(body ? { body: JSON.stringify(body) } : {}) }) return { status: res.status, json: await res.json() as any } } afterAll(() => server.close()) it('serves the dashboard, lists the queue and dismisses a reminder', async () => { token = (await call('POST', '/auth/login', { email: 'e2e@test.in', password: 'e2e-password' })).json.token await runDailyScan(db, scanDeps, '2026-07-10') const dash = (await call('GET', '/dashboard')).json expect(dash.ok).toBe(true) expect(dash.view.overdue.length).toBeGreaterThanOrEqual(1) const queue = (await call('GET', '/reminders')).json.reminders expect(queue.length).toBeGreaterThanOrEqual(1) const dismissed = (await call('POST', `/reminders/${queue[0].id}/dismiss`)).json expect(dismissed.reminder.status).toBe('dismissed') }) })