You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
102 lines
5.1 KiB
TypeScript
102 lines
5.1 KiB
TypeScript
// One-off deploy step for the Client Detail redesign: writes the live onboarding-template
|
|
// settings with the composed quotation-led pipeline (shared front + per-module tail), then runs
|
|
// the tick-preserving migration so existing SMS/RTGS/MobileApp projects adopt the new steps.
|
|
// Idempotent ONLY as long as the templates below haven't been owner-edited since: each setting
|
|
// is written only if it is still ABSENT, so a plain re-run never clobbers an owner's edit made
|
|
// in the Modules onboarding-template editor. Pass --force to unconditionally overwrite every
|
|
// template with the settings below — this DOES clobber owner edits, use with care.
|
|
// Run from repo root, naming the database explicitly (this header previously omitted the
|
|
// DATABASE_URL prefix — followed literally on the production host it resolves to SQLite, not
|
|
// the real database, and the script now refuses rather than migrating the wrong DB):
|
|
// DATABASE_URL=postgres://… npx tsx apps/hq/scripts/apply-onboarding-pipeline.ts [--force]
|
|
// To deliberately run against a local SQLite database instead, opt in explicitly:
|
|
// HQ_DB_TARGET=sqlite npx tsx apps/hq/scripts/apply-onboarding-pipeline.ts (or --sqlite)
|
|
import { openDb, sqliteFilePath } from '../src/db'
|
|
import { openPgDb, pgTargetDescription, requireDbUrlForEnv } from '../src/db-pg'
|
|
import { migrateOnboardingTemplates } from '../src/migrate-onboarding-templates'
|
|
import { getSetting, setSetting } from '../src/repos-reminders'
|
|
import type { DB } from '../src/db'
|
|
|
|
const FRONT = [
|
|
{ key: 'enquiry', label: 'Enquiry received' },
|
|
{ key: 'visit_meeting', label: 'Visit / meeting scheduled' },
|
|
{ key: 'quotation_sent', label: 'Quotation sent' },
|
|
{ key: 'quotation_approved', label: 'Quotation approved' },
|
|
]
|
|
const TAILS: Record<string, { key: string; label: string }[]> = {
|
|
SMS: [
|
|
{ key: 'document_collection', label: 'Document collection' },
|
|
{ key: 'dlt_registration', label: 'DLT registration' },
|
|
{ key: 'account_creation', label: 'Account creation & registration' },
|
|
{ key: 'installation', label: 'Installation' },
|
|
{ key: 'testing', label: 'Testing' },
|
|
{ key: 'training', label: 'Training' },
|
|
{ key: 'go_live', label: 'Go-live' },
|
|
{ key: 'payment_received', label: 'Payment received' },
|
|
],
|
|
RTGS: [
|
|
{ key: 'document_collection', label: 'Document collection' },
|
|
{ key: 'server_checkup', label: 'Server checkup' },
|
|
{ key: 'setup_configuration', label: 'Setup & configuration' },
|
|
{ key: 'installation', label: 'Installation' },
|
|
{ key: 'testing', label: 'Testing' },
|
|
{ key: 'training', label: 'Training' },
|
|
{ key: 'go_live', label: 'Go-live' },
|
|
{ key: 'payment_received', label: 'Payment received' },
|
|
],
|
|
MOBILEAPP: [
|
|
{ key: 'requirement_setup', label: 'Requirement & setup' },
|
|
{ key: 'configuration', label: 'Configuration' },
|
|
{ key: 'data_import', label: 'Data import' },
|
|
{ key: 'installation', label: 'Installation' },
|
|
{ key: 'testing', label: 'Testing' },
|
|
{ key: 'training', label: 'Training' },
|
|
{ key: 'go_live', label: 'Go-live' },
|
|
{ key: 'payment_received', label: 'Payment received' },
|
|
],
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
// Pre-ship audit FIX A: the SAME resolver server.ts uses (never re-derived locally) — and,
|
|
// unlike the server, this one-off script hard-fails rather than silently falling back to a
|
|
// throwaway SQLite file when NODE_ENV=production resolves no Postgres target.
|
|
const pgUrl = requireDbUrlForEnv()
|
|
if (pgUrl !== '') {
|
|
console.log(`[db] engine: postgres ${pgTargetDescription(pgUrl)}`)
|
|
} else {
|
|
console.log(`[db] engine: sqlite at ${sqliteFilePath(process.env['HQ_DATA_DIR'])}`)
|
|
}
|
|
const db: DB = pgUrl !== '' ? await openPgDb(pgUrl) : openDb(process.env['HQ_DATA_DIR'])
|
|
try {
|
|
const force = process.argv.includes('--force')
|
|
|
|
// Audited via setSetting (append-only-audit rule) and, unless --force, skips any key
|
|
// that's already set — so an owner's edit in the Modules onboarding-template editor is
|
|
// left alone on a re-run instead of being silently clobbered.
|
|
const upsert = async (key: string, list: unknown): Promise<void> => {
|
|
if (!force && (await getSetting(db, key)) !== null) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Skipped ${key} — already set (pass --force to overwrite an owner edit).`)
|
|
return
|
|
}
|
|
await setSetting(db, 'system', key, JSON.stringify(list))
|
|
}
|
|
await upsert('project.milestone_template.front', FRONT)
|
|
for (const [code, list] of Object.entries(TAILS)) await upsert(`project.milestone_template:${code}`, list)
|
|
// eslint-disable-next-line no-console
|
|
console.log('Template settings step done: front + SMS/RTGS/MOBILEAPP tails.')
|
|
|
|
const res = await migrateOnboardingTemplates(db)
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`Onboarding migration: ${res.projects} project(s) rebuilt, ${res.carried} tick(s) carried, ` +
|
|
`${res.dropped} dropped, ${res.collapsed} collapsed (two old ticks -> one key), ` +
|
|
`${res.preserved} custom step(s) preserved.`,
|
|
)
|
|
} finally {
|
|
await db.close()
|
|
}
|
|
}
|
|
|
|
main().catch((e) => { console.error(e); process.exit(1) })
|