// apps/hq/scripts/apply-ltd-numbers.ts — one-off (D35): append the directory-sourced LTD // number to the client name where the co-op directory found one. Keeps the number IN the // name (owner's choice), in a consistent " LTD NO. " suffix. Idempotent + guarded: // skips a client whose name already contains that number, and only touches found=true rows. // Does NOT overwrite the name's spelling/casing — only appends the number. // LTD_JSON= DATABASE_URL=postgres://… npx tsx apps/hq/scripts/apply-ltd-numbers.ts // SQLite is never the default: to target a local SQLite database, opt in explicitly with // HQ_DB_TARGET=sqlite (or the --sqlite flag). Without either, the script refuses to run. import { readFileSync } from 'node:fs' import { openDb, sqliteFilePath } from '../src/db' import { openPgDb, pgTargetDescription, requireDbUrlForEnv } from '../src/db-pg' import { updateClient } from '../src/repos-clients' import type { DB } from '../src/db' interface Res { clientId: string; ltdNo: string; found: boolean } async function main() { const path = process.env['LTD_JSON'] if (path === undefined) throw new Error('Set LTD_JSON to the workflow result json path') const parsed = JSON.parse(readFileSync(path, 'utf8')) as { results?: Res[] } | Res[] const results = Array.isArray(parsed) ? parsed : (parsed.results ?? []) // 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 { let applied = 0, already = 0, skipped = 0 const seen = new Set() for (const r of results) { const ltd = (r.ltdNo ?? '').trim() if (!r.found || ltd === '' || seen.has(r.clientId)) { skipped++; continue } seen.add(r.clientId) const cur = await db.get<{ name: string }>(`SELECT name FROM client WHERE id=?`, r.clientId) if (cur === undefined) { skipped++; continue } // Already carries this number (bare, or "LTD. K 1028" with spaces/punct)? Don't duplicate. // Compare with all non-alphanumerics stripped so "K 1028" matches "K1028". const squish = (s: string) => s.toUpperCase().replace(/[^A-Z0-9]/g, '') if (squish(cur.name).includes(squish(ltd))) { already++; continue } const newName = `${cur.name} LTD NO. ${ltd}` await updateClient(db, 'system', r.clientId, { name: newName }) applied++ } // eslint-disable-next-line no-console console.log(`LTD numbers: ${applied} appended, ${already} already present, ${skipped} skipped (not found / no number).`) } finally { await db.close() } } main().catch((e) => { console.error(e); process.exit(1) })