完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)

汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
2026-09-19 13:11:04 +08:00
父节点 729d90576f
当前提交 1f8d5b658a
共修改 367 个文件,包含 21079 行新增和 12287 行删除
+1 -1
查看文件
@@ -222,7 +222,7 @@ const { createPrismaClient } = require('./src/prismaClient');
process.exit(1);
}
if (!asset || !asset.asset_no.startsWith('ZC-') || asset.status !== 'in_use' || asset.assignee_employee_id !== financeMgrId || asset.location !== '深圳办公室 B-02') {
if (!asset || !/^FA-\d{4}-\d{6,}$/.test(asset.asset_no) || asset.status !== 'in_use' || asset.assignee_employee_id !== financeMgrId || asset.location !== '深圳办公室 B-02') {
console.error('[ERROR] asset card state mismatch', asset);
process.exit(1);
}
+4 -2
查看文件
@@ -701,12 +701,14 @@ assert_status "$test_status" "200" "test channel by id" "$TMP_DIR/channel-test.j
assert_jq "$TMP_DIR/channel-test.json" '.data.testResult.ok == true and .data.testResult.code == "mock_sent" and .data.testResult.retryable == false' "test channel endpoint mock sent"
echo "[8/10] create encrypted secret channel and verify masked read model"
SECRET_CHANNEL_ID="$(create_channel "$FINANCE_MGR_TOKEN" "IM Secret ${RUN_SUFFIX}" "mock" "" "secret-${RUN_SUFFIX}" "$TMP_DIR/create-secret-channel.json")"
SECRET_CHANNEL_ID="$(create_channel "$FINANCE_MGR_TOKEN" "IM Secret ${RUN_SUFFIX}" "wecom_webhook" "http://127.0.0.1:9/wecom" "secret-${RUN_SUFFIX}" "$TMP_DIR/create-secret-channel.json")"
track_regression_channel "$SECRET_CHANNEL_ID"
assert_jq "$TMP_DIR/create-secret-channel.json" '.data.channel.configJson.secretConfigured == true and (.data.channel.configJson.secretMasked | type == "string") and (.data.channel.configJson | has("secret") | not)' "create channel masks secret on read"
query_channel_secret_state "$SECRET_CHANNEL_ID" > "$TMP_DIR/secret-channel-storage.json"
assert_jq "$TMP_DIR/secret-channel-storage.json" '.hasSecret == false and .hasSecretEncrypted == true' "channel stores only encrypted secret"
secret_test_status="$(api_json_auth POST "$BASE_URL/api/notifications/channels/$SECRET_CHANNEL_ID/test" "$FINANCE_MGR_TOKEN" "$TMP_DIR/secret-channel-test.json" '{}')"
assert_status "$secret_test_status" "200" "test encrypted channel by id" "$TMP_DIR/secret-channel-test.json"
assert_jq "$TMP_DIR/secret-channel-test.json" '.data.testResult.ok == true and .data.testResult.code == "mock_sent" and .data.channel.configJson.secretConfigured == true' "encrypted secret channel test still works"
assert_jq "$TMP_DIR/secret-channel-test.json" '.data.testResult.ok == false and .data.testResult.code == "wecom_network_error" and .data.testResult.retryable == true and .data.channel.configJson.secretConfigured == true' "encrypted secret channel follows loopback provider failure path"
echo "[9/10] migrate legacy plain secret channel to encrypted storage and keep test path working"
LEGACY_CHANNEL_ID="$(create_legacy_secret_channel "IM Legacy ${RUN_SUFFIX}" "http://127.0.0.1:9/wecom" "legacy-${RUN_SUFFIX}")"
+79 -115
查看文件
@@ -493,6 +493,28 @@ const { createPrismaClient } = require('./src/prismaClient');
NODE
}
assert_direct_payment_unchanged() {
DATABASE_URL="$DATABASE_URL" node - "$1" "$2" <<'NODE'
const assert = require('node:assert/strict');
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
try {
const form = await prisma.reimbursementForm.findUniqueOrThrow({ where: { id: process.argv[2] } });
assert.equal(form.status, process.argv[3]);
for (const field of ['payment_voucher_no', 'payment_date', 'payment_channel', 'paid_at', 'paid_by']) {
assert.equal(form[field], null, `blocked direct payment changed ${field}`);
}
const invoices = await prisma.invoicePool.findMany({ where: { linked_reimbursement_id: form.id }, select: { status: true, archived_reason: true } });
for (const invoice of invoices) {
assert.equal(invoice.status, 'bound');
assert.equal(invoice.archived_reason, null);
}
} finally { await prisma.$disconnect(); await pool.end(); }
})().catch(() => { console.error('[ERROR] blocked direct payment changed state or invoice'); process.exitCode = 1; });
NODE
}
assert_reimbursement_status_db() {
local reimbursement_id="$1"
local expected_status="$2"
@@ -533,106 +555,6 @@ const { createPrismaClient } = require('./src/prismaClient');
NODE
}
assert_skip_history_record() {
local instance_id="$1"
local expected_step_id="$2"
local expected_reason="$3"
local scenario="$4"
DATABASE_URL="$DATABASE_URL" node - "$instance_id" "$expected_step_id" "$expected_reason" "$scenario" <<'NODE'
const [instanceId, expectedStepId, expectedReason, scenario] = process.argv.slice(2);
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const history = await prisma.workflowHistory.findFirst({
where: {
instance_id: instanceId,
action: 'skip',
},
orderBy: { created_at: 'asc' },
select: { payload: true },
});
if (!history) {
console.error(`[ERROR] ${scenario} expected skip history record but none found`);
process.exit(1);
}
const payload = history.payload ?? {};
const actualStepId = payload.step_id ?? null;
const actualReason = payload.reason ?? null;
if (actualStepId !== expectedStepId || actualReason !== expectedReason) {
console.error(
`[ERROR] ${scenario} expected skip payload step_id=${expectedStepId}, reason=${expectedReason} but got step_id=${actualStepId}, reason=${actualReason}`,
);
process.exit(1);
}
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
assert_task_step_and_role() {
local task_id="$1"
local expected_step_id="$2"
local expected_role="$3"
local scenario="$4"
DATABASE_URL="$DATABASE_URL" node - "$task_id" "$expected_step_id" "$expected_role" "$scenario" <<'NODE'
const [taskId, expectedStepId, expectedRole, scenario] = process.argv.slice(2);
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const task = await prisma.workflowTask.findUnique({
where: { id: taskId },
select: {
id: true,
step_id: true,
assignee_role: true,
assignee_id: true,
},
});
if (!task) {
console.error(`[ERROR] ${scenario} task not found: ${taskId}`);
process.exit(1);
}
if (task.step_id !== expectedStepId) {
console.error(
`[ERROR] ${scenario} expected step_id ${expectedStepId} but got ${task.step_id}`,
);
process.exit(1);
}
if (task.assignee_role !== expectedRole || task.assignee_id !== null) {
console.error(
`[ERROR] ${scenario} expected role assignee ${expectedRole} with null assignee_id but got assignee_role=${task.assignee_role}, assignee_id=${task.assignee_id}`,
);
process.exit(1);
}
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
assert_no_workflow_instance() {
local reimbursement_id="$1"
@@ -1049,19 +971,17 @@ echo "[7/23] Prepare manager-missing context"
set_employee_direct_manager "$EMPLOYEE_ID" "__NULL__"
EMPLOYEE_MANAGER_PATCHED=1
echo "[8/23] manager skip: missing manager -> skip with history + finance task"
echo "[8/23] required manager missing: reject before workflow creation"
RID_SKIP="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/skip-create.json")"
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_SKIP" "skip" >/dev/null
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_SKIP" "$TMP_DIR/skip-submit.json"
TASK_SKIP="$(jq -er '.data.currentTaskId' "$TMP_DIR/skip-submit.json")"
INSTANCE_SKIP="$(jq -er '.data.workflowInstanceId' "$TMP_DIR/skip-submit.json")"
assert_task_step_and_role "$TASK_SKIP" "approval_2_finance" "finance" "manager skip scenario"
assert_skip_history_record "$INSTANCE_SKIP" "approval_1_manager" "no_manager_found" "manager skip scenario"
skip_finance_approve_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/$TASK_SKIP/approve" "$FINANCE_TOKEN" "$TMP_DIR/skip-finance-approve.json" '{}')"
assert_status "$skip_finance_approve_status" "200" "manager skip finance approve" "$TMP_DIR/skip-finance-approve.json"
assert_reimbursement_status_db "$RID_SKIP" "approved" "manager skip scenario"
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_SKIP" "missing-manager" >/dev/null
missing_manager_status="$(api_json_auth POST "$BASE_URL/api/reimbursements/$RID_SKIP/submit" "$EMPLOYEE_TOKEN" "$TMP_DIR/skip-submit.json" '{}')"
assert_status "$missing_manager_status" "409" "required manager missing" "$TMP_DIR/skip-submit.json"
assert_error_code "$TMP_DIR/skip-submit.json" "VALID_001" "required manager missing"
assert_error_detail "$TMP_DIR/skip-submit.json" '.error.details.workflow' "no_actionable_approver" "required manager missing"
assert_error_detail "$TMP_DIR/skip-submit.json" '.error.details.reason' "no_manager_found" "required manager missing"
assert_error_detail "$TMP_DIR/skip-submit.json" '.error.details.reasonCode' "workflow.manager_unavailable" "required manager missing"
assert_no_workflow_instance "$RID_SKIP" "required manager missing"
assert_reimbursement_status_db "$RID_SKIP" "draft" "required manager missing"
echo "[9/23] no_actionable_approver: full chain non-actionable"
TMP_APPROVAL_RULE_ID="$(create_temp_manager_only_rule "tmp-manager-only-$RUN_SUFFIX" "9000.00" "9001.00" "1")"
@@ -1264,7 +1184,7 @@ fi
manager_approve_after_expired_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/$TASK_DELEGATION_EXPIRED/approve" "$MANAGER_TOKEN" "$TMP_DIR/delegation-expired-manager-approve.json" '{}')"
assert_status "$manager_approve_after_expired_status" "200" "manager approve after expired delegation" "$TMP_DIR/delegation-expired-manager-approve.json"
echo "[19/23] pay success: approved -> paid and invoices archived"
echo "[19/23] direct payment contract for the configured payment mode"
RID_PAY_OK="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/pay-ok-create.json")"
IID_PAY_OK="$(create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_PAY_OK" "pay-ok")"
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-ok-submit.json"
@@ -1283,13 +1203,55 @@ assert_reimbursement_status_db "$RID_PAY_OK" "approved" "pay success preconditio
pay_ok_body="{\"paymentVoucherNo\":\"VOUCHER-$RUN_SUFFIX\",\"paymentDate\":\"2026-02-07\",\"paymentChannel\":\"bank\"}"
pay_ok_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-ok-pay.json" "$pay_ok_body")"
if [[ "${PAYMENT_BATCH_ENABLED:-false}" == "true" ]]; then
assert_status "$pay_ok_status" "409" "batch mode forbids direct payment" "$TMP_DIR/pay-ok-pay.json"
assert_error_code "$TMP_DIR/pay-ok-pay.json" "VALID_001" "direct pay batch boundary"
assert_error_detail "$TMP_DIR/pay-ok-pay.json" '.error.details.payment' "payment_batch_v1_required" "direct pay batch boundary"
echo "[19/23] batch-required profile: direct pay denied; legacy success is not exercised"
assert_error_detail "$TMP_DIR/pay-ok-pay.json" '.error.details.reasonCode' "reimbursement.payment_payment_batch_v1_required" "direct pay batch boundary"
assert_direct_payment_unchanged "$RID_PAY_OK" "approved"
echo "[20/23] batch-required rejects draft/reviewing/rejected/cancelled without payment writes"
RID_PAY_REVIEWING="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/pay-reviewing-create.json")"
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_PAY_REVIEWING" "pay-reviewing" >/dev/null
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_PAY_REVIEWING" "$TMP_DIR/pay-reviewing-submit.json"
RID_PAY_CANCELLED="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/pay-cancelled-create.json")"
set_reimbursement_status "$RID_PAY_CANCELLED" "cancelled"
for fixture in "$RID_NON_POSITIVE_ITEM:draft" "$RID_PAY_REVIEWING:reviewing" "$RID_REJECT:rejected" "$RID_PAY_CANCELLED:cancelled"; do
rid="${fixture%:*}"
expected_state="${fixture##*:}"
blocked_status="$(pay_reimbursement "$FINANCE_TOKEN" "$rid" "$TMP_DIR/pay-blocked-$expected_state.json")"
assert_status "$blocked_status" "409" "batch-required $expected_state" "$TMP_DIR/pay-blocked-$expected_state.json"
assert_error_detail "$TMP_DIR/pay-blocked-$expected_state.json" '.error.details.payment' "payment_batch_v1_required" "batch-required $expected_state"
assert_direct_payment_unchanged "$rid" "$expected_state"
done
withdraw_reimbursement "$EMPLOYEE_TOKEN" "$RID_PAY_REVIEWING" "cleanup batch-required reviewing" "$TMP_DIR/pay-reviewing-withdraw-cleanup.json"
echo "[21/23] non-finance remains forbidden"
pay_perm_status="$(pay_reimbursement "$EMPLOYEE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-perm-employee.json")"
assert_status "$pay_perm_status" "403" "pay forbidden for non-finance" "$TMP_DIR/pay-perm-employee.json"
assert_error_code "$TMP_DIR/pay-perm-employee.json" "PERM_001" "pay forbidden for non-finance"
echo "[22/23] repeated and concurrent direct requests cannot bypass batches"
pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-repeat-a.json" "$pay_ok_body" > "$TMP_DIR/pay-repeat-a.status" &
pid_a=$!
pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-repeat-b.json" "$pay_ok_body" > "$TMP_DIR/pay-repeat-b.status" &
pid_b=$!
wait "$pid_a"
wait "$pid_b"
for suffix in a b; do
assert_status "$(cat "$TMP_DIR/pay-repeat-$suffix.status")" "409" "concurrent batch-required refusal" "$TMP_DIR/pay-repeat-$suffix.json"
assert_error_detail "$TMP_DIR/pay-repeat-$suffix.json" '.error.details.payment' "payment_batch_v1_required" "concurrent batch-required refusal"
done
assert_direct_payment_unchanged "$RID_PAY_OK" "approved"
else
assert_status "$pay_ok_status" "200" "pay approved reimbursement" "$TMP_DIR/pay-ok-pay.json"
assert_jq "$TMP_DIR/pay-ok-pay.json" '.data.outcome == "paid" and .data.reimbursementStatus == "paid" and (.data.archivedInvoiceCount | type == "number") and .data.archivedInvoiceCount >= 1 and .data.payment.idempotent == false and .data.payment.paymentChannel == "bank"' "pay success response"
assert_reimbursement_status_db "$RID_PAY_OK" "paid" "pay success"
assert_invoices_archived_used_complete "$RID_PAY_OK" "pay success"
echo "[20/23] pay invalid status: draft/reviewing/rejected/cancelled"
pay_invalid_draft_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_NEGATIVE" "$TMP_DIR/pay-invalid-draft.json")"
pay_invalid_draft_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_NON_POSITIVE_ITEM" "$TMP_DIR/pay-invalid-draft.json")"
assert_status "$pay_invalid_draft_status" "409" "pay invalid status draft" "$TMP_DIR/pay-invalid-draft.json"
assert_error_code "$TMP_DIR/pay-invalid-draft.json" "VALID_001" "pay invalid status draft"
assert_error_detail "$TMP_DIR/pay-invalid-draft.json" '.error.details.status' "invalid_pay_status" "pay invalid status draft"
@@ -1364,6 +1326,8 @@ if [[ "$RUN_PAY_CONCURRENCY_CHECK" == "1" ]]; then
assert_invoices_archived_used_complete "$RID_PAY_CONCURRENCY" "pay concurrency"
fi
fi
echo "[22.2/23] invoice unbind/rebind"
RID_UNBIND_A="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/unbind-a-create.json")"
INVOICE_UNBIND="$(create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_UNBIND_A" "unbind")"
@@ -1385,7 +1349,7 @@ echo "- return reimbursementId: $RID_RETURN"
echo "- withdraw reimbursementId: $RID_WITHDRAW"
echo "- perm reimbursementId: $RID_PERM"
echo "- non-positive item reimbursementId: $RID_NON_POSITIVE_ITEM"
echo "- manager-skip reimbursementId: $RID_SKIP"
echo "- manager-missing reimbursementId: $RID_SKIP"
echo "- no-actionable reimbursementId: $RID_NO_ACTIONABLE"
echo "- transfer-ok reimbursementId: $RID_TRANSFER_OK"
echo "- transfer-chain reimbursementId: $RID_TRANSFER_CHAIN"