#!/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