feat: 补齐报销付款待办交接(09-11-reimbursement-payment-handoffs)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-11 09:43:37 +08:00
父节点 f9aee4c932
当前提交 fedcaed1a1
共修改 15 个文件,包含 486 行新增和 15 行删除
+26 -1
查看文件
@@ -5,7 +5,7 @@ const { createRequire } = require("node:module");
const path = require("node:path");
module.exports = async function verifyPaymentExperience({
app, prisma, employee, approvers, form, post, origin, tokens, serverRequire, passed, browser, preview, loginPassword,
app, prisma, employee, approvers, form, post, origin, tokens, serverRequire, passed, browser, preview, loginPassword, handoffCheck,
}) {
app.config.fieldEncryptionKey = randomBytes(32).toString("base64");
app.config.paymentBatchEnabled = true;
@@ -23,6 +23,8 @@ module.exports = async function verifyPaymentExperience({
account_number_encrypted: encryptStringOrThrow(app.config, `6217${"0".repeat(8)}1234`),
} });
await prisma.featureFlag.create({ data: { flag_key: "payment_batch_v1", override_mode: "force_on", rollout_type: "global", department_ids: [] } });
await handoffCheck?.(approvers.finance, "reimbursement_batch", form.id, 1);
await handoffCheck?.(approvers.ceo, "reimbursement_batch", form.id, 0);
const readProgress = async (actor) => {
const response = await fetch(`${origin}/api/reimbursements/${form.id}`, {
headers: { authorization: `Bearer ${tokens.get(actor.id)}` }, signal: AbortSignal.timeout(15000),
@@ -36,6 +38,9 @@ module.exports = async function verifyPaymentExperience({
}, approvers.finance.id);
assert.equal(created.status, 201, `create payment batch: ${created.body.error?.details?.reasonCode ?? created.status}`);
const batch = created.body.data.item;
await handoffCheck?.(approvers.finance, "reimbursement_batch", form.id, 0);
await handoffCheck?.(approvers.ceo, "payment_review", batch.id, 1);
await handoffCheck?.(approvers.finance, "payment_review", batch.id, 0);
const reviewStage = (await readProgress(approvers.finance)).stages.find((stage) => stage.key === "payment_batch_review");
assert.equal(reviewStage.state, "current");
assert.equal(reviewStage.detail, "待非制单人复核付款");
@@ -46,6 +51,9 @@ module.exports = async function verifyPaymentExperience({
assert.equal((await download(approvers.finance)).status, 409);
assert.equal((await post(`/payment-batches/${batch.id}/review`, {}, approvers.finance.id)).status, 403);
assert.equal((await post(`/payment-batches/${batch.id}/review`, {}, approvers.ceo.id)).status, 200);
await handoffCheck?.(approvers.ceo, "payment_review", batch.id, 0);
await handoffCheck?.(approvers.finance, "payment_execute", batch.id, 1);
await handoffCheck?.(approvers.ceo, "payment_execute", batch.id, 0);
assert.equal((await readProgress(approvers.finance)).stages.find((stage) => stage.key === "payment_and_receipt").state, "current");
assert.equal((await download(approvers.ceo)).status, 403);
assert.equal((await download(employee)).status, 403);
@@ -125,4 +133,21 @@ module.exports = async function verifyPaymentExperience({
passed.push("payment: real authenticated desktop/mobile reimbursement-to-batch download and return agree on stage, unique link, action-before-progress; no reason input; repeat warning retained");
}
passed.push("payment: real finance create, CEO review, unauthorized export refused, Chinese BIFF8 first/re-download without reason, audited and still unpaid");
if (process.env.RUN_HANDOFF_PROJECTION_TEST === "1") {
await handoffCheck(approvers.finance, "payment_execute", batch.id, 1);
assert.equal((await post(`/payment-batches/${batch.id}/execute`, {}, approvers.finance.id)).status, 409);
const receipt = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jD1sAAAAASUVORK5CYII=", "base64");
const upload = new FormData();
upload.append("files", new Blob([receipt], { type: "image/png" }), "synthetic-receipt.png");
const uploaded = await fetch(`${origin}/api/payment-batches/${batch.id}/receipt-attachments`, {
method: "POST", headers: { authorization: `Bearer ${tokens.get(approvers.finance.id)}` }, body: upload, signal: AbortSignal.timeout(15000),
});
assert.equal(uploaded.status, 200, "synthetic receipt upload");
const executed = await post(`/payment-batches/${batch.id}/execute`, {}, approvers.finance.id);
assert.equal(executed.status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "paid");
await handoffCheck(approvers.finance, "payment_execute", batch.id, 0);
await handoffCheck(approvers.finance, "reimbursement_batch", form.id, 0);
passed.push("handoffs: real read-only portal return/resubmit/create/review/export/synthetic receipt/paid lifecycle; unique stable counts, no self-review, no bank connection");
}
};