统一付款详情与批次短编号(08-01-reimbursement-notification-progress-clarity)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-02 09:02:40 +08:00
父节点 171fc09524
当前提交 5d274af8d4
共修改 11 个文件,包含 467 行新增和 82 行删除
@@ -429,7 +429,17 @@ function buildPrisma(options = {}) {
employee: {
findMany: vi.fn(async () => state.employees.map((employee) => ({ ...employee }))),
},
$queryRaw: vi.fn(async () => [{ id: BATCH_ID }]),
$executeRaw: vi.fn(async () => 0),
$queryRaw: vi.fn(async (query, ...values) => {
const text = Array.isArray(query) ? query.join(" ") : String(query?.sql ?? query ?? "");
if (text.includes('ORDER BY "batch_no" DESC')) {
const pattern = values[1];
return typeof pattern === "string" && new RegExp(pattern).test(state.batch.batch_no)
? [{ batch_no: state.batch.batch_no }]
: [];
}
return [{ id: BATCH_ID }];
}),
};
const prisma = {
@@ -507,7 +517,7 @@ async function buildApp(options = {}) {
app.decorate("prisma", prisma);
app.decorate("config", {
appTimezone: "Asia/Shanghai",
appTimezone: options.appTimezone ?? "Asia/Shanghai",
storageDir: tempDir,
maxUploadSizeBytes: 5 * 1024 * 1024,
paymentBatchEnabled: true,
@@ -1050,7 +1060,8 @@ describe("payment batch reimbursement routes", () => {
expect(response.statusCode).toBe(201);
expect(tx.paymentBatch.create).toHaveBeenCalledWith({
data: expect.objectContaining({
name: `${currentDateString("Asia/Shanghai")} 付款批次`,
batch_no: `PB-${currentDateString("Asia/Shanghai").replaceAll("-", "")}-001`,
name: `${currentDateString("Asia/Shanghai")} 付款批次 001`,
default_payment_date: null,
default_payment_channel: "bank",
voucher_prefix: null,
@@ -1157,7 +1168,7 @@ describe("payment batch reimbursement routes", () => {
await app.close();
});
it("adds a non-identifying random suffix to concurrent-style batch numbers", async () => {
it("allocates an increasing company-local daily sequence and preserves an explicit name", async () => {
const { app, tx } = await buildApp();
const payload = {
entityType: "reimbursement",
@@ -1182,9 +1193,87 @@ describe("payment batch reimbursement routes", () => {
expect(second.statusCode).toBe(201);
const batchNos = tx.paymentBatch.create.mock.calls.map(([argument]) => argument.data.batch_no);
expect(batchNos).toHaveLength(2);
expect(batchNos[0]).toMatch(/^PB-\d{8}-\d{6}-\d{3}-[0-9A-F]{6}$/);
expect(batchNos[1]).toMatch(/^PB-\d{8}-\d{6}-\d{3}-[0-9A-F]{6}$/);
expect(batchNos[0]).not.toBe(batchNos[1]);
const compactDate = currentDateString("Asia/Shanghai").replaceAll("-", "");
expect(batchNos).toEqual([
`PB-${compactDate}-001`,
`PB-${compactDate}-002`,
]);
expect(tx.paymentBatch.create.mock.calls[0][0].data.name).toBe("付款批次编号验证");
expect(tx.paymentBatch.create.mock.calls[1][0].data.name).toBe("付款批次编号验证");
const advisoryLockCalls = tx.$executeRaw.mock.calls.filter(([query]) =>
Array.isArray(query) && query.join(" ").includes("pg_advisory_xact_lock"),
);
expect(advisoryLockCalls).toHaveLength(2);
await app.close();
});
it("uses the configured company timezone and starts its local day at 001", async () => {
const appTimezone = "Pacific/Honolulu";
const businessDate = currentDateString(appTimezone);
const { app, tx } = await buildApp({ appTimezone });
const response = await app.inject({
method: "POST",
url: "/api/payment-batches",
headers: { authorization: "Bearer test" },
payload: { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID] },
});
expect(response.statusCode).toBe(201);
expect(tx.paymentBatch.create).toHaveBeenCalledWith({
data: expect.objectContaining({
batch_no: `PB-${businessDate.replaceAll("-", "")}-001`,
name: `${businessDate} 付款批次 001`,
}),
});
await app.close();
});
it("ignores a legacy long batch number from the same company-local day", async () => {
const businessDate = currentDateString("Asia/Shanghai");
const compactDate = businessDate.replaceAll("-", "");
const { app, tx } = await buildApp({
batch: { batch_no: `PB-${compactDate}-063245-812-A1B2C3` },
});
const response = await app.inject({
method: "POST",
url: "/api/payment-batches",
headers: { authorization: "Bearer test" },
payload: { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID] },
});
expect(response.statusCode).toBe(201);
expect(tx.paymentBatch.create).toHaveBeenCalledWith({
data: expect.objectContaining({
batch_no: `PB-${compactDate}-001`,
name: `${businessDate} 付款批次 001`,
}),
});
await app.close();
});
it("fails closed when the company-local daily sequence is exhausted", async () => {
const businessDate = currentDateString("Asia/Shanghai");
const { app, tx } = await buildApp({
batch: { batch_no: `PB-${businessDate.replaceAll("-", "")}-999` },
});
const response = await app.inject({
method: "POST",
url: "/api/payment-batches",
headers: { authorization: "Bearer test" },
payload: { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID] },
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details.batchNo).toBe("daily_sequence_exhausted");
expect(tx.paymentBatch.create).not.toHaveBeenCalled();
expect(tx.paymentBatchItem.create).not.toHaveBeenCalled();
expect(tx.auditLog.create).not.toHaveBeenCalled();
await app.close();
});