feat(server): 票据增强闭环(F9/F10/F11/F12/F13/F28)

- /invoice-pool 新增 unbind、PATCH 修正、manual archive、OCR retry、判重例外 override(含 VALID_004/006/007)

- signed-url 签发/下载写入审计日志(脱敏 extra)

- 新增未绑定票据自动归档作业 + 一次性执行脚本

- 回归脚本增强:workflow-actions/dedup/audit/ocr 覆盖上述场景;v3 CSV 状态推进至 DONE
这个提交包含在:
2026-02-09 17:09:34 +08:00
父节点 3ae9e08692
当前提交 8bd068f2a3
共修改 15 个文件,包含 1882 行新增和 14 行删除
+107 -1
查看文件
@@ -5,6 +5,8 @@ BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-dedup}"
EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}"
EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}"
FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}"
FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}"
if ! command -v curl >/dev/null 2>&1; then
echo "[ERROR] curl not found" >&2
@@ -38,7 +40,7 @@ fi
TMP_DIR="$(mktemp -d)"
TMP_INDEX_CREATED=0
RUN_SUFFIX="$(date +%s)-$RANDOM"
RUN_SUFFIX="$(date +%s%N)-$RANDOM"
DEDUP_CODE="RCODE-$RUN_SUFFIX"
DEDUP_NUMBER="RNO-$RUN_SUFFIX"
@@ -84,6 +86,26 @@ api_json() {
fi
}
api_json_with_token() {
local method="$1"
local url="$2"
local token="$3"
local out_file="$4"
local body="${5:-}"
if [[ -n "$body" ]]; then
curl -sS -o "$out_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "authorization: Bearer $token" \
-H "content-type: application/json" \
-d "$body"
else
curl -sS -o "$out_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "authorization: Bearer $token"
fi
}
upload_file() {
local file_path="$1"
local out_file="$2"
@@ -135,6 +157,34 @@ const { createPrismaClient } = require('./src/prismaClient');
NODE
}
set_reimbursement_status_db() {
local reimbursement_id="$1"
local status="$2"
DATABASE_URL="$DATABASE_URL" REIMBURSEMENT_ID="$reimbursement_id" STATUS="$status" node <<'NODE'
const { createPrismaClient } = require('./src/prismaClient');
(async () => {
const reimbursementId = process.env.REIMBURSEMENT_ID;
const status = process.env.STATUS;
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
await prisma.reimbursementForm.update({
where: { id: reimbursementId },
data: { status },
});
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
create_tmp_non_dedup_index() {
local reimbursement_id="$1"
DATABASE_URL="$DATABASE_URL" REIMBURSEMENT_ID="$reimbursement_id" node <<'NODE'
@@ -205,6 +255,15 @@ login_code="$(curl -sS -o "$TMP_DIR/login.json" -w "%{http_code}" \
assert_status "$login_code" "200" "login" "$TMP_DIR/login.json"
TOKEN="$(jq -er '.data.accessToken' "$TMP_DIR/login.json")"
echo "[1.1/7] Login finance_mgr"
login_fm_code="$(curl -sS -o "$TMP_DIR/login-fm.json" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/login" \
-H "content-type: application/json" \
-H "x-device-fingerprint: $DEVICE_FINGERPRINT-finance-mgr" \
-d "{\"identifier\":\"$FINANCE_MGR_IDENTIFIER\",\"password\":\"$FINANCE_MGR_PASSWORD\"}")"
assert_status "$login_fm_code" "200" "login finance_mgr" "$TMP_DIR/login-fm.json"
TOKEN_FM="$(jq -er '.data.accessToken' "$TMP_DIR/login-fm.json")"
echo "[2/7] Dedup by file_sha256 => VALID_003"
cat > "$TMP_DIR/same-file.pdf" <<PDF
%PDF-1.4
@@ -226,6 +285,12 @@ status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_B/bind" "$TMP_DIR/bB.js
assert_status "$status" "409" "file_sha256 dedup bind" "$TMP_DIR/bB.json"
assert_error_code "$TMP_DIR/bB.json" "VALID_003"
echo "[2.1/7] finance_mgr override duplicate (file_sha256) -> bind ok"
override_status="$(api_json_with_token POST "$BASE_URL/api/invoice-pool/$IID_B/override-duplicate" "$TOKEN_FM" "$TMP_DIR/override-file.json" "{\"refFormNumber\":\"REF-$RUN_SUFFIX\",\"reason\":\"regression override file_sha256\"}")"
assert_status "$override_status" "200" "override duplicate (file_sha256)" "$TMP_DIR/override-file.json"
rebind_status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_B/bind" "$TMP_DIR/bB2.json" "{\"reimbursementId\":\"$RID_B\"}")"
assert_status "$rebind_status" "200" "bind after override (file_sha256)" "$TMP_DIR/bB2.json"
echo "[3/7] Dedup by invoice_code+invoice_number => VALID_003"
cat > "$TMP_DIR/code-1.pdf" <<PDF
%PDF-1.4
@@ -253,6 +318,47 @@ status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_D/bind" "$TMP_DIR/bD.js
assert_status "$status" "409" "code+number dedup bind" "$TMP_DIR/bD.json"
assert_error_code "$TMP_DIR/bD.json" "VALID_003"
echo "[3.1/7] finance_mgr override duplicate (code+number) -> bind ok"
override_status="$(api_json_with_token POST "$BASE_URL/api/invoice-pool/$IID_D/override-duplicate" "$TOKEN_FM" "$TMP_DIR/override-code.json" "{\"refFormNumber\":\"REF-$RUN_SUFFIX\",\"reason\":\"regression override code+number\"}")"
assert_status "$override_status" "200" "override duplicate (code+number)" "$TMP_DIR/override-code.json"
rebind_status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_D/bind" "$TMP_DIR/bD2.json" "{\"reimbursementId\":\"$RID_D\"}")"
assert_status "$rebind_status" "200" "bind after override (code+number)" "$TMP_DIR/bD2.json"
echo "[3.2/7] override blocked when original reimbursement approved => VALID_004"
BLOCK_CODE="BCODE-$RUN_SUFFIX"
BLOCK_NUMBER="BNO-$RUN_SUFFIX"
cat > "$TMP_DIR/block-1.pdf" <<PDF
%PDF-1.4
block-one-$RUN_SUFFIX
PDF
cat > "$TMP_DIR/block-2.pdf" <<PDF
%PDF-1.4
block-two-$RUN_SUFFIX
PDF
RID_BLOCK_A="$(create_reimbursement "$TMP_DIR/r-block-a.json")"
status="$(upload_file "$TMP_DIR/block-1.pdf" "$TMP_DIR/u-block-a.json")"
assert_status "$status" "200" "upload block-1" "$TMP_DIR/u-block-a.json"
IID_BLOCK_A="$(jq -er '.data.invoicePoolId' "$TMP_DIR/u-block-a.json")"
set_invoice_code_number "$IID_BLOCK_A" "$BLOCK_CODE" "$BLOCK_NUMBER"
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_BLOCK_A/bind" "$TMP_DIR/b-block-a.json" "{\"reimbursementId\":\"$RID_BLOCK_A\"}")"
assert_status "$status" "200" "bind block-1" "$TMP_DIR/b-block-a.json"
set_reimbursement_status_db "$RID_BLOCK_A" "approved"
RID_BLOCK_B="$(create_reimbursement "$TMP_DIR/r-block-b.json")"
status="$(upload_file "$TMP_DIR/block-2.pdf" "$TMP_DIR/u-block-b.json")"
assert_status "$status" "200" "upload block-2" "$TMP_DIR/u-block-b.json"
IID_BLOCK_B="$(jq -er '.data.invoicePoolId' "$TMP_DIR/u-block-b.json")"
set_invoice_code_number "$IID_BLOCK_B" "$BLOCK_CODE" "$BLOCK_NUMBER"
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_BLOCK_B/bind" "$TMP_DIR/b-block-b.json" "{\"reimbursementId\":\"$RID_BLOCK_B\"}")"
assert_status "$status" "409" "bind block-2 dedup" "$TMP_DIR/b-block-b.json"
assert_error_code "$TMP_DIR/b-block-b.json" "VALID_003"
override_block_status="$(api_json_with_token POST "$BASE_URL/api/invoice-pool/$IID_BLOCK_B/override-duplicate" "$TOKEN_FM" "$TMP_DIR/override-block.json" "{\"refFormNumber\":\"REF-$RUN_SUFFIX\",\"reason\":\"regression override blocked\"}")"
assert_status "$override_block_status" "409" "override blocked" "$TMP_DIR/override-block.json"
assert_error_code "$TMP_DIR/override-block.json" "VALID_004"
echo "[4/7] Non-dedup P2002 should not map to VALID_003"
cat > "$TMP_DIR/non-dedup-1.pdf" <<PDF
%PDF-1.4