R78 A2:补齐报销支付批次付款后回归

这个提交包含在:
2026-04-29 10:55:55 +08:00
父节点 5f68acfcc3
当前提交 300420aa25
共修改 3 个文件,包含 608 行新增和 1 行删除
+152 -1
查看文件
@@ -5,6 +5,11 @@ const { sendData } = require("../utils/reply");
const { buildAuditLogData } = require("../utils/audit");
const { parseDateOnlyString } = require("../utils/date");
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
const { createNotificationsWithRecords } = require("../services/notifications");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
const { sendNotificationEmailSafe } = require("../services/email");
const { flushNotificationDispatchTasks } = require("../services/notificationDispatcher");
const { enqueueReimbursementJournalExport } = require("../services/integrations/erpJournal");
const { ensureRiskFlag, buildPersistedRiskFlag, toMoneyNumber, toMoneyString } = require("../services/feeControl");
const UUID_SCHEMA = z.string().uuid();
@@ -371,6 +376,140 @@ async function executeReimbursementPayment(tx, { batch, item, actorId, index })
return { idempotent: false };
}
async function fetchPaidReimbursementForFollowUp(prisma, reimbursementId) {
return prisma.reimbursementForm.findUnique({
where: { id: reimbursementId },
select: {
id: true,
status: true,
form_number: true,
owner_id: true,
submitter_id: true,
department_id: true,
total_amount: true,
payment_voucher_no: true,
payment_date: true,
payment_channel: true,
paid_at: true,
owner: {
select: {
legal_entity: {
select: {
code: true,
},
},
},
},
submitter: {
select: {
legal_entity: {
select: {
code: true,
},
},
},
},
},
});
}
async function processPaidReimbursementFollowUps({ fastify, request, reimbursementId }) {
const reimbursement = await fetchPaidReimbursementForFollowUp(fastify.prisma, reimbursementId);
if (!reimbursement || reimbursement.status !== "paid") {
return;
}
const departmentId = reimbursement.department_id ?? request.employee.department_id ?? null;
const title = `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`;
const body = `报销单 ${reimbursement.form_number ?? reimbursementId} 已完成付款。`;
const targetPath = `/reimbursements/${reimbursementId}`;
try {
const erpGate = await evaluateFeatureFlag({
prisma: fastify.prisma,
config: fastify.config,
flagKey: FEATURE_FLAG_KEYS.erpSync,
departmentId,
strictDepartment: true,
});
if (erpGate.enabled) {
await enqueueReimbursementJournalExport(fastify.prisma, fastify.config, {
reimbursement,
actorId: request.employee.id,
});
} else {
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: "ERP_SYNC_SKIPPED_BY_FEATURE_FLAG",
targetType: "reimbursement_form",
targetId: reimbursement.id,
extra: {
featureFlag: FEATURE_FLAG_KEYS.erpSync,
departmentId,
reason: erpGate.reason,
source: "payment_batch",
},
}),
});
}
} catch (error) {
fastify.log.error(
{ err: error, reimbursementId },
"Failed to enqueue reimbursement ERP follow-up after payment batch execution",
);
}
try {
const recipients = Array.from(new Set([reimbursement.owner_id, reimbursement.submitter_id].filter(Boolean)));
const notifications = await createNotificationsWithRecords(fastify.prisma, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.reimbursementPaid,
title,
body,
targetType: "reimbursement_form",
targetId: reimbursementId,
});
for (const recipientEmployeeId of recipients) {
void sendNotificationEmailSafe({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
requestId: request.id,
type: NOTIFICATION_TYPES.reimbursementPaid,
recipientEmployeeIds: [recipientEmployeeId],
subject: title,
body,
targetPath,
});
}
await flushNotificationDispatchTasks({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
requestId: request.id,
tasks: [{
notifications,
type: NOTIFICATION_TYPES.reimbursementPaid,
title,
body,
targetType: "reimbursement_form",
targetId: reimbursementId,
targetPath,
actorId: request.employee.id,
}],
});
} catch (error) {
fastify.log.error(
{ err: error, reimbursementId },
"Failed to dispatch reimbursement paid notifications after payment batch execution",
);
}
}
async function executeTravelOrderPayment(tx, { batch, item, actorId, index }) {
const current = await tx.travelOrder.findUnique({
where: { id: item.entity_id },
@@ -680,6 +819,7 @@ async function paymentBatchesRoutes(fastify) {
await ensureFeatureEnabled(fastify);
ensureBatchManagePermission(fastify, request);
const batchId = parseId(request.params?.id);
const successfulReimbursementIds = [];
const result = await fastify.prisma.$transaction(async (tx) => {
const batch = await loadBatchWithItems(tx, batchId);
@@ -702,7 +842,10 @@ async function paymentBatchesRoutes(fastify) {
try {
if (batch.entity_type === "reimbursement") {
await executeReimbursementPayment(tx, { batch, item, actorId: request.employee.id, index });
const paymentResult = await executeReimbursementPayment(tx, { batch, item, actorId: request.employee.id, index });
if (!paymentResult.idempotent) {
successfulReimbursementIds.push(item.entity_id);
}
} else {
await executeTravelOrderPayment(tx, { batch, item, actorId: request.employee.id, index });
}
@@ -780,6 +923,14 @@ async function paymentBatchesRoutes(fastify) {
return refreshed;
});
for (const reimbursementId of new Set(successfulReimbursementIds)) {
await processPaidReimbursementFollowUps({
fastify,
request,
reimbursementId,
});
}
const openRiskFlags = await listOpenRiskFlagsForBatch(fastify.prisma, result);
return sendData(reply, request, { item: mapBatch(result, openRiskFlags) });
});