feat(P19IM1-3,P19IM1-4): 接入业务事件与通知渠道后台
这个提交包含在:
@@ -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,
|
||||
|
||||
@@ -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) });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
在新工单中引用
屏蔽一个用户