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.
80 lines
3.7 KiB
TypeScript
80 lines
3.7 KiB
TypeScript
/**
|
|
* Per-counter, per-FY document series (02-ARCHITECTURE §3 rule 3): counters
|
|
* number documents offline with zero coordination. GST caps a document number
|
|
* at 16 chars from the set [A-Z a-z 0-9 - /].
|
|
*/
|
|
export interface DocSeries {
|
|
tenantId: string
|
|
storeId: string
|
|
counterId: string
|
|
docType: string
|
|
fy: string
|
|
prefix: string
|
|
nextSeq: number
|
|
}
|
|
|
|
const GST_DOC_NO = /^[A-Za-z0-9/-]{1,16}$/
|
|
|
|
export function assertGstDocNo(docNo: string): void {
|
|
if (!GST_DOC_NO.test(docNo)) {
|
|
throw new Error(`Document number "${docNo}" violates the GST 16-char [A-Za-z0-9/-] rule`)
|
|
}
|
|
}
|
|
|
|
export function formatDocNo(prefix: string, seq: number, width = 6): string {
|
|
const docNo = `${prefix}-${String(seq).padStart(width, '0')}`
|
|
assertGstDocNo(docNo)
|
|
return docNo
|
|
}
|
|
|
|
/**
|
|
* The per-counter series prefix. It embeds the FULL 4-digit FY START YEAR (M9 collision fix):
|
|
* a 2-digit marker made FY 2026-27 and 2126-27 share the prefix "ST1C2/26", so a century apart
|
|
* their doc numbers collided on the bill's UNIQUE(tenant, doc_no) and billing on that prefix was
|
|
* blocked. The 4-digit start year ("2026" vs "2126") cannot collide within a millennium.
|
|
*
|
|
* GST caps a doc number at 16 chars from [A-Za-z0-9/-]. At a 6-digit sequence,
|
|
* store(3)+counter(2)+"/"+FY(4)+"-"+seq(6) = 17 would BREAK that rule — so the counter↔FY "/"
|
|
* separator is dropped (the sequence "-" from formatDocNo stays for readability):
|
|
* seriesPrefix("ST1", "C2", "2026-27") → "ST1C22026" ⇒ doc no "ST1C22026-000123" (16 chars).
|
|
* This keeps the 16-char rule for store+counter codes totalling ≤ 5 chars (the demo ST1+C2). A
|
|
* store with a 3-char counter code (C10+) plus a 4-digit FY would need a shorter store code — a
|
|
* documented onboarding constraint, the same tightness the 2-digit format already had at C10.
|
|
*
|
|
* `fy` is the full FY label ("2026-27"); only its 4-digit start year is used.
|
|
*/
|
|
export function seriesPrefix(storeCode: string, counterCode: string, fy: string): string {
|
|
return `${storeCode}${counterCode}${fy.slice(0, 4)}`
|
|
}
|
|
|
|
/**
|
|
* Per-counter credit-note (SALE_RETURN) series prefix. A credit note lives in the same
|
|
* document table as sales under the SAME UNIQUE(tenant, doc_no), so its number must never
|
|
* collide with a sale's — the leading "CN" marker guarantees that (a sale never starts "CN").
|
|
* It reuses M9's 4-digit-FY scheme (store + counter + 4-digit FY start year) so credit notes
|
|
* of FY 2026-27 and 2126-27 can't collide either.
|
|
*
|
|
* The "CN" marker costs two characters, so the sequence is 4 digits (`CREDIT_NOTE_SEQ_WIDTH`)
|
|
* rather than the sale's 6, keeping the GST 16-char [A-Za-z0-9/-] rule at the demo's ST1+C2
|
|
* sizing: `creditNotePrefix("ST1","C2","2026-27")` → "CNST1C22026" ⇒ "CNST1C22026-0001" (16).
|
|
* That caps a counter at 9,999 credit notes per FY — far beyond any real counter — and, as for
|
|
* sales, needs store+counter codes to total ≤ 5 chars (a documented onboarding constraint).
|
|
*/
|
|
export function creditNotePrefix(storeCode: string, counterCode: string, fy: string): string {
|
|
return `CN${storeCode}${counterCode}${fy.slice(0, 4)}`
|
|
}
|
|
|
|
/** Credit-note sequence width — 4 digits so the "CN"-marked doc no stays within 16 chars. */
|
|
export const CREDIT_NOTE_SEQ_WIDTH = 4
|
|
|
|
export function nextDocNo(series: DocSeries): { docNo: string; series: DocSeries } {
|
|
const docNo = formatDocNo(series.prefix, series.nextSeq)
|
|
return { docNo, series: { ...series, nextSeq: series.nextSeq + 1 } }
|
|
}
|
|
|
|
/** FY rollover starts a fresh series; the old one is retained for returns lookups. */
|
|
export function rolloverForFy(series: DocSeries, fy: string, prefix: string): DocSeries {
|
|
if (fy === series.fy) return series
|
|
return { ...series, fy, prefix, nextSeq: 1 }
|
|
}
|