#!/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}" 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" 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)" TMP_INDEX_CREATED=0 RUN_SUFFIX="$(date +%s)-$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 } 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 } 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 "[2/7] Dedup by file_sha256 => VALID_003" cat > "$TMP_DIR/same-file.pdf" < VALID_003" cat > "$TMP_DIR/code-1.pdf" < "$TMP_DIR/code-2.pdf" < "$TMP_DIR/non-dedup-1.pdf" < "$TMP_DIR/non-dedup-2.pdf" <&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"