// apps/hq/test/stalled-onboarding.test.ts — Task 8: stalled-onboarding query (dashboard/MIS tile). import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createClient } from '../src/repos-clients' import { createModule, assignModule } from '../src/repos-modules' import { setMilestone, stalledProjects } from '../src/repos-milestones' async function world() { const db = openDb(':memory:') await seedIfEmpty(db) const c = await createClient(db, 'u1', { name: 'Kothavara SCB', stateCode: '32' }) // A module code with no per-module milestone template seeded (D22 seed.ts only carries // SMS/RTGS/MOBILEAPP overrides), so the global template applies: advance_paid, setup_done, // installed, go_live, balance_paid, amc_start. const m = await createModule(db, 'u1', { code: 'MISC', name: 'Misc Module' }) return { db, c, m } } describe('stalledProjects (D22 — dashboard/MIS tile)', () => { it('lists a project parked past the threshold', async () => { const { db, c, m } = await world() const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) // Early steps done on an old date; later steps (incl. go_live) stay pending — project is // still active (status stays 'quoted'/'installed', never 'live'). await setMilestone(db, 'u1', cm.id, 'advance_paid', true, '2026-03-01') await setMilestone(db, 'u1', cm.id, 'setup_done', true, '2026-03-01') // last done_on = 2026-03-01, today = 2026-07-19 → ~140 days parked. const stalled = await stalledProjects(db, 30, '2026-07-19') expect(stalled.map((p) => p.clientModuleId)).toContain(cm.id) const row = stalled.find((p) => p.clientModuleId === cm.id)! expect(row.clientName).toBe('Kothavara SCB') expect(row.moduleCode).toBe('MISC') }) it('excludes a recently-progressed project', async () => { const { db, c, m } = await world() const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) await setMilestone(db, 'u1', cm.id, 'advance_paid', true, '2026-03-01') await setMilestone(db, 'u1', cm.id, 'setup_done', true, '2026-03-01') const stalled = await stalledProjects(db, 365, '2026-07-19') expect(stalled).toHaveLength(0) }) it('excludes projects with no pending steps (fully ticked) and inactive/live projects', async () => { const { db, c, m } = await world() // Fully ticked project: nothing pending, so it should never appear regardless of age. const done = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) for (const key of ['advance_paid', 'setup_done', 'installed', 'go_live', 'balance_paid', 'amc_start']) { await setMilestone(db, 'u1', done.id, key, true, '2026-01-01') } const stalled = await stalledProjects(db, 1, '2026-07-19') expect(stalled.map((p) => p.clientModuleId)).not.toContain(done.id) // 'live' status (from ticking go_live) also excludes it, which the loop above proves. expect((await db.get<{ status: string }>(`SELECT status FROM client_module WHERE id=?`, done.id))!.status).toBe('live') }) })