feat(onboarding): composed milestone templates (shared front + per-module tail)
Supersedes the flat per-module onboarding templates: milestoneTemplate(db, code) now returns [...front, ...tail]. front is a shared setting (project.milestone_template.front, FRONT_FALLBACK code default: enquiry -> visit/meeting -> quotation sent -> quotation approved) prepended to every module's checklist. tail resolves per-module setting -> global setting -> TAIL_FALLBACK (the old generic list), unchanged in precedence. - STEP_STATUS gains quotation_approved -> ordered (never-downgrade guard kept). - seed.ts seeds the front once plus SMS/RTGS/MOBILEAPP tails (advance/balance payment steps collapsed into a single payment_received tail step); no longer seeds a bare project.milestone_template default. - migrate-onboarding-templates.ts KEY_MAP updated so advance_paid, balance_paid, advance_payment and balance_payment all carry forward to payment_received; its idempotency check holds against the longer composed list. - Updated seed-templates, milestones-templates, milestone-status, migrate-onboarding-templates, milestones and milestone-payment tests to the composed content/mappings (TDD: reverted the implementation, confirmed the updated tests red, restored the implementation, confirmed green). Backend + tests only; the live-DB migration run is a separate step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>main
parent
e8f00503a4
commit
f1a7eac9dd
@ -1,30 +1,58 @@
|
|||||||
// apps/hq/test/milestones-templates.test.ts — per-module onboarding template resolution.
|
// apps/hq/test/milestones-templates.test.ts — composed (front + per-module tail) onboarding
|
||||||
|
// template resolution.
|
||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
import { openDb, type DB } from '../src/db'
|
import { openDb, type DB } from '../src/db'
|
||||||
import { milestoneTemplate } from '../src/repos-milestones'
|
import { milestoneTemplate, FRONT_FALLBACK, TAIL_FALLBACK } from '../src/repos-milestones'
|
||||||
|
|
||||||
describe('per-module milestone templates', () => {
|
describe('composed milestone templates (front + tail)', () => {
|
||||||
let db: DB
|
let db: DB
|
||||||
beforeEach(() => { db = openDb(':memory:') })
|
beforeEach(() => { db = openDb(':memory:') })
|
||||||
|
|
||||||
it('falls back to the global template when no module-specific one exists', async () => {
|
it('falls back to the code FRONT_FALLBACK + TAIL_FALLBACK when nothing is configured', async () => {
|
||||||
|
expect(await milestoneTemplate(db, 'SMS')).toEqual([...FRONT_FALLBACK, ...TAIL_FALLBACK])
|
||||||
|
expect(await milestoneTemplate(db)).toEqual([...FRONT_FALLBACK, ...TAIL_FALLBACK])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('starts with the 4 front keys then the SMS tail', async () => {
|
||||||
|
const keys = (await milestoneTemplate(db, 'SMS')).map((e) => e.key)
|
||||||
|
expect(keys.slice(0, 4)).toEqual(FRONT_FALLBACK.map((f) => f.key))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the configured front for every module and with no moduleCode', async () => {
|
||||||
|
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template.front', ?)`,
|
||||||
|
JSON.stringify([{ key: 'f1', label: 'F1' }]))
|
||||||
|
expect((await milestoneTemplate(db, 'SMS'))[0]).toEqual({ key: 'f1', label: 'F1' })
|
||||||
|
expect((await milestoneTemplate(db))[0]).toEqual({ key: 'f1', label: 'F1' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tail: falls back to the global tail template when no module-specific one exists', async () => {
|
||||||
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`,
|
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`,
|
||||||
JSON.stringify([{ key: 'a', label: 'A' }]))
|
JSON.stringify([{ key: 'a', label: 'A' }]))
|
||||||
expect(await milestoneTemplate(db, 'SMS')).toEqual([{ key: 'a', label: 'A' }])
|
expect(await milestoneTemplate(db, 'SMS')).toEqual([...FRONT_FALLBACK, { key: 'a', label: 'A' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prefers the module-specific template over the global one', async () => {
|
it('tail: prefers the module-specific template over the global one', async () => {
|
||||||
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`,
|
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`,
|
||||||
JSON.stringify([{ key: 'a', label: 'A' }]))
|
JSON.stringify([{ key: 'a', label: 'A' }]))
|
||||||
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template:SMS', ?)`,
|
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template:SMS', ?)`,
|
||||||
JSON.stringify([{ key: 'doc', label: 'Document collection' }]))
|
JSON.stringify([{ key: 'doc', label: 'Document collection' }]))
|
||||||
expect(await milestoneTemplate(db, 'SMS')).toEqual([{ key: 'doc', label: 'Document collection' }])
|
expect(await milestoneTemplate(db, 'SMS'))
|
||||||
// A different module still gets the global one.
|
.toEqual([...FRONT_FALLBACK, { key: 'doc', label: 'Document collection' }])
|
||||||
expect(await milestoneTemplate(db, 'RTGS')).toEqual([{ key: 'a', label: 'A' }])
|
// A different module still gets the global tail.
|
||||||
|
expect(await milestoneTemplate(db, 'RTGS')).toEqual([...FRONT_FALLBACK, { key: 'a', label: 'A' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('with no moduleCode: front + global tail when set, else TAIL_FALLBACK', async () => {
|
||||||
|
expect(await milestoneTemplate(db)).toEqual([...FRONT_FALLBACK, ...TAIL_FALLBACK])
|
||||||
|
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`,
|
||||||
|
JSON.stringify([{ key: 'a', label: 'A' }]))
|
||||||
|
expect(await milestoneTemplate(db)).toEqual([...FRONT_FALLBACK, { key: 'a', label: 'A' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to the code default when nothing is seeded', async () => {
|
it('a malformed or empty per-module tail setting is treated as absent', async () => {
|
||||||
const t = await milestoneTemplate(db, 'SMS')
|
await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template:SMS', ?)`, 'not json')
|
||||||
expect(t.map((e) => e.key)).toContain('go_live')
|
expect(await milestoneTemplate(db, 'SMS')).toEqual([...FRONT_FALLBACK, ...TAIL_FALLBACK])
|
||||||
|
await db.run(`UPDATE setting SET value=? WHERE key='project.milestone_template:SMS'`, '[]')
|
||||||
|
expect(await milestoneTemplate(db, 'SMS')).toEqual([...FRONT_FALLBACK, ...TAIL_FALLBACK])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue