diff --git a/functional_tests/README.md b/functional_tests/README.md index 1c972b2..072554d 100644 --- a/functional_tests/README.md +++ b/functional_tests/README.md @@ -65,3 +65,20 @@ --- +**Execution Date:** 4/28/2026, 7:14:16 AM + +**Test Unique Identifier:** "functional-test-generation" + +**Input(s):** + 1. Aegis_WebCC_SRS.pdf + Path: /var/tmp/Roost/RoostGPT/functional-test-generation/36b11f32-a33a-4901-adf0-30d3869d995c/Aegis_WebCC_SRS.pdf + +**Test Output Folder:** + 1. [functional-test-generation.json](functional-test-generation/functional-test-generation.json) + 2. [functional-test-generation.feature](functional-test-generation/functional-test-generation.feature) + 3. [functional-test-generation.csv](functional-test-generation/functional-test-generation.csv) + 4. [functional-test-generation.xlsx](functional-test-generation/functional-test-generation.xlsx) + 5. [functional-test-generation.docx](functional-test-generation/functional-test-generation.docx) + +--- + diff --git a/functional_tests/functional-test-generation/.roost/roost_metadata.json b/functional_tests/functional-test-generation/.roost/roost_metadata.json new file mode 100644 index 0000000..d061df5 --- /dev/null +++ b/functional_tests/functional-test-generation/.roost/roost_metadata.json @@ -0,0 +1,24 @@ +{ + "project": { + "name": "functional-test-generation", + "created_at": "2026-04-28T07:14:16.361Z", + "updated_at": "2026-04-28T07:14:16.361Z" + }, + "files": { + "input_files": [ + { + "fileName": "functional-test-generation.txt", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-generation/36b11f32-a33a-4901-adf0-30d3869d995c/functional_tests/functional-test-generation/functional-test-generation.txt", + "fileSha": "cf83e1357e" + }, + { + "fileName": "Aegis_WebCC_SRS.pdf", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-generation/36b11f32-a33a-4901-adf0-30d3869d995c/functional_tests/functional-test-generation/Aegis_WebCC_SRS.pdf", + "fileSha": "dcebdb1a12" + } + ] + }, + "api_files": { + "input_files": [] + } +} \ No newline at end of file diff --git a/functional_tests/functional-test-generation/functional-test-generation.csv b/functional_tests/functional-test-generation/functional-test-generation.csv new file mode 100644 index 0000000..3a606f1 --- /dev/null +++ b/functional_tests/functional-test-generation/functional-test-generation.csv @@ -0,0 +1,31 @@ +End-to-end registration, verification, MFA login, 3-step application, approval for FICO > 680, and masked PAN returned by summary +Application decision PENDING and DECLINED with duplicate application control and signature requirement +Login lockout after five failures, rate limiting, MFA enforcement, and remember_me cookie TTL +Refresh token rotation with single-use invalidation and concurrent refresh behavior +Application sequencing, conditional employment fields, sin_consent enforcement, and X-App-Session 30-minute expiry +Application Step 1 address and identity validation failures and success +Application Step 3 validation for invalid product id, malformed e_signature, and marketing_opt_in default/explicit +CSRF enforcement and SameSite=Strict cross-site POST rejection across endpoints +Transactions exchange rate precision, zero amount, foreign fee rounding, and non-essential over-limit rejection +Essential MCC 5% over-limit buffer boundary approvals and rejections vs non-essential MCC +Freeze card prevents transactions, unfreeze via OTP, essential buffer boundary, and 60-minute frequency limit with MFA requirement +Report card stolen is irreversible, forbids PIN and status changes, denies transactions, schedules replacement, and handles duplicates +List transactions with date boundaries, pagination limits, category filter, and owner-only access +Payments validation for minimum amount, bank account validity, date scheduling, owner-only access, and success cases +Account summary include_rewards toggle, owner-only access enforcement, not-found and unauthorized handling +Foreign purchase, fee and rewards, statements JSON/PDF, payment CSRF negative/positive, late fee webhook idempotency, session timeout and right to rescind +ADB interest calculation and grace period across consecutive cycles +Rewards accrual 1x with floor for non-travel and 3x for travel MCC in CAD +Set PIN with OTP success, mismatch/format errors, OTP invalid, and allowed while Frozen +Freeze/Unfreeze OTP attempt limits, resend OTP resets attempts, and successful transitions +Owner-only card controls and report-lost: IDOR prevention and invalid status value handling +WebSocket live transaction feed authentication, event delivery, unauthorized connection, and reconnect +Registration field validations and duplicate email handling with secure cookie attributes +Registration password complexity and leap-year age validation +Email verification invalid/tampered, expired, resend rate-limit, and successful activation +Credit limit change audit trail immutability and visibility, and role-based access +Notifications webhook validation, multi-channel delivery, and idempotency per-account scope +Right to rescind deletion leads to 404 on subsequent endpoints and idempotent DELETE +Dashboard displays masked PAN and no PII leakage +Application Step 1 draft auto-save every 60 seconds, restore, sanitize, and cleanup after submission +Session timeout warning at 13 minutes and expiry at 15 minutes \ No newline at end of file diff --git a/functional_tests/functional-test-generation/functional-test-generation.docx b/functional_tests/functional-test-generation/functional-test-generation.docx new file mode 100644 index 0000000..4098f72 Binary files /dev/null and b/functional_tests/functional-test-generation/functional-test-generation.docx differ diff --git a/functional_tests/functional-test-generation/functional-test-generation.feature b/functional_tests/functional-test-generation/functional-test-generation.feature new file mode 100644 index 0000000..8de2d03 --- /dev/null +++ b/functional_tests/functional-test-generation/functional-test-generation.feature @@ -0,0 +1,950 @@ +Feature: Aegis Card Portal and API End-to-End and Functional Validation + + # Common environment setup + Background: + Given the API base URL is 'https://api.aegiscard.com' + And the Portal URL is 'https://portal.aegiscard.com' + And transport security TLS 1.3 is enforced + And WAF and rate limits are active + And default request content type is 'application/json' + And no tokens are stored in browser localStorage or sessionStorage by design + + # API Tests + + @api @e2e @registration @applications @security + Scenario: End-to-end registration, verification, MFA login, 3-step application, approval for FICO > 680, and masked PAN returned by summary + Given I navigate to the portal Registration page over TLS 1.3 + When I send a POST request to '/v2/auth/register' with JSON payload + """ + { + "first_name": "Jo", + "last_name": "Tester", + "email": "user+test@domain.com", + "password": "weakPass1!", + "date_of_birth": "YYYY-17yo", + "phone_number": "+14165551234", + "ssn_last4": "1234", + "agree_terms": true + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 422 + And the response should contain field errors for 'date_of_birth' and 'password' + When I resend a POST request to '/v2/auth/register' with JSON payload + """ + { + "first_name": "Jo", + "last_name": "Tester", + "email": "user+test@domain.com", + "password": "Stronger!Passw0rd", + "date_of_birth": "YYYY-18y-1d", + "phone_number": "+14165551234", + "ssn_last4": "1234", + "agree_terms": true + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 201 + And the response should contain 'user_id' and 'verification_token' + And Set-Cookie headers must include HttpOnly, Secure, SameSite=Strict + When I send a GET request to '/v2/auth/verify?token=' + Then the response status should be 200 + And the response should indicate 'verified' true + When I send a POST request to '/v2/auth/login' with JSON payload + """ + { + "email": "user+test@domain.com", + "password": "Stronger!Passw0rd", + "device_id": "550e8400-e29b-41d4-a716-446655440000", + "remember_me": true, + "mfa_code": "123456" + } + """ + Then the response status should be 200 + And the response should contain 'access_token', 'refresh_token', 'expires_in' + And Set-Cookie headers must include HttpOnly, Secure, SameSite=Strict + When I acquire a CSRF token from the portal session + And I send a POST request to '/v2/applications/start' with JSON payload + """ + { + "full_legal_name": "Jo Tester", + "email": "user+test@domain.com", + "phone_number": "+14165551234", + "residential_address": { + "street": "123 King St", + "city": "Toronto", + "province": "ON", + "postal_code": "A1A 1A1" + }, + "id_type": "DRIVERS_LICENSE", + "id_number": "DL123456" + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 201 + And the response should contain 'application_id' and 'session_token' + When I send a POST request to '/v2/applications//financials' with JSON payload + """ + { + "employment_status": "EMPLOYED", + "employer_name": "ACME Corp", + "gross_annual_income": 85000.00, + "monthly_rent": 1200.00, + "existing_debt_payments": 300.00, + "sin_consent": true + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'status' equal to 'PENDING_REVIEW' and 'fico_pull_id' + When I send a POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "AEGIS_GOLD", + "e_signature": "Ym9iIHRlc3Rlcg==" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'decision' equal to 'APPROVED' + And the response should contain 'credit_limit' and 'card_number_masked' + When I send a GET request to '/v2/accounts//summary?include_rewards=true' + Then the response status should be 200 + And the response should contain a masked PAN field matching '**** **** **** 1234' + And the response should not contain any raw PAN + And the response should contain 'points_balance' + And all auth cookies remain HttpOnly, Secure, SameSite=Strict + And no token is stored in localStorage + + @api @applications + Scenario: Application decision PENDING and DECLINED with duplicate application control and signature requirement + Given I am logged in as 'pending_user@domain.com' with valid MFA and have a CSRF token + When I send a POST request to '/v2/applications/start' with valid Step 1 data and CSRF + Then the response status should be 201 + And I capture 'application_id' and 'session_token' + When I send a POST request to '/v2/applications//financials' with JSON payload + """ + { + "employment_status": "EMPLOYED", + "employer_name": "ACME", + "gross_annual_income": 60000.00, + "monthly_rent": 1500.00, + "existing_debt_payments": 400.00, + "sin_consent": true + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'fico_pull_id' and 'status' equal to 'PENDING_REVIEW' + When I send a POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER", + "e_signature": "SmFuZSBTaWduYXR1cmU=" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'decision' equal to 'PENDING' and 'review_eta_hours' + When I send a POST request to '/v2/applications/start' with valid Step 1 data and CSRF for the same user + Then the response status should be 409 + And the response should contain error 'DUPLICATE_APPLICATION' + When I logout and login as 'declined_user@domain.com' with valid MFA and CSRF + And I send a POST request to '/v2/applications/start' with valid Step 1 data and CSRF + Then the response status should be 201 + And I capture 'application_id' and 'session_token' + When I send a POST request to '/v2/applications//financials' with JSON payload + """ + { + "employment_status": "UNEMPLOYED", + "gross_annual_income": 12000.00, + "monthly_rent": 800.00, + "existing_debt_payments": 500.00, + "sin_consent": true + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'fico_pull_id' + When I send a POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 400 + And the response should contain error 'SIGNATURE_REQUIRED' + When I resend the POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER", + "e_signature": "SmFuZSBEb2U=" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'decision' equal to 'DECLINED' + And the response should contain 'reason_code' + And the response should have 'marketing_opt_in' false when omitted + + @api @auth + Scenario: Login lockout after five failures, rate limiting, MFA enforcement, and remember_me cookie TTL + Given the verified user 'user+lockout@domain.com' exists and the portal is reachable over TLS 1.3 + When I attempt 4 POST requests to '/v2/auth/login' within one minute with wrong password and no MFA + Then each response status should be 401 with error 'INVALID_CREDENTIALS' + When I attempt a 5th POST to '/v2/auth/login' with wrong password + Then the response status should be 403 + And the response should contain 'unlock_at' + When I immediately attempt a 6th login within the same minute + Then the response status should be 429 + And the response should contain 'retry_after' + When I wait until 'unlock_at' passes and login with correct password but missing 'mfa_code' + Then the response status should be 401 + And the response should contain error 'INVALID_CREDENTIALS' + When I login with correct password and incorrect 'mfa_code' + Then the response status should be 401 + And the response should contain error 'INVALID_CREDENTIALS' + When I login with correct password, valid TOTP, 'device_id' set, and 'remember_me' true + Then the response status should be 200 + And the response should contain 'access_token', 'refresh_token', 'expires_in' + And Set-Cookie headers must include HttpOnly, Secure, SameSite=Strict + And the refresh token cookie expiry should be approximately 30 days in the future + And the access token 'expires_in' should be approximately 900 seconds + + @api @auth + Scenario: Refresh token rotation with single-use invalidation and concurrent refresh behavior + Given I am logged in as 'user+refresh@domain.com' with valid MFA and captured cookies + When I call GET '/v2/accounts//summary' with access token A1 + Then the response status should be 200 + When I wait until near expiry (~14 minutes) and send POST '/v2/auth/token/refresh' with refresh token R1 + Then the response status should be 200 + And I capture new access token A2 and refresh token R2 + When I attempt POST '/v2/auth/token/refresh' again with old refresh token R1 + Then the response status should be 401 + And the response should contain error 'TOKEN_INVALID' + When I send two concurrent POST '/v2/auth/token/refresh' requests with R2 + Then one response status should be 200 with new tokens (A3,R3) + And the other response status should be 401 with error 'TOKEN_INVALID' + When I call GET '/v2/accounts//summary' with A3 + Then the response status should be 200 + When I attempt a protected GET with expired A1 after 15 minutes + Then the response status should be 401 + And the response should indicate token expired + And all auth cookies remain HttpOnly, Secure, SameSite=Strict + And no tokens exist in localStorage + + @api @applications + Scenario: Application sequencing, conditional employment fields, sin_consent enforcement, and X-App-Session 30-minute expiry + Given I am logged in as 'userA@domain.com' with valid MFA and CSRF token + When I send a POST request to '/v2/applications/00000000-0000-0000-0000-000000000000/submit' with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER", + "e_signature": "SmFuZSBB" + } + """ + Then the response status should be 400 + And the response should contain error 'INVALID_SEQUENCE' + When I send a POST request to '/v2/applications/start' with valid Step 1 data and CSRF + Then the response status should be 201 + And I capture 'application_id' and 'session_token' + When I send a POST request to '/v2/applications//financials' omitting 'employer_name' with 'employment_status' 'EMPLOYED' + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 400 + And the response should contain 'field' equal to 'employer_name' + When I resend Step 2 with 'sin_consent' false + Then the response status should be 400 + And the response should contain 'field' equal to 'sin_consent' + When I resend Step 2 with 'employment_status' 'SELF_EMPLOYED', no 'employer_name', and 'sin_consent' true + Then the response status should be 200 + And the response should contain 'fico_pull_id' + When I wait 31 minutes and send POST '/v2/applications//submit' with valid card_product_id and e_signature using old X-App-Session + Then the response status should be 401 + And the response should contain error 'SESSION_EXPIRED' + When I send a POST request to '/v2/applications/start' again for the same user + Then the response status should be 409 + And the response should contain error 'DUPLICATE_APPLICATION' + When I logout and login as 'userB@domain.com' with valid MFA and CSRF + And I complete Steps 1 and 2 with valid data using a fresh X-App-Session + And I send POST '/v2/applications//submit' within 30 minutes + Then the response status should be 200 + And the response should contain 'decision' and 'marketing_opt_in' false when omitted + + @api @applications + Scenario Outline: Application Step 1 address and identity validation failures and success + Given I am logged in as 'user+step1val@domain.com' with valid MFA and CSRF token + When I send a POST request to '/v2/applications/start' with JSON payload + """ + { + "full_legal_name": "", + "email": "", + "phone_number": "", + "residential_address": { + "street": "100 Main St", + "city": "Testville", + "province": "", + "postal_code": "" + }, + "id_type": "", + "id_number": "" + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be + And the response should contain + + Examples: + | full_name | email | phone | province | postal_code | id_type | id_number | status | expectation | + | Jane Applicant | user+mismatch@domain.com | +14165551234 | Ontario | 12345 | DRIVERS_LICENSE | DL123456789012345678901 | 400 | field errors for 'residential_address.province' or 'postal_code' | + | Jane Applicant | user+step1val@domain.com | +14165551234 | ON | A1A1A1 | DRIVERS_LICENSE | DL123456789012345678901 | 400 | field 'residential_address.postal_code' A1A 1A1 format required | + | Jane Applicant | user+step1val@domain.com | 14165551234 | ON | A1A 1A1 | DRIVERS_LICENSE | DL12345678901234567890 | 400 | field 'phone_number' E.164 violation | + | Jane Applicant | user+step1val@domain.com | +14165551234 | ON | A1A 1A1 | DRIVERS_LICENSE | DL12345678901234567890 | 201 | 'application_id' and 'session_token' present | + + @api @applications + Scenario: Application Step 3 validation for invalid product id, malformed e_signature, and marketing_opt_in default/explicit + Given I am logged in as 'user+submit@domain.com' with valid MFA and CSRF token + And I have created an application by completing Step 1 and Step 2 successfully + When I send a POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "UNKNOWN_TIER", + "e_signature": "SmFuZSBTaWdu" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 400 + And the response should contain error 'INVALID_PRODUCT_ID' + When I send a POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER", + "e_signature": "not_base64@@@" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 400 + And the response should contain error 'SIGNATURE_MALFORMED' + When I send a POST request to '/v2/applications//submit' with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER", + "e_signature": "SmFuZSBTaWduYXR1cmU=" + } + """ + And I include header 'X-App-Session' with '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'decision' + And the response should have 'marketing_opt_in' false + When I create a new application instance and submit Step 3 with JSON payload + """ + { + "card_product_id": "AEGIS_SILVER", + "e_signature": "SmFuZSBTaWduYXR1cmU=", + "marketing_opt_in": true + } + """ + And I include header 'X-App-Session' with a fresh '' + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should have 'marketing_opt_in' true + + @api @security @csrf + Scenario: CSRF enforcement and SameSite=Strict cross-site POST rejection across endpoints + Given I am logged in with secure HttpOnly, Secure, SameSite=Strict cookies and have a valid CSRF token + When a cross-site client attempts a POST to 'https://api.aegiscard.com/v2/cards//status' without 'X-CSRF-Token' + Then the request should be rejected with status 403 + And cookies should not be sent due to SameSite=Strict + When I attempt a PATCH to '/v2/cards//status' without 'X-CSRF-Token' + Then the response status should be 403 + When I attempt a POST to '/v2/applications//financials' with 'X-App-Session' but without 'X-CSRF-Token' + Then the response status should be 403 + When I attempt a POST to '/v2/accounts//payments' without 'X-CSRF-Token' + Then the response status should be 403 + When I attempt a PUT to '/v2/cards//pin' without 'X-CSRF-Token' + Then the response status should be 403 + When I PATCH '/v2/cards//status' to Frozen with valid OTP and 'X-CSRF-Token' + Then the response status should be 200 + And the response should contain 'new_status' equal to 'Frozen' + When I PUT '/v2/cards//pin' with a valid OTP and 'X-CSRF-Token' + Then the response status should be 200 + And the response should contain 'success' true + When I PATCH '/v2/cards//status' to Active with valid OTP and 'X-CSRF-Token' + Then the response status should be 200 + And the response should contain 'new_status' equal to 'Active' + + @api @transactions + Scenario Outline: Transactions exchange rate precision, zero amount, foreign fee rounding, and non-essential over-limit rejection + Given I have captured 'available_credit' from GET '/v2/accounts//summary' and card_status is Active + When I send a POST request to '/v2/accounts//transactions' with JSON payload + """ + { + "transaction_amount": , + "currency_code": "", + "exchange_rate": , + "mcc_code": "", + "merchant_name": "", + "merchant_id": "", + "transaction_type": "PURCHASE" + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be + And the response should contain + + Examples: + | amount | currency | rate | mcc | merchant | mid | status | expectation | + | 0.00 | CAD | null | 5812 | Test Zero | T0 | 422 | error 'INVALID_AMOUNT' | + | 0.01 | USD | 99.999999 | 7011 | Tiny Travel | TT01 | 200 | 'foreign_fee_amount' present and totals rounded to 2 decimals | + | 10.00 | USD | 100.000000 | 7011 | Bad Rate | BR01 | 400 | error 'INVALID_EXCHANGE_RATE' | + | 5.00 | USD | null | 7011 | Missing Rate | MR01 | 400 | field 'exchange_rate' required when currency != CAD | + | | CAD | null | 5812 | OverLimit Rstr | OL01 | 402 | error 'INSUFFICIENT_FUNDS' and 'available_credit' returned | + | 1.00 | CAD | null | 5411 | Normal OK | OK01 | 200 | 'transaction_id' and 'auth_code' present | + | 10.00 | USD | 1.2345678 | 7011 | OverPrecision | OP01 | 400 | precision validation error for 'exchange_rate' | + + @api @transactions @overlimit + Scenario: Essential MCC 5% over-limit buffer boundary approvals and rejections vs non-essential MCC + Given I have captured 'available_credit' from GET '/v2/accounts//summary' and card_status is Active + And I compute 'boundary_amount' as 'available_credit * 1.05 rounded 2dp' + And I compute 'beyond_amount' as 'available_credit * 1.051 rounded 2dp' + When I POST a CAD transaction at 'boundary_amount' with mcc_code '5912' (pharmacy) and X-CSRF-Token + Then the response status should be 200 + And the response should contain 'over_limit_flag' true + When I POST a CAD transaction at 'beyond_amount' with mcc_code '5912' (pharmacy) + Then the response status should be 402 + And the response should contain error 'INSUFFICIENT_FUNDS' + When I POST a CAD transaction at 'boundary_amount' with mcc_code '8062' (medical) + Then the response status should be 200 + And the response should contain 'over_limit_flag' true + When I POST a CAD transaction at 'beyond_amount' with mcc_code '8062' (medical) + Then the response status should be 402 + And the response should contain error 'INSUFFICIENT_FUNDS' + When I POST a CAD transaction of 'available_credit + 0.01' with mcc_code '5812' (restaurant) + Then the response status should be 402 + And the response should contain error 'INSUFFICIENT_FUNDS' + When I POST a CAD transaction of 5.00 with mcc_code '5912' + Then the response status should be 200 + And the response should not contain 'over_limit_flag' or it is false + + @api @cards @security + Scenario: Freeze card prevents transactions, unfreeze via OTP, essential buffer boundary, and 60-minute frequency limit with MFA requirement + Given I have captured 'available_credit' and card status Active via GET '/v2/accounts//summary' + When I PATCH '/v2/cards//status' with JSON payload + """ + { "status": "Frozen", "confirm_otp": "123456" } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'new_status' equal to 'Frozen' + When I POST '/v2/accounts//transactions' for CAD 10.00 mcc_code 5411 + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 403 + And the response should contain error 'CARD_INACTIVE' + When I PATCH '/v2/cards//status' with JSON payload + """ + { "status": "Active", "confirm_otp": "123456" } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'new_status' equal to 'Active' + And I compute 'essential_boundary_amount' as 'available_credit * 1.05 rounded 2dp' + When I POST an essential CAD transaction at 'essential_boundary_amount' with mcc_code 4900 (utilities) + Then the response status should be 200 + And the response should contain 'over_limit_flag' true + When I POST an essential CAD transaction at 'available_credit * 1.051 rounded 2dp' with mcc_code 4900 (utilities) + Then the response status should be 402 + And the response should contain error 'INSUFFICIENT_FUNDS' + When I execute 10 small CAD transactions within 60 minutes + Then the tenth response status should be 200 + When I submit an 11th transaction within the same 60 minutes window + Then the response status should be 429 + And the response should contain 'mfa_required' true + And the 'Retry-After' header should be present + When I complete the MFA challenge and retry the transaction with CSRF + Then the response status should be 200 + And the response should contain 'auth_code' + + @api @cards @security + Scenario: Report card stolen is irreversible, forbids PIN and status changes, denies transactions, schedules replacement, and handles duplicates + Given I confirm card status Active via GET '/v2/accounts//summary' + When I POST '/v2/cards//report-lost' with JSON payload + """ + { + "loss_type": "STOLEN", + "last_known_use": "2026-04-01T23:59:59Z", + "delivery_address": { "street": "1 New St", "city": "Toronto", "province": "ON", "postal_code": "A1A 1A1" } + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'blocked_card_id', 'new_card_eta', 'case_number' + When I PATCH '/v2/cards//status' to Active with valid OTP + Then the response status should be 400 + And the response should contain error 'INVALID_TRANSITION' + When I PUT '/v2/cards//pin' with JSON payload + """ + { "new_pin": "1234", "confirm_pin": "1234", "session_otp": "123456" } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 403 + And the response should contain error 'CARD_BLOCKED' + When I POST '/v2/accounts//transactions' for CAD 5.00 + Then the response status should be 403 + And the response should contain error 'CARD_INACTIVE' + When I POST '/v2/cards//report-lost' again with 'STOLEN' + Then the response status should be 409 + And the response should contain error 'ALREADY_BLOCKED' + + @api @transactions @listing + Scenario: List transactions with date boundaries, pagination limits, category filter, and owner-only access + Given I am logged in as 'user+history@domain.com' with valid MFA + When I GET '/v2/accounts/A-OWN/transactions' without query params + Then the response status should be 200 + And the response should contain 'transactions', 'total_count', 'page', 'total_pages' + When I GET '/v2/accounts/A-OWN/transactions?from_date=2026-03-01&to_date=2026-03-01' + Then the response status should be 200 + And only transactions from 2026-03-01 are returned + When I GET '/v2/accounts/A-OWN/transactions?from_date=2026-03-10&to_date=2026-03-05' + Then the response status should be 400 + And the response should contain error 'INVALID_DATE_RANGE' + When I GET '/v2/accounts/A-OWN/transactions?page=2&per_page=100' + Then the response status should be 200 + And up to 100 items are returned and paging metadata is correct + When I GET '/v2/accounts/A-OWN/transactions?per_page=101' + Then the response status should be 400 or clamped behavior is documented as observed + When I GET '/v2/accounts/A-OWN/transactions?category=REFUND' + Then the response status should be 200 + And all items have 'category' equal to 'REFUND' + When I GET '/v2/accounts/A-OTHER/transactions' + Then the response status should be 403 + And no data leakage occurs + And no raw PAN appears in any response payload + + @api @payments + Scenario Outline: Payments validation for minimum amount, bank account validity, date scheduling, owner-only access, and success cases + Given I am logged in as 'user+pay@domain.com' with valid MFA and CSRF token + And I have captured 'total_balance' and 'minimum_payment_due' from account summary and statement + When I send a POST request to '/v2/accounts//payments' with JSON payload + """ + { + "payment_amount": , + "payment_type": "", + "bank_account_id": "", + "scheduled_date": "" + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be + And the response should contain + + Examples: + | account | amount | ptype | bank | schedule | status | expectation | + | | 0.99 | MINIMUM | BANK-OK | null | 400 | error 'BELOW_MINIMUM' and 'minimum_payment_due' field | + | | 5.00 | CUSTOM | BANK-BAD | null | 422 | error 'INVALID_BANK_ACCOUNT' | + | | 2.00 | CUSTOM | BANK-OK | yesterday | 400 | error 'PAST_DATE' | + | | 1.00 | CUSTOM | BANK-OK | null | 200 | 'payment_id' and 'new_balance_estimate' | + | | | STATEMENT_BALANCE| BANK-OK | tomorrow | 200 | 'payment_id' and 'scheduled_date' | + | | 1.00 | CUSTOM | BANK-OK | null | 403 | error 'FORBIDDEN' | + | | | FULL_BALANCE | BANK-OK | null | 200 | 'payment_id' and 'new_balance_estimate' near zero | + + @api @accounts + Scenario: Account summary include_rewards toggle, owner-only access enforcement, not-found and unauthorized handling + Given I am logged in as 'user+summary@domain.com' with valid MFA + When I GET '/v2/accounts/ACC-OWN/summary' + Then the response status should be 200 + And 'points_balance' is absent by default + When I GET '/v2/accounts/ACC-OWN/summary?include_rewards=true' + Then the response status should be 200 + And 'points_balance' is present and numeric + And no PAN is exposed in the response + When I GET '/v2/accounts/ACC-OTHER/summary' + Then the response status should be 403 + When I GET '/v2/accounts/ACC-NONE/summary' + Then the response status should be 404 + When I retry GET '/v2/accounts/ACC-OWN/summary' without Authorization header + Then the response status should be 401 + + @api @statements @interest + Scenario: Foreign purchase, fee and rewards, statements JSON/PDF, payment CSRF negative/positive, late fee webhook idempotency, session timeout and right to rescind + Given I am logged in via POST '/v2/auth/login' with valid MFA and secure cookies + When I POST '/v2/accounts//transactions' with JSON payload + """ + { + "transaction_amount": 100.00, + "currency_code": "USD", + "exchange_rate": 1.350000, + "mcc_code": 7011, + "merchant_name": "Hotel ABC", + "transaction_type": "PURCHASE" + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'foreign_fee_amount' 4.05 and 'total_cad' 139.05 + When I GET '/v2/accounts//statements/?format=JSON' + Then the response status should be 200 + And 'content-type' should be 'application/json' + And 'total_spend' equals the sum of transactions within ±0.01 + When I GET '/v2/accounts//statements/?format=PDF' + Then the response status should be 200 + And 'content-type' should be 'application/pdf' + When I POST '/v2/accounts//payments' without 'X-CSRF-Token' + """ + { "payment_type": "MINIMUM", "payment_amount": "minimum_payment_due", "bank_account_id": "BANK-OK" } + """ + Then the response status should be 403 + When I POST '/v2/accounts//payments' with 'X-CSRF-Token' + """ + { "payment_type": "MINIMUM", "payment_amount": "minimum_payment_due", "bank_account_id": "BANK-OK" } + """ + Then the response status should be 200 + And the response should contain 'payment_id' and 'scheduled_date' + When the system advances past due_date + 2 days and I GET the next statement + Then the response should contain 'late_fee' 35.00 and 'interest_charged' per REQ-009 if previous balance not paid in full + When I POST '/v2/notifications/webhook' with JSON payload + """ + { + "account_id": "", + "alert_type": "LATE_PAYMENT", + "channel": "EMAIL", + "severity": "WARNING", + "idempotency_key": "00000000-0000-0000-0000-000000000001" + } + """ + Then the response status should be 200 + When I POST the same '/v2/notifications/webhook' with the same idempotency_key + Then the response status should be 409 + When I remain idle for 13 minutes + Then I should receive a session timeout warning at 13 minutes + When I wait until 15 minutes and POST '/v2/accounts//payments' again + Then the response status should be 401 due to session expiry + When I re-login and retry the payment with CSRF + Then the response status should be 200 + When I DELETE '/v2/accounts/' with CSRF within 14-day window + Then the response status should be 200 + When I simulate day 15 and DELETE '/v2/accounts/' again + Then the response status should be 403 + + @api @statements @interest + Scenario: ADB interest calculation and grace period across consecutive cycles + Given I am logged in and the account 'ACC-ADB' has APR 19.99% and prior statement paid in full + When I POST a CAD purchase of 300.00 on Cycle A Day 1 and a CAD purchase of 200.00 on Day 10 with CSRF + And I advance to end of Cycle A and GET '/v2/accounts/ACC-ADB/statements/?format=JSON' + Then the response should have 'interest_charged' 0.00 and 'total_spend' sum within ±0.01 + When I POST a payment of 200.00 during Cycle B with CSRF + And I simulate daily balances: Days 1–14 previous_cycle_unpaid; Day 15 add 100.00 purchase; Day 20 subtract 50.00 payment + And at Cycle B end I GET '/v2/accounts/ACC-ADB/statements/?format=JSON' + Then I compute ADB = sum of daily balances / 30 and expected_interest = (ADB × 0.1999 / 365) × 30 + And 'interest_charged' equals expected_interest within 0.01 tolerance + And 'late_fee' equals 0.00 + + @api @rewards + Scenario: Rewards accrual 1x with floor for non-travel and 3x for travel MCC in CAD + Given I am logged in as 'user+rewards@domain.com' with valid MFA and CSRF token + And I GET '/v2/accounts//summary?include_rewards=true' and record baseline 'points_balance' P0 + When I POST three CAD purchases: 0.99 (5411), 1.01 (5411), 2.99 (5812) with CSRF + And I GET '/v2/accounts//summary?include_rewards=true' + Then 'points_balance' equals P0 + floor(0.99*1) + floor(1.01*1) + floor(2.99*1) = P0 + 0 + 1 + 2 + When I POST a CAD travel purchase 10.00 (7011) + And I GET '/v2/accounts//summary?include_rewards=true' + Then 'points_balance' increased by an additional floor(10.00*3) = 30 + When I POST a micro CAD grocery purchase 0.49 (5411) + And I GET '/v2/accounts//summary?include_rewards=true' + Then 'points_balance' remains unchanged due to floor(0.49*1)=0 + + @api @pin @security + Scenario: Set PIN with OTP success, mismatch/format errors, OTP invalid, and allowed while Frozen + Given I have a valid CSRF token and OTP for PIN action + When I PUT '/v2/cards//pin' with JSON payload + """ + { "new_pin": "2580", "confirm_pin": "2580", "session_otp": "123456" } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'success' true + When I PUT '/v2/cards//pin' with mismatched confirm_pin using fresh valid OTP + """ + { "new_pin": "2580", "confirm_pin": "2581", "session_otp": "123456" } + """ + Then the response status should be 400 + And the response should contain error 'PIN_MISMATCH' + When I PUT '/v2/cards//pin' with invalid format '12a4' + Then the response status should be 400 + And the response should contain error 'PIN_FORMAT' + When I PUT '/v2/cards//pin' with expired/invalid OTP and valid matching 4-digit PIN + Then the response status should be 401 + And the response should contain error 'OTP_FAILED' + When I PATCH '/v2/cards//status' to Frozen with valid OTP and CSRF + Then the response status should be 200 + When I PUT '/v2/cards//pin' while status Frozen with valid OTP + Then the response status should be 200 + And the response should contain 'success' true + + @api @otp @cards + Scenario: Freeze/Unfreeze OTP attempt limits, resend OTP resets attempts, and successful transitions + Given I am logged in as 'user+otp@domain.com' with valid MFA and CSRF token and card status Active + When I PATCH '/v2/cards/CARD-OTP/status' to Frozen with invalid OTP '111111' + Then the response status should be 401 + And the response should contain error 'OTP_FAILED' and 'attempts_remaining' 2 + When I PATCH '/v2/cards/CARD-OTP/status' to Frozen with invalid OTP '222222' + Then the response status should be 401 + And the response should contain 'attempts_remaining' 1 + When I PATCH '/v2/cards/CARD-OTP/status' to Frozen with invalid OTP '333333' + Then the response status should be 401 + And the response should contain 'attempts_remaining' 0 + When I PATCH '/v2/cards/CARD-OTP/status' to Frozen again without new OTP + Then the response status should be 401 + And the response should contain 'attempts_remaining' 0 + When I POST '/v2/auth/otp/request' with JSON payload + """ + { "purpose": "card_status", "channel": "SMS" } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'otp_sent' true + When I PATCH '/v2/cards/CARD-OTP/status' to Frozen with the new valid OTP + Then the response status should be 200 + And the response should contain 'new_status' 'Frozen' + When I request a new OTP and PATCH '/v2/cards/CARD-OTP/status' to Active + Then the response status should be 200 + And the response should contain 'new_status' 'Active' + + @api @idor @cards + Scenario: Owner-only card controls and report-lost: IDOR prevention and invalid status value handling + Given two users exist and I am logged in as 'user+idor1@domain.com' with CSRF token + And I have card_id 'C-OWN' and another user's card_id 'C-OTHER' + When I PATCH '/v2/cards/C-OTHER/status' to Frozen with OTP and CSRF + Then the response status should be 403 + When I POST '/v2/cards/C-OTHER/report-lost' with 'LOST' and CSRF + Then the response status should be 403 + When I PATCH '/v2/cards/C-OWN/status' with status 'Blocked' (invalid) and valid OTP + Then the response status should be 400 + And the response should contain error 'INVALID_TRANSITION' with allowed 'Active'/'Frozen' + When I PATCH '/v2/cards/C-OWN/status' to Frozen with invalid OTP + Then the response status should be 401 + And the response should contain 'attempts_remaining' + When I request a new OTP and PATCH '/v2/cards/C-OWN/status' to Frozen then to Active with valid OTPs + Then both responses should be 200 with 'new_status' updated accordingly + + @api @websocket @transactions + Scenario: WebSocket live transaction feed authentication, event delivery, unauthorized connection, and reconnect + Given I am logged in as 'user+realtime@domain.com' with valid access token and CSRF token + When I open a WebSocket to 'wss://realtime.aegiscard.com/v2/stream' with 'Authorization: Bearer ' + Then the WebSocket handshake status should be 101 and I send subscription 'SUBSCRIBE ' and receive an ack + When I POST '/v2/accounts//transactions' with CAD 20.00 (mcc_code 5411) and CSRF + Then I should receive a WS event with 'event_type' 'TRANSACTION_POSTED' and fields 'transaction_id','amount','mcc_code','currency' + When I POST '/v2/accounts//transactions' with USD 10.00 (mcc_code 7011) exchange_rate 1.250000 and CSRF + Then I should receive a WS event including 'foreign_fee_amount' and 'total_cad' per REQ-006 + When I POST a USD transaction omitting 'exchange_rate' + Then the response status should be 400 and no WS event should be emitted + When I close the WebSocket connection + Then no further events are received + When I attempt to open WebSocket without Authorization header + Then the connection is refused or closed with 401 equivalent + When I reconnect WebSocket with valid Authorization and resubscribe + And I POST a small CAD transaction + Then I should receive a WS event for that transaction + + @api @registration @validation + Scenario Outline: Registration field validations and duplicate email handling with secure cookie attributes + Given I have obtained a valid CSRF token from the portal session + When I send a POST request to '/v2/auth/register' with JSON payload + """ + { + "first_name": "", + "last_name": "Tester", + "email": "", + "password": "", + "date_of_birth": "", + "phone_number": "", + "ssn_last4": "", + "agree_terms": + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be + And the response should contain + + Examples: + | first_name | email | password | dob | phone | ssn4 | terms | status | expectation | + | Jo | user..dots@domain.com | ValidPass123! | 1990-01-01 | +14165551234 | 1234 | true | 400 | field 'email' invalid per RFC 5322 | + | Jo | user+edge@sub.domain.co.uk | ValidPass123! | 1990-01-01 | 14165551234 | 1234 | true | 400 | field 'phone_number' E.164 violation | + | Jo | user+edge@domain.co.uk | ValidPass123! | 1990-01-01 | +14165551234 | 123 | true | 400 | field 'ssn_last4' exactly 4 digits required | + | Jo | user+edge@domain.co.uk | ValidPass123! | 1990-01-01 | +14165551234 | 12a4 | true | 400 | field 'ssn_last4' numeric-only | + | Jo | user+edge@domain.co.uk | ValidPass123! | 1990-01-01 | +14165551234 | 1234 | false | 400 | field 'agree_terms' must be true | + | Jo | user+edge@domain.co.uk | ValidPass123! | 1990-01-01 | +14165551234 | 1234 | true | 201 | 'user_id' and 'verification_token' present; Set-Cookie HttpOnly Secure Strict | + | Anne-Marie | user+edge@domain.co.uk | ValidPass123! | 1990-01-01 | +14165551234 | 1234 | true | 400 | field 'first_name' alpha-only rejects hyphens (if enforced) | + + @api @registration @passwords + Scenario Outline: Registration password complexity and leap-year age validation + Given I have a valid CSRF token and the sandbox date can be controlled + When I send a POST request to '/v2/auth/register' with JSON payload + """ + { + "first_name": "John", + "last_name": "Doe", + "email": "user+leap@domain.com", + "password": "", + "date_of_birth": "", + "phone_number": "+14165551234", + "ssn_last4": "1234", + "agree_terms": true + } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be + And the response should contain + + Examples: + | password | dob | status | expectation | + | Short11! | 2008-03-01 | 422 | error 'WEAK_PASSWORD' | + | alllowercase11! | 1990-01-01 | 422 | error 'WEAK_PASSWORD' missing uppercase | + | NOLOWERCASE11! | 1990-01-01 | 422 | error 'WEAK_PASSWORD' missing lowercase | + | NoDigits!!!! | 1990-01-01 | 422 | error 'WEAK_PASSWORD' missing digit | + | NoSymbol1234 | 1990-01-01 | 422 | error 'WEAK_PASSWORD' missing symbol | + | OkPassw0rd!! | <17y364d> | 400 | field 'date_of_birth' age < 18 | + | OkPassw0rd!! | 2008-02-29 | 400 | age < 18 on non-leap-year Feb 28 | + | OkPassw0rd!! | 2008-02-29 | 201 | 'user_id' and 'verification_token' present after Mar 1 | + + @api @verify @auth + Scenario: Email verification invalid/tampered, expired, resend rate-limit, and successful activation + Given I have registered 'user+verify@domain.com' and captured 'verification_token' + When I POST '/v2/auth/login' with correct credentials before verification + Then the response status should be 403 + And the response should contain error 'EMAIL_NOT_VERIFIED' + When I GET '/v2/auth/verify?token=' + Then the response status should be 400 + And the response should contain error 'INVALID_TOKEN' + When I GET '/v2/auth/verify?token=' + Then the response status should be 400 + And the response should contain error 'TOKEN_EXPIRED' + When I POST '/v2/auth/verify/resend' with JSON payload + """ + { "email": "user+verify@domain.com" } + """ + And I include header 'X-CSRF-Token' with a valid CSRF token + Then the response status should be 200 + And the response should contain 'resend_ack' true + When I POST '/v2/auth/verify/resend' again immediately + Then the response status should be 429 + And the response should contain 'retry_after' + When I GET '/v2/auth/verify?token=' + Then the response status should be 200 + And the response should indicate 'verified' true + When I POST '/v2/auth/login' with correct credentials and valid MFA + Then the response status should be 200 + And the response should contain 'access_token', 'refresh_token', 'expires_in' + And Set-Cookie headers must include HttpOnly, Secure, SameSite=Strict + + @api @audit @admin + Scenario: Credit limit change audit trail immutability and visibility, and role-based access + Given I am logged in as Cardholder and record 'credit_limit' L1 from GET '/v2/accounts//summary' + When an Admin posts to '/v2/admin/accounts//credit-limit' with JSON payload + """ + { "new_limit": } + """ + And the request includes 'X-CSRF-Token' + Then the response status should be 200 + When the Cardholder GETs '/v2/accounts//summary' + Then 'credit_limit' equals and 'available_credit' adjusted accordingly + When I GET '/v2/admin/audit?account_id=' as Admin + Then the response status should be 200 + And an audit record exists with 'user_id'(admin), 'session_id', 'ip_address', 'field' 'credit_limit', 'old_value' L1, 'new_value' L2 + When I attempt to DELETE '/v2/admin/audit/' as Admin + Then the response status should be 405 or 403 indicating immutability + When Admin posts a second change to and I GET audit records + Then a second immutable record exists with 'old_value' L2 and 'new_value' L3 + When the Cardholder attempts to POST '/v2/admin/accounts//credit-limit' + Then the response status should be 403 + + @api @webhook @notifications + Scenario Outline: Notifications webhook validation, multi-channel delivery, and idempotency per-account scope + Given the Notification Engine is authorized for '/v2/notifications/webhook' + When I send a POST request to '/v2/notifications/webhook' with JSON payload + """ + { + "account_id": "", + "alert_type": "", + "channel": "", + "severity": "", + "message_body": "", + "idempotency_key": "" + } + """ + Then the response status should be + And the response should contain + + Examples: + | account_id | alert_type | channel | severity | message_body | key | status | expectation | + | A-ONE | PIN_LOCKED | SMS | CRITICAL | Your PIN has been locked. | K-0001 | 200 | 'notification_id' and 'delivered_at' | + | A-ONE | PIN_LOCKED | SMS | CRITICAL | Your PIN has been locked. | K-0001 | 409 | error 'DUPLICATE_NOTIFICATION' | + | A-TWO | PIN_LOCKED | SMS | CRITICAL | Your PIN has been locked. | K-0001 | 200 | 'notification_id' and 'delivered_at' | + | A-ONE | FRAUD_FLAG | EMAIL | WARNING | Transaction flagged. | K-0002 | 200 | 'channel' equals 'EMAIL' | + | A-ONE | OVER_LIMIT | IN_APP | INFO | You are over limit. | K-0003 | 200 | 'channel' equals 'IN_APP' | + | A-ONE | UNKNOWN_TYPE | SMS | INFO | Invalid type | K-0004 | 400 | error 'INVALID_ALERT_TYPE' | + | A-ONE | STATEMENT_READY| FAX | INFO | Unsupported channel | K-0005 | 400 | channel validation error | + | A-ONE | PIN_LOCKED | SMS | INFO | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | K-0006 | 400 | field 'message_body' length validation | + + @api @right_to_rescind + Scenario: Right to rescind deletion leads to 404 on subsequent endpoints and idempotent DELETE + Given I am logged in as 'user+rescind@domain.com' with valid MFA and CSRF token and account 'ACC-RESC' is within 14-day window + When I DELETE '/v2/accounts/ACC-RESC' with 'X-CSRF-Token' + Then the response status should be 200 + When I GET '/v2/accounts/ACC-RESC/summary' + Then the response status should be 404 + When I GET '/v2/accounts/ACC-RESC/transactions' + Then the response status should be 404 + When I POST '/v2/accounts/ACC-RESC/transactions' with any valid payload + Then the response status should be 404 + When I POST '/v2/accounts/ACC-RESC/payments' with valid payload + Then the response status should be 404 + When I GET '/v2/accounts/ACC-RESC/statements/' + Then the response status should be 404 + When I DELETE '/v2/accounts/ACC-RESC' again + Then the response status should be 404 + + # UI Tests + + @ui @dashboard @masking + Scenario: Dashboard displays masked PAN and no PII leakage + Given I am on the Dashboard page as a logged-in cardholder + When the account summary loads + Then I should see the card number masked as '**** **** **** 1234' + And I should not see any raw PAN in the DOM + And I should not find any tokens in localStorage or sessionStorage + + @ui @application @draft + Scenario: Application Step 1 draft auto-save every 60 seconds, restore, sanitize, and cleanup after submission + Given I am on the Credit Application Step 1 page and authenticated with secure cookies + When I fill the form with non-sensitive fields (name, phone, address, id_type and masked id_number) and wait 60 seconds + Then localStorage should contain a key 'aegis_app_step1_draft' with recent timestamp + And the stored JSON should exclude or mask sensitive fields (id_number, ssn_last4) and contain no tokens + When I refresh the page + Then the form should auto-populate from the draft and prompt re-entry for any masked values + When I update the city and phone and wait another 60 seconds + Then the draft should reflect the updated values with an advanced timestamp + When I close and reopen the tab to Step 1 + Then the draft should restore across navigation and the auto-save cadence remains ~60s (±5s) + When I intentionally corrupt the draft JSON in localStorage and refresh + Then the UI should handle the error gracefully, clear the bad draft, and show an empty form with a non-blocking notice + When I submit Step 1 successfully from the UI + Then the draft key should be removed from localStorage and no further auto-save writes should occur for Step 1 + + @ui @session + Scenario: Session timeout warning at 13 minutes and expiry at 15 minutes + Given I am logged in on the Dashboard page and idle + When I remain idle for 13 minutes + Then I should see a session timeout warning modal + When I remain idle until 15 minutes + Then my session should expire and protected actions should prompt re-authentication diff --git a/functional_tests/functional-test-generation/functional-test-generation.json b/functional_tests/functional-test-generation/functional-test-generation.json new file mode 100644 index 0000000..dcd8e76 --- /dev/null +++ b/functional_tests/functional-test-generation/functional-test-generation.json @@ -0,0 +1,1088 @@ +[ + { + "type": "end-to-end", + "title": "Registration to Approved Application with JWT, PKCE, CSRF, and Masked PAN", + "description": "Validate full journey from user registration through email verification, secure login with MFA, multi-step credit application (Steps 1-3) using session_token and CSRF headers, FICO-based approval, and dashboard masked PAN display.", + "testId": "TC-AEGIS-001", + "testDescription": "Covers registration validation including age boundary, secure session establishment, application sequencing and token headers, approval path for FICO > 680, and PCI masking on dashboard.", + "prerequisites": "Portal reachable over TLS 1.3, WAF enabled, rate limits active, test email inbox accessible, MFA enabled for the test user after registration.", + "stepsToPerform": "1. Navigate to Registration page on portal.aegiscard.com and load over TLS 1.3.\n2. Submit POST /v2/auth/register with first_name=Jo, last_name=Tester, email=user+test@domain.com, password weakPass1!, date_of_birth making user 17 years old, phone +14165551234, ssn_last4 1234, agree_terms=true and X-CSRF-Token; expect validation error for age and password.\n3. Resubmit with password Stronger!Passw0rd and date_of_birth = 18 years minus 1 day boundary check; expect 201 with user_id and verification_token and HttpOnly, Secure cookies set SameSite=Strict.\n4. Click verification link (ASSUMPTION-GET /v2/auth/verify?token=) to activate account; expect success message and verified flag in UI.\n5. Login via POST /v2/auth/login with email and Stronger!Passw0rd, device_id=UUIDv4, remember_me=true and MFA TOTP code; expect 200 with access_token, refresh_token, expires_in and cookies HttpOnly, Secure.\n6. Acquire portal CSRF token from meta or preflight endpoint and include X-CSRF-Token for state-changing calls.\n7. Start application Step 1 via POST /v2/applications/start with full_legal_name, email matching login, phone E.164, address (street, city, province=ON, postal_code=A1A 1A1), id_type=DRIVERS_LICENSE, id_number=DL123456; expect 201 with application_id and session_token; verify SPA auto-saves draft every 60s and restores after page refresh.\n8. Submit Step 2 via POST /v2/applications/{application_id}/financials with header X-App-Session: session_token and X-CSRF-Token; include employment_status=EMPLOYED, employer_name=ACME Corp, gross_annual_income=85000.00, other_income omitted, monthly_rent=1200.00, existing_debt_payments=300.00, sin_consent=true; expect 200 with status=PENDING_REVIEW and fico_pull_id.\n9. Submit Step 3 via POST /v2/applications/{application_id}/submit with card_product_id=AEGIS_GOLD, e_signature=base64 of typed name, marketing_opt_in omitted; expect 200 decision=APPROVED with credit_limit and card_number_masked displayed in UI.\n10. Navigate to dashboard and call GET /v2/accounts/{account_id}/summary?include_rewards=true; validate masked PAN **** **** **** 1234, no raw PAN in DOM or network, and rewards points balance present.\n11. Validate session continuity across navigation and cookies remain HttpOnly, Secure, SameSite=Strict; ensure no tokens in localStorage.", + "expectedResult": "User successfully registers, verifies, logs in with MFA, completes all application steps in order with proper headers and CSRF, receives APPROVED decision for FICO > 680, and sees masked PAN on dashboard with secure cookies and no PII leakage.", + "endpoint": "POST /v2/auth/register, GET /v2/auth/verify, POST /v2/auth/login, POST /v2/applications/start, POST /v2/applications/{application_id}/financials, POST /v2/applications/{application_id}/submit, GET /v2/accounts/{account_id}/summary", + "method": "POST, GET", + "apiPath": "/v2/auth/register, /v2/auth/verify, /v2/auth/login, /v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit, /v2/accounts/{account_id}/summary", + "uiScreen": "Registration, Email Verification, Login, Application Step 1, Application Step 2, Application Step 3, Dashboard", + "role": "Applicant, Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, X-App-Session, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "201,200,400,422", + "responseKeysExpected": "user_id, verification_token, access_token, refresh_token, application_id, session_token, fico_pull_id, decision, credit_limit, card_number_masked", + "businessRuleIds": "NFR-05, NFR-06, REQ-014", + "calculationsValidated": "None", + "ficoScoreUsed": "720", + "decisionExpected": "APPROVED", + "cardStatusBefore": "N/A", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "Unauthenticated->Registered->Verified->LoggedIn->Application Session Issued->Approved", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms, portal TTI <= 3s on 4G", + "maskingCheck": "Mask PAN **** **** **** 1234, no raw PAN in DOM or network logs", + "piiFields": "email user+test@domain.com, phone +14165551234, DOB yyyy-mm-dd, ssn_last4 1234, address components", + "securityControls": "TLS1.3, HttpOnly Secure cookies, SameSite=Strict, CSRF header, PKCE, WAF, no tokens in localStorage", + "rateLimitExpectation": "Login limited to 10 req/min per IP", + "assumptions": "ASSUMPTION-Email verification via GET /v2/auth/verify?token=..., ASSUMPTION-Portal exposes CSRF token via meta or endpoint, ASSUMPTION-Postal code validator strict A1A 1A1 with space" + }, + { + "type": "functional", + "title": "Application Referral (PENDING) and Decline (DECLINED) with Duplicate Application Control", + "description": "Validate FICO-based decision paths for PENDING (600–680) and DECLINED (<600) including signature requirement and duplicate application prevention.", + "testId": "TC-AEGIS-002", + "testDescription": "Two separate application flows: first yields PENDING with review_eta_hours, second yields DECLINED with reason_code; verifies 409 on duplicate start and 400 on missing signature.", + "prerequisites": "Two verified user accounts exist: pending_user@domain.com and declined_user@domain.com; MFA enabled; CSRF token available in session.", + "stepsToPerform": "1. Login as pending_user@domain.com with MFA via POST /v2/auth/login; expect 200 with tokens.\n2. Start Step 1 via POST /v2/applications/start with valid personal details and X-CSRF-Token; expect 201 with application_id and session_token.\n3. Submit Step 2 via POST /v2/applications/{application_id}/financials with employment_status=EMPLOYED, employer_name=ACME, gross_annual_income=60000.00, monthly_rent=1500.00, existing_debt_payments=400.00, sin_consent=true and header X-App-Session; expect 200 with fico_pull_id and status PENDING_REVIEW (FICO emulated 650).\n4. Submit Step 3 via POST /v2/applications/{application_id}/submit with card_product_id=AEGIS_SILVER and valid base64 e_signature; expect 200 decision=PENDING with review_eta_hours present.\n5. Attempt to start another application for the same user via POST /v2/applications/start; expect 409 DUPLICATE_APPLICATION.\n6. Logout and login as declined_user@domain.com with MFA; expect 200.\n7. Start Step 1 for declined user; expect 201 with application_id and session_token.\n8. Submit Step 2 with employment_status=UNEMPLOYED, employer_name omitted, gross_annual_income=12000.00, monthly_rent=800.00, existing_debt_payments=500.00, sin_consent=true; expect 200 and fico_pull_id (FICO emulated 580).\n9. Submit Step 3 missing e_signature to validate error; expect 400 SIGNATURE_REQUIRED.\n10. Resubmit Step 3 with e_signature and marketing_opt_in omitted (defaults false); expect 200 decision=DECLINED with reason_code shown in UI and marketing_opt_in remains false.", + "expectedResult": "For FICO 650 the application is PENDING with review ETA and duplicate application attempts return 409; for FICO 580 the application is DECLINED and missing signature is rejected with 400.", + "endpoint": "POST /v2/auth/login, POST /v2/applications/start, POST /v2/applications/{application_id}/financials, POST /v2/applications/{application_id}/submit", + "method": "POST", + "apiPath": "/v2/auth/login, /v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit", + "uiScreen": "Login, Application Step 1, Application Step 2, Application Step 3", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, X-App-Session", + "httpCodesExpected": "200,201,400,409", + "responseKeysExpected": "application_id, session_token, fico_pull_id, decision, review_eta_hours, reason_code", + "businessRuleIds": "REQ-002-FICO-Decision, NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "650,580", + "decisionExpected": "PENDING, DECLINED", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "N/A", + "stateTransition": "New App->Step1->Step2->Pending or Declined", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN present in application steps; masked PII in UI where applicable", + "piiFields": "email, phone, DOB, address", + "securityControls": "TLS1.3, CSRF header required for POSTs, HttpOnly cookies", + "rateLimitExpectation": "Login limited to 10 req/min per IP", + "assumptions": "ASSUMPTION-FICO score stubbed by bureau soft pull; ASSUMPTION-Decision table strictly applies: >680 APPROVED, 600–680 PENDING, <600 DECLINED" + }, + { + "type": "functional", + "title": "Card Freeze/Unfreeze, Essential-Service Over-Limit Buffer, and Transaction Frequency Rate Limit", + "description": "Validate card status transitions with OTP, blocked transaction on Frozen, essential-service 5% over-limit buffer boundary, and 60-minute transaction frequency cap with MFA requirement.", + "testId": "TC-AEGIS-003", + "testDescription": "User freezes card, sees 403 on transaction; unfreezes with OTP; processes essential-service MCC within 5% buffer (approved with flag) and beyond 5% (insufficient funds); triggers 429 after >10 tx in 60 minutes with mfa_required true and proceeds after MFA.", + "prerequisites": "Active card with known available_credit; user logged in with valid JWT; CSRF token available; OTP delivery channel verified.", + "stepsToPerform": "1. Call GET /v2/accounts/{account_id}/summary to capture available_credit and card status Active.\n2. Freeze card via PATCH /v2/cards/{card_id}/status with status=Frozen, confirm_otp=valid 6-digit, and X-CSRF-Token; expect 200 new_status=Frozen.\n3. Attempt POST /v2/accounts/{account_id}/transactions for CAD 10.00, mcc_code=5411 (grocery), transaction_type=PURCHASE; expect 403 error=CARD_INACTIVE with card_status Frozen.\n4. Unfreeze via PATCH /v2/cards/{card_id}/status with status=Active and valid OTP; expect 200 new_status=Active.\n5. Attempt essential-service purchase at 5% over-limit boundary: compute amount = available_credit * 1.05 rounded to 2 decimals, mcc_code=4900 (utilities), transaction_type=PURCHASE; expect 200 with over_limit_flag=true.\n6. Attempt essential-service purchase just beyond 5% (available_credit * 1.051) with mcc_code=4900; expect 402 error=INSUFFICIENT_FUNDS.\n7. Execute 10 small approved CAD transactions within 60 minutes spaced to be within window; expect 200 on tenth.\n8. Submit the 11th transaction within the same 60 minutes; expect 429 error=FREQ_EXCEEDED with mfa_required=true and Retry-After header.\n9. Complete MFA challenge (ASSUMPTION-POST /v2/auth/mfa/verify) and retry the transaction with X-CSRF-Token; expect 200 approved and auth_code present.", + "expectedResult": "Freeze prevents transactions until unfreeze with OTP; essential-service MCC approves up to 5% over-limit with flag and rejects beyond; 11th transaction in 60 minutes is rate-limited with MFA requirement, and completion succeeds after MFA.", + "endpoint": "GET /v2/accounts/{account_id}/summary, PATCH /v2/cards/{card_id}/status, POST /v2/accounts/{account_id}/transactions", + "method": "GET, PATCH, POST", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/cards/{card_id}/status, /v2/accounts/{account_id}/transactions", + "uiScreen": "Dashboard, Card Controls, Transaction Form", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure", + "httpCodesExpected": "200,403,402,429", + "responseKeysExpected": "new_status, transaction_id, available_credit, auth_code, over_limit_flag, Retry-After", + "businessRuleIds": "REQ-007, REQ-006-OverLimitBuffer-ASSUMED, NFR-05", + "calculationsValidated": "5% over-limit threshold boundary", + "mccCode": "4900-utilities, 5411-grocery", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Frozen then back to Active", + "accountStatus": "Active", + "stateTransition": "Card Active->Frozen->Active", + "reversalAction": "Unfreeze as reversal of Freeze", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN exposure in card controls or transaction forms", + "piiFields": "phone for OTP delivery", + "securityControls": "OTP gating, CSRF header on PATCH/POST, HttpOnly cookies, WAF and rate limits", + "rateLimitExpectation": "Transactions >10 in 60 minutes return 429 with mfa_required true", + "assumptions": "ASSUMPTION-Essential MCCs include 4900 utilities; ASSUMPTION-MFA verify endpoint exists for rate-limit bypass after challenge" + }, + { + "type": "functional", + "title": "Report Card Stolen Irreversibility, Invalid Transition, PIN Restriction, and Replacement Flow", + "description": "Ensure reporting a card as STOLEN immediately blocks it irreversibly, forbids PIN changes and status transitions, denies transactions, and schedules replacement; verify duplicate report handling.", + "testId": "TC-AEGIS-004", + "testDescription": "Block card via report-lost with STOLEN, attempt invalid transition back to Active, attempt to set PIN and transact (both fail), confirm replacement scheduling and duplicate report 409.", + "prerequisites": "Active card with recent activity; user logged in with valid JWT; CSRF token available; replacement address available.", + "stepsToPerform": "1. Confirm current card status Active via GET /v2/accounts/{account_id}/summary.\n2. Report card as STOLEN via POST /v2/cards/{card_id}/report-lost with loss_type=STOLEN, last_known_use timestamp, and delivery_address override; include X-CSRF-Token; expect 200 with blocked_card_id, new_card_eta, case_number.\n3. Attempt to change status via PATCH /v2/cards/{card_id}/status with status=Active and valid OTP; expect 400 error=INVALID_TRANSITION and allowed_transitions show none from Blocked.\n4. Attempt to set PIN via PUT /v2/cards/{card_id}/pin with new_pin=1234, confirm_pin=1234, session_otp valid; expect 403 error=CARD_BLOCKED.\n5. Attempt transaction via POST /v2/accounts/{account_id}/transactions for CAD 5.00; expect 403 error=CARD_INACTIVE with card_status Blocked.\n6. Check dashboard for replacement card ETA and messaging and ensure old card remains Blocked.\n7. Attempt to report lost again via POST /v2/cards/{card_id}/report-lost; expect 409 error=ALREADY_BLOCKED.\n8. Verify audit trail presence for card status change (ASSUMPTION-Audit UI or logs show immutable record with user_id, session_id, ip_address).", + "expectedResult": "Card transitions to Blocked upon STOLEN report with replacement scheduled; further status changes, PIN updates, and transactions are forbidden; duplicate report returns 409; audit trail is present.", + "endpoint": "GET /v2/accounts/{account_id}/summary, POST /v2/cards/{card_id}/report-lost, PATCH /v2/cards/{card_id}/status, PUT /v2/cards/{card_id}/pin, POST /v2/accounts/{account_id}/transactions", + "method": "GET, POST, PATCH, PUT", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/cards/{card_id}/report-lost, /v2/cards/{card_id}/status, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/transactions", + "uiScreen": "Dashboard, Card Controls, Security Settings", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure", + "httpCodesExpected": "200,400,403,409", + "responseKeysExpected": "blocked_card_id, new_card_eta, case_number, new_status, error, card_status", + "businessRuleIds": "REQ-007, NFR-04, NFR-05", + "calculationsValidated": "None", + "mccCode": "N/A", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Blocked", + "accountStatus": "Active", + "stateTransition": "Active->Blocked (terminal)", + "reversalAction": "None (irreversible)", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "Masked PAN only on UI even when blocked", + "piiFields": "delivery_address override, contact info", + "securityControls": "CSRF header on POST/PATCH/PUT, OTP gating where applicable, TLS1.3", + "rateLimitExpectation": "Standard WAF protections apply", + "assumptions": "ASSUMPTION-Invalid transition from Blocked returns 400 with INVALID_TRANSITION; ASSUMPTION-Audit verification via UI/logs available" + }, + { + "type": "end-to-end", + "title": "Foreign Purchase, Rewards, Statements, Payments, CSRF Negative, Late Fee Webhook, Session Timeout, and Right to Rescind", + "description": "Validate foreign fee and CAD conversion, rewards accrual with floor rounding, statement accuracy, payment processing with CSRF enforcement, grace vs late fee logic, notification webhook idempotency, session timeout warning and expiry, and right-to-rescind window enforcement.", + "testId": "TC-AEGIS-005", + "testDescription": "Performs a USD purchase with exchange rate and verifies 3% fee and rewards; retrieves statements in JSON/PDF; attempts payment without CSRF (reject) and then succeeds; validates no grace and late fee with webhook; observes session warning at 13 minutes and expiry at 15; validates rescind success within 14 days and rejection after.", + "prerequisites": "Active account within 10 days of issuance; Travel MCC merchant available; linked active bank_account_id; CSRF token retrievable; notification webhook endpoint operational.", + "stepsToPerform": "1. Login via POST /v2/auth/login and navigate to Dashboard; confirm HttpOnly, Secure, SameSite=Strict cookies are set.\n2. Initiate foreign transaction via POST /v2/accounts/{account_id}/transactions with transaction_amount=100.00, currency_code=USD, exchange_rate=1.350000, mcc_code=7011 (travel), merchant_name=Hotel ABC, transaction_type=PURCHASE, with X-CSRF-Token; expect 200 and itemized foreign_fee_amount; validate Total_CAD=(100*1.35)*1.03=139.05 and fee=4.05.\n3. Validate rewards accrual per REQ-012 with floor rounding per REQ-013: Travel points floor(139.05*3)=417; confirm points increment on summary.\n4. Retrieve current statement via GET /v2/accounts/{account_id}/statements/{statement_id}?format=JSON; validate total_spend equals sum of transactions within ±$0.01 (REQ-015), and content-type application/json.\n5. Retrieve the same statement with format=PDF; expect 200 and application/pdf content-type.\n6. Attempt to make payment via POST /v2/accounts/{account_id}/payments without X-CSRF-Token for payment_type=MINIMUM, payment_amount=minimum_payment_due, bank_account_id linked; expect 403 rejection (CSRF enforced).\n7. Resubmit payment with valid X-CSRF-Token; expect 200 payment_accepted with payment_id and scheduled_date, new_balance_estimate.\n8. Simulate missed payment past due_date + 2 days (test clock or sandbox control); fetch next statement; validate late_fee = $35 (REQ-011) and interest applied via REQ-009 because previous balance not paid in full (REQ-010, no grace).\n9. Simulate Notification Engine calls POST /v2/notifications/webhook with alert_type=LATE_PAYMENT, channel=EMAIL, severity=WARNING, idempotency_key=UUIDv4; expect 200 notification queued; repeat with same idempotency_key; expect 409 duplicate.\n10. Observe session lifecycle: stay idle until 13 minutes and verify warning modal; wait until 15 minutes and attempt POST /v2/accounts/{account_id}/payments again; expect 401 due to session expiry; re-login and retry payment with CSRF; expect 200 success.\n11. Invoke right to rescind within 14-day window via DELETE /v2/accounts/{account_id} with X-CSRF-Token; expect success without fee (REQ-016); then simulate day 15 and attempt DELETE again; expect rejection after window (ASSUMPTION-403).", + "expectedResult": "Foreign transaction calculates correct CAD total and 3% fee, Travel rewards accrue with floor rounding, statements are accurate and retrievable in JSON/PDF, payments enforce CSRF and process correctly, late fee applied after grace rules and webhook delivered with idempotency, session warns at 13 minutes and expires at 15 requiring re-login, and rescind allowed within 14 days but rejected after.", + "endpoint": "POST /v2/auth/login, POST /v2/accounts/{account_id}/transactions, GET /v2/accounts/{account_id}/statements/{statement_id}, POST /v2/accounts/{account_id}/payments, POST /v2/notifications/webhook, DELETE /v2/accounts/{account_id}", + "method": "POST, GET, DELETE", + "apiPath": "/v2/auth/login, /v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/statements/{statement_id}, /v2/accounts/{account_id}/payments, /v2/notifications/webhook, /v2/accounts/{account_id}", + "uiScreen": "Dashboard, Transactions, Statements, Payments", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "both", + "headersUsed": "Authorization: Bearer, X-CSRF-Token (present and missing cases), Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,403,401,409,404", + "responseKeysExpected": "transaction_id, foreign_fee_amount, total_spend, adb, interest_charged, late_fee, minimum_payment_due, payment_id, notification_id", + "businessRuleIds": "REQ-006, REQ-009, REQ-010, REQ-011, REQ-012, REQ-013, REQ-015, REQ-016, NFR-05, NFR-06", + "calculationsValidated": "Foreign fee 3%, Total_CAD conversion, Rewards floor rounding, ADB interest, late fee $35, statement tolerance ±$0.01", + "mccCode": "7011-travel", + "currencyCode": "USD", + "exchangeRateUsed": "1.350000", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active then Closed after rescind", + "stateTransition": "LoggedIn->Active Session->Warning at 13m->Expired at 15m->Re-login, Account Active->Closed (within 14 days)", + "reversalAction": "Right to rescind (DELETE) as account closure action within window", + "performanceExpectation": "API p95 <= 1500ms, portal TTI <= 3s", + "maskingCheck": "No raw PAN in statements UI, masked PAN on dashboard, no PAN in network payloads", + "piiFields": "email, address, bank_account_id (tokenized), statement data", + "securityControls": "CSRF enforced on state-changing endpoints, SameSite=Strict, HttpOnly cookies, TLS1.3, session timeout per NFR-06", + "rateLimitExpectation": "Standard WAF and gateway limits apply", + "assumptions": "ASSUMPTION-Rewards calculated on CAD-posted amount, ASSUMPTION-CSRF failures return 403, ASSUMPTION-Delete after day 14 returns 403, ASSUMPTION-Statement PDF content-type application/pdf" + }, + { + "type": "functional", + "title": "Login Lockout, Rate Limiting, MFA Validation, and Remember-Me Cookie TTL", + "description": "Validate login hardening controls: 5 failed attempts lock the account, 10 req/min rate limit per IP, MFA enforcement, and remember_me extending refresh token TTL with secure cookies.", + "testId": "TC-AEGIS-006", + "testDescription": "Ensures credential validation, lockout threshold, WAF rate limiting, MFA behavior, and cookie security attributes with remember_me.", + "prerequisites": "Verified user user+lockout@domain.com exists with MFA enabled; portal reachable over TLS 1.3; WAF rate limits active; test IP can simulate repeated requests.", + "stepsToPerform": "1. Navigate to https://portal.aegiscard.com over TLS 1.3 and open Login screen.\n2. Attempt POST /v2/auth/login with email=user+lockout@domain.com and wrong password WrongPass1! (no mfa_code) four times within one minute; expect 401 INVALID_CREDENTIALS each.\n3. Attempt a fifth invalid login with wrong password; expect 403 ACCOUNT_LOCKED with unlock_at returned.\n4. Immediately attempt another login (6th) within the same minute; expect 429 RATE_LIMITED with retry_after indicating backoff.\n5. Wait until unlock_at time passes; attempt POST /v2/auth/login with correct password but omit mfa_code; expect 401 INVALID_CREDENTIALS due to missing second factor (ASSUMPTION-Invalid or missing TOTP yields 401 INVALID_CREDENTIALS).\n6. Retry login with correct password and an incorrect 6-digit mfa_code; expect 401 INVALID_CREDENTIALS.\n7. Retry with correct password, valid TOTP, device_id set to a UUID v4, remember_me=true; expect 200 with access_token, refresh_token, expires_in.\n8. Inspect Set-Cookie headers for tokens; verify HttpOnly=true, Secure=true, SameSite=Strict for all auth cookies; verify no tokens present in window.localStorage or DOM.\n9. Validate that remember_me extends refresh token TTL by inspecting cookie expiry (should reflect ~30 days) while access token expires in ~15 minutes (expires_in ~900 seconds).\n10. Navigate to Dashboard and verify session active; confirm that lockout counters reset after successful login (ASSUMPTION-Backoff cleared on success).", + "expectedResult": "Account locks on the 5th failed attempt and returns unlock_at; further rapid attempts hit 429; successful login with valid MFA and remember_me returns tokens with secure cookies and extended refresh TTL; no tokens are stored in localStorage; session is active.", + "endpoint": "POST /v2/auth/login", + "method": "POST", + "apiPath": "/v2/auth/login", + "uiScreen": "Login", + "role": "Applicant, Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "false", + "headersUsed": "Content-Type: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,401,403,429", + "responseKeysExpected": "access_token, refresh_token, expires_in, error, unlock_at, retry_after", + "businessRuleIds": "NFR-02, NFR-05, NFR-06", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "Active", + "stateTransition": "Unlocked->Locked after 5 failures->Unlocked with successful MFA", + "reversalAction": "Wait until unlock_at then successful login clears lock", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No tokens in localStorage; cookies HttpOnly Secure; no PII beyond masked email in logs", + "piiFields": "email user+lockout@domain.com", + "securityControls": "MFA TOTP required, WAF rate limit 10 req/min per IP, account lock after 5 failures, TLS1.3", + "rateLimitExpectation": "Max 10 login requests per minute per IP", + "assumptions": "ASSUMPTION-Invalid or missing mfa_code returns 401 INVALID_CREDENTIALS; ASSUMPTION-remember_me extends refresh token TTL to 30 days observable via cookie expiry" + }, + { + "type": "functional", + "title": "Refresh Token Rotation with Single-Use Enforcement and Concurrent Refresh Handling", + "description": "Verify refresh token rotation invalidates old tokens, rejects replay, and handles concurrent refresh attempts deterministically.", + "testId": "TC-AEGIS-007", + "testDescription": "Covers access expiry boundary, successful refresh issuing a new pair, replay of old refresh causing 401 TOKEN_INVALID, and simultaneous refresh where one succeeds and the other fails.", + "prerequisites": "User user+refresh@domain.com exists and can login; browser supports HttpOnly cookies; clock control or wait capability available to approach access expiry.", + "stepsToPerform": "1. Login via POST /v2/auth/login with valid credentials and MFA; capture access_token A1 and refresh_token R1 from response cookies/body.\n2. Call a protected GET /v2/accounts/{account_id}/summary with A1 to confirm session valid (200).\n3. Wait until ~14 minutes of inactivity or near expiry; verify A1 nearing expiry (expires_in observation or time passage).\n4. POST /v2/auth/token/refresh with R1; expect 200 with new access_token A2 and new refresh_token R2; confirm old refresh token R1 is invalidated.\n5. Immediately attempt to POST /v2/auth/token/refresh again with R1; expect 401 TOKEN_INVALID.\n6. Simulate two concurrent refresh requests using R2 (send two POST /v2/auth/token/refresh within 100ms); expect one returns 200 with new pair (A3,R3) and the other returns 401 TOKEN_INVALID.\n7. Use A3 to call GET /v2/accounts/{account_id}/summary; expect 200 confirming session continuity after rotation.\n8. Attempt a protected request with expired A1 after its 15-minute window without refresh; expect 401 and WWW-Authenticate or error indicating token expired.\n9. Verify cookies remain HttpOnly, Secure, SameSite=Strict across refresh operations and no tokens exist in localStorage.\n10. Optionally logout via session clear UI (ASSUMPTION-Portal provides logout that clears cookies) and ensure subsequent protected request returns 401.", + "expectedResult": "Refresh rotation issues a new pair and invalidates the previous refresh token; replay of R1 fails with 401; concurrent refresh on R2 yields one success and one TOKEN_INVALID; protected API calls succeed with fresh access and fail with expired access; cookies remain secure.", + "endpoint": "POST /v2/auth/login, POST /v2/auth/token/refresh, GET /v2/accounts/{account_id}/summary", + "method": "POST, GET", + "apiPath": "/v2/auth/login, /v2/auth/token/refresh, /v2/accounts/{account_id}/summary", + "uiScreen": "Login, Dashboard", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "false", + "headersUsed": "Authorization: Bearer, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,401", + "responseKeysExpected": "access_token, refresh_token, expires_in, error", + "businessRuleIds": "NFR-06, NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "Active", + "stateTransition": "LoggedIn A1->Refreshed A2/A3->Expired A1 rejected", + "reversalAction": "Logout clears cookies to terminate session", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No tokens visible in DOM/localStorage; cookies are HttpOnly Secure", + "piiFields": "email user+refresh@domain.com", + "securityControls": "Refresh token single-use rotation, TLS1.3, HttpOnly cookies", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-Concurrent refresh behavior ensures first wins, second fails with TOKEN_INVALID; ASSUMPTION-Logout endpoint or UI clears cookies" + }, + { + "type": "functional", + "title": "List Transactions with Date Range Boundaries, Pagination Limits, Category Filter, and Owner-Only Access", + "description": "Validate GET /transactions filtering, boundary conditions, pagination max enforcement, and 403 on cross-account access.", + "testId": "TC-AEGIS-008", + "testDescription": "Ensures accurate filtering by date and category, boundary handling when from_date equals to_date, pagination max 100 per_page, and IDOR prevention.", + "prerequisites": "User user+history@domain.com owns account_id A-OWN; another account A-OTHER not owned by user exists with sample transactions across categories.", + "stepsToPerform": "1. Login with user+history@domain.com and obtain access token.\n2. GET /v2/accounts/A-OWN/transactions without query params; expect 200 with transactions, total_count, page=1, total_pages.\n3. GET /v2/accounts/A-OWN/transactions?from_date=2026-03-01&to_date=2026-03-01; expect 200 and only transactions from that specific day (boundary equality).\n4. GET /v2/accounts/A-OWN/transactions?from_date=2026-03-10&to_date=2026-03-05; expect 400 INVALID_DATE_RANGE.\n5. GET /v2/accounts/A-OWN/transactions?page=2&per_page=100; expect 200 with up to 100 items and correct paging metadata at boundary.\n6. GET /v2/accounts/A-OWN/transactions?per_page=101; expect validation failure (ASSUMPTION-400 INVALID_PAGINATION when exceeding max) or clamp to 100 if implemented; assert behavior is documented.\n7. GET /v2/accounts/A-OWN/transactions?category=REFUND; expect 200 and verify all items returned have category=REFUND only.\n8. Attempt GET /v2/accounts/A-OTHER/transactions; expect 403 FORBIDDEN owner-only access enforced (no data leakage).\n9. Verify no raw PAN appears in any response payload and merchant descriptors do not include unmasked PAN data (PCI masking check).\n10. Measure response time to ensure p95 latency <= 1500ms in test environment.", + "expectedResult": "Transactions API honors date range equality, rejects inverted ranges, enforces per_page max, filters by category, and prevents cross-account access with 403; responses exclude raw PAN and meet performance expectations.", + "endpoint": "GET /v2/accounts/{account_id}/transactions", + "method": "GET", + "apiPath": "/v2/accounts/{account_id}/transactions", + "uiScreen": "Transactions History", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "false", + "headersUsed": "Authorization: Bearer", + "httpCodesExpected": "200,400,403", + "responseKeysExpected": "transactions, total_count, page, total_pages, error", + "businessRuleIds": "NFR-05, REQ-014, NFR-02", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in response; merchant and transaction data free of full PAN", + "piiFields": "transaction descriptors, merchant_name", + "securityControls": "Owner-only access control enforced, TLS1.3", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-If per_page > 100 results in 400 INVALID_PAGINATION; if clamped, document observed clamping behavior" + }, + { + "type": "functional", + "title": "Set Web-Based PIN with OTP: Positive Flow, PIN Mismatch and Format Errors, and Operation While Card Frozen", + "description": "Verify PIN set success with valid OTP, error handling for mismatch and non-4-digit formats, OTP invalid/expired behavior, and that Frozen status does not block PIN changes.", + "testId": "TC-AEGIS-009", + "testDescription": "Exercises PUT /pin with correct and incorrect inputs, uses CSRF header and OTP gating, and confirms RSA-OAEP encryption and PCI-safe handling.", + "prerequisites": "User logged in with valid JWT; card_id belongs to user and is Active; CSRF token available; OTP delivery channel working.", + "stepsToPerform": "1. Acquire X-CSRF-Token from portal session and ensure HttpOnly, Secure, SameSite=Strict cookies are present.\n2. Request a session OTP via /v2/auth/otp/request for PIN action (ASSUMPTION-OTP request endpoint exists) and capture valid 6-digit code.\n3. PUT /v2/cards/{card_id}/pin with new_pin=2580, confirm_pin=2580, session_otp=valid code and X-CSRF-Token; expect 200 success true and updated_at.\n4. Attempt PUT /v2/cards/{card_id}/pin with new_pin=2580 and confirm_pin=2581 using a fresh valid OTP; expect 400 PIN_MISMATCH.\n5. Attempt PUT with invalid format new_pin=12a4 (non-numeric) and confirm_pin=12a4; expect 400 PIN_FORMAT; repeat with new_pin=123 (length 3) to confirm 4-digit enforcement.\n6. Attempt PUT with an expired or invalid session_otp and valid matching 4-digit PIN; expect 401 OTP_FAILED (ASSUMPTION-PIN endpoint returns 401 OTP_FAILED on invalid/expired OTP).\n7. Freeze the card via PATCH /v2/cards/{card_id}/status with status=Frozen, confirm_otp valid, and X-CSRF-Token; expect 200 new_status Frozen.\n8. Attempt PUT /v2/cards/{card_id}/pin again while status Frozen with new_pin=9042, confirm_pin=9042 and a fresh valid OTP; expect 200 success (Frozen should not block per REQ-008; only Blocked restricts).\n9. Inspect network requests to confirm PIN payload is transported encrypted (RSA-OAEP) and no plaintext PIN appears in browser logs, DOM, or network tools.\n10. Verify audit or security logs do not expose PIN and CSRF header is present on all state-changing calls.", + "expectedResult": "PIN set succeeds with valid OTP and 4-digit numeric match; mismatches and improper formats return 400; invalid/expired OTP returns 401; setting PIN while Frozen is allowed; PIN data is encrypted in transit and never visible in UI or logs; CSRF enforced.", + "endpoint": "PUT /v2/cards/{card_id}/pin, PATCH /v2/cards/{card_id}/status", + "method": "PUT, PATCH", + "apiPath": "/v2/cards/{card_id}/pin, /v2/cards/{card_id}/status", + "uiScreen": "Security Settings, Card Controls", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,400,401", + "responseKeysExpected": "success, updated_at, new_status, error", + "businessRuleIds": "REQ-008, NFR-05, NFR-01", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Frozen", + "accountStatus": "Active", + "stateTransition": "Card Active->Frozen (user-initiated) while PIN action allowed", + "reversalAction": "Unfreeze later as needed (not part of PIN validation)", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "PIN never logged; encrypted via RSA-OAEP; no raw PAN in UI", + "piiFields": "phone +14165551234 for OTP delivery", + "securityControls": "OTP gating, CSRF header enforced, TLS1.3, RSA-OAEP encryption for PIN", + "rateLimitExpectation": "Standard WAF limits for PUT/PATCH", + "assumptions": "ASSUMPTION-/v2/auth/otp/request exists for session_otp; ASSUMPTION-Invalid OTP returns 401 OTP_FAILED; ASSUMPTION-Frozen status does not restrict PIN operations per REQ-008" + }, + { + "type": "functional", + "title": "WebSocket Live Transaction Feed Subscription, Event Delivery, and Auth Enforcement", + "description": "Validate real-time stream subscription with auth, delivery of transaction events for CAD and foreign purchases, error on unauthenticated connection, and reconnect behavior.", + "testId": "TC-AEGIS-010", + "testDescription": "Ensures WebSocket auth and subscription flow, verifies event payload fields after REST transactions, checks foreign fee itemization presence, and handles unauthorized WS attempts.", + "prerequisites": "User user+realtime@domain.com with active account and card; valid JWT; CSRF token for transaction POSTs; WebSocket client capable of sending Authorization header.", + "stepsToPerform": "1. Login and obtain access token; ensure HttpOnly, Secure, SameSite=Strict cookies and CSRF token are available.\n2. Open WebSocket to wss://realtime.aegiscard.com/v2/stream including Authorization: Bearer in the handshake (ASSUMPTION-WS supports Authorization header) and wait for 101 Switching Protocols.\n3. Send initial subscription message for account_id (ASSUMPTION-Protocol requires sending 'SUBSCRIBE ' as first message) and expect an ack event.\n4. POST /v2/accounts/{account_id}/transactions with transaction_amount=20.00, currency_code=CAD, mcc_code=5411, merchant_name=Grocery Mart, merchant_id=GM123, transaction_type=PURCHASE and X-CSRF-Token; expect 200 with transaction_id and auth_code.\n5. Verify WebSocket receives a transaction event for the above with event_type=TRANSACTION_POSTED and fields transaction_id, amount, mcc_code=5411, currency=CAD.\n6. POST /v2/accounts/{account_id}/transactions with transaction_amount=10.00, currency_code=USD, exchange_rate=1.250000, mcc_code=7011, merchant_name=Travel Inn, merchant_id=TI777, transaction_type=PURCHASE with X-CSRF-Token; expect 200 with foreign_fee_amount itemized.\n7. Verify WebSocket event includes foreign_fee_amount and total_cad consistent with REQ-006 for the USD transaction.\n8. Attempt a foreign transaction POST with currency_code=USD but omit exchange_rate; expect 400 validation error indicating exchange_rate required (field, message present) and no WS event emitted for this failed attempt.\n9. Close the WebSocket connection and verify no further events are received.\n10. Attempt to open WebSocket without Authorization header or with an invalid token; expect connection refused or 401 equivalent during handshake (ASSUMPTION-Server returns 401/close code for unauthenticated WS).\n11. Reconnect WebSocket with valid Authorization and resubscribe; POST another small CAD transaction and confirm event delivery resumes.", + "expectedResult": "Authenticated WS connection established and subscription acknowledged; events received for successful CAD and foreign transactions including foreign_fee_amount itemization; unauthorized WS attempts are rejected; events stop on disconnect and resume on reconnect.", + "endpoint": "wss://realtime.aegiscard.com/v2/stream, POST /v2/accounts/{account_id}/transactions", + "method": "WS, POST", + "apiPath": "wss://realtime.aegiscard.com/v2/stream, /v2/accounts/{account_id}/transactions", + "uiScreen": "Dashboard Real-Time Feed, Transactions", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true for POST, false for WS", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "101,200,400,401", + "responseKeysExpected": "transaction_id, available_credit, auth_code, event_type, amount, mcc_code, foreign_fee_amount", + "businessRuleIds": "REQ-006, NFR-05, NFR-02", + "calculationsValidated": "Total_CAD = (amount × exchange_rate) × 1.03 for foreign tx", + "mccCode": "5411-grocery, 7011-travel", + "currencyCode": "CAD, USD", + "exchangeRateUsed": "1.250000", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "WS Disconnected->Connected->Subscribed->Disconnected->Reconnected", + "reversalAction": "Disconnect and reconnect to restore stream", + "performanceExpectation": "API p95 <= 1500ms; WS event delivery within 2s of transaction post (ASSUMPTION)", + "maskingCheck": "No raw PAN in WS events or network payloads; masked PAN only in UI if displayed", + "piiFields": "merchant_name synthetic, no PAN", + "securityControls": "WS requires Bearer auth; CSRF enforced for POST; TLS1.3", + "rateLimitExpectation": "Standard gateway rate limits for POST; WS connection limits per client apply", + "assumptions": "ASSUMPTION-WS uses Authorization header for auth; ASSUMPTION-Subscribe message format is 'SUBSCRIBE '; ASSUMPTION-Unauthorized WS returns 401 or closes the connection" + }, + { + "type": "functional", + "title": "Registration Field Validation: RFC 5322 Email, E.164 Phone, SSN Last4, Terms Enforcement, and Duplicate Email", + "description": "Validate registration input rules including complex email formats, strict E.164 phone, ssn_last4 exactly 4 digits, agree_terms must be true, and duplicate email handling.", + "testId": "TC-AEGIS-011", + "testDescription": "Focuses on negative and boundary validations for registration separate from age and password complexity, and verifies duplicate email 409 behavior and secure cookie attributes.", + "prerequisites": "Portal reachable over TLS 1.3; no existing user with email user+edge@domain.co.uk; WAF and rate limits active; CSRF token retrievable from portal session.", + "stepsToPerform": "1. Navigate to https://portal.aegiscard.com over TLS 1.3 and open Registration.\n2. POST /v2/auth/register with first_name=Jo, last_name=Tester, email=user..dots@domain.com, password=ValidPass123!, date_of_birth=1990-01-01, phone_number=+14165551234, ssn_last4=1234, agree_terms=true and X-CSRF-Token; expect 400 with field=email for invalid RFC 5322.\n3. Retry with email=user+edge@sub.domain.co.uk (valid RFC 5322), keep other fields, but phone_number=14165551234 (missing '+'); expect 400 with field=phone_number E.164 violation.\n4. Retry with phone_number=+14165551234 but ssn_last4=123 (3 digits); expect 400 with field=ssn_last4 and message exactly 4 digits required.\n5. Retry with ssn_last4=123a (non-numeric); expect 400 with field=ssn_last4 numeric-only enforcement.\n6. Retry with agree_terms=false while all other fields valid; expect 400 with field=agree_terms and message must be true.\n7. Retry with all valid fields: email=user+edge@domain.co.uk, phone_number=+14165551234, ssn_last4=1234, agree_terms=true; expect 201 with user_id and verification_token, HttpOnly Secure cookies set SameSite=Strict.\n8. Inspect Set-Cookie headers to confirm HttpOnly=true, Secure=true, SameSite=Strict; verify no tokens in window.localStorage or DOM.\n9. Attempt to register again with the same email=user+edge@domain.co.uk and otherwise valid data; expect 409 error EMAIL_EXISTS.\n10. Attempt registration with first_name containing a hyphen (e.g., first_name=Anne-Marie) to validate alpha-only rule; expect 400 if hyphens not allowed per alpha-only constraint (ASSUMPTION-alpha only rejects hyphens).", + "expectedResult": "Malformed email is rejected; valid complex email accepted; E.164 phone enforced with leading '+'; ssn_last4 must be exactly 4 numeric digits; agree_terms must be true; duplicate email returns 409; cookies are HttpOnly, Secure, SameSite=Strict with no tokens in localStorage.", + "endpoint": "POST /v2/auth/register", + "method": "POST", + "apiPath": "/v2/auth/register", + "uiScreen": "Registration", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Content-Type: application/json, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "201,400,409", + "responseKeysExpected": "user_id, verification_token, error, field, message", + "businessRuleIds": "NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "N/A", + "stateTransition": "Unauthenticated->Registered (pending verification)", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PII beyond masked email logging; no tokens in DOM/localStorage", + "piiFields": "email user+edge@domain.co.uk, phone +14165551234, DOB 1990-01-01, ssn_last4 1234", + "securityControls": "TLS1.3, CSRF header, HttpOnly Secure cookies, SameSite=Strict", + "rateLimitExpectation": "Registration endpoint protected by gateway limits", + "assumptions": "ASSUMPTION-alpha only for names rejects hyphens; ASSUMPTION-CSRF token obtained via portal meta or preflight" + }, + { + "type": "functional", + "title": "Credit Application Sequencing, Employment Conditional Fields, and X-App-Session Expiry at 30 Minutes", + "description": "Validate strict step order, employment_status vs employer_name requirement, sin_consent must be true, and 30-minute X-App-Session expiry behavior.", + "testId": "TC-AEGIS-012", + "testDescription": "Covers out-of-order Step 3 rejection, Step 2 conditional validations, sin_consent enforcement, session_token expiry handling, and duplicate application 409 on restart attempts.", + "prerequisites": "Two verified users exist: userA@domain.com and userB@domain.com; both can login with MFA; CSRF token available post-login.", + "stepsToPerform": "1. Login as userA@domain.com with valid MFA via POST /v2/auth/login; expect 200 tokens and cookies.\n2. Attempt to POST /v2/applications/{fake_application_id}/submit with a fabricated UUID and base64 e_signature while no Step 1 exists; expect 400 INVALID_SEQUENCE or 404 not found (ASSUMPTION-returns 400 INVALID_SEQUENCE for step order).\n3. Start Step 1 via POST /v2/applications/start with valid personal info (full_legal_name, email, phone, address with province=ON and postal_code=A1A 1A1, id_type=DRIVERS_LICENSE, id_number=DL123456) and X-CSRF-Token; expect 201 with application_id and session_token.\n4. Submit Step 2 via POST /v2/applications/{application_id}/financials with employment_status=EMPLOYED but omit employer_name; include header X-App-Session: session_token; expect 400 with field=employer_name required.\n5. Resubmit Step 2 with employer_name present but sin_consent=false; expect 400 with field=sin_consent must be true.\n6. Resubmit Step 2 with employment_status=SELF_EMPLOYED, employer_name omitted, sin_consent=true, valid incomes; expect 200 with status=PENDING_REVIEW and fico_pull_id.\n7. Wait 31 minutes to exceed the 30-minute X-App-Session validity; attempt POST /v2/applications/{application_id}/submit with card_product_id and valid base64 e_signature using the old session_token; expect 401 error SESSION_EXPIRED.\n8. Attempt to POST /v2/applications/start again for userA while the previous application is active; expect 409 DUPLICATE_APPLICATION.\n9. Logout userA and login as userB with MFA; POST /v2/applications/start to create a new application, receive application_id and session_token.\n10. For userB, POST Step 2 with employment_status=EMPLOYED, employer_name=Contoso, sin_consent=true and valid financials using X-App-Session; expect 200 with fico_pull_id.\n11. For userB, within 30 minutes, POST Step 3 with valid card_product_id and base64 e_signature; expect 200 with decision field present (APPROVED or PENDING or DECLINED per FICO), and marketing_opt_in default false when omitted.", + "expectedResult": "Step 3 without prior steps is rejected; Step 2 enforces employer_name when EMPLOYED and sin_consent=true; SELF_EMPLOYED allows employer_name omission; session_token expires after 30 minutes causing 401 on subsequent steps; duplicate application attempts return 409; a fresh user can complete Steps 1–3 in order with valid headers.", + "endpoint": "POST /v2/auth/login, POST /v2/applications/start, POST /v2/applications/{application_id}/financials, POST /v2/applications/{application_id}/submit", + "method": "POST", + "apiPath": "/v2/auth/login, /v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit", + "uiScreen": "Login, Application Step 1, Application Step 2, Application Step 3", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, X-App-Session", + "httpCodesExpected": "200,201,400,401,409", + "responseKeysExpected": "application_id, session_token, fico_pull_id, decision, error, field, message", + "businessRuleIds": "NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "ASSUMPTION-sandbox returns deterministic FICO for test users", + "decisionExpected": "Varies by test user, ensure decision present", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "N/A", + "stateTransition": "New App->Step1->Step2->Session Expired or Step3 Completed", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in any application responses or UI", + "piiFields": "email userA@domain.com,userB@domain.com, phone +14165551234, address A1A 1A1", + "securityControls": "X-CSRF-Token on state changes, X-App-Session header required, TLS1.3", + "rateLimitExpectation": "Standard gateway limits", + "assumptions": "ASSUMPTION-Step 3 before Step 2 returns 400 INVALID_SEQUENCE; ASSUMPTION-session_token lifetime strictly 30 minutes per SRS" + }, + { + "type": "functional", + "title": "CSRF Enforcement Across Endpoints and SameSite=Strict Cross-Site POST Rejection", + "description": "Verify X-CSRF-Token is required for multiple state-changing endpoints and that SameSite=Strict cookies prevent credentialed cross-site requests.", + "testId": "TC-AEGIS-013", + "testDescription": "Covers CSRF negatives on PATCH /cards/status, POST /applications/{id}/financials, POST /accounts/{id}/payments, PUT /cards/{id}/pin, and cross-site attempts blocked by SameSite=Strict; then demonstrates success when CSRF token is present.", + "prerequisites": "Verified user with MFA, active account and card; test harness capable of simulating cross-site origin requests; CSRF token retrievable from portal; valid session_otp for PIN action.", + "stepsToPerform": "1. Login at https://portal.aegiscard.com and ensure HttpOnly, Secure, SameSite=Strict cookies are set; capture valid X-CSRF-Token from portal.\n2. From a different origin (e.g., https://evil.example.com test harness), attempt a cross-site POST to https://api.aegiscard.com/v2/cards/{card_id}/status with body {status: Frozen, confirm_otp: 123456} and include cookies but no X-CSRF-Token; expect 403 CSRF enforced and cookies not sent due to SameSite=Strict.\n3. From same-site context, attempt PATCH /v2/cards/{card_id}/status with status=Frozen and confirm_otp valid but omit X-CSRF-Token; expect 403 CSRF required.\n4. Attempt POST /v2/applications/{application_id}/financials with X-App-Session present but without X-CSRF-Token; expect 403 CSRF required despite X-App-Session.\n5. Attempt POST /v2/accounts/{account_id}/payments without X-CSRF-Token; expect 403 CSRF required.\n6. Attempt PUT /v2/cards/{card_id}/pin with new_pin=1234, confirm_pin=1234, session_otp valid but without X-CSRF-Token; expect 403 CSRF required.\n7. Repeat PATCH /v2/cards/{card_id}/status with X-CSRF-Token included and confirm_otp valid; expect 200 with new_status=Frozen.\n8. Repeat PUT /v2/cards/{card_id}/pin with X-CSRF-Token and valid session_otp to set new_pin=2580; expect 200 success true.\n9. Verify network inspector shows X-CSRF-Token present on successful state changes and that no cookies were transmitted during the cross-site attempt due to SameSite=Strict.\n10. Unfreeze card with PATCH /v2/cards/{card_id}/status using X-CSRF-Token and OTP to restore Active; expect 200 new_status=Active.", + "expectedResult": "All state-changing requests without X-CSRF-Token are rejected with 403; cross-site attempts fail due to SameSite=Strict and CSRF enforcement; including X-CSRF-Token with valid OTP allows operations to proceed successfully.", + "endpoint": "PATCH /v2/cards/{card_id}/status, POST /v2/applications/{application_id}/financials, POST /v2/accounts/{account_id}/payments, PUT /v2/cards/{card_id}/pin", + "method": "PATCH, POST, PUT", + "apiPath": "/v2/cards/{card_id}/status, /v2/applications/{application_id}/financials, /v2/accounts/{account_id}/payments, /v2/cards/{card_id}/pin", + "uiScreen": "Card Controls, Application Step 2, Payments, Security Settings", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true for positive, false for negatives", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, X-App-Session (for financials), Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,403", + "responseKeysExpected": "new_status, success, error, message", + "businessRuleIds": "NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Frozen then back to Active", + "accountStatus": "Active", + "stateTransition": "Card Active->Frozen->Active via CSRF-protected calls", + "reversalAction": "Unfreeze reversal after freeze", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN exposure; tokens only in HttpOnly cookies", + "piiFields": "phone for OTP delivery (masked), email masked in logs", + "securityControls": "CSRF header required for POST/PATCH/PUT, SameSite=Strict prevents cross-site cookies, TLS1.3", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-cross-site requests simulated via separate origin harness; ASSUMPTION-403 returned for CSRF failures as per NFR-05" + }, + { + "type": "functional", + "title": "Transactions: Invalid Amount 0.00, Exchange Rate Precision Boundaries, Foreign Fee Rounding, and Non-Essential Over-Limit Rejection", + "description": "Validate 422 on zero amount, enforce Decimal(8,6) on exchange_rate, calculate 3% foreign fee with correct rounding, and reject non-essential over-limit transactions.", + "testId": "TC-AEGIS-014", + "testDescription": "Covers strict validations on POST /transactions, including amount > 0, exchange_rate precision and maximum boundary, foreign fee and CAD total computation, omission of exchange_rate when required, and over-limit behavior for non-essential MCC.", + "prerequisites": "Active card and account with known available_credit; user logged in with valid JWT; X-CSRF-Token available; card status Active.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to capture available_credit and ensure card status Active.\n2. POST /v2/accounts/{account_id}/transactions with transaction_amount=0.00, merchant_name=Test Zero, merchant_id=T0, mcc_code=5812, currency_code=CAD, transaction_type=PURCHASE and X-CSRF-Token; expect 422 error INVALID_AMOUNT.\n3. POST a USD transaction with transaction_amount=0.01, currency_code=USD, exchange_rate=99.999999 (max Decimal(8,6) boundary), mcc_code=7011, merchant_name=Tiny Travel, merchant_id=TT01, transaction_type=PURCHASE; expect 200 with foreign_fee_amount and approve if within available_credit.\n4. Validate Total_CAD = (0.01 × 99.999999) × 1.03 ≈ 1.03 and foreign_fee_amount ≈ 0.03 with response values rounded to 2 decimals; verify itemization present per REQ-006.\n5. POST another USD transaction with transaction_amount=10.00, currency_code=USD, exchange_rate=100.000000 (exceeds Decimal(8,6) total digits); expect 400 error INVALID_EXCHANGE_RATE (ASSUMPTION-error code name for out-of-range precision).\n6. POST USD transaction with transaction_amount=5.00, currency_code=USD but omit exchange_rate; expect 400 indicating exchange_rate required when currency != CAD.\n7. POST a non-essential purchase exceeding available_credit by CAD 0.01 with mcc_code=5812 (restaurant) and transaction_type=PURCHASE; expect 402 INSUFFICIENT_FUNDS and available_credit returned.\n8. POST a CAD transaction with transaction_amount=1.00, mcc_code=5411, ensure success 200 to confirm normal path still works.\n9. Attempt a foreign transaction with exchange_rate=1.2345678 (more than 6 fractional digits); expect 400 precision validation error.\n10. Confirm response times p95 <= 1500ms and that responses contain no raw PAN.", + "expectedResult": "Zero-amount transactions are rejected with 422; exchange_rate accepts up to 6 fractional digits and max total digits per Decimal(8,6); foreign fee and Total_CAD are computed and rounded correctly; missing exchange_rate for non-CAD is rejected; non-essential over-limit results in 402; valid CAD transactions succeed.", + "endpoint": "GET /v2/accounts/{account_id}/summary, POST /v2/accounts/{account_id}/transactions", + "method": "GET, POST", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions", + "uiScreen": "Dashboard, Transactions", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure", + "httpCodesExpected": "200,400,402,422", + "responseKeysExpected": "transaction_id, available_credit, auth_code, foreign_fee_amount, error, message", + "businessRuleIds": "REQ-006, NFR-05", + "calculationsValidated": "Foreign fee 3% itemization, Total_CAD rounding to 2 decimals, Decimal(8,6) precision enforcement", + "mccCode": "5812-restaurant, 5411-grocery, 7011-travel", + "currencyCode": "CAD, USD", + "exchangeRateUsed": "99.999999 boundary, 100.000000 invalid, 1.2345678 invalid", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in responses or network payloads", + "piiFields": "merchant_name synthetic only", + "securityControls": "TLS1.3, CSRF header on POST, WAF rate limits", + "rateLimitExpectation": "Transactions frequency limits may apply but not exercised here", + "assumptions": "ASSUMPTION-Server returns 400 INVALID_EXCHANGE_RATE for out-of-range or over-precision exchange_rate; ASSUMPTION-responses round monetary values to 2 decimals" + }, + { + "type": "functional", + "title": "Payments Validation: Minimum Amount Boundary, Invalid Bank Account, Past vs Future Scheduling, and Owner-Only Access", + "description": "Verify payment_amount minimum $1.00, invalid or inactive bank account rejection, scheduled_date must be future, successful immediate and scheduled payments, and 403 on cross-account access.", + "testId": "TC-AEGIS-015", + "testDescription": "Exercises POST /payments validation rules with both negative and positive paths and ensures owner-only access control; avoids CSRF overlap tests by focusing on payment-specific validations.", + "prerequisites": "User user+pay@domain.com logged in with MFA; account with outstanding balance > $10 and minimum_payment_due >= $1; at least one linked active bank_account_id BANK-OK and one inactive or nonexistent BANK-BAD; CSRF token available.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to capture total_balance and note that balance > $10.\n2. GET /v2/accounts/{account_id}/statements/{statement_id}?format=JSON to read minimum_payment_due for boundary value reference (ensure statement exists or use current cycle data as available).\n3. Attempt POST /v2/accounts/{account_id}/payments with payment_amount=0.99, payment_type=MINIMUM, bank_account_id=BANK-OK and X-CSRF-Token; expect 400 BELOW_MINIMUM with minimum_payment_due field returned.\n4. Attempt POST /v2/accounts/{account_id}/payments with payment_amount=5.00, payment_type=CUSTOM, bank_account_id=BANK-BAD and X-CSRF-Token; expect 422 INVALID_BANK_ACCOUNT.\n5. Attempt POST /v2/accounts/{account_id}/payments with payment_amount=2.00, payment_type=CUSTOM, bank_account_id=BANK-OK, scheduled_date set to yesterday (past date) and X-CSRF-Token; expect 400 PAST_DATE or equivalent validation error (ASSUMPTION-error code PAST_DATE).\n6. Submit immediate boundary-minimum payment with payment_amount=1.00, payment_type=CUSTOM, bank_account_id=BANK-OK and X-CSRF-Token; expect 200 payment_id and new_balance_estimate decreased appropriately.\n7. Submit a scheduled payment for payment_type=STATEMENT_BALANCE with payment_amount equal to the current statement balance (or test-provided value), bank_account_id=BANK-OK, scheduled_date = tomorrow (future) and X-CSRF-Token; expect 200 with payment_id and scheduled_date accepted.\n8. Attempt POST /v2/accounts/{other_account_id}/payments using the same token (account not owned by user); expect 403 FORBIDDEN owner-only access.\n9. Verify that making a FULL_BALANCE payment with payment_amount equal to total_balance (captured at step 1) is accepted: POST with payment_type=FULL_BALANCE and X-CSRF-Token; expect 200 payment_accepted and new_balance_estimate near zero (accounting for pending transactions).\n10. Confirm response times p95 <= 1500ms and that no raw PAN appears in any payment responses or UI.", + "expectedResult": "Payments below $1.00 are rejected with BELOW_MINIMUM; invalid or inactive bank accounts are rejected with 422; scheduled_date must be in the future; immediate and scheduled valid payments succeed and return payment_id; cross-account payment attempt yields 403; responses contain secure fields only.", + "endpoint": "POST /v2/accounts/{account_id}/payments, GET /v2/accounts/{account_id}/statements/{statement_id}", + "method": "POST, GET", + "apiPath": "/v2/accounts/{account_id}/payments, /v2/accounts/{account_id}/statements/{statement_id}", + "uiScreen": "Payments, Statements", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,400,403,422", + "responseKeysExpected": "payment_id, scheduled_date, new_balance_estimate, minimum_payment_due, error, message", + "businessRuleIds": "REQ-011 (late fee reference context only), NFR-05", + "calculationsValidated": "Minimum amount $1.00 boundary, new_balance_estimate decreases as expected", + "mccCode": "N/A", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in payment responses or UI; bank_account_id tokenized", + "piiFields": "bank_account_id BANK-OK (token), email user+pay@domain.com", + "securityControls": "Owner-only access, CSRF enforcement, TLS1.3", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-Error code PAST_DATE returned for scheduled_date in the past; ASSUMPTION-FULL_BALANCE requires payment_amount equal to total_balance in this API" + }, + { + "type": "functional", + "title": "Registration Password Complexity Matrix and Leap-Year Age Validation", + "description": "Validate registration rejects weak passwords across all missing-character classes, accepts exactly 12-char compliant password, and enforces age >= 18 with leap-year and timezone edge cases.", + "testId": "TC-AEGIS-020", + "testDescription": "Covers password complexity permutations and precise age computation for leap-day birthdays, ensuring regulatory-grade input validation.", + "prerequisites": "Portal accessible over TLS1.3; CSRF token obtainable from portal; test clock or sandbox date control available; no existing account for user+leap@domain.com.", + "stepsToPerform": "1. Navigate to https://portal.aegiscard.com/register over TLS 1.3 and retrieve X-CSRF-Token from session.\n2. POST /v2/auth/register with email=user+leap@domain.com, first_name=John, last_name=Doe, phone=+14165551234, ssn_last4=1234, agree_terms=true, date_of_birth=2008-03-01, password=Short11! (11 chars); expect 422 WEAK_PASSWORD.\n3. Retry with password=alllowercase11! (no uppercase, 14 chars); expect 422 WEAK_PASSWORD missing uppercase.\n4. Retry with password=NOLOWERCASE11! (no lowercase); expect 422 WEAK_PASSWORD missing lowercase.\n5. Retry with password=NoDigits!!!! (no digit, 12+ chars); expect 422 WEAK_PASSWORD missing digit.\n6. Retry with password=NoSymbol1234 (no symbol, 12+ chars); expect 422 WEAK_PASSWORD missing symbol.\n7. Retry with a fully compliant minimal password=OkPassw0rd!! (12 chars, has upper, lower, digit, symbol) but with date_of_birth making user 17 years 364 days old relative to today; expect 400 with field=date_of_birth age<18.\n8. Set sandbox date to a non-leap year Feb 28 for user born on leap-day: date_of_birth=2008-02-29; submit compliant password; expect 400 age<18 due to not yet reached birthday (non-leap-year handling).\n9. Advance sandbox date to Mar 1 same year; resubmit identical payload; expect 201 with user_id and verification_token (age now >=18); verify Set-Cookie attributes HttpOnly, Secure, SameSite=Strict.\n10. Confirm no tokens stored in window.localStorage or window.sessionStorage and no sensitive PII echoed in DOM.\n11. Attempt a login with the new account (optional) to confirm account created pending email verification (ASSUMPTION-returns 401 INVALID_CREDENTIALS until verified).", + "expectedResult": "All weak password permutations are rejected with 422; compliant 12-char password passes; age check rejects just-under-18 and leap-day before birthday, accepts on or after the qualifying date; secure cookies set; no tokens in localStorage.", + "endpoint": "POST /v2/auth/register", + "method": "POST", + "apiPath": "/v2/auth/register", + "uiScreen": "Registration", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Content-Type: application/json, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "201,400,422", + "responseKeysExpected": "user_id, verification_token, error, field, message", + "businessRuleIds": "NFR-05", + "calculationsValidated": "Age >= 18, leap-year boundary", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "Registered (pending verification)", + "stateTransition": "Unauthenticated->Registration Attempt->Registered", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No tokens in localStorage; no PII leaked; cookies HttpOnly Secure Strict", + "piiFields": "email user+leap@domain.com, phone +14165551234, DOB 2008-02-29, ssn_last4 1234", + "securityControls": "TLS1.3, CSRF enforced, HttpOnly Secure cookies, PKCE used by portal", + "rateLimitExpectation": "Standard gateway limits for registration", + "assumptions": "ASSUMPTION-Sandbox clock control allows date manipulation; ASSUMPTION-Age computed in UTC calendar; ASSUMPTION-Invalid or unverified account cannot login until email verified" + }, + { + "type": "functional", + "title": "Application Draft Auto-Save, Restore, Sanitization, and Post-Submission Cleanup", + "description": "Validate Step 1 draft auto-save every 60 seconds persists non-sensitive fields only, restores after refresh or tab close, handles corrupted storage gracefully, and cleans up upon successful Step 1 submission.", + "testId": "TC-AEGIS-021", + "testDescription": "Ensures localStorage auto-save cadence and sanitization, no sensitive tokens stored, resilience to corrupted draft data, and cleanup after creating application.", + "prerequisites": "Verified user user+draft@domain.com with MFA; logged into portal; CSRF token available; browser DevTools accessible.", + "stepsToPerform": "1. Login via portal and navigate to Credit Application Step 1; confirm session cookies are HttpOnly, Secure, SameSite=Strict.\n2. Fill Step 1 form with full_legal_name=Jane Draft, email=user+draft@domain.com, phone=+14165550000, address (street, city, province ON, postal_code A1A 1A1), id_type=DRIVERS_LICENSE, id_number=DL555555; do not click Continue.\n3. Wait 60 seconds; verify a draft key (e.g., aegis_app_step1_draft) appears in localStorage with recent timestamp; confirm values like name, phone, province, postal_code are present.\n4. Inspect the stored JSON to ensure sensitive fields are sanitized: id_number absent or masked, no JWT/refresh_token/session_token present, ssn_last4 not present.\n5. Refresh the page; verify fields auto-populate from draft, with any masked values prompted for re-entry as needed.\n6. Update residential_address.city and phone, then wait another 60 seconds; verify updated values persisted in the draft record (timestamp advanced).\n7. Close the tab, reopen Step 1, and verify the draft is restored correctly across navigation; ensure the auto-save interval remains 60 seconds (±5s tolerance).\n8. Manually corrupt the draft value in localStorage to invalid JSON and refresh; verify the UI catches parse errors gracefully, clears the bad draft, and shows an empty form with a non-blocking notice.\n9. Submit Step 1 via POST /v2/applications/start with the on-screen data and X-CSRF-Token; expect 201 with application_id and session_token.\n10. After successful submission, verify the localStorage draft key is removed and no further auto-save writes occur for Step 1.\n11. Confirm network logs show no raw PAN anywhere and no auth tokens stored in DOM or storage.", + "expectedResult": "Draft saves every 60 seconds, restores accurately, excludes sensitive data, handles corruption gracefully, and is cleared after successful Step 1 submission; no tokens or raw PAN stored or transmitted.", + "endpoint": "POST /v2/applications/start", + "method": "POST", + "apiPath": "/v2/applications/start", + "uiScreen": "Application Step 1", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "201,400", + "responseKeysExpected": "application_id, session_token, error, field, message", + "businessRuleIds": "NFR-05, NFR-01", + "calculationsValidated": "Auto-save interval timing tolerance", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "Application created", + "stateTransition": "Draft->Step1 Submitted->Draft Cleared", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms; portal TTI <= 3s", + "maskingCheck": "No JWT/session_token/id_number plaintext in localStorage; no PAN in DOM/network", + "piiFields": "full_legal_name, phone, address, id_number masked", + "securityControls": "SameSite=Strict, HttpOnly cookies, CSRF required, no tokens in storage", + "rateLimitExpectation": "Standard gateway limits", + "assumptions": "ASSUMPTION-localStorage key is aegis_app_step1_draft or equivalent; ASSUMPTION-id_number masked or excluded by SPA when saving draft" + }, + { + "type": "functional", + "title": "Essential MCC Mapping and 5% Over-Limit Buffer Decision Table (Healthcare and Pharmacy vs Restaurant)", + "description": "Verify essential-service buffer applies to healthcare and pharmacy MCCs at 5% boundary, rejects beyond 5%, and is not applied to non-essential MCCs (e.g., restaurants).", + "testId": "TC-AEGIS-022", + "testDescription": "Exercises MCC-based essential mapping for over-limit tolerance: 5912 (pharmacy), 8062 (medical), contrasted with 5812 (restaurant), with boundary and just-beyond cases.", + "prerequisites": "Active cardholder logged in with valid JWT; CSRF token available; card status Active; capture available_credit from summary.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to capture available_credit and confirm card_status=Active.\n2. Compute pharmacy boundary amount = round(available_credit * 1.05, 2); POST /v2/accounts/{account_id}/transactions with transaction_amount=boundary amount, mcc_code=5912, currency_code=CAD, transaction_type=PURCHASE, X-CSRF-Token; expect 200 and over_limit_flag=true.\n3. Attempt pharmacy just-beyond amount = round(available_credit * 1.051, 2); POST with mcc_code=5912; expect 402 INSUFFICIENT_FUNDS with available_credit returned.\n4. Compute medical boundary amount = round(available_credit * 1.05, 2) after any credit updates; POST with mcc_code=8062; expect 200 and over_limit_flag=true.\n5. Attempt medical just-beyond amount = round(available_credit * 1.051, 2) recalculated; POST with mcc_code=8062; expect 402 INSUFFICIENT_FUNDS.\n6. Attempt non-essential restaurant slightly over-limit: transaction_amount=available_credit + 0.01, mcc_code=5812; expect 402 INSUFFICIENT_FUNDS (no buffer applied).\n7. Post a small essential pharmacy purchase well within limit (e.g., $5.00, mcc_code=5912); expect 200 and over_limit_flag absent or false.\n8. Confirm rate-limit not triggered (<=10 tx in 60m) and responses include transaction_id/auth_code for approved cases.\n9. Ensure no raw PAN in responses and verify p95 latency within 1500ms.\n10. Document observed MCC behavior for healthcare and pharmacy as essential; restaurant as non-essential.", + "expectedResult": "Essential MCCs 5912 and 8062 allow approvals up to +5% over-limit with over_limit_flag; amounts beyond +5% reject with 402; non-essential MCC 5812 rejects any over-limit; normal within-limit essential purchase approves without flag.", + "endpoint": "GET /v2/accounts/{account_id}/summary, POST /v2/accounts/{account_id}/transactions", + "method": "GET, POST", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions", + "uiScreen": "Dashboard, Transactions", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure", + "httpCodesExpected": "200,402", + "responseKeysExpected": "transaction_id, available_credit, auth_code, over_limit_flag, error", + "businessRuleIds": "REQ-006-OverLimitBuffer, NFR-05", + "calculationsValidated": "5% boundary approval, >5% rejection", + "mccCode": "5912-pharmacy, 8062-medical, 5812-restaurant", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN returned in transaction responses", + "piiFields": "merchant_name synthetic only", + "securityControls": "TLS1.3, CSRF enforced, owner-only account access", + "rateLimitExpectation": "Stay within 10 tx/60m to avoid 429", + "assumptions": "ASSUMPTION-Essential MCCs include 5912 and 8062; mapping provided by product; boundary defined as <=5% inclusive" + }, + { + "type": "functional", + "title": "Audit Trail for Credit Limit Changes: Immutable Logging and Visibility", + "description": "Validate that credit_limit changes create immutable audit records with user_id, session_id, ip_address, timestamp, and old/new values; ensure cardholder sees updated limit and cannot modify audit.", + "testId": "TC-AEGIS-023", + "testDescription": "Exercises NFR-04 by simulating admin-performed credit_limit changes and verifying audit immutability and correct propagation to account summary.", + "prerequisites": "Cardholder account exists with known credit_limit; Backoffice Admin test user available; both can login; audit UI or API accessible for verification.", + "stepsToPerform": "1. Cardholder logs in and GET /v2/accounts/{account_id}/summary; record current credit_limit L1 and available_credit.\n2. Admin logs in to backoffice and posts a credit limit increase: POST /v2/admin/accounts/{account_id}/credit-limit with new_limit=L2>L1 and X-CSRF-Token; expect 200 acknowledgement (ASSUMPTION-admin endpoint exists).\n3. Cardholder GETs /v2/accounts/{account_id}/summary; verify credit_limit=L2 and available_credit adjusted accordingly.\n4. Access audit records via audit UI or GET /v2/admin/audit?account_id={account_id}; verify a new record with fields user_id (admin), session_id, ip_address, timestamp_utc, field=credit_limit, old_value=L1, new_value=L2.\n5. Attempt to DELETE /v2/admin/audit/{audit_id} or PATCH it as Admin; expect 405 METHOD_NOT_ALLOWED or 403 FORBIDDEN indicating immutability.\n6. Admin posts a second change to decrease limit: POST /v2/admin/accounts/{account_id}/credit-limit with new_limit=L3L2->L3 with immutable audit entries", + "reversalAction": "Second limit change acts as functional reversal while preserving logs", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "Audit and summary reveal no PAN; masked PAN only in UI if shown", + "piiFields": "user_id, session_id, ip_address (internal use)", + "securityControls": "Role-based access, CSRF enforced for admin POSTs, TLS1.3", + "rateLimitExpectation": "Standard gateway limits for admin and user endpoints", + "assumptions": "ASSUMPTION-Admin endpoint /v2/admin/accounts/{id}/credit-limit and audit API/Viewer exist; ASSUMPTION-Deleting or modifying audit entries is disallowed by design" + }, + { + "type": "functional", + "title": "Notifications Webhook: Invalid Types, Channel Validation, Multi-Channel Delivery, and Idempotency Scope", + "description": "Validate webhook rejects unknown alert_type or channel with 400, delivers notifications across supported channels, and enforces idempotency_key per account scope.", + "testId": "TC-AEGIS-024", + "testDescription": "Focuses on webhook input validation, multi-channel success paths, and deduplication semantics without overlapping late-fee idempotency tests.", + "prerequisites": "Two test accounts A-ONE and A-TWO; Notification Engine client credentialed to call webhook; ability to generate UUIDv4 idempotency keys.", + "stepsToPerform": "1. POST /v2/notifications/webhook with account_id=A-ONE, alert_type=PIN_LOCKED, channel=SMS, severity=CRITICAL, message_body=\"Your PIN has been locked.\", idempotency_key=K-0001; expect 200 with notification_id and delivered_at.\n2. Repeat the exact POST with the same idempotency_key=K-0001 for A-ONE; expect 409 DUPLICATE_NOTIFICATION.\n3. POST the same idempotency_key=K-0001 but for account_id=A-TWO; expect 200 (idempotency scoped per account).\n4. POST with account_id=A-ONE, alert_type=FRAUD_FLAG, channel=EMAIL, severity=WARNING, message_body=\"Transaction flagged.\", idempotency_key=K-0002; expect 200 and channel=EMAIL in response.\n5. POST with account_id=A-ONE, alert_type=OVER_LIMIT, channel=IN_APP, severity=INFO, message_body=\"You are over limit.\", idempotency_key=K-0003; expect 200 and channel=IN_APP.\n6. Negative: POST with alert_type=UNKNOWN_TYPE, channel=SMS, severity=INFO, idempotency_key=K-0004; expect 400 INVALID_ALERT_TYPE.\n7. Negative: POST with alert_type=STATEMENT_READY, channel=FAX, severity=INFO, idempotency_key=K-0005; expect 400 INVALID_ALERT_TYPE or channel validation error for unsupported channel.\n8. Negative: POST with message_body length > 500 chars (e.g., 600 chars), valid alert_type/channel; expect 400 with field=message_body.\n9. Confirm no raw PAN appears in any payloads or responses and that response times meet p95 <= 1500ms.\n10. Document that idempotency prevents duplicate delivery for the same (account_id, idempotency_key) and allows delivery for a different account or a new key.", + "expectedResult": "Webhook accepts supported alert types/channels and returns 200 with notification_id; unknown types/channels reject with 400; duplicates for same account and key return 409; dedupe scope is per account; no PAN exposure.", + "endpoint": "POST /v2/notifications/webhook", + "method": "POST", + "apiPath": "/v2/notifications/webhook", + "uiScreen": "N/A (server-to-server webhook)", + "role": "Notification Engine", + "authType": "Service-to-Service", + "csrfTokenUsed": "false", + "headersUsed": "Content-Type: application/json, Authorization: Service Token or mTLS", + "httpCodesExpected": "200,400,409", + "responseKeysExpected": "notification_id, delivered_at, channel, error, field, message", + "businessRuleIds": "NFR-02", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in webhook request/response bodies", + "piiFields": "message_body contains no PII beyond generic text", + "securityControls": "Server-to-server auth (mTLS or token), TLS1.3", + "rateLimitExpectation": "Standard gateway rate limits for internal services", + "assumptions": "ASSUMPTION-Idempotency scoped by (account_id, idempotency_key); ASSUMPTION-Webhook authenticated via mTLS or internal token and exempt from CSRF" + }, + { + "type": "functional", + "title": "Email Verification: Invalid Token, Expired Token, Resend Rate-Limit, and Successful Activation", + "description": "Validate email verification failure modes and resend workflow prior to permitting login.", + "testId": "TC-AEGIS-025", + "testDescription": "Covers registration success, blocked login when unverified, invalid/tampered verification token rejection, expired token handling, resend verification with rate limit, and final successful verification enabling login.", + "prerequisites": "Portal reachable over TLS 1.3; CSRF token available from portal; no existing user user+verify@domain.com; test email inbox or token capture available.", + "stepsToPerform": "1. Navigate to https://portal.aegiscard.com/register over TLS 1.3 and obtain X-CSRF-Token.\n2. POST /v2/auth/register with first_name=Jane, last_name=Verify, email=user+verify@domain.com, password=CompliantP@ssw0rd!, date_of_birth=1990-02-01, phone_number=+14165551234, ssn_last4=1234, agree_terms=true and X-CSRF-Token; expect 201 with user_id and verification_token; confirm HttpOnly, Secure, SameSite=Strict cookies set.\n3. Attempt POST /v2/auth/login with correct email/password (without completing email verification); expect 403 with error EMAIL_NOT_VERIFIED (ASSUMPTION-login requires verified email).\n4. Attempt to verify using a tampered token via GET /v2/auth/verify?token=; expect 400 error INVALID_TOKEN and verified=false.\n5. Simulate token expiry by advancing test clock beyond token TTL (ASSUMPTION-24h) or use an expired token fixture; GET /v2/auth/verify?token=; expect 400 error TOKEN_EXPIRED and message prompting resend.\n6. POST /v2/auth/verify/resend with body {email:user+verify@domain.com} and X-CSRF-Token; expect 200 with resend_ack=true (ASSUMPTION-resend endpoint exists and requires CSRF).\n7. Immediately POST /v2/auth/verify/resend again for the same email; expect 429 error RESEND_RATE_LIMIT with retry_after header (ASSUMPTION-3 resends/hour policy).\n8. Retrieve the new verification link from test inbox or capture service; GET /v2/auth/verify?token=; expect 200 verified=true and UI success message.\n9. Retry POST /v2/auth/login with correct credentials and valid 6-digit MFA code; expect 200 with access_token, refresh_token, expires_in and secure cookies.\n10. Verify no tokens in window.localStorage or DOM; ensure all subsequent state-changing requests require X-CSRF-Token per NFR-05.\n11. Measure latency for verification and login calls to ensure p95 ≤ 1500 ms.", + "expectedResult": "Invalid and expired tokens are rejected; resend enforces rate limits; successful verification sets account verified; login remains blocked until verification completes; post-verification login succeeds with secure cookies.", + "endpoint": "POST /v2/auth/register, GET /v2/auth/verify, POST /v2/auth/verify/resend, POST /v2/auth/login", + "method": "POST, GET", + "apiPath": "/v2/auth/register, /v2/auth/verify, /v2/auth/verify/resend, /v2/auth/login", + "uiScreen": "Registration, Email Verification, Login", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true for register and resend, false for login and verify", + "headersUsed": "X-CSRF-Token, Authorization: Bearer for post-verify actions, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "201,200,400,403,429", + "responseKeysExpected": "user_id, verification_token, verified, access_token, refresh_token, expires_in, error, retry_after", + "businessRuleIds": "NFR-05, NFR-02", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "Registered then Verified", + "stateTransition": "Registered (unverified)->Verification Failed->Verification Resent->Verified->Logged In", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN; tokens not in DOM/localStorage; cookies HttpOnly Secure Strict", + "piiFields": "email user+verify@domain.com, phone +14165551234, DOB 1990-02-01, ssn_last4 1234", + "securityControls": "TLS1.3, HttpOnly Secure cookies, SameSite=Strict, CSRF required for POSTs, PKCE for portal", + "rateLimitExpectation": "Resend verification limited (e.g., 3/hour) with 429 on excess", + "assumptions": "ASSUMPTION-Login requires verified email returning 403 EMAIL_NOT_VERIFIED; ASSUMPTION-/v2/auth/verify/resend exists and enforces rate limits; ASSUMPTION-Verification token TTL is 24 hours" + }, + { + "type": "functional", + "title": "Account Summary: include_rewards Toggle, Owner-Only Access, and Not-Found Handling", + "description": "Verify GET /summary honors include_rewards flag, enforces owner-only access, and returns 404 for nonexistent account.", + "testId": "TC-AEGIS-026", + "testDescription": "Covers default omission of rewards, explicit include_rewards true behavior, 403 on cross-account access, 404 for missing account, and performance checks.", + "prerequisites": "Two users exist: owner user+summary@domain.com with account ACC-OWN and non-owner account ACC-OTHER; a nonexistent account ACC-NONE; rewards program active with nonzero points balance.", + "stepsToPerform": "1. Login as user+summary@domain.com with MFA; verify HttpOnly, Secure, SameSite=Strict cookies present.\n2. GET /v2/accounts/ACC-OWN/summary without query params; expect 200 and fields current_balance, available_credit, credit_limit, account_status; verify points_balance is absent by default when include_rewards is omitted.\n3. GET /v2/accounts/ACC-OWN/summary?include_rewards=true; expect 200 and points_balance present and numeric; verify account_status is Active or appropriate state.\n4. Validate that no PAN appears in the API response; UI shows masked PAN **** **** **** 1234 only if displayed (REQ-014).\n5. Attempt GET /v2/accounts/ACC-OTHER/summary using the same token; expect 403 FORBIDDEN with error FORBIDDEN and no data leakage.\n6. Attempt GET /v2/accounts/ACC-NONE/summary; expect 404 NOT_FOUND (ASSUMPTION-returns 404 for nonexistent account).\n7. Toggle include_rewards=false explicitly: GET /v2/accounts/ACC-OWN/summary?include_rewards=false; expect 200 and points_balance omitted.\n8. Repeat GET with include_rewards=true and measure latency; confirm p95 ≤ 1500 ms.\n9. Confirm Authorization header required by retrying the request without Bearer token; expect 401 UNAUTHORIZED.\n10. Ensure response fields adhere to schema and content types are application/json.", + "expectedResult": "Summary omits rewards by default and includes points_balance only when include_rewards=true; cross-account access returns 403; nonexistent account returns 404; no PAN exposed; requests meet performance expectations.", + "endpoint": "GET /v2/accounts/{account_id}/summary", + "method": "GET", + "apiPath": "/v2/accounts/ACC-OWN/summary, /v2/accounts/ACC-OWN/summary?include_rewards=true, /v2/accounts/ACC-OTHER/summary, /v2/accounts/ACC-NONE/summary", + "uiScreen": "Dashboard", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "false", + "headersUsed": "Authorization: Bearer, Accept: application/json", + "httpCodesExpected": "200,403,404,401", + "responseKeysExpected": "current_balance, available_credit, credit_limit, account_status, points_balance, error", + "businessRuleIds": "REQ-014, NFR-02", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in API; masked PAN in UI only", + "piiFields": "None beyond account balances", + "securityControls": "Owner-only access enforced, TLS1.3, JWT required", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-404 returned for nonexistent account IDs; ASSUMPTION-include_rewards=false omits points_balance field" + }, + { + "type": "functional", + "title": "Billing ADB Interest Detailed Calculation and Grace Period Boundary Across Cycles", + "description": "Validate REQ-009 ADB interest calculation with multi-day balances and REQ-010 grace period when previous statement paid in full.", + "testId": "TC-AEGIS-027", + "testDescription": "Simulates two consecutive billing cycles: Cycle A with previous balance paid in full to verify 0 interest (grace), and Cycle B with partial payment to verify exact ADB interest computation and statement accuracy tolerance.", + "prerequisites": "Test account ACC-ADB with APR=19.99% and sandbox time control; ability to post transactions and payments; statements API accessible; minimum rewards not relevant; CSRF token for payments.", + "stepsToPerform": "1. Login and GET /v2/accounts/ACC-ADB/summary to capture initial balances; ensure prior statement is marked paid in full (ASSUMPTION-flag available in statement detail or test fixture).\n2. Cycle A Day 1: POST /v2/accounts/ACC-ADB/transactions with transaction_amount=300.00, currency_code=CAD, mcc_code=5411, transaction_type=PURCHASE and X-CSRF-Token; expect 200.\n3. Cycle A Day 10: POST a second purchase for 200.00 with mcc_code=5812; expect 200.\n4. Advance sandbox to end of Cycle A (30-day cycle end) and GET /v2/accounts/ACC-ADB/statements/{statement_id}?format=JSON; verify interest_charged=0.00 due to grace (REQ-010) and total_spend equals sum within ±$0.01 (REQ-015).\n5. Make a partial payment before Cycle B due date: POST /v2/accounts/ACC-ADB/payments with payment_amount=200.00, payment_type=CUSTOM, bank_account_id=BANK-OK and X-CSRF-Token; expect 200.\n6. Cycle B Day 1: Carry remaining balance forward; no new purchases; record daily balance schedule.\n7. Cycle B Day 15: POST a purchase for 100.00 (CAD, mcc_code=5411); expect 200; for Days 1–14 balance is previous_cycle_unpaid, Days 15–30 includes +100.\n8. Cycle B Day 20: POST a payment of 50.00; expect 200; adjust daily balance for Days 20–30.\n9. At Cycle B end, GET /v2/accounts/ACC-ADB/statements/{statement_id_B}?format=JSON; compute ADB manually: sum of daily balances / 30; compute expected_interest=(ADB × 0.1999 / 365) × 30; compare to interest_charged within 1-cent rounding tolerance (REQ-009, REQ-015).\n10. Verify late_fee is 0.00 since payment_received_date <= due_date + 2 days (ensure no late payment in Cycle B), and rewards_earned uses floor only (REQ-013) but not central to interest assertion.\n11. Confirm content-type application/json and p95 latency ≤ 1500 ms.", + "expectedResult": "Cycle A charges no interest due to grace when previous balance was paid in full; Cycle B interest equals the precise ADB-based computation within tolerance; statement totals match transaction sums within ±$0.01; no late fee applied if not past due.", + "endpoint": "POST /v2/accounts/{account_id}/transactions, GET /v2/accounts/{account_id}/statements/{statement_id}, POST /v2/accounts/{account_id}/payments", + "method": "POST, GET", + "apiPath": "/v2/accounts/ACC-ADB/transactions, /v2/accounts/ACC-ADB/statements/{statement_id}, /v2/accounts/ACC-ADB/payments", + "uiScreen": "Transactions, Statements, Payments", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true for payments and transactions", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Accept: application/json", + "httpCodesExpected": "200", + "responseKeysExpected": "statement_date, total_spend, adb, interest_charged, late_fee, rewards_earned, minimum_payment_due, due_date, payment_id", + "businessRuleIds": "REQ-009, REQ-010, REQ-013, REQ-015, NFR-02", + "calculationsValidated": "ADB interest precise computation, statement sum tolerance ±$0.01", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "Cycle A (grace) -> Cycle B (interest accrual)", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in statements JSON; masked PAN only in UI", + "piiFields": "statement financial amounts only, no PAN", + "securityControls": "TLS1.3, CSRF required for state-changing POSTs", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-Sandbox time travel available; ASSUMPTION-APR is 19.99%; ASSUMPTION-30-day billing cycle; ASSUMPTION-Statement exposes adb and interest_charged fields" + }, + { + "type": "functional", + "title": "Application Step 3 Validation: Invalid card_product_id, Malformed e_signature, and Default vs Explicit Marketing Opt-in", + "description": "Validate Step 3 rejects invalid product selection and malformed base64 signature, and enforces marketing_opt_in default false.", + "testId": "TC-AEGIS-028", + "testDescription": "Executes Steps 1 and 2 successfully, then exercises Step 3 negative validations for card_product_id and e_signature format, followed by a successful submission with explicit marketing_opt_in true.", + "prerequisites": "Verified user user+submit@domain.com with MFA; CSRF token available; access to a valid card_product_id AEGIS_SILVER.", + "stepsToPerform": "1. Login via POST /v2/auth/login as user+submit@domain.com with MFA; expect 200 and secure cookies.\n2. POST /v2/applications/start with valid Step 1 data (email matches login, phone E.164, address with province=ON and postal_code=A1A 1A1, id_type=DRIVERS_LICENSE, id_number=DL222222) and X-CSRF-Token; expect 201 with application_id and session_token.\n3. POST /v2/applications/{application_id}/financials with employment_status=EMPLOYED, employer_name=Fabrikam, gross_annual_income=70000.00, monthly_rent=1200.00, existing_debt_payments=300.00, sin_consent=true and header X-App-Session: session_token; expect 200 with fico_pull_id and status PENDING_REVIEW.\n4. Attempt Step 3 with invalid card_product_id=UNKNOWN_TIER and a valid-looking base64 signature; POST /v2/applications/{application_id}/submit with X-CSRF-Token and X-App-Session; expect 400 error INVALID_PRODUCT_ID (ASSUMPTION-error code for invalid product).\n5. Attempt Step 3 with valid card_product_id=AEGIS_SILVER but malformed e_signature string 'not_base64@@@'; expect 400 error SIGNATURE_MALFORMED (ASSUMPTION-malformed base64 distinct from missing signature).\n6. Attempt Step 3 with card_product_id=AEGIS_SILVER and valid base64 of typed name (e.g., base64('Jane Verify')), marketing_opt_in omitted; expect 200 with decision present and marketing_opt_in=false by default.\n7. If decision is PENDING, show review_eta_hours; if APPROVED, capture credit_limit and card_number_masked; if DECLINED, capture reason_code; assert response adheres to decision table (>680 APPROVED, 600–680 PENDING, <600 DECLINED) per test user profile.\n8. Repeat Step 3 for a new application instance to set marketing_opt_in=true explicitly and confirm it's true in response and persisted in UI.\n9. Confirm session_token lifetime not exceeded (within 30 minutes) and CSRF header present on all POSTs.\n10. Validate no PAN appears in any application responses or network logs.", + "expectedResult": "Invalid product ID and malformed base64 signature each return 400 with appropriate error; a valid submission returns a decision per FICO logic with marketing_opt_in defaulting to false when omitted and true when explicitly set.", + "endpoint": "POST /v2/auth/login, POST /v2/applications/start, POST /v2/applications/{application_id}/financials, POST /v2/applications/{application_id}/submit", + "method": "POST", + "apiPath": "/v2/auth/login, /v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit", + "uiScreen": "Login, Application Step 1, Application Step 2, Application Step 3", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, X-App-Session, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,201,400", + "responseKeysExpected": "application_id, session_token, fico_pull_id, decision, credit_limit, card_number_masked, review_eta_hours, reason_code, error", + "businessRuleIds": "NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "ASSUMPTION-test user profile yields deterministic decision", + "decisionExpected": "Per decision table", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "N/A", + "stateTransition": "Step1->Step2->Step3 (invalid)->Step3 (valid)", + "reversalAction": "Reattempt Step 3 with corrected inputs", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in responses; masked PAN only when approved", + "piiFields": "email user+submit@domain.com, phone +14165551234, address A1A 1A1", + "securityControls": "TLS1.3, CSRF enforced, X-App-Session header required", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-Invalid product returns 400 INVALID_PRODUCT_ID; ASSUMPTION-malformed e_signature returns 400 SIGNATURE_MALFORMED" + }, + { + "type": "functional", + "title": "Freeze/Unfreeze OTP Attempt Limits, Resend OTP, and Successful Status Transitions", + "description": "Validate OTP failure handling with attempts_remaining, lockout behavior, OTP resend, and final success for Freeze and Unfreeze.", + "testId": "TC-AEGIS-029", + "testDescription": "Exercises PATCH /cards/status with invalid OTP attempts decreasing attempts_remaining to 0, verifies lockout/cooldown until a new OTP is issued, then completes Freeze and Unfreeze successfully with valid OTPs.", + "prerequisites": "Active card CARD-OTP belonging to user user+otp@domain.com; MFA enabled; CSRF token available; ability to request OTP via /v2/auth/otp/request.", + "stepsToPerform": "1. Login as user+otp@domain.com and GET /v2/accounts/{account_id}/summary; verify card_status=Active.\n2. Attempt to Freeze card via PATCH /v2/cards/CARD-OTP/status with status=Frozen, confirm_otp=111111 (invalid) and X-CSRF-Token; expect 401 OTP_FAILED with attempts_remaining=2 (ASSUMPTION-3 attempts allowed).\n3. Retry Freeze with another invalid OTP 222222; expect 401 OTP_FAILED with attempts_remaining=1.\n4. Retry Freeze with third invalid OTP 333333; expect 401 OTP_FAILED with attempts_remaining=0 and message indicating lockout/cooldown period (ASSUMPTION-cooldown applies until new OTP requested or time passes).\n5. Immediately attempt Freeze again without requesting a new OTP; expect 401 OTP_FAILED with attempts_remaining=0.\n6. Request a new OTP via POST /v2/auth/otp/request with purpose=card_status and channel=SMS or EMAIL; expect 200 otp_sent=true (ASSUMPTION-endpoint exists and purpose accepted).\n7. Use the newly delivered valid OTP and PATCH /v2/cards/CARD-OTP/status with status=Frozen and X-CSRF-Token; expect 200 with new_status=Frozen and updated_at.\n8. Attempt to Freeze again (idempotent same status) with valid OTP; expect 200 new_status=Frozen (no change) or 200 with idempotent acknowledgement (ASSUMPTION-idempotent behavior allowed).\n9. Request a new OTP and PATCH /v2/cards/CARD-OTP/status with status=Active and X-CSRF-Token; expect 200 new_status=Active.\n10. Verify attempts_remaining counter resets after a new OTP issuance by intentionally entering one invalid OTP on Unfreeze path before the valid one and seeing attempts_remaining decrement from 2.\n11. Confirm no PAN is exposed in any responses and that CSRF headers are present on all PATCH and OTP POST calls.", + "expectedResult": "OTP failures decrement attempts_remaining until 0 and block further attempts; requesting a new OTP resets the counter and allows successful Freeze/Unfreeze; idempotent Freeze re-attempt does not error; security headers and masking are enforced.", + "endpoint": "PATCH /v2/cards/{card_id}/status, POST /v2/auth/otp/request, GET /v2/accounts/{account_id}/summary", + "method": "PATCH, POST, GET", + "apiPath": "/v2/cards/CARD-OTP/status, /v2/auth/otp/request, /v2/accounts/{account_id}/summary", + "uiScreen": "Card Controls, Security Settings, Dashboard", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true for PATCH and OTP request", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,401", + "responseKeysExpected": "new_status, updated_at, error, attempts_remaining, otp_sent", + "businessRuleIds": "REQ-007, NFR-05", + "calculationsValidated": "None", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Frozen then Active", + "accountStatus": "Active", + "stateTransition": "Active->Frozen (after OTP reset)->Active", + "reversalAction": "Unfreeze as reversal of Freeze", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in responses; OTP codes not logged", + "piiFields": "phone or email used for OTP delivery (masked)", + "securityControls": "OTP gating with attempts_remaining, CSRF enforced, TLS1.3", + "rateLimitExpectation": "Standard gateway limits for PATCH/POST apply", + "assumptions": "ASSUMPTION-3 OTP attempts allowed per OTP; ASSUMPTION-/v2/auth/otp/request supports purpose=card_status; ASSUMPTION-idempotent same-status PATCH returns 200 without change" + }, + { + "type": "functional", + "title": "Transactions: CASH_ADVANCE and BALANCE_TRANSFER Types, MCC Format, and Description Boundary", + "description": "Validate POST /transactions supports CASH_ADVANCE and BALANCE_TRANSFER, enforces 4-digit MCC, rejects invalid transaction_type, and enforces description 255-char limit.", + "testId": "TC-AEGIS-031", + "testDescription": "Covers positive flows for CASH_ADVANCE and BALANCE_TRANSFER, and negatives for invalid MCC length, invalid transaction_type, and overlong description; also validates listings by category and PCI masking.", + "prerequisites": "User user+types@domain.com with active account and card in Active status; sufficient available_credit; user can login with MFA; CSRF token available; test MCCs 6011 (cash advance), 6012 (financial institution).", + "stepsToPerform": "1. Login via POST /v2/auth/login with MFA as user+types@domain.com; verify 200 and secure cookies.\n2. GET /v2/accounts/{account_id}/summary to capture available_credit and card_status=Active.\n3. POST /v2/accounts/{account_id}/transactions with transaction_amount=50.00, merchant_name=ATM Kiosk, merchant_id=ATM001, mcc_code=6011, currency_code=CAD, transaction_type=CASH_ADVANCE, description=\"Cash advance at ATM\" and X-CSRF-Token; expect 200 with transaction_id and auth_code.\n4. POST /v2/accounts/{account_id}/transactions with transaction_amount=25.00, merchant_name=Bank Transfer, merchant_id=BTX001, mcc_code=6012, currency_code=CAD, transaction_type=BALANCE_TRANSFER, description=\"Promo balance transfer\" and X-CSRF-Token; expect 200 with transaction_id.\n5. Attempt POST with invalid mcc_code=123 (3 digits), transaction_type=PURCHASE, valid amount/fields; expect 400 with field=mcc_code and validation message for 4-digit ISO 18245 MCC.\n6. Attempt POST with invalid transaction_type=PURCHAZE (typo), mcc_code=5411, valid amount; expect 400 with field=transaction_type and message enumerated values only.\n7. Attempt POST with description length 256 chars (one over max) and valid PURCHASE; expect 400 with field=description and message max length 255.\n8. POST with description exactly 255 chars, PURCHASE, mcc_code=5411, amount=5.00; expect 200 approved with transaction_id.\n9. GET /v2/accounts/{account_id}/transactions?category=CASH_ADVANCE; expect 200 and confirm the cash advance from step 3 appears in results.\n10. Verify all responses contain no raw PAN, only masked if displayed in UI; confirm p95 latency ≤ 1500 ms for API calls.", + "expectedResult": "CASH_ADVANCE and BALANCE_TRANSFER both approve with 200; invalid 3-digit MCC and invalid transaction_type return 400 with field errors; description length >255 is rejected, 255 is accepted; category filter returns the cash advance; no PAN exposure and performance meets NFR.", + "endpoint": "POST /v2/accounts/{account_id}/transactions, GET /v2/accounts/{account_id}/transactions, GET /v2/accounts/{account_id}/summary", + "method": "POST, GET", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/transactions?category=CASH_ADVANCE, /v2/accounts/{account_id}/summary", + "uiScreen": "Transactions, Dashboard", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Accept: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,400", + "responseKeysExpected": "transaction_id, available_credit, auth_code, error, field, message", + "businessRuleIds": "NFR-02, NFR-05", + "calculationsValidated": "None", + "mccCode": "6011-cash advance, 6012-financial institution, 5411-grocery", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in any response, masked PAN only in UI", + "piiFields": "merchant_name synthetic only", + "securityControls": "TLS1.3, CSRF required on POST, owner-only access enforced", + "rateLimitExpectation": "Stay under 10 transactions/60m to avoid 429", + "assumptions": "ASSUMPTION-Server returns 400 with field-level errors for invalid mcc_code and invalid transaction_type" + }, + { + "type": "functional", + "title": "Credit Application Step 1 Address and Identity Validation: Province ISO, Postal Code A1A 1A1, ID Length, Email Match, E.164 Phone", + "description": "Validate strict Step 1 field validations for province ISO 3166-2, Canadian postal code format, id_number max length, email must match login, and E.164 phone format.", + "testId": "TC-AEGIS-032", + "testDescription": "Executes multiple negative corrections before a successful Step 1 submission, ensuring address and identity rules are enforced without creating duplicate applications.", + "prerequisites": "Verified user user+step1val@domain.com with MFA, logged in to portal; CSRF token available; no active application in progress for this user.", + "stepsToPerform": "1. Login via POST /v2/auth/login as user+step1val@domain.com with MFA; expect 200 and secure cookies.\n2. Attempt POST /v2/applications/start with full_legal_name=\"Jane Applicant\", email different from login (user+mismatch@domain.com), phone_number=+14165551234, residential_address.province=\"Ontario\", postal_code=\"12345\", id_type=DRIVERS_LICENSE, id_number=\"DL123456789012345678901\" (23 chars) and X-CSRF-Token; expect 400 with field=address.province or postal_code and explicit validation messages.\n3. Resubmit with province=\"ON\" but postal_code=\"A1A1A1\" (missing space); expect 400 with field=address.postal_code requiring A1A 1A1 format with space.\n4. Resubmit with postal_code=\"A1A 1A1\" but keep id_number length >20; expect 400 with field=id_number length limit.\n5. Resubmit with id_number truncated to 20 but phone_number=\"14165551234\" (missing '+'); expect 400 with field=phone_number E.164 violation.\n6. Resubmit with phone_number=\"+14165551234\" but email still user+mismatch@domain.com; expect 400 with field=email must match authenticated user email.\n7. Resubmit with email matching login, and full_legal_name of 101 chars; expect 400 with field=full_legal_name max length 100.\n8. Final resubmission with all valid fields: full_legal_name=\"Jane Applicant\", email=user+step1val@domain.com, phone_number=+14165551234, province=ON, postal_code=A1A 1A1, id_type=PR_CARD, id_number=DL12345678 and X-CSRF-Token; expect 201 with application_id and session_token.\n9. Verify response headers and UI show no raw PAN or sensitive identifiers; ensure tokens are in HttpOnly cookies only.\n10. Confirm p95 ≤ 1500 ms for POST success response.", + "expectedResult": "Province requires 2-char ISO code; postal_code must be A1A 1A1 with space; id_number max 20 enforced; phone requires E.164 with '+'; email must match login; final valid submission returns 201 with application_id and session_token; no PII/PAN leakage.", + "endpoint": "POST /v2/applications/start", + "method": "POST", + "apiPath": "/v2/applications/start", + "uiScreen": "Application Step 1", + "role": "Applicant", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Content-Type: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "201,400", + "responseKeysExpected": "application_id, session_token, error, field, message", + "businessRuleIds": "NFR-05", + "calculationsValidated": "None", + "mccCode": "N/A", + "currencyCode": "N/A", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "N/A", + "cardStatusAfter": "N/A", + "accountStatus": "N/A", + "stateTransition": "Unauthenticated->LoggedIn->Step1 Created", + "reversalAction": "None", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in DOM or responses; tokens not stored in localStorage", + "piiFields": "email user+step1val@domain.com, phone +14165551234, address A1A 1A1, id_number masked in UI", + "securityControls": "TLS1.3, CSRF on POST, SameSite=Strict, HttpOnly cookies", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-Validation errors return field and message indicating exact rule violated" + }, + { + "type": "functional", + "title": "Owner-Only Card Controls and Report-Lost: IDOR Prevention and Invalid Status Value Handling", + "description": "Ensure PATCH /cards/status and POST /cards/report-lost enforce owner-only access (403 on other users' card_id) and reject invalid status values outside {Active, Frozen}.", + "testId": "TC-AEGIS-033", + "testDescription": "Attempts to operate on another user's card fail with 403; own card operations require OTP; attempting to set status to Blocked via PATCH is rejected; valid Freeze/Unfreeze flows succeed after OTP.", + "prerequisites": "Two users: user+idor1@domain.com owning card_id C-OWN, and user+idor2@domain.com owning card_id C-OTHER; OTP delivery working; CSRF token available.", + "stepsToPerform": "1. Login as user+idor1@domain.com with MFA; verify secure cookies and obtain X-CSRF-Token.\n2. Confirm own card status via GET /v2/accounts/{account_id}/summary; record card_status=Active and card_id=C-OWN.\n3. Attempt PATCH /v2/cards/C-OTHER/status with body {status: Frozen, confirm_otp: 123456} and X-CSRF-Token; expect 403 FORBIDDEN (IDOR prevented).\n4. Attempt POST /v2/cards/C-OTHER/report-lost with loss_type=LOST and X-CSRF-Token; expect 403 FORBIDDEN.\n5. Attempt PATCH /v2/cards/C-OWN/status with status=Blocked (not allowed) and a valid OTP; expect 400 error=INVALID_TRANSITION with allowed_transitions showing Active<->Frozen.\n6. Attempt PATCH /v2/cards/C-OWN/status with status=Frozen and invalid confirm_otp=000000; expect 401 OTP_FAILED with attempts_remaining.\n7. Request a new OTP via POST /v2/auth/otp/request purpose=card_status; expect 200 otp_sent=true.\n8. PATCH /v2/cards/C-OWN/status with status=Frozen and valid confirm_otp; expect 200 with new_status=Frozen.\n9. PATCH /v2/cards/C-OWN/status with status=Active and valid confirm_otp; expect 200 with new_status=Active.\n10. Verify no PAN present in any response payloads and that X-CSRF-Token was included for all state-changing requests; confirm API p95 ≤ 1500 ms.", + "expectedResult": "Cross-account card operations return 403; PATCH with invalid status value is rejected with 400; OTP invalid attempts return 401; valid Freeze and Unfreeze succeed with 200; PCI masking and CSRF enforcement intact.", + "endpoint": "PATCH /v2/cards/{card_id}/status, POST /v2/cards/{card_id}/report-lost, POST /v2/auth/otp/request, GET /v2/accounts/{account_id}/summary", + "method": "PATCH, POST, GET", + "apiPath": "/v2/cards/C-OTHER/status, /v2/cards/C-OTHER/report-lost, /v2/cards/C-OWN/status, /v2/auth/otp/request, /v2/accounts/{account_id}/summary", + "uiScreen": "Card Controls, Security Settings, Dashboard", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,400,401,403", + "responseKeysExpected": "new_status, updated_at, error, allowed_transitions, attempts_remaining, otp_sent", + "businessRuleIds": "REQ-007, NFR-05", + "calculationsValidated": "None", + "mccCode": "N/A", + "currencyCode": "N/A", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "Active->Frozen->Active (own card), no change for other user's card", + "reversalAction": "Unfreeze as reversal of Freeze", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in any response; masked PAN only in UI if shown", + "piiFields": "phone or email for OTP delivery (masked)", + "securityControls": "Owner-only access enforced, OTP gating, CSRF required, TLS1.3", + "rateLimitExpectation": "Standard gateway limits", + "assumptions": "ASSUMPTION-/v2/auth/otp/request supports purpose=card_status and returns otp_sent" + }, + { + "type": "functional", + "title": "Rewards 1x Accrual and Floor Rounding for Non-Travel MCCs, plus CAD Travel 3x Verification", + "description": "Validate REQ-012 and REQ-013 for non-travel purchases earning 1x with floor rounding and travel MCC earning 3x even in CAD.", + "testId": "TC-AEGIS-034", + "testDescription": "Posts small CAD purchases to exercise floor() rounding for 1x categories and verifies 3x for travel MCC; checks include_rewards behavior and points deltas.", + "prerequisites": "Active account with rewards enabled and points balance available; user user+rewards@domain.com can login with MFA; CSRF token available; card Active.", + "stepsToPerform": "1. Login as user+rewards@domain.com and navigate to Dashboard; ensure secure cookies and acquire X-CSRF-Token.\n2. GET /v2/accounts/{account_id}/summary?include_rewards=true; record baseline points_balance P0.\n3. POST /v2/accounts/{account_id}/transactions with amount=0.99, currency=CAD, mcc_code=5411 (grocery), transaction_type=PURCHASE, merchant_name=Grocer A and X-CSRF-Token; expect 200.\n4. POST another CAD grocery purchase amount=1.01, mcc_code=5411, merchant_name=Grocer B; expect 200.\n5. POST CAD restaurant purchase amount=2.99, mcc_code=5812, merchant_name=Cafe C; expect 200.\n6. GET /v2/accounts/{account_id}/summary?include_rewards=true; compute expected delta for non-travel: floor(0.99*1)+floor(1.01*1)+floor(2.99*1)=0+1+2=3; assert points_balance == P0 + 3.\n7. GET /v2/accounts/{account_id}/transactions?category=PURCHASE&from_date=today&to_date=today; verify three purchases are present.\n8. POST CAD travel purchase amount=10.00, mcc_code=7011 (travel), merchant_name=Hotel X; expect 200.\n9. GET /v2/accounts/{account_id}/summary?include_rewards=true; assert points increased by additional floor(10.00*3)=30 (P0 + 3 + 30 total from baseline).\n10. POST CAD grocery micro purchase amount=0.49, mcc_code=5411; expect 200, then GET summary to confirm no points increase due to floor(0.49*1)=0; verify p95 ≤ 1500 ms; ensure no raw PAN in any responses.", + "expectedResult": "Non-travel purchases accrue 1x with floor rounding (0,1,2 total 3 points); CAD travel purchase accrues 3x (30 points for $10); micro 0.49 purchase adds 0 points; transaction history and summary reflect correct totals; no PAN exposure.", + "endpoint": "POST /v2/accounts/{account_id}/transactions, GET /v2/accounts/{account_id}/summary, GET /v2/accounts/{account_id}/transactions", + "method": "POST, GET", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/summary?include_rewards=true, /v2/accounts/{account_id}/transactions?category=PURCHASE&from_date=YYYY-MM-DD&to_date=YYYY-MM-DD", + "uiScreen": "Dashboard, Transactions", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Accept: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200", + "responseKeysExpected": "transaction_id, points_balance, available_credit, auth_code", + "businessRuleIds": "REQ-012, REQ-013, NFR-02", + "calculationsValidated": "Rewards 1x and 3x with floor rounding on CAD amounts", + "mccCode": "5411-grocery, 5812-restaurant, 7011-travel", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Active", + "accountStatus": "Active", + "stateTransition": "N/A", + "reversalAction": "N/A", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No raw PAN in responses; masked PAN only in UI if displayed", + "piiFields": "merchant_name synthetic only", + "securityControls": "Owner-only access, TLS1.3, CSRF enforced", + "rateLimitExpectation": "Stay within 10 tx/60m to avoid 429", + "assumptions": "ASSUMPTION-Rewards computed on CAD amount regardless of MCC; travel MCC 7011 qualifies for 3x in CAD" + }, + { + "type": "functional", + "title": "Right to Rescind: Post-Deletion Access Restriction, Endpoint Idempotency, and Not-Found Propagation", + "description": "Validate that after DELETE /accounts/{id} within 14 days (REQ-016), subsequent account endpoints return 404, and repeated DELETE is idempotent (treated as not found).", + "testId": "TC-AEGIS-035", + "testDescription": "Executes rescind within window, then verifies all related endpoints deny access with 404; confirms second DELETE returns 404 and no further transactions or payments can be posted.", + "prerequisites": "Test account ACC-RESC within 10 days of issuance; user user+rescind@domain.com can login with MFA; CSRF token available; no pending critical holds; card status Active.", + "stepsToPerform": "1. Login as user+rescind@domain.com with MFA; verify 200 and secure cookies.\n2. GET /v2/accounts/ACC-RESC/summary to confirm account is Active and capture baseline fields.\n3. DELETE /v2/accounts/ACC-RESC with X-CSRF-Token; expect success (200) and confirmation message (if provided) that account closed under right to rescind.\n4. Immediately GET /v2/accounts/ACC-RESC/summary; expect 404 NOT_FOUND.\n5. GET /v2/accounts/ACC-RESC/transactions; expect 404 NOT_FOUND and no data leakage.\n6. POST /v2/accounts/ACC-RESC/transactions with any valid purchase payload; expect 404 NOT_FOUND.\n7. POST /v2/accounts/ACC-RESC/payments with valid bank_account_id and amount; expect 404 NOT_FOUND.\n8. GET /v2/accounts/ACC-RESC/statements/{statement_id}; expect 404 NOT_FOUND.\n9. Repeat DELETE /v2/accounts/ACC-RESC with X-CSRF-Token; expect 404 NOT_FOUND (idempotent after closure) and no further effect.\n10. Verify no raw PAN present in any error responses, ensure X-CSRF-Token was used for DELETE, and confirm API p95 ≤ 1500 ms across calls.", + "expectedResult": "Account deletion within 14-day window succeeds; all subsequent account-related endpoints return 404; repeated DELETE returns 404 idempotently; no PII/PAN leakage and CSRF is enforced.", + "endpoint": "DELETE /v2/accounts/{account_id}, GET /v2/accounts/{account_id}/summary, GET /v2/accounts/{account_id}/transactions, POST /v2/accounts/{account_id}/transactions, POST /v2/accounts/{account_id}/payments, GET /v2/accounts/{account_id}/statements/{statement_id}", + "method": "DELETE, GET, POST", + "apiPath": "/v2/accounts/ACC-RESC, /v2/accounts/ACC-RESC/summary, /v2/accounts/ACC-RESC/transactions, /v2/accounts/ACC-RESC/payments, /v2/accounts/ACC-RESC/statements/{statement_id}", + "uiScreen": "Dashboard, Transactions, Statements, Payments", + "role": "Cardholder", + "authType": "OAuth2-JWT", + "csrfTokenUsed": "true", + "headersUsed": "Authorization: Bearer, X-CSRF-Token, Accept: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "httpCodesExpected": "200,404", + "responseKeysExpected": "error, message, payment_id (absent on 404), transaction_id (absent on 404)", + "businessRuleIds": "REQ-016, NFR-05, NFR-02", + "calculationsValidated": "None", + "mccCode": "N/A", + "currencyCode": "CAD", + "exchangeRateUsed": "N/A", + "ficoScoreUsed": "N/A", + "decisionExpected": "N/A", + "cardStatusBefore": "Active", + "cardStatusAfter": "Closed", + "accountStatus": "Closed after rescind", + "stateTransition": "Active -> Closed (rescinded within 14 days)", + "reversalAction": "None (closure is final)", + "performanceExpectation": "API p95 <= 1500ms", + "maskingCheck": "No PAN in any error or success payloads", + "piiFields": "None beyond generic error messages", + "securityControls": "CSRF required for DELETE, SameSite=Strict, TLS1.3", + "rateLimitExpectation": "Standard gateway limits apply", + "assumptions": "ASSUMPTION-DELETE success returns 200; subsequent DELETE returns 404; all child endpoints for a deleted account return 404" + } +] \ No newline at end of file diff --git a/functional_tests/functional-test-generation/functional-test-generation.xlsx b/functional_tests/functional-test-generation/functional-test-generation.xlsx new file mode 100644 index 0000000..47c03c2 Binary files /dev/null and b/functional_tests/functional-test-generation/functional-test-generation.xlsx differ