关联 Issue: P25-1, P25-2, P25-3, P25-4, P25-5, P26-1, P26-3, P24-1, P24-2, P29-1, P29-2, P29-3, P29-4
408 行
14 KiB
Bash
可执行文件
408 行
14 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
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
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "[ERROR] jq not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
echo "[ERROR] node not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT_DIR"
|
|
|
|
# shellcheck source=scripts/lib/load-env-defaults.sh
|
|
source "$ROOT_DIR/scripts/lib/load-env-defaults.sh"
|
|
load_env_defaults ".env"
|
|
|
|
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)"
|
|
TMP_INDEX_CREATED=0
|
|
RUN_SUFFIX="$(date +%s%N)-$RANDOM"
|
|
DEDUP_CODE="RCODE-$RUN_SUFFIX"
|
|
DEDUP_NUMBER="RNO-$RUN_SUFFIX"
|
|
|
|
cleanup() {
|
|
if [[ "$TMP_INDEX_CREATED" == "1" ]]; then
|
|
drop_tmp_non_dedup_index || true
|
|
fi
|
|
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
|
|
}
|
|
|
|
api_json() {
|
|
local method="$1"
|
|
local url="$2"
|
|
local out_file="$3"
|
|
local body="${4:-}"
|
|
|
|
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
|
|
}
|
|
|
|
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"
|
|
|
|
curl -sS -o "$out_file" -w "%{http_code}" \
|
|
-X POST "$BASE_URL/api/invoice-pool/upload" \
|
|
-H "authorization: Bearer $TOKEN" \
|
|
-F "file=@$file_path"
|
|
}
|
|
|
|
create_reimbursement() {
|
|
local out_file="$1"
|
|
local status
|
|
status="$(api_json POST "$BASE_URL/api/reimbursements" "$out_file" '{}')"
|
|
assert_status "$status" "200" "create reimbursement" "$out_file"
|
|
jq -er '.data.id' "$out_file"
|
|
}
|
|
|
|
set_invoice_code_number() {
|
|
local invoice_id="$1"
|
|
local invoice_code="$2"
|
|
local invoice_number="$3"
|
|
|
|
DATABASE_URL="$DATABASE_URL" node - "$invoice_id" "$invoice_code" "$invoice_number" <<'NODE'
|
|
const invoiceId = process.argv[2];
|
|
const invoiceCode = process.argv[3];
|
|
const invoiceNumber = process.argv[4];
|
|
const { createPrismaClient } = require('./src/prismaClient');
|
|
|
|
(async () => {
|
|
const { prisma, pool } = createPrismaClient();
|
|
await prisma.$connect();
|
|
try {
|
|
await prisma.invoicePool.update({
|
|
where: { id: invoiceId },
|
|
data: {
|
|
invoice_code: invoiceCode,
|
|
invoice_number: invoiceNumber,
|
|
},
|
|
});
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
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'
|
|
const { createPrismaClient } = require('./src/prismaClient');
|
|
|
|
(async () => {
|
|
const reimbursementId = process.env.REIMBURSEMENT_ID;
|
|
if (!reimbursementId) {
|
|
throw new Error('REIMBURSEMENT_ID is required');
|
|
}
|
|
|
|
const { prisma, pool } = createPrismaClient();
|
|
await prisma.$connect();
|
|
try {
|
|
await prisma.$executeRawUnsafe('DROP INDEX IF EXISTS "tmp_unique_linked_reim"');
|
|
await prisma.$executeRawUnsafe(
|
|
`CREATE UNIQUE INDEX "tmp_unique_linked_reim" ON "invoice_pool" ("linked_reimbursement_id") WHERE "linked_reimbursement_id" = '${reimbursementId}'`,
|
|
);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
NODE
|
|
}
|
|
|
|
drop_tmp_non_dedup_index() {
|
|
DATABASE_URL="$DATABASE_URL" node <<'NODE'
|
|
const { createPrismaClient } = require('./src/prismaClient');
|
|
|
|
(async () => {
|
|
const { prisma, pool } = createPrismaClient();
|
|
await prisma.$connect();
|
|
try {
|
|
await prisma.$executeRawUnsafe('DROP INDEX IF EXISTS "tmp_unique_linked_reim"');
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
await pool.end();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
NODE
|
|
}
|
|
|
|
assert_error_code() {
|
|
local file_path="$1"
|
|
local expected="$2"
|
|
local actual
|
|
actual="$(jq -r '.error.code // empty' "$file_path")"
|
|
if [[ "$actual" != "$expected" ]]; then
|
|
echo "[ERROR] expected error code $expected but got $actual" >&2
|
|
cat "$file_path" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
echo "[1/7] Login employee"
|
|
login_code="$(curl -sS -o "$TMP_DIR/login.json" -w "%{http_code}" \
|
|
-X POST "$BASE_URL/api/auth/login" \
|
|
-H "content-type: application/json" \
|
|
-H "x-device-fingerprint: $DEVICE_FINGERPRINT" \
|
|
-d "{\"identifier\":\"$EMPLOYEE_IDENTIFIER\",\"password\":\"$EMPLOYEE_PASSWORD\"}")"
|
|
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
|
|
same-content-$RUN_SUFFIX
|
|
PDF
|
|
|
|
RID_A="$(create_reimbursement "$TMP_DIR/rA.json")"
|
|
status="$(upload_file "$TMP_DIR/same-file.pdf" "$TMP_DIR/uA.json")"
|
|
assert_status "$status" "200" "upload first same-file" "$TMP_DIR/uA.json"
|
|
IID_A="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uA.json")"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_A/bind" "$TMP_DIR/bA.json" "{\"reimbursementId\":\"$RID_A\"}")"
|
|
assert_status "$status" "200" "bind first same-file" "$TMP_DIR/bA.json"
|
|
|
|
RID_B="$(create_reimbursement "$TMP_DIR/rB.json")"
|
|
status="$(upload_file "$TMP_DIR/same-file.pdf" "$TMP_DIR/uB.json")"
|
|
assert_status "$status" "200" "upload second same-file" "$TMP_DIR/uB.json"
|
|
IID_B="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uB.json")"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_B/bind" "$TMP_DIR/bB.json" "{\"reimbursementId\":\"$RID_B\"}")"
|
|
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
|
|
code-one-$RUN_SUFFIX
|
|
PDF
|
|
cat > "$TMP_DIR/code-2.pdf" <<PDF
|
|
%PDF-1.4
|
|
code-two-$RUN_SUFFIX
|
|
PDF
|
|
|
|
RID_C="$(create_reimbursement "$TMP_DIR/rC.json")"
|
|
status="$(upload_file "$TMP_DIR/code-1.pdf" "$TMP_DIR/uC.json")"
|
|
assert_status "$status" "200" "upload code-1" "$TMP_DIR/uC.json"
|
|
IID_C="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uC.json")"
|
|
set_invoice_code_number "$IID_C" "$DEDUP_CODE" "$DEDUP_NUMBER"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_C/bind" "$TMP_DIR/bC.json" "{\"reimbursementId\":\"$RID_C\"}")"
|
|
assert_status "$status" "200" "bind code-1" "$TMP_DIR/bC.json"
|
|
|
|
RID_D="$(create_reimbursement "$TMP_DIR/rD.json")"
|
|
status="$(upload_file "$TMP_DIR/code-2.pdf" "$TMP_DIR/uD.json")"
|
|
assert_status "$status" "200" "upload code-2" "$TMP_DIR/uD.json"
|
|
IID_D="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uD.json")"
|
|
set_invoice_code_number "$IID_D" "$DEDUP_CODE" "$DEDUP_NUMBER"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_D/bind" "$TMP_DIR/bD.json" "{\"reimbursementId\":\"$RID_D\"}")"
|
|
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
|
|
non-dedup-1-$RUN_SUFFIX
|
|
PDF
|
|
cat > "$TMP_DIR/non-dedup-2.pdf" <<PDF
|
|
%PDF-1.4
|
|
non-dedup-2-$RUN_SUFFIX
|
|
PDF
|
|
|
|
RID_E="$(create_reimbursement "$TMP_DIR/rE.json")"
|
|
create_tmp_non_dedup_index "$RID_E"
|
|
TMP_INDEX_CREATED=1
|
|
status="$(upload_file "$TMP_DIR/non-dedup-1.pdf" "$TMP_DIR/uE1.json")"
|
|
assert_status "$status" "200" "upload non-dedup-1" "$TMP_DIR/uE1.json"
|
|
IID_E1="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uE1.json")"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_E1/bind" "$TMP_DIR/bE1.json" "{\"reimbursementId\":\"$RID_E\"}")"
|
|
assert_status "$status" "200" "bind non-dedup-1" "$TMP_DIR/bE1.json"
|
|
|
|
status="$(upload_file "$TMP_DIR/non-dedup-2.pdf" "$TMP_DIR/uE2.json")"
|
|
assert_status "$status" "200" "upload non-dedup-2" "$TMP_DIR/uE2.json"
|
|
IID_E2="$(jq -er '.data.invoicePoolId' "$TMP_DIR/uE2.json")"
|
|
status="$(api_json POST "$BASE_URL/api/invoice-pool/$IID_E2/bind" "$TMP_DIR/bE2.json" "{\"reimbursementId\":\"$RID_E\"}")"
|
|
if [[ "$status" == "409" ]] && [[ "$(jq -r '.error.code // empty' "$TMP_DIR/bE2.json")" == "VALID_003" ]]; then
|
|
echo "[ERROR] non-dedup P2002 was incorrectly mapped to VALID_003" >&2
|
|
cat "$TMP_DIR/bE2.json" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[5/7] Validate non-dedup result is server-side failure path"
|
|
if [[ "$status" != "500" ]]; then
|
|
echo "[ERROR] expected HTTP 500 for temporary non-dedup unique index violation, got $status" >&2
|
|
cat "$TMP_DIR/bE2.json" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[6/7] Ensure non-dedup response is not VALID_003"
|
|
unexpected_code="$(jq -r '.error.code // empty' "$TMP_DIR/bE2.json")"
|
|
if [[ "$unexpected_code" == "VALID_003" ]]; then
|
|
echo "[ERROR] unexpected VALID_003 for non-dedup conflict" >&2
|
|
cat "$TMP_DIR/bE2.json" >&2
|
|
exit 1
|
|
fi
|
|
|
|
drop_tmp_non_dedup_index
|
|
TMP_INDEX_CREATED=0
|
|
|
|
echo "[7/7] Done"
|
|
echo "[OK] Dedup regression passed"
|