补齐报销待组批财务入口 [09-04-reimbursement-state-gap-finance-view][R1]

这个提交包含在:
2026-09-05 09:14:13 +08:00
父节点 8e0af8235e
当前提交 5fe9f78bc9
共修改 9 个文件,包含 243 行新增和 67 行删除
+10 -3
查看文件
@@ -392,6 +392,7 @@ const PROCESSING_STAGE_DEFINITIONS = Object.freeze([
{ key: "submitted", label: "申请提交" },
{ key: "manager_approval", label: "主管审批" },
{ key: "finance_review", label: "财务复核" },
PAYMENT_PROCESSING_STAGE_DEFINITIONS.created,
PAYMENT_PROCESSING_STAGE_DEFINITIONS.review,
PAYMENT_PROCESSING_STAGE_DEFINITIONS.payment,
]);
@@ -566,13 +567,16 @@ function buildProcessingProgress({ reimbursement, workflowInstance, paymentBatch
const batchTargetHref = batch && canReadPaymentBatch
? `/payment-batches?id=${encodeURIComponent(batch.id)}`
: null;
const batchCreatedStage = stageByKey.get("payment_batch_created");
batchCreatedStage.label = "组建付款批次";
const batchReviewStage = stageByKey.get("payment_batch_review");
const paymentStage = stageByKey.get("payment_and_receipt");
if (!batch) {
Object.assign(batchReviewStage, reimbursement.status === "approved"
? { state: "current", result: "待安排", detail: "待财务安排付款" }
: { detail: "待财务安排付款" });
Object.assign(batchCreatedStage, reimbursement.status === "approved"
? { state: "current", result: "待组批", detail: "待财务组建付款批次" }
: { detail: "待财务组建付款批次" });
batchReviewStage.detail = "待付款批次创建并提交复核";
paymentStage.detail = "待付款复核后由财务登记,非审批环节";
} else {
const semantics = buildPaymentProcessingSemantics({
@@ -587,11 +591,13 @@ function buildProcessingProgress({ reimbursement, workflowInstance, paymentBatch
paymentOrderExportCount: batch.payment_order_export_count ?? 0,
});
const actorIds = {
creator: batch.created_by,
reviewer: batch.reviewed_by,
executor: batch.executed_by,
canceller: batch.cancelled_by,
};
for (const [target, source] of [
[batchCreatedStage, semantics.created],
[batchReviewStage, semantics.review],
[paymentStage, semantics.payment],
]) {
@@ -630,6 +636,7 @@ function buildProcessingProgress({ reimbursement, workflowInstance, paymentBatch
...(approvalStages.length > 0
? approvalStages
: [stageByKey.get("manager_approval"), stageByKey.get("finance_review")]),
stageByKey.get("payment_batch_created"),
stageByKey.get("payment_batch_review"),
stageByKey.get("payment_and_receipt"),
],
@@ -444,7 +444,8 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
expect.objectContaining({ key: "submitted", state: "completed", actorName: "李四", result: "已提交" }),
expect.objectContaining({ key: "approval_manager-task", label: "主管审批", state: "completed", actorName: "王经理", result: "通过" }),
expect.objectContaining({ key: "approval_finance-task", label: "财务审批", state: "current", actorName: "财务专员", result: "处理中" }),
expect.objectContaining({ key: "payment_batch_review", state: "pending", actorName: null, detail: "待财务安排付款" }),
expect.objectContaining({ key: "payment_batch_created", label: "组建付款批次", state: "pending", actorName: null }),
expect.objectContaining({ key: "payment_batch_review", state: "pending", actorName: null, detail: "待付款批次创建并提交复核" }),
expect.objectContaining({ key: "payment_and_receipt", state: "pending", actorName: null }),
]);
@@ -516,6 +517,7 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
expect.objectContaining({ key: "submitted", state: "current", actorName: "张三", detail: "待申请人提交" }),
expect.objectContaining({ key: "manager_approval", state: "pending", actorName: null }),
expect.objectContaining({ key: "finance_review", state: "pending", actorName: null }),
expect.objectContaining({ key: "payment_batch_created", state: "pending", actorName: null }),
expect.objectContaining({ key: "payment_batch_review", state: "pending", actorName: null }),
expect.objectContaining({ key: "payment_and_receipt", state: "pending", actorName: null }),
]);
@@ -856,9 +858,10 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
});
expect(response.statusCode).toBe(200);
const stages = response.json().data.processingProgress.stages;
expect(stages[3]).toMatchObject({ state: reviewState, targetHref: null });
expect(stages[4]).toMatchObject({ state: paymentState, detail, targetHref: null });
if (status === "draft") expect(stages[3].actorName).toBe(null);
expect(stages[3]).toMatchObject({ key: "payment_batch_created", state: "completed", targetHref: null });
expect(stages[4]).toMatchObject({ state: reviewState, targetHref: null });
expect(stages[5]).toMatchObject({ state: paymentState, detail, targetHref: null });
if (status === "draft") expect(stages[4].actorName).toBe(null);
await app.close();
});
@@ -895,7 +898,7 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
headers: { authorization: "Bearer test" },
});
expect(response.statusCode).toBe(200);
expect(response.json().data.processingProgress.stages[3]).toMatchObject({
expect(response.json().data.processingProgress.stages[4]).toMatchObject({
detail: "待非制单人复核付款",
actorName: null,
targetHref: "/payment-batches?id=batch-1",
@@ -906,12 +909,12 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
it("attaches the payment-batch link to exactly one status-owning stage", async () => {
const cases = [
{ status: "draft", reviewedAt: null, targetIndex: 3 },
{ status: "reviewed", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 4 },
{ status: "executed", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 4 },
{ status: "failed", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 4 },
{ status: "cancelled", reviewedAt: null, targetIndex: 3 },
{ status: "cancelled", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 4 },
{ status: "draft", reviewedAt: null, targetIndex: 4 },
{ status: "reviewed", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 5 },
{ status: "executed", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 5 },
{ status: "failed", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 5 },
{ status: "cancelled", reviewedAt: null, targetIndex: 4 },
{ status: "cancelled", reviewedAt: new Date("2026-04-11T08:00:00.000Z"), targetIndex: 5 },
];
for (const item of cases) {
@@ -967,10 +970,11 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
});
expect(response.statusCode).toBe(200);
expect(response.json().data.processingProgress.stages[3]).toMatchObject({
key: "payment_batch_created",
state: "current",
actorName: null,
result: "待安排",
detail: "待财务安排付款",
result: "待组批",
detail: "待财务组建付款批次",
});
await app.close();
@@ -991,7 +995,7 @@ describe("GET /api/reimbursements/:id detail enhancements", () => {
headers: { authorization: "Bearer test" },
});
expect(response.statusCode).toBe(200);
expect(response.json().data.processingProgress.stages[4]).toMatchObject({
expect(response.json().data.processingProgress.stages[5]).toMatchObject({
state: "completed",
actorName: "实际付款人",
occurredAt: "2026-04-12T08:00:00.000Z",