#!/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" # 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)" 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: 'finance_mgr@hyinda.com' }, 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 } set_verification_flag() { local mode="$1" DATABASE_URL="$DATABASE_URL" MODE="$mode" node <<'NODE' require("dotenv/config"); const { createPrismaClient } = require("./src/prismaClient"); (async () => { const mode = process.env.MODE; const { prisma, pool } = createPrismaClient(); await prisma.$connect(); try { await prisma.featureFlag.upsert({ where: { flag_key: "invoice_verification" }, create: { flag_key: "invoice_verification", override_mode: mode, rollout_type: "global", department_ids: [], note: "regression-verification", }, update: { override_mode: mode, rollout_type: "global", department_ids: [], note: "regression-verification", version: { increment: 1 }, }, }); } finally { await prisma.$disconnect(); await pool.end(); } })().catch((error) => { console.error(error); process.exit(1); }); NODE } echo "[0/6] Ensure verification feature flag force_on" set_verification_flag "force_on" 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" echo "[post] Reset verification feature flag force_off" set_verification_flag "force_off"