AP6 增加审批催办与效率摘要

这个提交包含在:
2026-05-12 10:41:47 +08:00
父节点 f99fbce3d8
当前提交 72e38db4e5
共修改 23 个文件,包含 1362 行新增和 12 行删除
@@ -0,0 +1,23 @@
const {
IM_SUPPORTED_NOTIFICATION_TYPES,
NOTIFICATION_DOMAIN_SCOPES,
NOTIFICATION_TYPES,
resolveNotificationDomainScope,
} = require("../../../src/constants/notificationTypes");
describe("notification types", () => {
it("allows workflow reminder and overdue notifications through IM routing", () => {
expect(IM_SUPPORTED_NOTIFICATION_TYPES).toEqual(
expect.arrayContaining([
NOTIFICATION_TYPES.workflowTaskReminded,
NOTIFICATION_TYPES.workflowTaskOverdue,
]),
);
expect(resolveNotificationDomainScope(NOTIFICATION_TYPES.workflowTaskReminded, "workflow_task")).toBe(
NOTIFICATION_DOMAIN_SCOPES.workflow,
);
expect(resolveNotificationDomainScope(NOTIFICATION_TYPES.workflowTaskOverdue, "workflow_task")).toBe(
NOTIFICATION_DOMAIN_SCOPES.workflow,
);
});
});
@@ -0,0 +1,84 @@
const {
calculateDueAt,
remindOverdueWorkflowTask,
} = require("../../../src/jobs/workflowOverdueReminderJob");
const { NOTIFICATION_TYPES } = require("../../../src/constants/notificationTypes");
describe("workflow overdue reminder job", () => {
it("calculates fallback due time from task creation", () => {
const dueAt = calculateDueAt({
created_at: new Date("2026-05-11T10:00:00.000Z"),
due_date: null,
});
expect(dueAt.toISOString()).toBe("2026-05-12T10:00:00.000Z");
});
it("creates history, audit and notification once cooldown allows", async () => {
const task = {
id: "11111111-1111-4111-8111-111111111111",
instance_id: "22222222-2222-4222-8222-222222222222",
step_id: "manager",
step_name: "经理审批",
assignee_id: "33333333-3333-4333-8333-333333333333",
assignee_role: null,
created_at: new Date("2026-05-11T07:00:00.000Z"),
due_date: null,
workflow_instance: {
id: "22222222-2222-4222-8222-222222222222",
entity_type: "reimbursement",
entity_id: "44444444-4444-4444-8444-444444444444",
reimbursement_form: {
form_number: "BX-20260512-001",
total_amount: "120.00",
owner: { name: "测试员工" },
},
},
};
const createdHistory = [];
const createdAudit = [];
const createdNotifications = [];
const tx = {
workflowHistory: {
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn().mockImplementation(async ({ data }) => {
createdHistory.push(data);
return { id: "history-1", ...data };
}),
},
auditLog: {
create: vi.fn().mockImplementation(async ({ data }) => {
createdAudit.push(data);
return { id: "audit-1", ...data };
}),
},
notification: {
create: vi.fn().mockImplementation(async ({ data }) => {
const row = { id: `notification-${createdNotifications.length + 1}`, ...data };
createdNotifications.push(row);
return row;
}),
},
};
const result = await remindOverdueWorkflowTask(tx, task, new Date("2026-05-12T08:00:00.000Z"));
expect(result.skipped).toBe(false);
expect(createdHistory[0]).toEqual(expect.objectContaining({ action: "overdue_remind" }));
expect(createdAudit[0]).toEqual(expect.objectContaining({ action: "WORKFLOW_TASK_OVERDUE_NOTIFIED" }));
expect(createdNotifications[0]).toEqual(
expect.objectContaining({
recipient_employee_id: "33333333-3333-4333-8333-333333333333",
type: NOTIFICATION_TYPES.workflowTaskOverdue,
target_type: "workflow_task",
target_id: task.id,
}),
);
expect(result.dispatchTask).toEqual(
expect.objectContaining({
type: NOTIFICATION_TYPES.workflowTaskOverdue,
targetPath: `/mobile/approvals?taskId=${task.id}`,
}),
);
});
});
@@ -4,9 +4,14 @@ const workflowRoutes = require("../../../src/routes/workflow");
describe("workflow travel helpers", () => {
const {
buildApprovedCopy,
buildOverdueTaskCopy,
buildPendingTaskCopy,
buildRejectedCopy,
buildRemindedTaskCopy,
buildReturnedCopy,
buildWorkflowTaskSummaryPayload,
calculateWorkflowTaskDueAt,
isWorkflowTaskOverdue,
buildWorkflowEntityMeta,
resolveWorkflowOutcomeNotificationType,
} = workflowRoutes.__testables;
@@ -82,6 +87,14 @@ describe("workflow travel helpers", () => {
title: "【已退回】报销单 BX-20260325-001",
body: "申请人:刘报销\n金额:880.00\n单号:BX-20260325-001\n结果:已退回:请补发票",
});
expect(buildRemindedTaskCopy(reimbursementMeta)).toEqual({
title: "【催办】报销单 BX-20260325-001 待审批",
body: "申请人:刘报销\n金额:880.00\n单号:BX-20260325-001\n动作:申请人已催办,请及时处理审批任务。",
});
expect(buildOverdueTaskCopy(reimbursementMeta)).toEqual({
title: "【超时提醒】报销单 BX-20260325-001 待审批",
body: "申请人:刘报销\n金额:880.00\n单号:BX-20260325-001\n动作:该审批已超时,请尽快处理。",
});
});
it("resolves approved and rejected notification types for travel workflows", () => {
@@ -93,4 +106,47 @@ describe("workflow travel helpers", () => {
);
expect(resolveWorkflowOutcomeNotificationType("travel_request", "returned")).toBeNull();
});
it("builds pending task productivity summary with overdue buckets", () => {
const now = new Date("2026-05-12T08:00:00.000Z");
const tasks = [
{
id: "task-1",
status: "pending",
step_name: "经理审批",
assignee_role: "manager",
created_at: new Date("2026-05-11T07:00:00.000Z"),
due_date: null,
workflow_instance: { entity_type: "reimbursement" },
},
{
id: "task-2",
status: "pending",
step_name: "财务审批",
assignee_role: "finance",
created_at: new Date("2026-05-12T07:00:00.000Z"),
due_date: new Date("2026-05-12T07:30:00.000Z"),
workflow_instance: { entity_type: "purchase_request" },
},
];
expect(calculateWorkflowTaskDueAt(tasks[0], now).toISOString()).toBe("2026-05-12T07:00:00.000Z");
expect(isWorkflowTaskOverdue(tasks[0], now)).toBe(true);
const summary = buildWorkflowTaskSummaryPayload(tasks, "mine", now);
expect(summary).toEqual(
expect.objectContaining({
scope: "mine",
pendingTotal: 2,
overdueTotal: 2,
earliestPendingAt: "2026-05-11T07:00:00.000Z",
}),
);
expect(summary.byEntityType).toEqual(
expect.arrayContaining([
{ key: "reimbursement", pendingTotal: 1, overdueTotal: 1 },
{ key: "purchase_request", pendingTotal: 1, overdueTotal: 1 },
]),
);
});
});