// apps/hq/test/scheduler-scan.test.ts import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createClient } from '../src/repos-clients' import { assignModule, createModule, setPrice, updateClientModule } from '../src/repos-modules' import { createDraft, issueDocument } from '../src/repos-documents' import { createAmc } from '../src/repos-amc' import { createInteraction } from '../src/repos-interactions' import { listReminders } from '../src/repos-reminders' import { runDailyScan, type ScanDeps } from '../src/scheduler' const deps: 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 world() { 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' }) return { db, c, m } } describe('runDailyScan — detection rules', () => { it('raises overdue, renewal, amc and follow-up reminders, and is idempotent', async () => { const { db, c, m } = world() // Overdue invoice: issued 2026-06-01, unpaid, > 7 days before 2026-07-10. 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) // Renewal within 15 days. const cm = assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) updateClientModule(db, 'u1', cm.id, { nextRenewal: '2026-07-20' }) // AMC expiring within 30 days. createAmc(db, 'u1', { clientId: c.id, coverage: 'Support', periodFrom: '2025-08-01', periodTo: '2026-08-01', amountPaise: 20_000_00 }) // Follow-up due. createInteraction(db, 'u1', { clientId: c.id, typeCode: 'call', onDate: '2026-07-01', followUpOn: '2026-07-09' }) const res = await runDailyScan(db, deps, '2026-07-10') expect(res.created['invoice_overdue']).toBe(1) expect(res.created['renewal_due']).toBe(1) expect(res.created['amc_expiring']).toBe(1) expect(res.created['follow_up']).toBe(1) expect(listReminders(db, { status: 'queued' })).toHaveLength(4) // Re-run same day → the idempotency key blocks every duplicate. const again = await runDailyScan(db, deps, '2026-07-10') expect(Object.values(again.created).reduce((a, b) => a + b, 0)).toBe(0) expect(listReminders(db, {})).toHaveLength(4) }) it('does not raise a reminder for an invoice that is not yet overdue', async () => { const { db, c, m } = world() 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-07-08' WHERE id=?`).run(inv.id) // only 2 days old const res = await runDailyScan(db, deps, '2026-07-10') expect(res.created['invoice_overdue'] ?? 0).toBe(0) }) })