feat(P19IM1-3,P19IM1-4): 接入业务事件与通知渠道后台

这个提交包含在:
2026-03-08 13:14:22 +08:00
父节点 f6d9f23553
当前提交 faff86c947
共修改 9 个文件,包含 1268 行新增和 135 行删除
+143
查看文件
@@ -3,6 +3,10 @@ const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { buildAuditLogData } = require("../utils/audit");
const { sendData } = require("../utils/reply");
const { createNotifications } = require("../services/notifications");
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
const { sendNotificationEmailSafe } = require("../services/email");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
const UUID_SCHEMA = z.string().uuid();
const NOTE_SCHEMA = z.string().trim().max(500).nullable().optional();
@@ -72,6 +76,10 @@ function normalizeLocation(value) {
return trimmed ? trimmed : null;
}
function uniq(values) {
return Array.from(new Set((values ?? []).filter(Boolean)));
}
function ensureMovementAllowed(asset) {
if (BLOCKED_MOVEMENT_STATUSES.has(asset.status)) {
throw buildValidationError({ status: "movement_not_allowed" }, 409);
@@ -195,6 +203,7 @@ async function assetMovementsRoutes(fastify) {
const assetId = parseUuid("id", request.params?.id);
const body = parseBody(ASSIGN_BODY_SCHEMA, request.body);
const note = normalizeNote(body.note);
const emailTasks = [];
const result = await fastify.prisma.$transaction(async (tx) => {
const asset = await ensureAssetExists(tx, assetId);
@@ -249,9 +258,52 @@ async function assetMovementsRoutes(fastify) {
}),
});
const recipients = uniq([employee.id]);
const title = `【资产领用】资产卡片 ${asset.asset_no}`;
const bodyText = `资产卡片 ${asset.asset_no} 已分配给你,请在资产台账中查看。`;
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.assetAssigned,
title,
body: bodyText,
targetType: "asset_card",
targetId: asset.id,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.assetAssigned,
title,
body: bodyText,
targetType: "asset_card",
targetId: asset.id,
targetPath: "/assets",
entityType: "asset_card",
entityId: asset.id,
actorId: request.employee.id,
});
emailTasks.push({
type: NOTIFICATION_TYPES.assetAssigned,
recipientEmployeeIds: recipients,
subject: title,
body: bodyText,
targetPath: "/assets",
});
return { updatedAsset, assignmentId: assignment.id };
});
for (const task of emailTasks) {
void sendNotificationEmailSafe({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
requestId: request.id,
...task,
});
}
return sendData(reply, request, {
assetId: result.updatedAsset.id,
assetNo: result.updatedAsset.asset_no,
@@ -273,6 +325,7 @@ async function assetMovementsRoutes(fastify) {
const assetId = parseUuid("id", request.params?.id);
const body = parseBody(RETURN_BODY_SCHEMA, request.body);
const note = normalizeNote(body.note);
const emailTasks = [];
const result = await fastify.prisma.$transaction(async (tx) => {
const asset = await ensureAssetExists(tx, assetId);
@@ -312,9 +365,52 @@ async function assetMovementsRoutes(fastify) {
}),
});
const recipients = uniq([asset.assignee_employee_id]);
const title = `【资产归还】资产卡片 ${asset.asset_no}`;
const bodyText = `资产卡片 ${asset.asset_no} 已完成归还。`;
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.assetReturned,
title,
body: bodyText,
targetType: "asset_card",
targetId: asset.id,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.assetReturned,
title,
body: bodyText,
targetType: "asset_card",
targetId: asset.id,
targetPath: "/assets",
entityType: "asset_card",
entityId: asset.id,
actorId: request.employee.id,
});
emailTasks.push({
type: NOTIFICATION_TYPES.assetReturned,
recipientEmployeeIds: recipients,
subject: title,
body: bodyText,
targetPath: "/assets",
});
return { updatedAsset, assignmentId: closedAssignment?.id ?? null };
});
for (const task of emailTasks) {
void sendNotificationEmailSafe({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
requestId: request.id,
...task,
});
}
return sendData(reply, request, {
assetId: result.updatedAsset.id,
assetNo: result.updatedAsset.asset_no,
@@ -339,6 +435,7 @@ async function assetMovementsRoutes(fastify) {
const hasEmployeeField = Object.prototype.hasOwnProperty.call(body, "toEmployeeId");
const hasLocationField = Object.prototype.hasOwnProperty.call(body, "toLocation");
const nextLocation = hasLocationField ? normalizeLocation(body.toLocation) : undefined;
const emailTasks = [];
const result = await fastify.prisma.$transaction(async (tx) => {
const asset = await ensureAssetExists(tx, assetId);
@@ -428,9 +525,55 @@ async function assetMovementsRoutes(fastify) {
}),
});
const recipients = uniq([
currentEmployeeId,
employeeChange ? requestedEmployeeId : currentEmployeeId,
]);
const title = `【资产调拨】资产卡片 ${asset.asset_no}`;
const bodyText = `资产卡片 ${asset.asset_no} 已完成调拨,请在资产台账中查看最新归属与位置。`;
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.assetTransferred,
title,
body: bodyText,
targetType: "asset_card",
targetId: asset.id,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.assetTransferred,
title,
body: bodyText,
targetType: "asset_card",
targetId: asset.id,
targetPath: "/assets",
entityType: "asset_card",
entityId: asset.id,
actorId: request.employee.id,
});
emailTasks.push({
type: NOTIFICATION_TYPES.assetTransferred,
recipientEmployeeIds: recipients,
subject: title,
body: bodyText,
targetPath: "/assets",
});
return { updatedAsset, transferId: transfer.id, assignmentId };
});
for (const task of emailTasks) {
void sendNotificationEmailSafe({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
requestId: request.id,
...task,
});
}
return sendData(reply, request, {
assetId: result.updatedAsset.id,
assetNo: result.updatedAsset.asset_no,
+60
查看文件
@@ -3,6 +3,10 @@ const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { buildAuditLogData } = require("../utils/audit");
const { issueDocumentNumber } = require("../services/documentSequence");
const { createNotifications, resolveRecipientsForAssignee } = require("../services/notifications");
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
const { sendNotificationEmailSafe } = require("../services/email");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
const { sendData } = require("../utils/reply");
const UUID_SCHEMA = z.string().uuid();
@@ -79,6 +83,10 @@ function normalizeOptionalText(value) {
return trimmed ? trimmed : null;
}
function uniq(values) {
return Array.from(new Set((values ?? []).filter(Boolean)));
}
async function ensureScopeReference(tx, scopeType, scopeRefId) {
if (scopeType === "all") {
return null;
@@ -161,6 +169,7 @@ async function ensureInventoryCheck(tx, inventoryCheckId) {
completed_at: true,
created_at: true,
updated_at: true,
created_by: true,
items: {
orderBy: [{ asset: { asset_no: "asc" } }],
select: {
@@ -302,6 +311,7 @@ async function inventoryChecksRoutes(fastify) {
completed_at: true,
created_at: true,
updated_at: true,
created_by: true,
items: {
select: {
id: true,
@@ -375,6 +385,7 @@ async function inventoryChecksRoutes(fastify) {
completed_at: true,
created_at: true,
updated_at: true,
created_by: true,
items: {
orderBy: [{ asset: { asset_no: "asc" } }],
select: {
@@ -505,6 +516,7 @@ async function inventoryChecksRoutes(fastify) {
},
async (request, reply) => {
const inventoryCheckId = parseUuid("id", request.params?.id);
const emailTasks = [];
const inventoryCheck = await fastify.prisma.$transaction(async (tx) => {
const current = await ensureInventoryCheck(tx, inventoryCheckId);
ensureCheckStatus(current, "counting");
@@ -526,6 +538,7 @@ async function inventoryChecksRoutes(fastify) {
completed_at: true,
created_at: true,
updated_at: true,
created_by: true,
items: {
orderBy: [{ asset: { asset_no: "asc" } }],
select: {
@@ -563,9 +576,56 @@ async function inventoryChecksRoutes(fastify) {
}),
});
const financeMgrRecipients = await resolveRecipientsForAssignee(tx, { assigneeRole: "finance_mgr" });
const recipients = uniq([updated.created_by, ...financeMgrRecipients]);
const title = `【盘点完成】盘点单 ${updated.check_no}`;
const bodyText = `盘点单 ${updated.check_no} 已完成,请在资产盘点中查看结果。`;
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.inventoryCheckCompleted,
title,
body: bodyText,
targetType: "inventory_check",
targetId: updated.id,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.inventoryCheckCompleted,
title,
body: bodyText,
targetType: "inventory_check",
targetId: updated.id,
targetPath: "/inventory-checks",
entityType: "inventory_check",
entityId: updated.id,
actorId: request.employee.id,
});
emailTasks.push({
type: NOTIFICATION_TYPES.inventoryCheckCompleted,
recipientEmployeeIds: recipients,
subject: title,
body: bodyText,
targetPath: "/inventory-checks",
});
return updated;
});
for (const task of emailTasks) {
void sendNotificationEmailSafe({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
requestId: request.id,
...task,
});
}
return sendData(reply, request, { inventoryCheck: mapInventoryCheck(inventoryCheck) });
},
);
+19 -2
查看文件
@@ -14,6 +14,8 @@ const { issueDocumentNumber } = require("../services/documentSequence");
const { reserveBudgetForEntity, releaseReservedBudgetIfAny } = require("../services/purchaseBudget");
const { sendNotificationEmailSafe } = require("../services/email");
const { createNotifications, resolveRecipientsForAssignee } = require("../services/notifications");
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
const { openReadStream, StorageNotFoundError, writeBuffer } = require("../services/storage");
const UUID_SCHEMA = z.string().uuid();
@@ -1294,15 +1296,30 @@ async function purchaseRequestRoutes(fastify) {
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: "workflow.task_assigned",
type: NOTIFICATION_TYPES.workflowTaskAssigned,
title,
body,
targetType: "workflow_task",
targetId: workflowTask.id,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.workflowTaskAssigned,
title,
body,
targetType: "workflow_task",
targetId: workflowTask.id,
targetPath: `/workflow/tasks/${workflowTask.id}`,
entityType: "purchase_request",
entityId: purchaseRequestId,
actorId: request.employee.id,
});
emailTasks.push({
type: "workflow.task_assigned",
type: NOTIFICATION_TYPES.workflowTaskAssigned,
recipientEmployeeIds: recipients,
subject: title,
body,
+36 -4
查看文件
@@ -8,6 +8,8 @@ const {
createNotifications,
resolveRecipientsForAssignee,
} = require("../services/notifications");
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
const { sendNotificationEmailSafe } = require("../services/email");
const { enqueueIntegrationEvent } = require("../services/integrations/outbox");
const { buildErpPayload } = require("../services/integrations/erp");
@@ -1458,15 +1460,30 @@ async function reimbursementRoutes(fastify) {
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: "workflow.task_assigned",
type: NOTIFICATION_TYPES.workflowTaskAssigned,
title: `【待办】报销单 ${formNumber} 待审批`,
body: `报销单 ${formNumber} 已提交,请及时处理。`,
targetType: "workflow_task",
targetId: workflowTask.id,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.workflowTaskAssigned,
title: `【待办】报销单 ${formNumber} 待审批`,
body: `报销单 ${formNumber} 已提交,请及时处理。`,
targetType: "workflow_task",
targetId: workflowTask.id,
targetPath: `/workflow/tasks/${workflowTask.id}`,
entityType: "reimbursement",
entityId: reimbursementId,
actorId: request.employee.id,
});
emailTasks.push({
type: "workflow.task_assigned",
type: NOTIFICATION_TYPES.workflowTaskAssigned,
recipientEmployeeIds: recipients,
subject: `【待办】报销单 ${formNumber} 待审批`,
body: `报销单 ${formNumber} 已提交,请及时处理。`,
@@ -1799,15 +1816,30 @@ async function reimbursementRoutes(fastify) {
);
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: "reimbursement.paid",
type: NOTIFICATION_TYPES.reimbursementPaid,
title: `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`,
body: `报销单 ${reimbursement.form_number ?? reimbursementId} 已完成付款。`,
targetType: "reimbursement_form",
targetId: reimbursementId,
});
await enqueueImNotifications(tx, {
config: fastify.config,
logger: fastify.log,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.reimbursementPaid,
title: `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`,
body: `报销单 ${reimbursement.form_number ?? reimbursementId} 已完成付款。`,
targetType: "reimbursement_form",
targetId: reimbursementId,
targetPath: `/reimbursements/${reimbursementId}`,
entityType: "reimbursement",
entityId: reimbursementId,
actorId: request.employee.id,
});
emailTasks.push({
type: "reimbursement.paid",
type: NOTIFICATION_TYPES.reimbursementPaid,
recipientEmployeeIds: recipients,
subject: `【已付款】报销单 ${reimbursement.form_number ?? reimbursementId}`,
body: `报销单 ${reimbursement.form_number ?? reimbursementId} 已完成付款。`,
+262 -129
查看文件
@@ -9,7 +9,9 @@ const {
createNotifications,
resolveRecipientsForAssignee,
} = require("../services/notifications");
const { enqueueImNotifications } = require("../services/notifications/imEnqueue");
const { sendNotificationEmailSafe } = require("../services/email");
const { NOTIFICATION_TYPES } = require("../constants/notificationTypes");
const {
findActiveDelegationForDelegate,
listActiveDelegationPrincipals,
@@ -407,6 +409,201 @@ function buildReturnedCopy(entityMeta, comment) {
};
}
function uniq(values) {
return Array.from(new Set((values ?? []).filter(Boolean)));
}
async function loadLeaveNotifyContext(tx, leaveRequestId) {
return tx.leaveRequest.findUnique({
where: { id: leaveRequestId },
select: {
id: true,
owner_id: true,
},
});
}
async function loadPurchaseNotifyContext(tx, purchaseRequestId) {
return tx.purchaseRequest.findUnique({
where: { id: purchaseRequestId },
select: {
id: true,
request_no: true,
applicant_id: true,
submitter_id: true,
},
});
}
async function loadWorkflowNotifyContext(tx, entityType, entityId) {
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
if (normalizedEntityType === 'reimbursement') {
const record = await loadReimbursementNotifyContext(tx, entityId);
return {
record,
recipientEmployeeIds: uniq([record?.owner_id, record?.submitter_id]),
};
}
if (normalizedEntityType === 'leave') {
const record = await loadLeaveNotifyContext(tx, entityId);
return {
record,
recipientEmployeeIds: uniq([record?.owner_id]),
};
}
if (normalizedEntityType === 'purchase_request') {
const record = await loadPurchaseNotifyContext(tx, entityId);
return {
record,
recipientEmployeeIds: uniq([record?.applicant_id, record?.submitter_id]),
};
}
return {
record: null,
recipientEmployeeIds: [],
};
}
function resolveWorkflowOutcomeNotificationType(entityType, outcome) {
const normalizedEntityType = entityType === 'procurement' ? 'purchase_request' : entityType;
if (outcome === 'approved') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementApproved;
if (normalizedEntityType === 'leave') return NOTIFICATION_TYPES.leaveApproved;
if (normalizedEntityType === 'purchase_request') return NOTIFICATION_TYPES.purchaseRequestApproved;
}
if (outcome === 'rejected') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementRejected;
if (normalizedEntityType === 'leave') return NOTIFICATION_TYPES.leaveRejected;
if (normalizedEntityType === 'purchase_request') return NOTIFICATION_TYPES.purchaseRequestRejected;
}
if (outcome === 'returned') {
if (normalizedEntityType === 'reimbursement') return NOTIFICATION_TYPES.reimbursementReturned;
}
return null;
}
async function queueTaskAssignedNotifications({
tx,
config,
logger,
actorId,
emailTasks,
recipientEmployeeIds,
title,
body,
taskId,
entityType,
entityId,
}) {
const recipients = uniq(recipientEmployeeIds);
if (!recipients.length) {
return;
}
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.workflowTaskAssigned,
title,
body,
targetType: 'workflow_task',
targetId: taskId,
});
await enqueueImNotifications(tx, {
config,
logger,
recipientEmployeeIds: recipients,
type: NOTIFICATION_TYPES.workflowTaskAssigned,
title,
body,
targetType: 'workflow_task',
targetId: taskId,
targetPath: `/workflow/tasks/${taskId}`,
entityType,
entityId,
actorId,
});
emailTasks.push({
type: NOTIFICATION_TYPES.workflowTaskAssigned,
recipientEmployeeIds: recipients,
subject: title,
body,
targetPath: `/workflow/tasks/${taskId}`,
});
}
async function queueWorkflowOutcomeNotifications({
tx,
config,
logger,
actorId,
emailTasks,
workflowInstance,
outcome,
comment = null,
}) {
const normalizedEntityType = workflowInstance.entity_type === 'procurement' ? 'purchase_request' : workflowInstance.entity_type;
const notificationType = resolveWorkflowOutcomeNotificationType(normalizedEntityType, outcome);
if (!notificationType) {
return;
}
const context = await loadWorkflowNotifyContext(tx, normalizedEntityType, workflowInstance.entity_id);
const recipients = uniq(context.recipientEmployeeIds);
if (!recipients.length) {
return;
}
const entityMeta = getWorkflowEntityMeta(normalizedEntityType, context.record, workflowInstance.entity_id);
const copy =
outcome === 'approved'
? buildApprovedCopy(entityMeta)
: outcome === 'rejected'
? buildRejectedCopy(entityMeta, comment)
: buildReturnedCopy(entityMeta, comment);
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: notificationType,
title: copy.title,
body: copy.body,
targetType: entityMeta.targetType,
targetId: workflowInstance.entity_id,
});
await enqueueImNotifications(tx, {
config,
logger,
recipientEmployeeIds: recipients,
type: notificationType,
title: copy.title,
body: copy.body,
targetType: entityMeta.targetType,
targetId: workflowInstance.entity_id,
targetPath: entityMeta.detailPath,
entityType: normalizedEntityType,
entityId: workflowInstance.entity_id,
actorId,
});
emailTasks.push({
type: notificationType,
recipientEmployeeIds: recipients,
subject: copy.title,
body: copy.body,
targetPath: entityMeta.detailPath,
});
}
async function createSkipHistoryRecords(tx, workflowInstanceId, actorId, ownerId, skippedSteps) {
for (const skipped of skippedSteps) {
await tx.workflowHistory.create({
@@ -1069,21 +1266,18 @@ async function workflowRoutes(fastify) {
assigneeRole: actionable.assigneeRole,
});
await createNotifications(tx, {
await queueTaskAssignedNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
recipientEmployeeIds: recipients,
type: "workflow.task_assigned",
title: pendingCopy.title,
body: pendingCopy.body,
targetType: "workflow_task",
targetId: nextTask.id,
});
emailTasks.push({
type: "workflow.task_assigned",
recipientEmployeeIds: recipients,
subject: pendingCopy.title,
body: pendingCopy.body,
targetPath: `/workflow/tasks/${nextTask.id}`,
taskId: nextTask.id,
entityType: workflowInstance.entity_type,
entityId: workflowInstance.entity_id,
});
} catch (error) {
fastify.log.error(
@@ -1197,41 +1391,21 @@ async function workflowRoutes(fastify) {
},
});
if (workflowInstance.entity_type === "reimbursement") {
try {
const reimbursement = await loadReimbursementNotifyContext(tx, workflowInstance.entity_id);
const entityMeta = getWorkflowEntityMeta(
workflowInstance.entity_type,
reimbursement,
workflowInstance.entity_id,
);
const approvedCopy = buildApprovedCopy(entityMeta);
const recipients = Array.from(
new Set([reimbursement?.owner_id, reimbursement?.submitter_id].filter(Boolean)),
);
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: "reimbursement.approved",
title: approvedCopy.title,
body: approvedCopy.body,
targetType: entityMeta.targetType,
targetId: workflowInstance.entity_id,
});
emailTasks.push({
type: "reimbursement.approved",
recipientEmployeeIds: recipients,
subject: approvedCopy.title,
body: approvedCopy.body,
targetPath: entityMeta.detailPath,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId, reimbursementId: workflowInstance.entity_id },
"Failed to create reimbursement approved notifications",
);
}
try {
await queueWorkflowOutcomeNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
workflowInstance,
outcome: 'approved',
});
} catch (error) {
fastify.log.error(
{ err: error, taskId, entityType: workflowInstance.entity_type, entityId: workflowInstance.entity_id },
"Failed to create approval outcome notifications",
);
}
result = {
@@ -1385,41 +1559,22 @@ async function workflowRoutes(fastify) {
},
});
if (workflowInstance.entity_type === "reimbursement") {
try {
const reimbursement = await loadReimbursementNotifyContext(tx, workflowInstance.entity_id);
const entityMeta = getWorkflowEntityMeta(
workflowInstance.entity_type,
reimbursement,
workflowInstance.entity_id,
);
const rejectedCopy = buildRejectedCopy(entityMeta, comment);
const recipients = Array.from(
new Set([reimbursement?.owner_id, reimbursement?.submitter_id].filter(Boolean)),
);
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: "reimbursement.rejected",
title: rejectedCopy.title,
body: rejectedCopy.body,
targetType: entityMeta.targetType,
targetId: workflowInstance.entity_id,
});
emailTasks.push({
type: "reimbursement.rejected",
recipientEmployeeIds: recipients,
subject: rejectedCopy.title,
body: rejectedCopy.body,
targetPath: entityMeta.detailPath,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId, reimbursementId: workflowInstance.entity_id },
"Failed to create reimbursement rejected notifications",
);
}
try {
await queueWorkflowOutcomeNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
workflowInstance,
outcome: 'rejected',
comment,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId, entityType: workflowInstance.entity_type, entityId: workflowInstance.entity_id },
"Failed to create rejection outcome notifications",
);
}
result = {
@@ -1572,41 +1727,22 @@ async function workflowRoutes(fastify) {
},
});
if (workflowInstance.entity_type === "reimbursement") {
try {
const reimbursement = await loadReimbursementNotifyContext(tx, workflowInstance.entity_id);
const entityMeta = getWorkflowEntityMeta(
workflowInstance.entity_type,
reimbursement,
workflowInstance.entity_id,
);
const returnedCopy = buildReturnedCopy(entityMeta, comment);
const recipients = Array.from(
new Set([reimbursement?.owner_id, reimbursement?.submitter_id].filter(Boolean)),
);
await createNotifications(tx, {
recipientEmployeeIds: recipients,
type: "reimbursement.returned",
title: returnedCopy.title,
body: returnedCopy.body,
targetType: entityMeta.targetType,
targetId: workflowInstance.entity_id,
});
emailTasks.push({
type: "reimbursement.returned",
recipientEmployeeIds: recipients,
subject: returnedCopy.title,
body: returnedCopy.body,
targetPath: entityMeta.detailPath,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId, reimbursementId: workflowInstance.entity_id },
"Failed to create reimbursement returned notifications",
);
}
try {
await queueWorkflowOutcomeNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
workflowInstance,
outcome: 'returned',
comment,
});
} catch (error) {
fastify.log.error(
{ err: error, taskId, entityType: workflowInstance.entity_type, entityId: workflowInstance.entity_id },
"Failed to create return outcome notifications",
);
}
result = {
@@ -1719,21 +1855,18 @@ async function workflowRoutes(fastify) {
workflowInstance.entity_id,
);
const pendingCopy = buildPendingTaskCopy(entityMeta, { transfer: true });
await createNotifications(tx, {
await queueTaskAssignedNotifications({
tx,
config: fastify.config,
logger: fastify.log,
actorId: request.employee.id,
emailTasks,
recipientEmployeeIds: [toApproverId],
type: "workflow.task_assigned",
title: pendingCopy.title,
body: pendingCopy.body,
targetType: "workflow_task",
targetId: taskId,
});
emailTasks.push({
type: "workflow.task_assigned",
recipientEmployeeIds: [toApproverId],
subject: pendingCopy.title,
body: pendingCopy.body,
targetPath: `/workflow/tasks/${taskId}`,
taskId,
entityType: workflowInstance.entity_type,
entityId: workflowInstance.entity_id,
});
} catch (error) {
fastify.log.error(