feat: 完成P6-P8后端能力与P9路由重构(V1-V2,D1,D3,B1-B2,B4,I1-I4,P9A2-P9A3)

这个提交包含在:
2026-02-10 11:28:28 +08:00
父节点 bec21e1b38
当前提交 6bc09f557b
共修改 33 个文件,包含 3467 行新增和 13 行删除
+41 -1
查看文件
@@ -6,6 +6,10 @@ RUN_AUDIT_REGRESSION="${RUN_AUDIT_REGRESSION:-1}"
RUN_OCR_REGRESSION="${RUN_OCR_REGRESSION:-1}"
RUN_ORG_SETTINGS_REGRESSION="${RUN_ORG_SETTINGS_REGRESSION:-0}"
RUN_AUTH_SECURITY_REGRESSION="${RUN_AUTH_SECURITY_REGRESSION:-0}"
RUN_VERIFICATION_REGRESSION="${RUN_VERIFICATION_REGRESSION:-0}"
RUN_DASHBOARD_REGRESSION="${RUN_DASHBOARD_REGRESSION:-0}"
RUN_BUDGET_REGRESSION="${RUN_BUDGET_REGRESSION:-0}"
RUN_INTEGRATIONS_REGRESSION="${RUN_INTEGRATIONS_REGRESSION:-0}"
echo "=== [1/7] Smoke Test ==="
"$SCRIPT_DIR/smoke-test.sh"
@@ -47,7 +51,7 @@ else
fi
if [[ "$RUN_AUTH_SECURITY_REGRESSION" == "1" ]]; then
echo "=== [7/7] Auth Security Regression ==="
echo "=== [7/7] Auth Security Regression ==="
"$SCRIPT_DIR/regression-auth-security.sh"
echo ""
else
@@ -55,4 +59,40 @@ else
echo ""
fi
if [[ "$RUN_VERIFICATION_REGRESSION" == "1" ]]; then
echo "=== [8/11] Verification Regression ==="
"$SCRIPT_DIR/regression-verification.sh"
echo ""
else
echo "=== [8/11] Verification Regression (skipped, RUN_VERIFICATION_REGRESSION=$RUN_VERIFICATION_REGRESSION) ==="
echo ""
fi
if [[ "$RUN_DASHBOARD_REGRESSION" == "1" ]]; then
echo "=== [9/11] Dashboard Regression ==="
"$SCRIPT_DIR/regression-dashboard.sh"
echo ""
else
echo "=== [9/11] Dashboard Regression (skipped, RUN_DASHBOARD_REGRESSION=$RUN_DASHBOARD_REGRESSION) ==="
echo ""
fi
if [[ "$RUN_BUDGET_REGRESSION" == "1" ]]; then
echo "=== [10/11] Budget Regression ==="
"$SCRIPT_DIR/regression-budget.sh"
echo ""
else
echo "=== [10/11] Budget Regression (skipped, RUN_BUDGET_REGRESSION=$RUN_BUDGET_REGRESSION) ==="
echo ""
fi
if [[ "$RUN_INTEGRATIONS_REGRESSION" == "1" ]]; then
echo "=== [11/11] Integrations Regression ==="
"$SCRIPT_DIR/regression-integrations.sh"
echo ""
else
echo "=== [11/11] Integrations Regression (skipped, RUN_INTEGRATIONS_REGRESSION=$RUN_INTEGRATIONS_REGRESSION) ==="
echo ""
fi
echo "[OK] All regressions passed"
+168
查看文件
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-budget}"
FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}"
FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}"
EMPLOYEE_IDENTIFIER="${EMPLOYEE_IDENTIFIER:-employee@hyinda.com}"
EMPLOYEE_PASSWORD="${EMPLOYEE_PASSWORD:-EmployeePassword1234}"
if ! command -v curl >/dev/null 2>&1; then
echo "[ERROR] curl not found" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "[ERROR] jq not found" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
RUN_SUFFIX="$(date +%s)-$RANDOM"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
assert_status() {
local actual="$1"
local expected="$2"
local name="$3"
local body_file="${4:-}"
if [[ "$actual" != "$expected" ]]; then
echo "[ERROR] $name expected HTTP $expected but got $actual" >&2
if [[ -n "$body_file" && -f "$body_file" ]]; then
cat "$body_file" >&2
fi
exit 1
fi
}
login() {
local identifier="$1"
local password="$2"
local out_file="$3"
local status
status="$(curl -sS -o "$out_file" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/login" \
-H "content-type: application/json" \
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
-d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")"
assert_status "$status" "200" "login $identifier" "$out_file"
jq -er '.data.accessToken' "$out_file"
}
api_json() {
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
}
echo "[1/7] Login users"
FINANCE_TOKEN="$(login "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/login-finance.json")"
EMPLOYEE_TOKEN="$(login "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "$TMP_DIR/login-employee.json")"
echo "[2/7] Create budget pool"
BUDGET_CODE="BGT-$RUN_SUFFIX"
status="$(api_json POST "$BASE_URL/api/budgets" "$FINANCE_TOKEN" "$TMP_DIR/budget-create.json" "{\"code\":\"$BUDGET_CODE\",\"name\":\"Regression Budget\",\"periodStart\":\"2026-01-01\",\"periodEnd\":\"2026-12-31\",\"allocatedAmount\":\"5000.00\",\"warningThresholdPercent\":80}")"
assert_status "$status" "200" "create budget" "$TMP_DIR/budget-create.json"
BUDGET_ID="$(jq -er '.data.id' "$TMP_DIR/budget-create.json")"
echo "[3/7] Query budgets"
status="$(api_json GET "$BASE_URL/api/budgets?page=1&pageSize=20" "$FINANCE_TOKEN" "$TMP_DIR/budget-list.json")"
assert_status "$status" "200" "list budgets" "$TMP_DIR/budget-list.json"
jq -e '.data.items | length >= 1' "$TMP_DIR/budget-list.json" >/dev/null
echo "[4/7] Build reimbursement with item + invoice"
status="$(api_json POST "$BASE_URL/api/reimbursements" "$EMPLOYEE_TOKEN" "$TMP_DIR/reimbursement-create.json" '{}')"
assert_status "$status" "200" "create reimbursement" "$TMP_DIR/reimbursement-create.json"
REIMBURSEMENT_ID="$(jq -er '.data.id' "$TMP_DIR/reimbursement-create.json")"
EXPENSE_TYPE_ID="$(api_json GET "$BASE_URL/api/expense-types?enabled=1" "$EMPLOYEE_TOKEN" "$TMP_DIR/expense-types.json" >/dev/null; jq -er '.data.items[0].id' "$TMP_DIR/expense-types.json")"
status="$(api_json POST "$BASE_URL/api/reimbursements/$REIMBURSEMENT_ID/items" "$EMPLOYEE_TOKEN" "$TMP_DIR/reimbursement-item.json" "{\"expenseTypeId\":\"$EXPENSE_TYPE_ID\",\"expenseDate\":\"2026-02-09\",\"amount\":\"6000.00\",\"description\":\"budget regression\"}")"
assert_status "$status" "200" "create reimbursement item" "$TMP_DIR/reimbursement-item.json"
DUMMY_FILE="$TMP_DIR/invoice.png"
printf 'budget-regression-%s-%s' "$RUN_SUFFIX" "$RANDOM" >"$DUMMY_FILE"
status="$(curl -sS -o "$TMP_DIR/invoice-upload.json" -w "%{http_code}" \
-X POST "$BASE_URL/api/invoice-pool/upload" \
-H "authorization: Bearer $EMPLOYEE_TOKEN" \
-F "file=@$DUMMY_FILE;type=image/png")"
assert_status "$status" "200" "upload invoice" "$TMP_DIR/invoice-upload.json"
INVOICE_ID="$(jq -er '.data.invoicePoolId' "$TMP_DIR/invoice-upload.json")"
status="$(api_json POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/bind" "$EMPLOYEE_TOKEN" "$TMP_DIR/invoice-bind.json" "{\"reimbursementId\":\"$REIMBURSEMENT_ID\"}")"
assert_status "$status" "200" "bind invoice" "$TMP_DIR/invoice-bind.json"
echo "[5/7] Submit reimbursement (warn mode should pass)"
status="$(BUDGET_CONTROL_MODE=warn api_json POST "$BASE_URL/api/reimbursements/$REIMBURSEMENT_ID/submit" "$EMPLOYEE_TOKEN" "$TMP_DIR/reimbursement-submit-warn.json" '{}')"
assert_status "$status" "200" "submit reimbursement warn" "$TMP_DIR/reimbursement-submit-warn.json"
echo "[6/7] Archive budget"
status="$(api_json POST "$BASE_URL/api/budgets/$BUDGET_ID/archive" "$FINANCE_TOKEN" "$TMP_DIR/budget-archive.json" '{}')"
assert_status "$status" "200" "archive budget" "$TMP_DIR/budget-archive.json"
status="$(api_json GET "$BASE_URL/api/budgets?status=archived" "$FINANCE_TOKEN" "$TMP_DIR/budget-archived-list.json")"
assert_status "$status" "200" "list archived budgets" "$TMP_DIR/budget-archived-list.json"
echo "[7/7] Done"
echo "[OK] Budget regression passed"
if [[ -n "${BLOCK_BASE_URL:-}" ]]; then
echo "[extra] Block mode scenario on BLOCK_BASE_URL=$BLOCK_BASE_URL"
BLOCK_FINANCE_TOKEN="$(BASE_URL="$BLOCK_BASE_URL" login "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/block-login-finance.json")"
BLOCK_EMPLOYEE_TOKEN="$(BASE_URL="$BLOCK_BASE_URL" login "$EMPLOYEE_IDENTIFIER" "$EMPLOYEE_PASSWORD" "$TMP_DIR/block-login-employee.json")"
BLOCK_BUDGET_CODE="BGT-BLOCK-$RUN_SUFFIX"
status="$(BASE_URL="$BLOCK_BASE_URL" api_json POST "$BLOCK_BASE_URL/api/budgets" "$BLOCK_FINANCE_TOKEN" "$TMP_DIR/block-budget-create.json" "{\"code\":\"$BLOCK_BUDGET_CODE\",\"name\":\"Regression Block Budget\",\"periodStart\":\"2026-01-01\",\"periodEnd\":\"2026-12-31\",\"allocatedAmount\":\"5000.00\",\"warningThresholdPercent\":80}")"
assert_status "$status" "200" "create block budget" "$TMP_DIR/block-budget-create.json"
status="$(BASE_URL="$BLOCK_BASE_URL" api_json POST "$BLOCK_BASE_URL/api/reimbursements" "$BLOCK_EMPLOYEE_TOKEN" "$TMP_DIR/block-reimbursement-create.json" '{}')"
assert_status "$status" "200" "create block reimbursement" "$TMP_DIR/block-reimbursement-create.json"
BLOCK_REIMBURSEMENT_ID="$(jq -er '.data.id' "$TMP_DIR/block-reimbursement-create.json")"
EXPENSE_TYPE_ID="$(BASE_URL="$BLOCK_BASE_URL" api_json GET "$BLOCK_BASE_URL/api/expense-types?enabled=1" "$BLOCK_EMPLOYEE_TOKEN" "$TMP_DIR/block-expense-types.json" >/dev/null; jq -er '.data.items[0].id' "$TMP_DIR/block-expense-types.json")"
status="$(BASE_URL="$BLOCK_BASE_URL" api_json POST "$BLOCK_BASE_URL/api/reimbursements/$BLOCK_REIMBURSEMENT_ID/items" "$BLOCK_EMPLOYEE_TOKEN" "$TMP_DIR/block-reimbursement-item.json" "{\"expenseTypeId\":\"$EXPENSE_TYPE_ID\",\"expenseDate\":\"2026-02-09\",\"amount\":\"6000.00\",\"description\":\"budget block regression\"}")"
assert_status "$status" "200" "create block reimbursement item" "$TMP_DIR/block-reimbursement-item.json"
BLOCK_DUMMY_FILE="$TMP_DIR/block-invoice.png"
printf "budget-block-regression-%s-%s" "$RUN_SUFFIX" "$RANDOM" >"$BLOCK_DUMMY_FILE"
status="$(curl -sS -o "$TMP_DIR/block-invoice-upload.json" -w "%{http_code}" \
-X POST "$BLOCK_BASE_URL/api/invoice-pool/upload" \
-H "authorization: Bearer $BLOCK_EMPLOYEE_TOKEN" \
-F "file=@$BLOCK_DUMMY_FILE;type=image/png")"
assert_status "$status" "200" "upload block invoice" "$TMP_DIR/block-invoice-upload.json"
BLOCK_INVOICE_ID="$(jq -er '.data.invoicePoolId' "$TMP_DIR/block-invoice-upload.json")"
status="$(BASE_URL="$BLOCK_BASE_URL" api_json POST "$BLOCK_BASE_URL/api/invoice-pool/$BLOCK_INVOICE_ID/bind" "$BLOCK_EMPLOYEE_TOKEN" "$TMP_DIR/block-invoice-bind.json" "{\"reimbursementId\":\"$BLOCK_REIMBURSEMENT_ID\"}")"
assert_status "$status" "200" "bind block invoice" "$TMP_DIR/block-invoice-bind.json"
status="$(BASE_URL="$BLOCK_BASE_URL" api_json POST "$BLOCK_BASE_URL/api/reimbursements/$BLOCK_REIMBURSEMENT_ID/submit" "$BLOCK_EMPLOYEE_TOKEN" "$TMP_DIR/block-reimbursement-submit.json" '{}')"
if [[ "$status" != "409" ]]; then
echo "[ERROR] block mode expected 409 but got $status" >&2
cat "$TMP_DIR/block-reimbursement-submit.json" >&2
exit 1
fi
jq -e '.error.details.budget == "exceeded_block_mode"' "$TMP_DIR/block-reimbursement-submit.json" >/dev/null
echo "[OK] Budget block-mode regression passed"
fi
+146
查看文件
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-dashboard}"
FINANCE_MGR_IDENTIFIER="${FINANCE_MGR_IDENTIFIER:-finance_mgr@hyinda.com}"
FINANCE_MGR_PASSWORD="${FINANCE_MGR_PASSWORD:-FinanceMgrPassword1234}"
DASHBOARD_PERF_CHECK="${DASHBOARD_PERF_CHECK:-0}"
if ! command -v curl >/dev/null 2>&1; then
echo "[ERROR] curl not found" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "[ERROR] jq not found" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
assert_status() {
local actual="$1"
local expected="$2"
local name="$3"
local body_file="${4:-}"
if [[ "$actual" != "$expected" ]]; then
echo "[ERROR] $name expected HTTP $expected but got $actual" >&2
if [[ -n "$body_file" && -f "$body_file" ]]; then
cat "$body_file" >&2
fi
exit 1
fi
}
login() {
local out_file="$1"
local status
status="$(curl -sS -o "$out_file" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/login" \
-H "content-type: application/json" \
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
-d "{\"identifier\":\"$FINANCE_MGR_IDENTIFIER\",\"password\":\"$FINANCE_MGR_PASSWORD\"}")"
assert_status "$status" "200" "login" "$out_file"
jq -er '.data.accessToken' "$out_file"
}
api_json() {
local method="$1"
local url="$2"
local token="$3"
local out_file="$4"
curl -sS -o "$out_file" -w "%{http_code}" \
-X "$method" "$url" \
-H "authorization: Bearer $token"
}
assert_json_shape() {
local file="$1"
local expr="$2"
local name="$3"
if ! jq -e "$expr" "$file" >/dev/null; then
echo "[ERROR] $name json assertion failed: $expr" >&2
cat "$file" >&2
exit 1
fi
}
echo "[1/5] Login"
TOKEN="$(login "$TMP_DIR/login.json")"
FILTER_QS="dateFrom=2026-01-01&dateTo=2026-12-31"
echo "[2/5] Query dashboard endpoints"
status="$(api_json GET "$BASE_URL/api/dashboard/expense-overview?$FILTER_QS" "$TOKEN" "$TMP_DIR/overview.json")"
assert_status "$status" "200" "dashboard overview" "$TMP_DIR/overview.json"
assert_json_shape "$TMP_DIR/overview.json" '.data.totalCount >= 0 and (.data.totalAmount | type == "string")' "overview"
status="$(api_json GET "$BASE_URL/api/dashboard/trends?$FILTER_QS" "$TOKEN" "$TMP_DIR/trends-1.json")"
assert_status "$status" "200" "dashboard trends(1)" "$TMP_DIR/trends-1.json"
assert_json_shape "$TMP_DIR/trends-1.json" '.data.points | type == "array"' "trends-array"
assert_json_shape "$TMP_DIR/trends-1.json" '.data.points | all(.[]; (.date | type == "string") and (.amount | type == "string") and (.count | type == "number"))' "trends-shape"
assert_json_shape "$TMP_DIR/trends-1.json" '
.data.points
| reduce .[] as $p ({prev:null, ok:true};
if .ok == false then .
elif .prev == null or $p.date >= .prev then {prev: $p.date, ok: true}
else {prev: $p.date, ok: false}
end
)
| .ok
' "trends-order"
status="$(api_json GET "$BASE_URL/api/dashboard/trends?$FILTER_QS" "$TOKEN" "$TMP_DIR/trends-2.json")"
assert_status "$status" "200" "dashboard trends(2)" "$TMP_DIR/trends-2.json"
if ! diff -u <(jq -S '.data.points' "$TMP_DIR/trends-1.json") <(jq -S '.data.points' "$TMP_DIR/trends-2.json") >/dev/null; then
echo "[ERROR] trends response not stable across repeated calls" >&2
exit 1
fi
status="$(api_json GET "$BASE_URL/api/dashboard/top-categories?$FILTER_QS&limit=5" "$TOKEN" "$TMP_DIR/top-categories.json")"
assert_status "$status" "200" "dashboard top-categories" "$TMP_DIR/top-categories.json"
assert_json_shape "$TMP_DIR/top-categories.json" '.data.items | type == "array" and (length <= 5)' "top-categories-array"
assert_json_shape "$TMP_DIR/top-categories.json" '.data.items | all(.[]; (.code | type == "string") and (.name | type == "string") and (.amount | type == "string") and (.count | type == "number"))' "top-categories-shape"
status="$(api_json GET "$BASE_URL/api/dashboard/approval-efficiency?$FILTER_QS" "$TOKEN" "$TMP_DIR/efficiency.json")"
assert_status "$status" "200" "dashboard approval-efficiency" "$TMP_DIR/efficiency.json"
assert_json_shape "$TMP_DIR/efficiency.json" '.data.averageHours >= 0 and .data.approvedCount >= 0' "efficiency"
echo "[3/5] Export dashboard data"
status="$(curl -sS -o "$TMP_DIR/export.csv" -w "%{http_code}" \
-X GET "$BASE_URL/api/exports/dashboard/reimbursements?format=csv&$FILTER_QS" \
-H "authorization: Bearer $TOKEN")"
assert_status "$status" "200" "export dashboard csv" "$TMP_DIR/export.csv"
if [[ ! -s "$TMP_DIR/export.csv" ]]; then
echo "[ERROR] dashboard csv export is empty" >&2
exit 1
fi
status="$(curl -sS -o "$TMP_DIR/export.xlsx" -w "%{http_code}" \
-X GET "$BASE_URL/api/exports/dashboard/reimbursements?format=xlsx&$FILTER_QS" \
-H "authorization: Bearer $TOKEN")"
assert_status "$status" "200" "export dashboard xlsx" "$TMP_DIR/export.xlsx"
if [[ ! -s "$TMP_DIR/export.xlsx" ]]; then
echo "[ERROR] dashboard xlsx export is empty" >&2
exit 1
fi
if [[ "$DASHBOARD_PERF_CHECK" == "1" ]]; then
echo "[4/5] Optional performance probe"
PERF_TIME="$(curl -sS -o /dev/null -w "%{time_total}" \
-X GET "$BASE_URL/api/dashboard/trends?$FILTER_QS" \
-H "authorization: Bearer $TOKEN")"
echo "[INFO] trends endpoint time_total=${PERF_TIME}s"
else
echo "[4/5] Optional performance probe (skipped, DASHBOARD_PERF_CHECK=$DASHBOARD_PERF_CHECK)"
fi
echo "[5/5] Done"
echo "[OK] Dashboard regression passed"
+186
查看文件
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-verification}"
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
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "[ERROR] jq not found" >&2
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [[ -z "${DATABASE_URL:-}" && -f ".env" ]]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
if [[ -z "${DATABASE_URL:-}" ]]; then
echo "[ERROR] DATABASE_URL is required (export it or provide server/.env)" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
RUN_SUFFIX="$(date +%s)-$RANDOM"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
assert_status() {
local actual="$1"
local expected="$2"
local name="$3"
local body_file="${4:-}"
if [[ "$actual" != "$expected" ]]; then
echo "[ERROR] $name expected HTTP $expected but got $actual" >&2
if [[ -n "$body_file" && -f "$body_file" ]]; then
cat "$body_file" >&2
fi
exit 1
fi
}
login() {
local identifier="$1"
local password="$2"
local out_file="$3"
local status
status="$(curl -sS -o "$out_file" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/login" \
-H "content-type: application/json" \
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
-d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")"
assert_status "$status" "200" "login" "$out_file"
jq -er '.data.accessToken' "$out_file"
}
api_json() {
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
}
create_available_invoice() {
local marker="$1"
DATABASE_URL="$DATABASE_URL" RUN_SUFFIX="$RUN_SUFFIX" MARKER="$marker" node <<'NODE'
const fs = require('node:fs/promises');
const path = require('node:path');
const crypto = require('node:crypto');
const { createPrismaClient } = require('./src/prismaClient');
const { getConfig } = require('./src/config');
(async () => {
const marker = process.env.MARKER || 'VERIFY_OK';
const runSuffix = process.env.RUN_SUFFIX || 'default';
const config = getConfig();
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
try {
const owner = await prisma.employee.findUnique({
where: { email: '[email protected]' },
select: { id: true },
});
if (!owner) {
throw new Error('finance_mgr not found');
}
const relPath = path.join('verification-regression', runSuffix, `${crypto.randomUUID()}.txt`);
const absPath = path.resolve(config.storageDir, relPath);
await fs.mkdir(path.dirname(absPath), { recursive: true });
await fs.writeFile(absPath, marker, 'utf8');
const fileSha256 = crypto.createHash('sha256').update(marker).digest('hex');
const created = await prisma.invoicePool.create({
data: {
owner_employee_id: owner.id,
uploaded_by_employee_id: owner.id,
storage_path: relPath,
original_name: `verify-${runSuffix}.txt`,
mime_type: 'text/plain',
file_size: Buffer.byteLength(marker),
file_sha256: fileSha256,
status: 'available',
ocr_type: 'general',
invoice_code: 'MOCKPASS',
invoice_number: 'PASS',
remarks: marker.includes('FAIL') ? 'verify-fail' : null,
created_by: owner.id,
updated_by: owner.id,
},
select: { id: true },
});
process.stdout.write(`${created.id}\n`);
} finally {
await prisma.$disconnect();
await pool.end();
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
}
echo "[1/6] Login finance_mgr"
TOKEN="$(login "$FINANCE_MGR_IDENTIFIER" "$FINANCE_MGR_PASSWORD" "$TMP_DIR/login.json")"
echo "[2/6] Create synthetic available invoice"
INVOICE_ID="$(create_available_invoice "VERIFY_OK")"
echo "[3/6] Request verification"
status="$(api_json POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/verify" "$TOKEN" "$TMP_DIR/verify.json" '{}')"
assert_status "$status" "200" "invoice verify" "$TMP_DIR/verify.json"
VERIFICATION_ID="$(jq -er '.data.verificationId' "$TMP_DIR/verify.json")"
echo "[4/6] Run verification worker inline"
VERIFICATION_RECORD_ID="$VERIFICATION_ID" INVOICE_VERIFICATION_ENABLED=true VERIFICATION_PROVIDER=mock npm run --silent job:verification-once >"$TMP_DIR/verify-once.log"
sleep 0.2
echo "[5/6] Query verification records"
status="$(api_json GET "$BASE_URL/api/invoice-pool/$INVOICE_ID/verification?limit=5" "$TOKEN" "$TMP_DIR/records.json")"
assert_status "$status" "200" "list verification" "$TMP_DIR/records.json"
jq -e '.data.items | length >= 1' "$TMP_DIR/records.json" >/dev/null
jq -e '.data.items[0].status == "passed"' "$TMP_DIR/records.json" >/dev/null
echo "[6/6] Retry should be throttled immediately"
status="$(api_json POST "$BASE_URL/api/invoice-pool/$INVOICE_ID/verification/retry" "$TOKEN" "$TMP_DIR/retry.json" '{}')"
if [[ "$status" != "409" ]]; then
echo "[ERROR] expected retry throttled 409 but got $status" >&2
cat "$TMP_DIR/retry.json" >&2
exit 1
fi
echo "[OK] Verification regression passed"