From 2710455b7888dd5db09be61f12ded4f5f0c27a56 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 10 Jul 2026 01:02:33 +0530 Subject: [PATCH] feat(hq): seedable per-FY document series (HQ-1 task 7) Co-Authored-By: Claude Fable 5 --- apps/hq/src/series.ts | 27 +++++++++++++++++++++++++++ apps/hq/test/series.test.ts | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 apps/hq/src/series.ts create mode 100644 apps/hq/test/series.test.ts diff --git a/apps/hq/src/series.ts b/apps/hq/src/series.ts new file mode 100644 index 0000000..3effca5 --- /dev/null +++ b/apps/hq/src/series.ts @@ -0,0 +1,27 @@ +import { formatDocNo } from '@sims/domain' +import type { DB } from './db' + +export const TYPE_PREFIX: Record = { + QUOTATION: 'QT', PROFORMA: 'PI', INVOICE: 'INV', RECEIPT: 'RCT', CREDIT_NOTE: 'CN', +} + +export function nextDocNo(db: DB, docType: string, fy: string): string { + const prefix = `${TYPE_PREFIX[docType]}/${fy.slice(2)}` + db.prepare( + `INSERT INTO doc_series (doc_type, fy, prefix, next_seq) VALUES (?, ?, ?, 1) + ON CONFLICT (doc_type, fy) DO NOTHING`, + ).run(docType, fy, prefix) + const row = db.prepare(`SELECT prefix, next_seq FROM doc_series WHERE doc_type=? AND fy=?`) + .get(docType, fy) as { prefix: string; next_seq: number } + db.prepare(`UPDATE doc_series SET next_seq = next_seq + 1 WHERE doc_type=? AND fy=?`).run(docType, fy) + return formatDocNo(row.prefix, row.next_seq, 4) +} + +/** Mid-FY cutover (spec ยง4): continue after the last APEX-issued number. */ +export function seedSeries(db: DB, docType: string, fy: string, lastUsedSeq: number): void { + const prefix = `${TYPE_PREFIX[docType]}/${fy.slice(2)}` + db.prepare( + `INSERT INTO doc_series (doc_type, fy, prefix, next_seq) VALUES (?, ?, ?, ?) + ON CONFLICT (doc_type, fy) DO UPDATE SET next_seq = excluded.next_seq`, + ).run(docType, fy, prefix, lastUsedSeq + 1) +} diff --git a/apps/hq/test/series.test.ts b/apps/hq/test/series.test.ts new file mode 100644 index 0000000..30c3ffb --- /dev/null +++ b/apps/hq/test/series.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest' +import { openDb } from '../src/db' +import { nextDocNo, seedSeries } from '../src/series' + +describe('hq doc series', () => { + it('starts at 0001 per type+fy and increments', () => { + const db = openDb(':memory:') + expect(nextDocNo(db, 'QUOTATION', '2026-27')).toBe('QT/26-27-0001') + expect(nextDocNo(db, 'QUOTATION', '2026-27')).toBe('QT/26-27-0002') + expect(nextDocNo(db, 'INVOICE', '2026-27')).toBe('INV/26-27-0001') + expect(nextDocNo(db, 'QUOTATION', '2027-28')).toBe('QT/27-28-0001') + }) + it('seeds from the last APEX number at cutover', () => { + const db = openDb(':memory:') + seedSeries(db, 'INVOICE', '2026-27', 412) + expect(nextDocNo(db, 'INVOICE', '2026-27')).toBe('INV/26-27-0413') + }) +})