文件
hyb-oa/server/tests/unit/businessNumbering.service.test.js
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

165 行
11 KiB
JavaScript

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);
});