统一付款详情与批次短编号(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -4,6 +4,8 @@ const { Client } = require("pg");
|
||||
|
||||
const { buildApp } = require("../app");
|
||||
const { createPrismaClient } = require("../prismaClient");
|
||||
const { ADVISORY_LOCK_NAMESPACES } = require("../constants/advisoryLocks");
|
||||
const { currentDateString } = require("../utils/date");
|
||||
|
||||
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
||||
|
||||
@@ -567,6 +569,56 @@ async function runDoubleCreateRace({ app, prisma, context, fixture, tokens }) {
|
||||
assertCondition(notificationCount === 0, "double_create_notification_created");
|
||||
}
|
||||
|
||||
async function runDailySequenceRace({ app, prisma, context, fixture, tokens }) {
|
||||
const [firstReimbursement, secondReimbursement] = await Promise.all([
|
||||
createApprovedReimbursement(prisma, fixture, 6),
|
||||
createApprovedReimbursement(prisma, fixture, 7),
|
||||
]);
|
||||
const businessDate = currentDateString(process.env.APP_TIMEZONE ?? "Asia/Shanghai");
|
||||
const compactDate = businessDate.replaceAll("-", "");
|
||||
const prefix = `PB-${compactDate}-`;
|
||||
const existing = await prisma.paymentBatch.findMany({
|
||||
where: { batch_no: { startsWith: prefix } },
|
||||
select: { batch_no: true },
|
||||
});
|
||||
const sequencePattern = new RegExp(`^PB-${compactDate}-([0-9]{3})$`);
|
||||
const previousMax = existing.reduce((maximum, batch) => {
|
||||
const match = sequencePattern.exec(batch.batch_no);
|
||||
return Math.max(maximum, match ? Number.parseInt(match[1], 10) : 0);
|
||||
}, 0);
|
||||
|
||||
const responses = await runBlockedRace({
|
||||
databaseUrl: context.databaseUrl,
|
||||
lockSql: "SELECT pg_advisory_xact_lock($1::int, $2::int)",
|
||||
lockParams: [
|
||||
ADVISORY_LOCK_NAMESPACES.paymentBatchDailySequence,
|
||||
Number.parseInt(compactDate, 10),
|
||||
],
|
||||
requestFactories: [firstReimbursement, secondReimbursement].map((reimbursement) => () =>
|
||||
injectJson(app, {
|
||||
method: "POST",
|
||||
url: "/api/payment-batches",
|
||||
token: tokens.finance,
|
||||
payload: {
|
||||
entityType: "reimbursement",
|
||||
entityIds: [reimbursement.id],
|
||||
defaultPaymentChannel: "cash",
|
||||
},
|
||||
})),
|
||||
timeoutCode: "daily_sequence_race_timeout",
|
||||
});
|
||||
assertStatusPair(responses, [201, 201], "daily_sequence_create_status_invalid");
|
||||
const batchNos = responses.map((response) => response.body?.data?.item?.batchNo).sort();
|
||||
assertCondition(batchNos.every((batchNo) => sequencePattern.test(batchNo)), "daily_sequence_format_invalid");
|
||||
const sequences = batchNos.map((batchNo) => Number.parseInt(sequencePattern.exec(batchNo)[1], 10));
|
||||
assertCondition(
|
||||
JSON.stringify(sequences) === JSON.stringify([previousMax + 1, previousMax + 2]),
|
||||
"daily_sequence_not_unique_incrementing",
|
||||
);
|
||||
const createdCount = await prisma.paymentBatch.count({ where: { batch_no: { in: batchNos } } });
|
||||
assertCondition(createdCount === 2, "daily_sequence_batch_count_invalid");
|
||||
}
|
||||
|
||||
async function runMissingMakerReviewCheck({ app, prisma, fixture, tokens }) {
|
||||
const reimbursement = await createApprovedReimbursement(prisma, fixture, 5);
|
||||
const created = await injectJson(app, {
|
||||
@@ -737,6 +789,9 @@ async function runPaymentPostgresConcurrencyCheck(env = process.env) {
|
||||
console.log("[PAYMENT-CONCURRENCY] double-create: start");
|
||||
await runDoubleCreateRace({ app, prisma, context, fixture, tokens });
|
||||
console.log("[PAYMENT-CONCURRENCY] double-create: pass");
|
||||
console.log("[PAYMENT-CONCURRENCY] daily-sequence: start");
|
||||
await runDailySequenceRace({ app, prisma, context, fixture, tokens });
|
||||
console.log("[PAYMENT-CONCURRENCY] daily-sequence: pass");
|
||||
console.log("[PAYMENT-CONCURRENCY] direct-pay-batch: start");
|
||||
await runDirectPayBatchRace({ app, prisma, context, fixture, tokens });
|
||||
console.log("[PAYMENT-CONCURRENCY] direct-pay-batch: pass");
|
||||
|
||||
在新工单中引用
屏蔽一个用户