// apps/hq/test/milestone-status.test.ts — milestone→status auto-drive (never downgrade). 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, getClientModule, updateClientModule } from '../src/repos-modules' import { setMilestone } 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' }) const m = await createModule(db, 'u1', { code: 'SMS', name: 'Bulk SMS' }) const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) return { db, c, m, cmId: cm.id } } describe('milestone → status auto-drive (D22 status line)', () => { it('ticking quotation_approved advances status to ordered', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'quotation_approved', true) const cm = await getClientModule(db, cmId) expect(cm!.status).toBe('ordered') }) it('ticking installation advances status to installed', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'installation', true) const cm = await getClientModule(db, cmId) expect(cm!.status).toBe('installed') }) it('ticking go_live advances status to live', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'go_live', true) expect((await getClientModule(db, cmId))!.status).toBe('live') }) it('unticking never downgrades status', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'go_live', true) await setMilestone(db, 'u1', cmId, 'go_live', false) expect((await getClientModule(db, cmId))!.status).toBe('live') }) it('a lower step never downgrades a higher status', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'go_live', true) // live await setMilestone(db, 'u1', cmId, 'installation', true) // still live expect((await getClientModule(db, cmId))!.status).toBe('live') }) }) // Final whole-branch review FIX 5: installedOn / trainedOn became write-only once the UI's // RowDate / summary-table date writers were removed with nothing to replace them — the // milestone-driven status advance is now that replacement. describe('milestone-driven installed_on / trained_on stamping (FIX 5)', () => { it("ticking 'installation' (which advances status to installed) stamps installed_on from the milestone's done_on", async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'installation', true, '2026-03-25') const cm = await getClientModule(db, cmId) expect(cm!.status).toBe('installed') expect(cm!.installedOn).toBe('2026-03-25') }) it("ticking 'training' (which advances status to trained) stamps trained_on from the milestone's done_on", async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'training', true, '2026-05-02') const cm = await getClientModule(db, cmId) expect(cm!.status).toBe('trained') expect(cm!.trainedOn).toBe('2026-05-02') }) it('never overwrites an installed_on already on file (e.g. set by import / direct edit)', async () => { const { db, cmId } = await world() await updateClientModule(db, 'u1', cmId, { installedOn: '2025-01-01' }) await setMilestone(db, 'u1', cmId, 'installation', true, '2026-03-25') const cm = await getClientModule(db, cmId) expect(cm!.status).toBe('installed') // status still advances expect(cm!.installedOn).toBe('2025-01-01') // date untouched }) it('re-ticking after an untick does not move an already-stamped installed_on', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'installation', true, '2026-03-25') await setMilestone(db, 'u1', cmId, 'installation', false) await setMilestone(db, 'u1', cmId, 'installation', true, '2026-04-01') expect((await getClientModule(db, cmId))!.installedOn).toBe('2026-03-25') }) it('leaves installed_on / trained_on untouched for a step with no date-column mapping (e.g. go_live)', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'go_live', true, '2026-06-01') const cm = await getClientModule(db, cmId) expect(cm!.status).toBe('live') expect(cm!.installedOn).toBeNull() expect(cm!.trainedOn).toBeNull() }) it('the status-advance audit payload reflects the installed_on stamp', async () => { const { db, cmId } = await world() await setMilestone(db, 'u1', cmId, 'installation', true, '2026-03-25') const audits = await db.all<{ after_json: string | null }>( `SELECT after_json FROM audit_log WHERE entity='client_module' AND entity_id=? AND action='update' ORDER BY id`, cmId, ) const stamped = audits.find((a) => (a.after_json ?? '').includes('installedOn')) expect(stamped).toBeDefined() expect(JSON.parse(stamped!.after_json!)).toMatchObject({ status: 'installed', installedOn: '2026-03-25' }) }) })