修复(08-28-reimbursement-review-routing-copy): 规范财务兜底文案
这个提交包含在:
@@ -57,6 +57,7 @@ const {
|
||||
buildApprovalWorkflowSnapshot,
|
||||
getWorkflowStepsSnapshot,
|
||||
resolveActionableStep,
|
||||
resolveWorkflowStepDisplayName,
|
||||
toMoneyString,
|
||||
} = require("../utils/workflow");
|
||||
|
||||
@@ -355,11 +356,12 @@ function isOrdinalApprovalLabel(value) {
|
||||
}
|
||||
|
||||
function resolveApprovalStageLabel(task) {
|
||||
const stepName = String(task?.step_name ?? "").trim();
|
||||
const stepName = resolveWorkflowStepDisplayName(task);
|
||||
const stepId = String(task?.step_id ?? "").trim();
|
||||
const isTechnicalIdentifier = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+)+$/i.test(stepName);
|
||||
const isGenericLabel = ["审批", "审核", "复核"].includes(stepName);
|
||||
if (stepName && stepName !== stepId && !isOrdinalApprovalLabel(stepName) && !isTechnicalIdentifier && !isGenericLabel) {
|
||||
if (stepName && stepName !== stepId && !isOrdinalApprovalLabel(stepName)
|
||||
&& !isTechnicalIdentifier && !isGenericLabel) {
|
||||
return stepName;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
getWorkflowStepsSnapshot,
|
||||
isBusinessApprovalRoleCode,
|
||||
resolveActionableStep,
|
||||
resolveWorkflowStepDisplayName,
|
||||
toMoneyString,
|
||||
} = require("../utils/workflow");
|
||||
const { consumeReservedLeave, releaseReservedLeave, formatDateOnly } = require("../services/leave");
|
||||
@@ -336,6 +337,7 @@ function isoOrNull(value) {
|
||||
|
||||
function mapTaskSummary(task, allowedActions = []) {
|
||||
const businessSummary = buildTaskBusinessSummary(task);
|
||||
const stepDisplayName = resolveWorkflowStepDisplayName(task);
|
||||
const workflowInstance = task.workflow_instance
|
||||
? {
|
||||
entity_type: task.workflow_instance.entity_type,
|
||||
@@ -348,7 +350,7 @@ function mapTaskSummary(task, allowedActions = []) {
|
||||
id: task.id,
|
||||
instance_id: task.instance_id,
|
||||
step_id: task.step_id,
|
||||
step_name: task.step_name,
|
||||
step_name: stepDisplayName,
|
||||
assignee_id: task.assignee_id,
|
||||
assignee_role: task.assignee_role,
|
||||
status: task.status,
|
||||
@@ -359,7 +361,7 @@ function mapTaskSummary(task, allowedActions = []) {
|
||||
|
||||
instanceId: task.instance_id,
|
||||
stepId: task.step_id,
|
||||
stepName: task.step_name,
|
||||
stepName: stepDisplayName,
|
||||
assigneeId: task.assignee_id,
|
||||
assigneeRole: task.assignee_role,
|
||||
createdAt: isoOrNull(task.created_at),
|
||||
@@ -531,7 +533,7 @@ function buildWorkflowTaskSummaryPayload(tasks, scope = "mine", now = new Date()
|
||||
}
|
||||
|
||||
incrementSummaryBucket(byEntityType, task.workflow_instance?.entity_type ?? "unknown", task, now);
|
||||
incrementSummaryBucket(byStep, task.step_name ?? task.step_id ?? "unknown", task, now);
|
||||
incrementSummaryBucket(byStep, resolveWorkflowStepDisplayName(task) || task.step_id || "unknown", task, now);
|
||||
incrementSummaryBucket(byAssigneeRole, task.assignee_role ?? (task.assignee_id ? "指定审批人" : "未分配"), task, now);
|
||||
}
|
||||
|
||||
@@ -2433,7 +2435,7 @@ async function workflowRoutes(fastify) {
|
||||
id: task.id,
|
||||
instanceId: task.instance_id,
|
||||
stepId: task.step_id,
|
||||
stepName: task.step_name,
|
||||
stepName: resolveWorkflowStepDisplayName(task),
|
||||
assigneeId: task.assignee_id,
|
||||
assigneeRole: task.assignee_role,
|
||||
status: task.status,
|
||||
|
||||
+14
-1
@@ -107,13 +107,25 @@ function getRoleCodeFromStep(step) {
|
||||
return rule?.type === "role" && typeof rule.value === "string" ? rule.value : null;
|
||||
}
|
||||
|
||||
function resolveWorkflowStepDisplayName(step) {
|
||||
const stepName = String(step?.step_name ?? step?.stepName ?? step?.name ?? "").trim();
|
||||
const assigneeRole = String(step?.assignee_role ?? step?.assigneeRole ?? "").toLowerCase();
|
||||
const isLegacyOwnerFallback = /^负责人本人申请兜底\s*[::]/u.test(stepName);
|
||||
|
||||
if (isLegacyOwnerFallback && ["finance", "finance_mgr"].includes(assigneeRole)) {
|
||||
return "财务复核";
|
||||
}
|
||||
|
||||
return stepName;
|
||||
}
|
||||
|
||||
function buildRoleApprovalStep(roleCode, index, sourceStep, reason) {
|
||||
const suffix = reason === "owner_self_fallback" ? "_owner_self_fallback" : "";
|
||||
return {
|
||||
id: `approval_${index + 1}_${roleCode}${suffix}`,
|
||||
name:
|
||||
reason === "owner_self_fallback"
|
||||
? `负责人本人申请兜底:${roleCode}`
|
||||
? "财务复核"
|
||||
: sourceStep?.name ?? `审批节点${index + 1}`,
|
||||
type: sourceStep?.type ?? "approval",
|
||||
assignee_rule: { type: "role", value: roleCode },
|
||||
@@ -565,5 +577,6 @@ module.exports = {
|
||||
resolveActionableStep,
|
||||
resolveAssigneeFromRule,
|
||||
resolveDepartmentManagerAssignee,
|
||||
resolveWorkflowStepDisplayName,
|
||||
toMoneyString,
|
||||
};
|
||||
|
||||
@@ -443,6 +443,59 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("projects legacy owner-self fallback task names as finance review", async () => {
|
||||
const { app } = await buildApp({
|
||||
latestWorkflowInstance: {
|
||||
id: PENDING_INSTANCE_ID,
|
||||
current_step_id: "approval_1_finance_owner_self_fallback",
|
||||
current_status: "pending",
|
||||
created_at: new Date("2026-08-28T11:08:00.000Z"),
|
||||
started_at: new Date("2026-08-28T11:08:00.000Z"),
|
||||
context_data: {
|
||||
workflow_steps_snapshot: [{
|
||||
id: "approval_1_finance_owner_self_fallback",
|
||||
name: "负责人本人申请兜底:finance",
|
||||
type: "approval",
|
||||
assignee_rule: { type: "role", value: "finance" },
|
||||
}],
|
||||
},
|
||||
workflow_definition: { definition_json: { steps: [] } },
|
||||
workflow_tasks: [{
|
||||
id: "owner-fallback-task",
|
||||
step_id: "approval_1_finance_owner_self_fallback",
|
||||
step_name: "负责人本人申请兜底:finance",
|
||||
assignee_role: "finance",
|
||||
status: "pending",
|
||||
outcome: null,
|
||||
comment: null,
|
||||
created_at: new Date("2026-08-28T11:08:00.000Z"),
|
||||
completed_at: null,
|
||||
assignee_employee: { name: "财务人员" },
|
||||
completer_employee: null,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().data.processingProgress.stages[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
key: "approval_owner-fallback-task",
|
||||
label: "财务复核",
|
||||
state: "current",
|
||||
actorName: "财务人员",
|
||||
detail: "待财务复核处理",
|
||||
}),
|
||||
);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("appends future snapshot approvals that do not have workflow tasks yet", async () => {
|
||||
const { app } = await buildApp({
|
||||
latestWorkflowInstance: {
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("workflow travel helpers", () => {
|
||||
{
|
||||
id: "task-2",
|
||||
status: "pending",
|
||||
step_name: "财务审批",
|
||||
step_name: "负责人本人申请兜底:finance",
|
||||
assignee_role: "finance",
|
||||
created_at: new Date("2026-05-12T07:00:00.000Z"),
|
||||
due_date: new Date("2026-05-12T07:30:00.000Z"),
|
||||
@@ -148,5 +148,10 @@ describe("workflow travel helpers", () => {
|
||||
{ key: "purchase_request", pendingTotal: 1, overdueTotal: 1 },
|
||||
]),
|
||||
);
|
||||
expect(summary.byStep).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ key: "财务复核", pendingTotal: 1, overdueTotal: 1 },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,6 +114,22 @@ describe("workflow assignee resolution", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("resolveWorkflowStepDisplayName", () => {
|
||||
it("hides the internal role code in legacy owner fallback tasks", () => {
|
||||
expect(workflowUtils.resolveWorkflowStepDisplayName({
|
||||
step_name: "负责人本人申请兜底:finance",
|
||||
assignee_role: "finance",
|
||||
})).toBe("财务复核");
|
||||
});
|
||||
|
||||
it("keeps normal readable workflow names unchanged", () => {
|
||||
expect(workflowUtils.resolveWorkflowStepDisplayName({
|
||||
step_name: "高额财务终审",
|
||||
assignee_role: "finance",
|
||||
})).toBe("高额财务终审");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildApprovalWorkflowSnapshot", () => {
|
||||
it("keeps ordinary employee approval snapshots unchanged", async () => {
|
||||
const tx = buildTx({ ownerRoles: ["employee"], managerId: MANAGER_ID });
|
||||
@@ -153,6 +169,7 @@ describe("workflow assignee resolution", () => {
|
||||
{ type: "role", value: "finance" },
|
||||
]);
|
||||
expect(steps[0].id).toContain("owner_self_fallback");
|
||||
expect(steps[0].name).toBe("财务复核");
|
||||
|
||||
const resolved = await resolveActionableStep({
|
||||
tx,
|
||||
@@ -184,6 +201,7 @@ describe("workflow assignee resolution", () => {
|
||||
{ type: "role", value: "finance" },
|
||||
]);
|
||||
expect(steps[0].id).toContain("owner_self_fallback");
|
||||
expect(steps[0].name).toBe("财务复核");
|
||||
});
|
||||
|
||||
it("removes CEO self node and duplicate fallback role for large CEO owner applications", async () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户