feat(hq): seedable per-FY document series (HQ-1 task 7)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
78771dad8d
commit
2710455b78
@ -0,0 +1,27 @@
|
|||||||
|
import { formatDocNo } from '@sims/domain'
|
||||||
|
import type { DB } from './db'
|
||||||
|
|
||||||
|
export const TYPE_PREFIX: Record<string, string> = {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue