const { BUSINESS_NUMBER_FAMILIES, businessDateAt, resolveNumberingContext, issueBusinessNumber, reserveProjectNumber, setNumberingPolicy } = require("../../src/services/businessNumbering"); const { issueDocumentNumber } = require("../../src/services/documentSequence"); const NOW = new Date("2026-09-30T16:00:00Z"); const ACTOR = "11111111-1111-4111-8111-111111111111"; function fixture({ policy = null, maximum = 0n } = {}) { const counters = new Map(); const tx = { $executeRaw: vi.fn(async () => 0), $queryRaw: vi.fn(async () => [{ maximum }]), businessNumberPolicy: { findUnique: vi.fn(async () => policy), create: vi.fn(async ({ data }) => (policy = { ...data })), update: vi.fn(async ({ data }) => (policy = { ...policy, ...data })), }, businessNumberCounter: { findUnique: vi.fn(async ({ where }) => counters.get(where.key) ?? null), upsert: vi.fn(async ({ where, create, update }) => { const value = { ...(counters.get(where.key) ?? create), ...update, updated_at: NOW }; counters.set(where.key, value); return value; }), }, documentSequence: { findUnique: vi.fn(async () => null), upsert: vi.fn(async ({ create }) => create) }, documentCategory: { findUnique: vi.fn(async () => null), findFirst: vi.fn(async () => null) }, auditLog: { create: vi.fn(async ({ data }) => data) }, }; return { tx, counters }; } const issue = (tx, family, now = NOW, rest = {}) => issueBusinessNumber(tx, { family, now, ...rest }); describe("business numbering v2", () => { it.each([ ["reimbursement", "BX-202610-0001"], ["payment_batch", "PB-2026-0001"], ["company_loan", "JK-2026-0001"], ["loan_repayment", "HK-2026-0001"], ["purchase_request", "CG-2026-0001"], ["inventory_check", "PD-2026-0001"], ["project", "PRJ-2026-0001"], ["asset_card", "FA-0001"], ])("issues %s with its approved period", async (family, expected) => { const { tx } = fixture(); expect((await issue(tx, family)).documentNumber).toBe(expected); expect((await issue(tx, family)).documentNumber).toBe(expected.replace("0001", "0002")); expect(tx.$executeRaw.mock.invocationCallOrder[0]).toBeLessThan(tx.$queryRaw.mock.invocationCallOrder[0]); expect(tx.$executeRaw.mock.calls[0].slice(1)).toEqual([1004, BUSINESS_NUMBER_FAMILIES[family].lockKey]); }); it("uses one instant and configured timezone at month and year boundaries", async () => { const { tx } = fixture(); expect((await issue(tx, "reimbursement", new Date("2026-09-30T15:59:59Z"))).documentNumber).toBe("BX-202609-0001"); expect((await issue(tx, "reimbursement")).documentNumber).toBe("BX-202610-0001"); expect((await issue(tx, "reimbursement", NOW, { timeZone: "Pacific/Honolulu" })).documentNumber).toBe("BX-202609-0002"); expect((await issue(tx, "payment_batch", new Date("2026-12-31T15:59:59Z"))).documentNumber).toBe("PB-2026-0001"); expect((await issue(tx, "payment_batch", new Date("2026-12-31T16:00:00Z"))).documentNumber).toBe("PB-2027-0001"); expect((await issue(tx, "asset_card", new Date("2027-01-01T00:00:00Z"))).documentNumber).toBe("FA-0001"); expect((await issue(tx, "asset_card", new Date("2028-01-01T00:00:00Z"))).documentNumber).toBe("FA-0002"); expect(businessDateAt(NOW)).toBe("2026-10-01"); }); it("keeps EA categories independent and counters shared for the same prefix", async () => { const { tx } = fixture(); for (const [prefix, serial] of [["EA-DOC", "0001"], ["EA-SYS", "0001"], ["EA-DOC", "0002"]]) { expect((await issue(tx, "asset_card", NOW, { prefix })).documentNumber).toBe(`${prefix}-${serial}`); } }); it("continues after matching occupied numbers and expands beyond four digits", async () => { const { tx } = fixture({ maximum: 9999n }); expect((await issue(tx, "payment_batch")).documentNumber).toBe("PB-2026-10000"); tx.$queryRaw.mockResolvedValue([{ maximum: 0n }]); expect((await issue(tx, "payment_batch")).documentNumber).toBe("PB-2026-10001"); }); it("never rounds large counters and fails at bigint capacity", async () => { const { tx } = fixture({ maximum: 9007199254740992n }); const result = await issue(tx, "asset_card"); expect(result.documentNumber).toBe("FA-9007199254740993"); expect(result.config.currentNumber).toBe("9007199254740993"); tx.$queryRaw.mockResolvedValue([{ maximum: 9223372036854775807n }]); await expect(issue(tx, "asset_card")).rejects.toThrow("capacity exhausted"); expect(tx.businessNumberCounter.upsert).toHaveBeenCalledTimes(1); }); it("reserves manual project codes without renaming them or activating policy", async () => { const { tx } = fixture(); await reserveProjectNumber(tx, "CLIENT-FORMAL-A"); expect(tx.$executeRaw).not.toHaveBeenCalled(); await reserveProjectNumber(tx, "PRJ-2026-0010"); await reserveProjectNumber(tx, "PRJ-2026-0002"); expect((await issue(tx, "project")).documentNumber).toBe("PRJ-2026-0011"); expect(tx.businessNumberPolicy.findUnique).not.toHaveBeenCalled(); }); it.each([null, { enabled: false, effective_at: NOW }, { enabled: true, effective_at: new Date(NOW.getTime() + 1) }])("keeps legacy without an effective active policy: %j", async (policy) => { const { tx } = fixture({ policy }); expect((await resolveNumberingContext(tx, { now: NOW })).version).toBe(1); expect(tx.$executeRaw.mock.invocationCallOrder[0]).toBeLessThan(tx.businessNumberPolicy.findUnique.mock.invocationCallOrder[0]); }); it("activates at the exact boundary but a pinned legacy or v2 draft keeps its version", async () => { const { tx } = fixture({ policy: { enabled: true, effective_at: NOW } }); expect((await resolveNumberingContext(tx, { now: NOW })).version).toBe(2); expect((await resolveNumberingContext(tx, { version: 1, now: NOW })).version).toBe(1); tx.businessNumberPolicy.findUnique.mockResolvedValue(null); expect((await resolveNumberingContext(tx, { version: 2, now: NOW })).version).toBe(2); await expect(resolveNumberingContext(tx, { version: 3 })).rejects.toThrow("Invalid numbering version"); }); it("keeps historical missing reimbursement numbers legacy and leaves old counters untouched for new drafts", async () => { const { tx } = fixture({ policy: { enabled: true, effective_at: NOW } }); const old = await issueDocumentNumber(tx, { documentType: "reimbursement", numberingVersion: 1, now: NOW }); expect(old.documentNumber).toMatch(/^BX-\d{8}-0001$/); expect(tx.businessNumberCounter.upsert).not.toHaveBeenCalled(); tx.documentSequence.upsert.mockClear(); const fresh = await issueDocumentNumber(tx, { documentType: "reimbursement", now: NOW }); expect(fresh.documentNumber).toBe("BX-202610-0001"); expect(tx.documentSequence.upsert).not.toHaveBeenCalled(); }); it("read-only config context acquires no locks or writes", async () => { const { tx } = fixture(); await resolveNumberingContext(tx, { readOnly: true, now: NOW }); expect(tx.$executeRaw).not.toHaveBeenCalled(); expect(tx.businessNumberPolicy.create).not.toHaveBeenCalled(); }); it("fails closed on a policy read failure rather than silently taking legacy", async () => { const { tx } = fixture(); tx.businessNumberPolicy.findUnique.mockRejectedValue(new Error("synthetic")); await expect(resolveNumberingContext(tx)).rejects.toThrow("synthetic"); }); it("persists the activation time through pause/resume and audits each transition", async () => { const { tx } = fixture(); const input = { actorId: ACTOR, reference: "synthetic-change-1", writersCompatible: true, now: NOW }; const effectiveAt = new Date(NOW.getTime() + 1000); await setNumberingPolicy(tx, { ...input, action: "activate", effectiveAt }); await setNumberingPolicy(tx, { ...input, action: "pause" }); const resumed = await setNumberingPolicy(tx, { ...input, action: "resume" }); expect(resumed.effective_at).toEqual(effectiveAt); expect(resumed.enabled).toBe(true); expect(tx.auditLog.create).toHaveBeenCalledTimes(3); await expect(setNumberingPolicy(tx, { ...input, action: "activate", effectiveAt })).rejects.toThrow(); }); it("requires forward-only explicit activation and compatible writers", async () => { const { tx } = fixture(); const input = { action: "activate", actorId: ACTOR, reference: "synthetic-1", writersCompatible: true, now: NOW }; await expect(setNumberingPolicy(tx, { ...input, effectiveAt: new Date(NOW.getTime() - 1) })).rejects.toThrow(); await expect(setNumberingPolicy(tx, { ...input, effectiveAt: NOW, writersCompatible: false })).rejects.toThrow(); expect(tx.businessNumberPolicy.create).not.toHaveBeenCalled(); }); }); it("rejects activation whose effective time elapsed while waiting for the policy lock", async () => { vi.useFakeTimers(); vi.setSystemTime(NOW); try { const { tx } = fixture(); tx.$executeRaw.mockImplementation(async () => { vi.setSystemTime(new Date(NOW.getTime() + 2000)); return 0; }); await expect(setNumberingPolicy(tx, { action: "activate", effectiveAt: new Date(NOW.getTime() + 1000), actorId: ACTOR, reference: "synthetic-wait", writersCompatible: true })).rejects.toThrow("future effective time"); expect(tx.businessNumberPolicy.create).not.toHaveBeenCalled(); expect(tx.auditLog.create).not.toHaveBeenCalled(); } finally { vi.useRealTimers(); } }); it.each([["reimbursement", "YYYYMM"], ["purchase_request", "YYYY"], ["inventory_check", "YYYY"]])("refuses to enable independent counters over an active legacy period for %s", async (documentType, dateFormat) => { const { tx } = fixture(); tx.documentSequence.findUnique.mockImplementation(async ({ where }) => where.document_type === documentType ? { date_format: dateFormat } : null); await expect(setNumberingPolicy(tx, { action: "activate", effectiveAt: new Date(NOW.getTime() + 1000), now: NOW, actorId: ACTOR, reference: "synthetic-overlap", writersCompatible: true })).rejects.toThrow("compatibility review"); expect(tx.businessNumberPolicy.create).not.toHaveBeenCalled(); expect(tx.documentSequence.upsert).not.toHaveBeenCalled(); expect(tx.auditLog.create).not.toHaveBeenCalled(); }); it("allows pause but refuses resume after an overlapping legacy configuration is introduced", async () => { const { tx } = fixture({ policy: { enabled: true, effective_at: NOW } }); tx.documentSequence.findUnique.mockResolvedValue({ date_format: "YYYYMM" }); const input = { actorId: ACTOR, reference: "synthetic-overlap", writersCompatible: true }; expect((await setNumberingPolicy(tx, { ...input, action: "pause" })).enabled).toBe(false); await expect(setNumberingPolicy(tx, { ...input, action: "resume" })).rejects.toThrow("compatibility review"); expect(tx.businessNumberPolicy.update).toHaveBeenCalledTimes(1); });