feat(server): 完成 P2 后端能力(支付幂等、委派回收、OCR异步、审计)
这个提交包含在:
@@ -188,8 +188,13 @@ pay_reimbursement() {
|
||||
local token="$1"
|
||||
local reimbursement_id="$2"
|
||||
local out_file="$3"
|
||||
local body="${4:-}"
|
||||
|
||||
api_json_auth POST "$BASE_URL/api/reimbursements/$reimbursement_id/pay" "$token" "$out_file" '{}'
|
||||
if [[ -z "$body" ]]; then
|
||||
body='{}'
|
||||
fi
|
||||
|
||||
api_json_auth POST "$BASE_URL/api/reimbursements/$reimbursement_id/pay" "$token" "$out_file" "$body"
|
||||
}
|
||||
|
||||
transfer_task() {
|
||||
@@ -213,6 +218,25 @@ withdraw_reimbursement() {
|
||||
assert_status "$status" "200" "withdraw reimbursement $reimbursement_id" "$out_file"
|
||||
}
|
||||
|
||||
create_delegation() {
|
||||
local token="$1"
|
||||
local delegate_id="$2"
|
||||
local scope="$3"
|
||||
local valid_from="$4"
|
||||
local valid_to="$5"
|
||||
local out_file="$6"
|
||||
|
||||
api_json_auth POST "$BASE_URL/api/delegations" "$token" "$out_file" "{\"delegateEmployeeId\":\"$delegate_id\",\"scope\":\"$scope\",\"validFrom\":\"$valid_from\",\"validTo\":\"$valid_to\"}"
|
||||
}
|
||||
|
||||
revoke_delegation() {
|
||||
local token="$1"
|
||||
local delegation_id="$2"
|
||||
local out_file="$3"
|
||||
|
||||
api_json_auth POST "$BASE_URL/api/delegations/$delegation_id/revoke" "$token" "$out_file" '{}'
|
||||
}
|
||||
|
||||
create_and_bind_invoice() {
|
||||
local token="$1"
|
||||
local reimbursement_id="$2"
|
||||
@@ -761,7 +785,86 @@ const { createPrismaClient } = require('./src/prismaClient');
|
||||
NODE
|
||||
}
|
||||
|
||||
echo "[1/19] Health check + login baseline users"
|
||||
assert_delegation_history_evidence() {
|
||||
local task_id="$1"
|
||||
local delegation_id="$2"
|
||||
local principal_id="$3"
|
||||
local delegate_id="$4"
|
||||
local scenario="$5"
|
||||
|
||||
DATABASE_URL="$DATABASE_URL" node - "$task_id" "$delegation_id" "$principal_id" "$delegate_id" "$scenario" <<'NODE'
|
||||
const [taskId, delegationId, principalId, delegateId, 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: { task_id: taskId },
|
||||
orderBy: { created_at: 'desc' },
|
||||
select: { payload: true },
|
||||
});
|
||||
|
||||
if (!history) {
|
||||
console.error(`[ERROR] ${scenario} history not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const payload = history.payload ?? {};
|
||||
if (
|
||||
payload.delegation_id !== delegationId ||
|
||||
payload.principal_id !== principalId ||
|
||||
payload.delegate_id !== delegateId
|
||||
) {
|
||||
console.error(
|
||||
`[ERROR] ${scenario} delegation payload mismatch: delegation_id=${payload.delegation_id}, principal_id=${payload.principal_id}, delegate_id=${payload.delegate_id}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_delegation_revoked_db() {
|
||||
local delegation_id="$1"
|
||||
local scenario="$2"
|
||||
|
||||
DATABASE_URL="$DATABASE_URL" node - "$delegation_id" "$scenario" <<'NODE'
|
||||
const [delegationId, scenario] = process.argv.slice(2);
|
||||
const { createPrismaClient } = require('./src/prismaClient');
|
||||
|
||||
(async () => {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
try {
|
||||
const delegation = await prisma.delegationGrant.findUnique({
|
||||
where: { id: delegationId },
|
||||
select: { revoked_at: true },
|
||||
});
|
||||
|
||||
if (!delegation || delegation.revoked_at == null) {
|
||||
console.error(`[ERROR] ${scenario} expected revoked delegation but got none`);
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
NODE
|
||||
}
|
||||
echo "[1/23] Health check + login baseline users"
|
||||
health_status="$(curl -sS -o "$TMP_DIR/health.json" -w "%{http_code}" "$BASE_URL/health")"
|
||||
assert_status "$health_status" "200" "health check" "$TMP_DIR/health.json"
|
||||
|
||||
@@ -783,7 +886,7 @@ ceo_me_status="$(api_json_auth GET "$BASE_URL/api/me" "$CEO_TOKEN" "$TMP_DIR/me-
|
||||
assert_status "$ceo_me_status" "200" "get /api/me ceo" "$TMP_DIR/me-ceo.json"
|
||||
CEO_ID="$(jq -er '.data.id' "$TMP_DIR/me-ceo.json")"
|
||||
|
||||
echo "[2/19] reject: instance=rejected, reimbursement=rejected"
|
||||
echo "[2/23] reject: instance=rejected, reimbursement=rejected"
|
||||
RID_REJECT="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/reject-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_REJECT" "reject" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_REJECT" "$TMP_DIR/reject-submit.json"
|
||||
@@ -796,7 +899,7 @@ assert_jq "$TMP_DIR/reject-action.json" '.data.outcome == "rejected" and .data.r
|
||||
assert_instance_status "$INSTANCE_REJECT" "rejected" "reject scenario"
|
||||
assert_reimbursement_status_db "$RID_REJECT" "rejected" "reject scenario"
|
||||
|
||||
echo "[3/19] return + resubmit: terminated/draft and new instance"
|
||||
echo "[3/23] return + resubmit: terminated/draft and new instance"
|
||||
RID_RETURN="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/return-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_RETURN" "return" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_RETURN" "$TMP_DIR/return-submit-1.json"
|
||||
@@ -821,7 +924,7 @@ withdraw_reimbursement "$EMPLOYEE_TOKEN" "$RID_RETURN" "cleanup after return-res
|
||||
assert_instance_status "$INSTANCE_RETURN_2" "cancelled" "return resubmit cleanup"
|
||||
assert_reimbursement_status_db "$RID_RETURN" "draft" "return resubmit cleanup"
|
||||
|
||||
echo "[4/19] withdraw: before approver processing"
|
||||
echo "[4/23] withdraw: before approver processing"
|
||||
RID_WITHDRAW="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/withdraw-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_WITHDRAW" "withdraw" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_WITHDRAW" "$TMP_DIR/withdraw-submit.json"
|
||||
@@ -832,7 +935,7 @@ assert_jq "$TMP_DIR/withdraw-action.json" '.data.outcome == "withdrawn" and .dat
|
||||
assert_instance_status "$INSTANCE_WITHDRAW" "cancelled" "withdraw scenario"
|
||||
assert_reimbursement_status_db "$RID_WITHDRAW" "draft" "withdraw scenario"
|
||||
|
||||
echo "[5/19] PERM_002: non-assignee cannot operate"
|
||||
echo "[5/23] PERM_002: non-assignee cannot operate"
|
||||
RID_PERM="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/perm-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_PERM" "perm" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_PERM" "$TMP_DIR/perm-submit.json"
|
||||
@@ -847,7 +950,7 @@ withdraw_reimbursement "$EMPLOYEE_TOKEN" "$RID_PERM" "cleanup after PERM_002" "$
|
||||
assert_instance_status "$INSTANCE_PERM" "cancelled" "PERM_002 cleanup"
|
||||
assert_reimbursement_status_db "$RID_PERM" "draft" "PERM_002 cleanup"
|
||||
|
||||
echo "[6/19] negative_not_allowed: submit blocked when total_amount < 0"
|
||||
echo "[6/23] negative_not_allowed: submit blocked when total_amount < 0"
|
||||
RID_NEGATIVE="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/negative-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_NEGATIVE" "negative" >/dev/null
|
||||
set_reimbursement_total_amount "$RID_NEGATIVE" "-1.00"
|
||||
@@ -858,11 +961,11 @@ assert_error_code "$TMP_DIR/negative-submit.json" "VALID_001" "negative_not_allo
|
||||
assert_error_detail "$TMP_DIR/negative-submit.json" '.error.details.total_amount' "negative_not_allowed" "negative_not_allowed submit"
|
||||
assert_no_workflow_instance "$RID_NEGATIVE" "negative_not_allowed"
|
||||
|
||||
echo "[7/19] Prepare manager-missing context"
|
||||
echo "[7/23] Prepare manager-missing context"
|
||||
set_employee_direct_manager "$EMPLOYEE_ID" "__NULL__"
|
||||
EMPLOYEE_MANAGER_PATCHED=1
|
||||
|
||||
echo "[8/19] manager skip: missing manager -> skip with history + finance task"
|
||||
echo "[8/23] manager skip: missing manager -> skip with history + finance task"
|
||||
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"
|
||||
@@ -876,7 +979,7 @@ skip_finance_approve_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/
|
||||
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"
|
||||
|
||||
echo "[9/19] no_actionable_approver: full chain non-actionable"
|
||||
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")"
|
||||
|
||||
RID_NO_ACTIONABLE="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/no-actionable-create.json")"
|
||||
@@ -892,11 +995,11 @@ assert_no_workflow_instance "$RID_NO_ACTIONABLE" "no_actionable_approver"
|
||||
delete_approval_rule "$TMP_APPROVAL_RULE_ID"
|
||||
TMP_APPROVAL_RULE_ID=""
|
||||
|
||||
echo "[10/19] restore manager relation before transfer/pay scenarios"
|
||||
echo "[10/23] restore manager relation before transfer/pay scenarios"
|
||||
set_employee_direct_manager "$EMPLOYEE_ID" "$ORIGINAL_EMPLOYEE_MANAGER_ID"
|
||||
EMPLOYEE_MANAGER_PATCHED=0
|
||||
|
||||
echo "[11/19] transfer success: manager -> ceo with 3 evidences"
|
||||
echo "[11/23] transfer success: manager -> ceo with 3 evidences"
|
||||
RID_TRANSFER_OK="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/transfer-ok-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_TRANSFER_OK" "transfer-ok" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_OK" "$TMP_DIR/transfer-ok-submit.json"
|
||||
@@ -923,7 +1026,7 @@ fi
|
||||
|
||||
assert_reimbursement_status_db "$RID_TRANSFER_OK" "approved" "transfer success approval"
|
||||
|
||||
echo "[12/19] transfer reason too short => VALID_002"
|
||||
echo "[12/23] transfer reason too short => VALID_002"
|
||||
RID_TRANSFER_SHORT="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/transfer-short-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_TRANSFER_SHORT" "transfer-short" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_SHORT" "$TMP_DIR/transfer-short-submit.json"
|
||||
@@ -936,7 +1039,7 @@ assert_error_code "$TMP_DIR/transfer-short-action.json" "VALID_002" "transfer re
|
||||
withdraw_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_SHORT" "cleanup transfer short" "$TMP_DIR/transfer-short-withdraw-cleanup.json"
|
||||
assert_reimbursement_status_db "$RID_TRANSFER_SHORT" "draft" "transfer short cleanup"
|
||||
|
||||
echo "[13/19] transfer target not approver => VALID_001"
|
||||
echo "[13/23] transfer target not approver => VALID_001"
|
||||
RID_TRANSFER_TARGET="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/transfer-target-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_TRANSFER_TARGET" "transfer-target" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_TARGET" "$TMP_DIR/transfer-target-submit.json"
|
||||
@@ -950,7 +1053,7 @@ assert_error_detail "$TMP_DIR/transfer-target-action.json" '.error.details.toApp
|
||||
withdraw_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_TARGET" "cleanup transfer target" "$TMP_DIR/transfer-target-withdraw-cleanup.json"
|
||||
assert_reimbursement_status_db "$RID_TRANSFER_TARGET" "draft" "transfer target cleanup"
|
||||
|
||||
echo "[14/19] transfer same assignee => VALID_001"
|
||||
echo "[14/23] transfer same assignee => VALID_001"
|
||||
RID_TRANSFER_SAME="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/transfer-same-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_TRANSFER_SAME" "transfer-same" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_SAME" "$TMP_DIR/transfer-same-submit.json"
|
||||
@@ -964,7 +1067,7 @@ assert_error_detail "$TMP_DIR/transfer-same-action.json" '.error.details.transfe
|
||||
withdraw_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_SAME" "cleanup transfer same assignee" "$TMP_DIR/transfer-same-withdraw-cleanup.json"
|
||||
assert_reimbursement_status_db "$RID_TRANSFER_SAME" "draft" "transfer same cleanup"
|
||||
|
||||
echo "[15/19] transfer chain limit: 4th transfer blocked"
|
||||
echo "[15/23] transfer chain limit: 4th transfer blocked"
|
||||
RID_TRANSFER_CHAIN="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/transfer-chain-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_TRANSFER_CHAIN" "transfer-chain" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_TRANSFER_CHAIN" "$TMP_DIR/transfer-chain-submit.json"
|
||||
@@ -998,7 +1101,54 @@ fi
|
||||
|
||||
assert_reimbursement_status_db "$RID_TRANSFER_CHAIN" "approved" "transfer chain final approval"
|
||||
|
||||
echo "[16/19] pay success: approved -> paid and invoices archived"
|
||||
echo "[17/23] delegation active window: delegate can approve"
|
||||
RID_DELEGATION_OK="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/delegation-ok-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_DELEGATION_OK" "delegation-ok" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_DELEGATION_OK" "$TMP_DIR/delegation-ok-submit.json"
|
||||
TASK_DELEGATION_OK="$(jq -er '.data.currentTaskId' "$TMP_DIR/delegation-ok-submit.json")"
|
||||
|
||||
DELEGATION_VALID_FROM="$(date -u -d '-5 minutes' '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
DELEGATION_VALID_TO="$(date -u -d '+1 hour' '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
delegation_create_status="$(create_delegation "$MANAGER_TOKEN" "$CEO_ID" "all" "$DELEGATION_VALID_FROM" "$DELEGATION_VALID_TO" "$TMP_DIR/delegation-create-ok.json")"
|
||||
assert_status "$delegation_create_status" "200" "create delegation active" "$TMP_DIR/delegation-create-ok.json"
|
||||
DELEGATION_OK_ID="$(jq -er '.data.delegation.id' "$TMP_DIR/delegation-create-ok.json")"
|
||||
|
||||
ceo_approve_via_delegation_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/$TASK_DELEGATION_OK/approve" "$CEO_TOKEN" "$TMP_DIR/delegation-ok-ceo-approve.json" '{}')"
|
||||
assert_status "$ceo_approve_via_delegation_status" "200" "delegated approve by ceo" "$TMP_DIR/delegation-ok-ceo-approve.json"
|
||||
assert_delegation_history_evidence "$TASK_DELEGATION_OK" "$DELEGATION_OK_ID" "$MANAGER_ID" "$CEO_ID" "delegation active approve"
|
||||
|
||||
revoke_delegation_status="$(revoke_delegation "$MANAGER_TOKEN" "$DELEGATION_OK_ID" "$TMP_DIR/delegation-revoke-ok.json")"
|
||||
assert_status "$revoke_delegation_status" "200" "revoke delegation" "$TMP_DIR/delegation-revoke-ok.json"
|
||||
assert_delegation_revoked_db "$DELEGATION_OK_ID" "delegation revoke"
|
||||
|
||||
echo "[18/23] delegation expired: delegate forbidden"
|
||||
DELEGATION_EXPIRED_FROM="$(date -u -d '-2 hours' '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
DELEGATION_EXPIRED_TO="$(date -u -d '-1 hour' '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
delegation_expired_create_status="$(create_delegation "$MANAGER_TOKEN" "$CEO_ID" "all" "$DELEGATION_EXPIRED_FROM" "$DELEGATION_EXPIRED_TO" "$TMP_DIR/delegation-create-expired.json")"
|
||||
assert_status "$delegation_expired_create_status" "200" "create expired delegation" "$TMP_DIR/delegation-create-expired.json"
|
||||
DELEGATION_EXPIRED_ID="$(jq -er '.data.delegation.id' "$TMP_DIR/delegation-create-expired.json")"
|
||||
|
||||
RID_DELEGATION_EXPIRED="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/delegation-expired-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_DELEGATION_EXPIRED" "delegation-expired" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_DELEGATION_EXPIRED" "$TMP_DIR/delegation-expired-submit.json"
|
||||
TASK_DELEGATION_EXPIRED="$(jq -er '.data.currentTaskId' "$TMP_DIR/delegation-expired-submit.json")"
|
||||
|
||||
ceo_approve_expired_status="$(api_json_auth POST "$BASE_URL/api/workflow/tasks/$TASK_DELEGATION_EXPIRED/approve" "$CEO_TOKEN" "$TMP_DIR/delegation-expired-ceo-approve.json" '{}')"
|
||||
assert_status "$ceo_approve_expired_status" "403" "expired delegation should be forbidden" "$TMP_DIR/delegation-expired-ceo-approve.json"
|
||||
assert_error_code "$TMP_DIR/delegation-expired-ceo-approve.json" "PERM_002" "expired delegation should be forbidden"
|
||||
|
||||
revoke_job_output="$(npm run --silent job:revoke-expired-delegations)"
|
||||
revoke_job_status="$(printf '%s\n' "$revoke_job_output" | tail -n 1)"
|
||||
if [[ "$revoke_job_status" != revoked=* ]]; then
|
||||
echo "[ERROR] revoke expired delegations script output invalid: $revoke_job_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_delegation_revoked_db "$DELEGATION_EXPIRED_ID" "delegation auto revoke"
|
||||
|
||||
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"
|
||||
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"
|
||||
@@ -1015,13 +1165,14 @@ fi
|
||||
|
||||
assert_reimbursement_status_db "$RID_PAY_OK" "approved" "pay success precondition"
|
||||
|
||||
pay_ok_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-ok-pay.json")"
|
||||
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")"
|
||||
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' "pay success response"
|
||||
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 "[17/19] pay invalid status: draft/reviewing/rejected/cancelled"
|
||||
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")"
|
||||
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"
|
||||
@@ -1050,20 +1201,27 @@ assert_status "$pay_invalid_cancelled_status" "409" "pay invalid status cancelle
|
||||
assert_error_code "$TMP_DIR/pay-invalid-cancelled.json" "VALID_001" "pay invalid status cancelled"
|
||||
assert_error_detail "$TMP_DIR/pay-invalid-cancelled.json" '.error.details.status' "invalid_pay_status" "pay invalid status cancelled"
|
||||
|
||||
echo "[18/19] pay permission: non-finance forbidden"
|
||||
echo "[21/23] pay permission: non-finance 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 "[19/19] pay idempotent: paid reimbursement can be paid again"
|
||||
pay_idempotent_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-idempotent.json")"
|
||||
echo "[22/23] pay idempotent: paid reimbursement can be paid again"
|
||||
pay_idempotent_body="{\"paymentVoucherNo\":\"VOUCHER-$RUN_SUFFIX\",\"paymentDate\":\"2026-02-07\",\"paymentChannel\":\"bank\"}"
|
||||
pay_idempotent_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-idempotent.json" "$pay_idempotent_body")"
|
||||
assert_status "$pay_idempotent_status" "200" "pay idempotent" "$TMP_DIR/pay-idempotent.json"
|
||||
assert_jq "$TMP_DIR/pay-idempotent.json" '.data.outcome == "paid" and .data.reimbursementStatus == "paid" and (.data.archivedInvoiceCount | type == "number") and .data.archivedInvoiceCount >= 0' "pay idempotent response"
|
||||
assert_jq "$TMP_DIR/pay-idempotent.json" '.data.outcome == "paid" and .data.reimbursementStatus == "paid" and (.data.archivedInvoiceCount | type == "number") and .data.archivedInvoiceCount >= 0 and .data.payment.idempotent == true and .data.payment.paymentVoucherNo != null' "pay idempotent response"
|
||||
assert_reimbursement_status_db "$RID_PAY_OK" "paid" "pay idempotent"
|
||||
assert_invoices_archived_used_complete "$RID_PAY_OK" "pay idempotent"
|
||||
|
||||
pay_conflict_body="{\"paymentVoucherNo\":\"VOUCHER-CONFLICT-$RUN_SUFFIX\",\"paymentDate\":\"2026-02-06\",\"paymentChannel\":\"alipay\"}"
|
||||
pay_conflict_status="$(pay_reimbursement "$FINANCE_TOKEN" "$RID_PAY_OK" "$TMP_DIR/pay-conflict.json" "$pay_conflict_body")"
|
||||
assert_status "$pay_conflict_status" "409" "pay idempotent conflict" "$TMP_DIR/pay-conflict.json"
|
||||
assert_error_code "$TMP_DIR/pay-conflict.json" "VALID_001" "pay idempotent conflict"
|
||||
assert_error_detail "$TMP_DIR/pay-conflict.json" '.error.details.payment' "already_paid_conflict" "pay idempotent conflict"
|
||||
|
||||
if [[ "$RUN_PAY_CONCURRENCY_CHECK" == "1" ]]; then
|
||||
echo "[19.1/19] optional pay concurrency check"
|
||||
echo "[22.1/23] optional pay concurrency check"
|
||||
RID_PAY_CONCURRENCY="$(create_reimbursement "$EMPLOYEE_TOKEN" "$TMP_DIR/pay-concurrency-create.json")"
|
||||
create_and_bind_invoice "$EMPLOYEE_TOKEN" "$RID_PAY_CONCURRENCY" "pay-concurrency" >/dev/null
|
||||
submit_reimbursement "$EMPLOYEE_TOKEN" "$RID_PAY_CONCURRENCY" "$TMP_DIR/pay-concurrency-submit.json"
|
||||
@@ -1090,7 +1248,7 @@ if [[ "$RUN_PAY_CONCURRENCY_CHECK" == "1" ]]; then
|
||||
assert_invoices_archived_used_complete "$RID_PAY_CONCURRENCY" "pay concurrency"
|
||||
fi
|
||||
|
||||
echo "[19/19] Final manager relation restore"
|
||||
echo "[23/23] Final manager relation restore"
|
||||
set_employee_direct_manager "$EMPLOYEE_ID" "$ORIGINAL_EMPLOYEE_MANAGER_ID"
|
||||
EMPLOYEE_MANAGER_PATCHED=0
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户