feat: 补齐报销付款待办交接(09-11-reimbursement-payment-handoffs)
这个提交包含在:
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户