From 9b1ceb93debf027b0ee289c037f5617dce0b84a8 Mon Sep 17 00:00:00 2001 From: roost-io <16ucs063@lnmiit.ac.in> Date: Tue, 28 Apr 2026 07:19:28 +0000 Subject: [PATCH] Functional test generated by RoostGPT Using AI Model gpt-5 --- functional_tests/README.md | 17 + .../.roost/roost_metadata.json | 24 + .../functional-test-aegis.csv | 29 + .../functional-test-aegis.docx | Bin 0 -> 60244 bytes .../functional-test-aegis.feature | 931 +++++++++++++++ .../functional-test-aegis.json | 1027 +++++++++++++++++ .../functional-test-aegis.xlsx | Bin 0 -> 49257 bytes 7 files changed, 2028 insertions(+) create mode 100644 functional_tests/functional-test-aegis/.roost/roost_metadata.json create mode 100644 functional_tests/functional-test-aegis/functional-test-aegis.csv create mode 100644 functional_tests/functional-test-aegis/functional-test-aegis.docx create mode 100644 functional_tests/functional-test-aegis/functional-test-aegis.feature create mode 100644 functional_tests/functional-test-aegis/functional-test-aegis.json create mode 100644 functional_tests/functional-test-aegis/functional-test-aegis.xlsx diff --git a/functional_tests/README.md b/functional_tests/README.md index 1c972b2..485e3e5 100644 --- a/functional_tests/README.md +++ b/functional_tests/README.md @@ -65,3 +65,20 @@ --- +**Execution Date:** 4/28/2026, 7:19:28 AM + +**Test Unique Identifier:** "functional-test-aegis" + +**Input(s):** + 1. Aegis_WebCC_SRS.pdf + Path: /var/tmp/Roost/RoostGPT/functional-test-aegis/d6bf0345-379c-48af-b81d-1f8f88aaab5e/Aegis_WebCC_SRS.pdf + +**Test Output Folder:** + 1. [functional-test-aegis.json](functional-test-aegis/functional-test-aegis.json) + 2. [functional-test-aegis.feature](functional-test-aegis/functional-test-aegis.feature) + 3. [functional-test-aegis.csv](functional-test-aegis/functional-test-aegis.csv) + 4. [functional-test-aegis.xlsx](functional-test-aegis/functional-test-aegis.xlsx) + 5. [functional-test-aegis.docx](functional-test-aegis/functional-test-aegis.docx) + +--- + diff --git a/functional_tests/functional-test-aegis/.roost/roost_metadata.json b/functional_tests/functional-test-aegis/.roost/roost_metadata.json new file mode 100644 index 0000000..1c6a258 --- /dev/null +++ b/functional_tests/functional-test-aegis/.roost/roost_metadata.json @@ -0,0 +1,24 @@ +{ + "project": { + "name": "functional-test-aegis", + "created_at": "2026-04-28T07:19:28.118Z", + "updated_at": "2026-04-28T07:19:28.118Z" + }, + "files": { + "input_files": [ + { + "fileName": "functional-test-aegis.txt", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-aegis/d6bf0345-379c-48af-b81d-1f8f88aaab5e/functional_tests/functional-test-aegis/functional-test-aegis.txt", + "fileSha": "cf83e1357e" + }, + { + "fileName": "Aegis_WebCC_SRS.pdf", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-aegis/d6bf0345-379c-48af-b81d-1f8f88aaab5e/functional_tests/functional-test-aegis/Aegis_WebCC_SRS.pdf", + "fileSha": "dcebdb1a12" + } + ] + }, + "api_files": { + "input_files": [] + } +} \ No newline at end of file diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.csv b/functional_tests/functional-test-aegis/functional-test-aegis.csv new file mode 100644 index 0000000..0a3f51c --- /dev/null +++ b/functional_tests/functional-test-aegis/functional-test-aegis.csv @@ -0,0 +1,29 @@ +End-to-End Applicant Journey with Security Controls (Register -> Verify -> Login MFA -> Application -> APPROVED -> Token Rotation -> CSRF Enforcement -> PIN -> Summary -> Session Timeout) +Registration Validations and Case-Insensitive Email Uniqueness +Authentication Lockout, Rate Limit, Remember Me TTL, Logout Invalidation +Refresh Token Rotation Concurrency Across Tabs and CSRF Session Binding +Access Control and CSRF Enforcement Matrix with SameSite=Strict +Foreign Currency Purchase, Rewards, Pagination, CSRF, Rate Limit, Statements and Grace +Transaction Field Validation and FX Precision +Valid Small-Value FX with Rounding and Listing Page Validation +Transactions Date/Time Boundaries, UTC/DST, per_page Max, Stable Pagination +Rewards Accrual Boundary and MCC Classification with Floor Rounding +Transaction Rate-Limit Recovery via MFA +WebSocket Live Feed Resilience and Authorization +Card Controls Freeze/Unfreeze with OTP and CSRF, Transactions Blocked While Frozen +Report Card STOLEN with Delivery Address Override, OTP, Audit, Irreversibility +Payments, Late Fee, Interest/Grace, Scheduling, CSRF, Rescind Window +Payments CUSTOM Boundary and CSRF +Statement Calculations Boundary: Interest Rounding, Grace, Late Fee UTC Boundary, Not Found +Application Step Order Enforcement, Session Token Validation, PENDING and DECLINED +Application Step 2 Credit Pull: sin_consent Enforcement, 503 Retry Idempotency, Session Expiry +Application Step 1 Validation, Autosave Privacy and Cross-User Isolation +Notifications Webhook Validation, PII Masking, Severity Rendering +Transaction Currency and Field Validation with Success at Precision Boundaries +CSRF Regeneration After Refresh Rotation +Phone OTP Verification Flow with Expiry, Resend Throttle, Attempts Remaining +Web-Based PIN Set Edge Cases: Numeric Format, Leading Zeros, OTP Expiry, Attempts Throttle, CSRF +Audit Trail for Credit Limit Changes and Access Control +Right to Rescind Security Sweep Post-Delete +Session Timeout Warning - Stay Signed In Flow and Auto-Logout +PAN Masking and Iframe Tokenization Compliance Across Portal Surfaces \ No newline at end of file diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.docx b/functional_tests/functional-test-aegis/functional-test-aegis.docx new file mode 100644 index 0000000000000000000000000000000000000000..ae139d383c1ab31f0378d6262392d71a87bf0bc9 GIT binary patch literal 60244 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 Verify -> Login MFA -> Application -> APPROVED -> Token Rotation -> CSRF Enforcement -> PIN -> Summary -> Session Timeout) + Given there is no existing account for 'user@example.com' and MFA seed is provisioned + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"Jane", + "last_name":"Public", + "email":"user@example.com", + "password":"Str0ng-P@ssword!2026", + "date_of_birth":"", + "phone_number":"+14165550123", + "ssn_last4":"1234", + "agree_terms":true + } + """ + Then the response status should be 201 + And the response JSON should contain fields 'user_id' and 'verification_token' + + When I send a POST request to '/auth/verify' with JSON payload + """ + { "verification_token": "" } + """ + Then the response status should be 200 + + When I send a POST request to '/auth/login' with JSON payload + """ + { + "email":"user@example.com", + "password":"Str0ng-P@ssword!2026", + "device_id":"550e8400-e29b-41d4-a716-446655440000", + "remember_me":true, + "mfa_code":"" + } + """ + Then the response status should be 200 + And HttpOnly, Secure, SameSite=Strict cookies for access_token and refresh_token should be set + And no tokens should exist in localStorage or be accessible to JavaScript + + When I send a GET request to '/auth/csrf' + Then the response status should be 200 + And I save the 'X-CSRF-Token' value for subsequent requests + + When I send a POST request to '/applications/start' with headers 'X-CSRF-Token' and JSON payload + """ + { + "full_legal_name":"Jane Q Public", + "email":"user@example.com", + "phone_number":"+14165550123", + "residential_address":{ + "street":"123 King St", + "city":"Toronto", + "province":"ON", + "postal_code":"A1A 1A1" + }, + "id_type":"PASSPORT", + "id_number":"X1234567" + } + """ + Then the response status should be 201 + And the response JSON should contain fields 'application_id' and 'session_token' + + When I immediately resend the same POST to '/applications/start' with the same payload and CSRF + Then the response status should be 409 + And the error code should be 'DUPLICATE_APPLICATION' + + When I send a POST request to '/auth/otp/request' with JSON payload + """ + {"channel":"SMS","purpose":"PHONE_VERIFY"} + """ + And I send a POST request to '/auth/otp/verify' with JSON payload + """ + {"code":""} + """ + Then the response status should be 200 + + When I wait for autosave interval to elapse and reload the application page + Then only non-sensitive draft fields should be restored in the UI + And no ssn_last4, tokens, or PAN should be present in DOM or localStorage + + When I send a POST request to '/applications//financials' with headers 'X-CSRF-Token' and 'X-App-Session' and JSON payload + """ + { + "employment_status":"EMPLOYED", + "employer_name":"Aegis Ltd", + "gross_annual_income":85000.00, + "monthly_rent":1500.00, + "existing_debt_payments":300.00, + "sin_consent":true + } + """ + Then the response status should be 200 + And the response JSON should contain fields 'status' with value 'PENDING_REVIEW' and 'fico_pull_id' + + When I send a POST request to '/applications//financials' with headers 'X-CSRF-Token' and 'X-App-Session' and JSON payload + """ + { + "employment_status":"EMPLOYED", + "gross_annual_income":85000.00, + "monthly_rent":1500.00, + "existing_debt_payments":300.00, + "sin_consent":true + } + """ + Then the response status should be 400 + And the error field should indicate 'employer_name' is required + + When I send a POST request to '/applications//submit' with headers 'X-CSRF-Token' and 'X-App-Session' and JSON payload + """ + { + "card_product_id":"AEGIS_GOLD", + "e_signature":"SmFuZSBRIFB1YmxpYw==" + } + """ + Then the response status should be 200 + And the decision should be 'APPROVED' + And the response should include 'credit_limit' and 'card_number_masked' matching '**** **** **** ####' + And no full PAN should be present in the response or DOM + + When I send a POST request to '/auth/token/refresh' + Then the response status should be 200 + And new rotated refresh_token and access_token cookies should be set + + When I send a POST request to '/auth/token/refresh' reusing the previous refresh_token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + + When I send a PUT request to '/cards//pin' without 'X-CSRF-Token' and JSON payload + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + + When I send a PUT request to '/cards//pin' with headers 'X-CSRF-Token' and JSON payload + """ + {"new_pin":"1234","confirm_pin":"4321","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + + When I send a PUT request to '/cards//pin' with headers 'X-CSRF-Token' and JSON payload + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 200 + And the response JSON should contain 'updated_at' + + When I send a GET request to '/accounts//summary' + Then the response status should be 200 + And 'available_credit', 'credit_limit', and 'account_status' should be present + And rewards are excluded by default + + When I send a GET request to '/accounts//summary?include_rewards=true' + Then the response status should be 200 + And 'points_balance' should be present + + When I remain idle in the portal for 13 minutes + Then a session-timeout warning modal should appear + + When I remain idle until 15 minutes and invoke a protected API + Then the response status should be 401 + And the UI should be auto-logged out + + When I login again with MFA and fetch the application/account artifacts + Then previously created application and account are accessible + And no sensitive data is persisted in frontend storage + + @api + Scenario Outline: Registration Validations and Case-Insensitive Email Uniqueness + Given the API enforces registration field rules + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"John", + "last_name":"Public", + "email":"", + "password":"", + "date_of_birth":"", + "phone_number":"", + "ssn_last4":"", + "agree_terms": + } + """ + Then the response status should be + And the response should contain or omit 'verification_token' as '' + And the error code should be '' + + Examples: + | email | password | date_of_birth | phone | ssn_last4 | agree_terms | status | verification_token_expected | error_code | + | new_user@example.com| Str0ngPass!2026 | 2008-01-01 | +14165550123 | 1234 | false | 400 | absent | TERMS_REQUIRED | + | new_user@example.com| Short1! | 2000-01-01 | +14165550123 | 1234 | true | 422 | absent | WEAK_PASSWORD | + | new_user@example.com| Str0ngPass!2026 | <17y_364d_ago> | +14165550123 | 1234 | true | 400 | absent | DOB_INVALID | + | new_user@example.com| Str0ngPass!2026 | 2000-01-01 | 4165550123 | 1234 | true | 400 | absent | PHONE_INVALID | + | new_user@example.com| Str0ngPass!2026 | 2000-01-01 | +14165550123 | 123 | true | 400 | absent | SSN_LAST4_INVALID | + | user@example.com | Str0ngPass!2026 | | +14165550123 | 1234 | true | 201 | present | | + | USER@EXAMPLE.COM | Str0ngPass!2026 | | +14165550123 | 1234 | true | 409 | absent | EMAIL_EXISTS | + + @api @auth + Scenario: Authentication Lockout, Rate Limit, Remember Me TTL, Logout Invalidation + Given the user 'user@example.com' exists with MFA enabled + When I perform 4 POST requests to '/auth/login' with wrong password within 60 seconds + Then each response status should be 401 + And the error code should be 'INVALID_CREDENTIALS' + When I perform a 5th POST to '/auth/login' with wrong password + Then the response status should be 403 + And the error code should be 'ACCOUNT_LOCKED' + When I attempt a correct login with valid TOTP during lockout + Then the response status should be 403 + And the error code should be 'ACCOUNT_LOCKED' + When I wait until 'unlock_at' plus 1 minute and login with remember_me true + Then the response status should be 200 + And refresh cookie expiry should be approximately 30 days + When I send 10 more login requests within a minute and a 11th attempt + Then the 11th response status should be 429 + And the error code should be 'RATE_LIMITED' + And the 'retry_after' header should be present + When I send a POST to '/auth/logout' with a valid 'X-CSRF-Token' + Then the response status should be 200 + And access and refresh cookies should be cleared + When I send POST '/auth/token/refresh' using the pre-logout refresh token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I GET '/accounts//summary' without fresh login + Then the response status should be 401 + + @api @auth + Scenario: Refresh Token Rotation Concurrency Across Tabs and CSRF Session Binding + Given Tab A and Tab B share an initial authenticated session for 'user@example.com' + When Tab A sends POST '/auth/token/refresh' + Then Tab A receives 200 and rotated refresh_token R1-new + When Tab B sends POST '/auth/token/refresh' with the now-stale refresh token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When Tab A GETs '/auth/csrf' and stores CSRF-A and sends POST '/auth/logout' with CSRF-A + Then Tab A receives 200 and cookies are expired + When Tab B sends POST '/auth/logout' using CSRF-A + Then the response status should be 403 + And the error code should be 'CSRF_INVALID' + When Tab B logs in and obtains CSRF-B then calls POST '/auth/token/refresh' twice in quick succession + Then the first refresh returns 200 and rotates tokens + And the second refresh returns 401 with 'TOKEN_INVALID' + And no tokens are present in localStorage or sessionStorage in either tab + + @api @csrf + Scenario: Access Control and CSRF Enforcement Matrix with SameSite=Strict + Given I am logged out + When I GET '/accounts//summary' without auth + Then the response status should be 401 + When I login and GET '/auth/csrf' and store CSRF-1 + And I GET '/accounts//summary' + Then the response status should be 200 + When I GET '/accounts//summary' + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + When I POST '/accounts//transactions' without 'X-CSRF-Token' and JSON payload + """ + {"transaction_amount":5.00,"mcc_code":5999,"merchant_name":"Test","merchant_id":"T001","transaction_type":"PURCHASE"} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I retry the same POST with 'X-CSRF-Token' CSRF-1 + Then the response status should be 200 + And 'transaction_id' should be present + When a cross-site form POST is submitted from a different origin without credentials + Then the API returns 401 and no side effects occur + And all cookies have Secure, HttpOnly, SameSite=Strict flags + + @api @transactions + Scenario: Foreign Currency Purchase, Rewards, Pagination, CSRF, Rate Limit, Statements and Grace + Given I am authenticated with a valid 'X-CSRF-Token' and rewards baseline captured + And I connect to the WebSocket stream with a valid JWT and subscribe to 'account::transactions' + When I POST '/accounts//transactions' with 'X-CSRF-Token' and JSON payload + """ + { + "transaction_amount":100.00, + "currency_code":"USD", + "exchange_rate":1.250000, + "mcc_code":3000, + "merchant_name":"Aegis Air", + "merchant_id":"AA123", + "transaction_type":"PURCHASE", + "description":"Flight" + } + """ + Then the response status should be 200 + And 'foreign_fee_amount' should equal 3.75 and 'total_cad' should equal 128.75 + And a WebSocket event for the transaction should be received with masked PAN and no PII + When I disconnect and reconnect the WebSocket without JWT + Then the connection is rejected with 401 or 403 + When I POST another PURCHASE in CAD with JSON payload + """ + { + "transaction_amount":10.99, + "mcc_code":5999, + "merchant_name":"Shop", + "merchant_id":"S001", + "transaction_type":"PURCHASE" + } + """ + Then the response status should be 200 + And rewards expected increment is Travel=floor(128.75*3)=386 and Other=floor(10.99*1)=10 + When I POST a transaction without 'X-CSRF-Token' + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I GET '/accounts//transactions?from_date=&to_date=&page=1&per_page=100&category=PURCHASE' + Then the response status should be 200 + And 'transactions', 'total_count', and 'total_pages' should be present + When I GET '/accounts//transactions' + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + When I rapidly POST 10 more small PURCHASES within 60 minutes + Then all 10 responses should be 200 + When I POST an 11th within the same window + Then the response status should be 429 + And the error code should be 'FREQ_EXCEEDED' + And 'mfa_required' is true and 'Retry-After' header is present + When I GET '/accounts//statements/?format=JSON' after cycle close + Then 'total_spend' equals the sum of transaction amounts within ±0.01 + And if 'prev_statement_balance_paid_in_full' is true then 'interest_charged' equals 0 else it matches REQ-009 formula rounded to cents + When I GET '/accounts//statements/?format=PDF' + Then the response status should be 200 and content type is application/pdf + + @api @transactions + Scenario Outline: Transaction Field Validation and FX Precision + Given available_credit is at least $50.00 + When I POST '/accounts//transactions' with JSON payload + """ + { "transaction_amount": , "currency_code": "", "exchange_rate": , "mcc_code": , "merchant_name": "X", "merchant_id":"Y", "transaction_type":"PURCHASE" } + """ + Then the response status should be + And the error code should be '' + + Examples: + | amount | currency | rate | mcc | status | error_code | + | 0.00 | CAD | null | 5999 | 422 | INVALID_AMOUNT | + | 5.00 | USD | null | 5999 | 400 | EXCHANGE_RATE_MISSING| + | 1.23 | USD | 1.3333337 | 3000 | 400 | EXCHANGE_RATE_PREC | + + @api @transactions + Scenario: Valid Small-Value FX with Rounding and Listing Page Validation + Given available_credit baseline is recorded + When I POST '/accounts//transactions' with JSON payload + """ + { + "transaction_amount":1.23, + "currency_code":"USD", + "exchange_rate":1.333333, + "mcc_code":3000, + "merchant_name":"MiniTravel", + "merchant_id":"MT001", + "transaction_type":"PURCHASE" + } + """ + Then the response status should be 200 + And 'foreign_fee_amount' equals 0.05 and 'total_cad' equals 1.69 + When I POST '/accounts//transactions' with JSON payload + """ + {"transaction_amount":10.00,"mcc_code":6010,"merchant_name":"CashPoint","merchant_id":"CA001","transaction_type":"CASH_ADVANCE"} + """ + Then the response status should be 200 + When I POST '/accounts//transactions' with JSON payload + """ + {"transaction_amount":15.00,"mcc_code":6012,"merchant_name":"BalanceXfer","merchant_id":"BT001","transaction_type":"BALANCE_TRANSFER"} + """ + Then the response status should be 200 + When I GET '/accounts//transactions?page=0&per_page=25' + Then the response status should be 400 + And the error describes 'page must be >= 1' + When I GET '/accounts//transactions?page=1&per_page=25&category=PURCHASE' + Then the response status should be 200 + + @api @transactions + Scenario: Transactions Date/Time Boundaries, UTC/DST, per_page Max, Stable Pagination + Given I have created transactions at T1=2026-03-14T00:00:00Z, T2=2026-03-14T23:59:59Z, T3=2026-03-15T12:00:00Z + When I GET '/accounts//transactions?from_date=2026-03-14&to_date=2026-03-14&page=1&per_page=25' + Then results include T1 and T2 only + When I GET '/accounts//transactions?from_date=2026-03-15&to_date=2026-03-15&page=1&per_page=25' + Then results include T3 only + When I ensure UTC around DST by creating transactions at 2026-03-08T01:59:59Z and 2026-03-08T03:00:01Z + And I GET '/accounts//transactions?from_date=2026-03-08&to_date=2026-03-08' + Then both are included + When I GET '/accounts//transactions?per_page=101' + Then the response status should be 400 + And the error describes 'per_page max 100' + When I populate more than 25 transactions and list page 1 and page 2 with per_page=25 + Then no duplicate items appear across pages and ordering is stable + And response contains 'total_count','page','total_pages' + And masked PAN is shown and no PII leaked + + @api @rewards + Scenario: Rewards Accrual Boundary and MCC Classification with Floor Rounding + Given I capture baseline points P0 via GET '/accounts//summary?include_rewards=true' + When I POST four CAD PURCHASES for MCC 3000 with 1.00, MCC 3000 with 0.99, MCC 5999 with 1.00, MCC 5999 with 0.01 + Then each response status should be 200 + When I POST a transaction with invalid mcc_code '123' (3 digits) + Then the response status should be 400 + When I GET '/accounts//transactions?category=PURCHASE&page=1&per_page=25' + Then the four purchases are present + When I GET '/accounts//summary?include_rewards=true' + Then points increment equals 6 over P0 (or appears on next statement per accrual policy) + When I GET '/accounts//statements/' + Then rewards_earned includes +6 and total_spend reconciles to ±0.01 + + @api @transactions @mfa + Scenario: Transaction Rate-Limit Recovery via MFA + Given I have posted 10 small PURCHASES within 60 minutes successfully + When I POST an 11th transaction within the same window + Then the response status should be 429 + And 'mfa_required' is true and 'Retry-After' header is present + When I POST '/auth/otp/request' with JSON payload + """ + {"channel":"SMS","purpose":"TRANSACTION_RATE_LIMIT"} + """ + And I POST '/auth/otp/verify' with JSON payload + """ + {"code":""} + """ + Then the response status should be 200 + When I retry the previously blocked transaction + Then the response status should be 200 + And available_credit is updated accordingly + When I submit an invalid OTP before success in a new attempt + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + + @api @realtime + Scenario: WebSocket Live Feed Resilience and Authorization + Given I connect to the WebSocket with Authorization: Bearer and subscribe to 'account::transactions' + Then I receive a subscription ack + When I POST a CAD PURCHASE to '/accounts//transactions' + Then a single WebSocket event is received with masked PAN and correct amounts + When I keep the socket open until JWT expiry and send a ping + Then the server closes the connection or emits an unauthorized event + When I POST '/auth/token/refresh' to obtain a new access_token and reconnect the socket and resubscribe + Then I receive a new subscription ack and heartbeats + When I attempt to subscribe to 'account::transactions' + Then subscription is rejected with 403 FORBIDDEN and no events delivered + When I attempt to pass JWT in query string on connect + Then the connection is rejected + And all frames remain over TLS 1.3 without downgrade + + @api @card + Scenario: Card Controls Freeze/Unfreeze with OTP and CSRF, Transactions Blocked While Frozen + Given account summary shows card is Active + When I PATCH '/cards//status' to Frozen with headers 'X-CSRF-Token' and JSON payload + """ + {"status":"Frozen","reason":"Travel","confirm_otp":""} + """ + Then the response status should be 200 + And 'new_status' is 'Frozen' + When I POST '/accounts//transactions' while card is Frozen + Then the response status should be 403 + And the error code should be 'CARD_INACTIVE' + When I PATCH '/cards//status' to Active with valid OTP and CSRF + Then the response status should be 200 + And 'new_status' is 'Active' + When I attempt unfreeze with wrong OTP + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + + @api @card + Scenario: Report Card STOLEN with Delivery Address Override, OTP, Audit, Irreversibility + Given card is Active and masked PAN is visible in summary only + When I POST '/cards//report-lost' without 'X-CSRF-Token' + """ + {"loss_type":"STOLEN"} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I POST '/auth/otp/request' with JSON payload + """ + {"channel":"SMS","purpose":"CARD_REPORT_STOLEN"} + """ + And I POST '/cards//report-lost' with headers 'X-CSRF-Token' and JSON payload + """ + { + "loss_type":"STOLEN", + "confirm_otp":"", + "delivery_address":{"street":"123 King","city":"Toronto","province":"Ontario","postal_code":"12345"} + } + """ + Then the response status should be 400 + And address validation errors for 'province' and 'postal_code' are returned + When I POST '/cards//report-lost' with valid address override and last_known_use + """ + { + "loss_type":"STOLEN", + "confirm_otp":"", + "delivery_address":{"street":"123 King","city":"Toronto","province":"ON","postal_code":"A1A 1A1"}, + "last_known_use":"2026-03-01T12:00:00Z" + } + """ + Then the response status should be 200 + And 'blocked_card_id','new_card_eta','case_number' are present + When I PATCH '/cards//status' to Active or Frozen after Blocked + Then the response status should be 400 + And the error code should be 'INVALID_TRANSITION' + When I PUT '/cards//pin' while Blocked + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CARD_BLOCKED' + When I POST '/cards//report-lost' again + Then the response status should be 409 + And the error code should be 'ALREADY_BLOCKED' + + @api @payments @billing + Scenario: Payments, Late Fee, Interest/Grace, Scheduling, CSRF, Rescind Window + Given I GET '/accounts//statements/' and capture due_date, minimum_payment_due, total_balance + When system time is advanced to due_date + 3 days and I GET the statement + Then 'late_fee' equals 35.00 + When I POST '/accounts//payments' with 'X-CSRF-Token' and JSON payload + """ + {"payment_type":"MINIMUM","payment_amount":49.99,"bank_account_id":"00000000-0000-0000-0000-000000000000"} + """ + Then the response status should be 422 + And the error code should be 'INVALID_BANK_ACCOUNT' + When I POST '/accounts//payments' with JSON payload + """ + {"payment_type":"MINIMUM","payment_amount":45.00,"bank_account_id":"11111111-2222-3333-4444-555555555555"} + """ + Then the response status should be 400 + And the error code should be 'BELOW_MINIMUM' + When I POST a valid FULL_BALANCE payment + """ + {"payment_type":"FULL_BALANCE","bank_account_id":"11111111-2222-3333-4444-555555555555"} + """ + Then the response status should be 200 + And 'payment_id' is present + When I POST a scheduled payment with past scheduled_date + """ + {"payment_type":"CUSTOM","payment_amount":10.00,"scheduled_date":"2000-01-01","bank_account_id":"11111111-2222-3333-4444-555555555555"} + """ + Then the response status should be 400 + And the error code should be 'INVALID_SCHEDULED_DATE' + When I POST '/auth/token/refresh' and then POST '/auth/token/refresh' again using the same refresh token + Then the second response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I POST '/accounts/' DELETE to exercise Right to Rescind within 14 days with 'X-CSRF-Token' + Then the response status should be 200 + And subsequent GET '/accounts//summary' returns 404 or 403 + + @api @payments + Scenario Outline: Payments CUSTOM Boundary and CSRF + Given total_balance and an active bank_account_id are known + When I POST '/accounts//payments' with headers '' and JSON payload + """ + {"payment_type":"CUSTOM","payment_amount":,"bank_account_id":"","scheduled_date":} + """ + Then the response status should be + And the error code should be '' + + Examples: + | csrf_header | amount | bank_id | scheduled | status | error_code | + | X-CSRF-Token | 1.00 | 11111111-2222-3333-4444-555555555555 | null | 200 | | + | X-CSRF-Token | 0.999 | 11111111-2222-3333-4444-555555555555 | null | 400 | SCALE_INVALID | + | X-CSRF-Token | | 11111111-2222-3333-4444-555555555555 | null | 400 | ABOVE_MAX | + | X-CSRF-Token | | 11111111-2222-3333-4444-555555555555 | "tomorrow" | 200 | | + | (missing) | 5.00 | 11111111-2222-3333-4444-555555555555 | null | 403 | CSRF_MISSING | + + @api @statements + Scenario: Statement Calculations Boundary: Interest Rounding, Grace, Late Fee UTC Boundary, Not Found + Given I GET the previous statement to read 'prev_statement_balance_paid_in_full' and 'due_date' + When I set prev_statement_balance_paid_in_full to true and GET current statement after cycle close + Then 'interest_charged' equals 0 + When I set prev_statement_balance_paid_in_full to false with $0.01 remainder and GET current statement + Then 'interest_charged' matches (ADB × APR / 365) × Days formula rounded to cents + And 'total_spend' equals the sum of transactions within ±0.01 + When payment_received_date is exactly due_date + 2 days 23:59:59Z and I GET recalculated statement + Then 'late_fee' equals 0 + When payment_received_date is due_date + 2 days + 1 second and I GET recalculated statement + Then 'late_fee' equals 35.00 + When I GET '/accounts//statements/' + Then the response status should be 404 + + @api @applications + Scenario: Application Step Order Enforcement, Session Token Validation, PENDING and DECLINED + Given userA starts Step 1 via POST '/applications/start' and receives application_id_A and session_token_A + When userA POSTs '/applications//submit' skipping Step 2 with valid e_signature + Then the response status should be 400 + And the error code should be 'INVALID_STEP_ORDER' + When userA POSTs '/applications//financials' with tampered 'X-App-Session' + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When userA POSTs Step 2 with employment_status EMPLOYED but missing employer_name + Then the response status should be 400 + When corrected Step 2 is submitted with sin_consent true and bureau is configured for FICO=620 + Then the response status should be 200 + And status is 'PENDING_REVIEW' + When userA submits Step 3 with valid e_signature and marketing_opt_in omitted + Then decision is 'PENDING' and marketing_opt_in defaults to false + When userB completes Step 1 and Step 2 with FICO=580 and submits Step 3 with malformed e_signature + Then the response status should be 400 + And the error code should be 'SIGNATURE_REQUIRED' + When userB corrects e_signature and resubmits + Then the decision is 'DECLINED' with 'reason_code' + When userB attempts to start another application immediately + Then the response status should be 409 or 201 per business rule + And no cross-user data leakage occurs + + @api @applications + Scenario: Application Step 2 Credit Pull: sin_consent Enforcement, 503 Retry Idempotency, Session Expiry + Given application_id and session_token for user@example.com are available + When I POST Step 2 with EMPLOYED and missing employer_name + Then the response status should be 400 + And field 'employer_name' is required + When I POST Step 2 with employer_name but sin_consent=false + Then the response status should be 400 + And error indicates sin_consent must be true + When I POST Step 2 valid and bureau returns 503 + Then the response status should be 200 + And status is 'PENDING_REVIEW' with a 'fico_pull_id' queued + When I resubmit identical Step 2 + Then the response status should be 200 + And the same 'fico_pull_id' is returned (idempotent) + When I POST Step 2 with a tampered X-App-Session + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When the session_token expires and I retry Step 2 + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When I obtain a fresh session_token and submit Step 2 then Step 3 with FICO=650 + Then the decision is 'PENDING' + + @api @applications @privacy + Scenario: Application Step 1 Validation, Autosave Privacy and Cross-User Isolation + Given userA is logged in with CSRF token and opens Step 1 + When userA POSTs Step 1 with email userB@example.com + Then the response status should be 400 + And error 'email must match authenticated user' + When userA POSTs Step 1 with invalid province 'Ontario' + Then the response status should be 400 + And field 'address.province' invalid + When userA POSTs Step 1 with invalid postal_code '12345' + Then the response status should be 400 + And field 'address.postal_code' invalid + When userA POSTs Step 1 with id_type 'NATIONAL_ID' + Then the response status should be 400 + When userA POSTs Step 1 with id_type DRIVERS_LICENSE and overlength id_number + Then the response status should be 400 + When userA submits valid Step 1 + Then the response status should be 201 + And 'application_id' and 'session_token' are returned + When autosave triggers and the page reloads + Then only non-sensitive fields are restored and no ssn_last4 or tokens appear in DOM/storage + When userA logs out and userB logs in + Then userB sees no draft from userA + When userB attempts to POST '/applications/start' again immediately + Then the response status should be 409 + And the error code should be 'DUPLICATE_APPLICATION' + When userA attempts to POST '/applications/start' again + Then the response status should be 409 + And no cross-user leakage of application_id is observed + + @api @webhook @notifications + Scenario: Notifications Webhook Validation, PII Masking, Severity Rendering + Given the notifications webhook endpoint is reachable + When I POST '/notifications/webhook' with JSON payload + """ + {"account_id":"","alert_type":"UNKNOWN_EVENT","channel":"IN_APP","message_body":"Test","severity":"INFO","idempotency_key":""} + """ + Then the response status should be 400 + And the error code should be 'INVALID_ALERT_TYPE' + When I POST '/notifications/webhook' with invalid channel + """ + {"account_id":"","alert_type":"LATE_PAYMENT","channel":"PAGER","message_body":"Test","severity":"INFO","idempotency_key":""} + """ + Then the response status should be 400 + When I POST a valid STATEMENT_READY IN_APP alert + """ + {"account_id":"","alert_type":"STATEMENT_READY","channel":"IN_APP","message_body":"Your statement is ready","severity":"INFO","idempotency_key":""} + """ + Then the response status should be 200 + When I POST an OVER_LIMIT EMAIL alert with masked PAN + """ + {"account_id":"","alert_type":"OVER_LIMIT","channel":"EMAIL","message_body":"Over-limit used on card **** **** **** 1234","severity":"WARNING","idempotency_key":""} + """ + Then the response status should be 200 + When I POST a FRAUD_FLAG IN_APP alert containing a PAN-like string + """ + {"account_id":"","alert_type":"FRAUD_FLAG","channel":"IN_APP","message_body":"Suspicious charge on card 4111 1111 1111 1111 at Merchant X","severity":"CRITICAL","idempotency_key":""} + """ + Then the response status should be 200 + And stored/rendered message masks to '**** **** **** 1111' + When I POST an alert with message_body > 500 chars + Then the response status should be 400 + When I resend STATEMENT_READY with a different idempotency_key and same content + Then a distinct alert is created + + @api @transactions + Scenario: Transaction Currency and Field Validation with Success at Precision Boundaries + Given available_credit baseline is recorded + When I POST with invalid currency_code + """ + {"transaction_amount":10.00,"currency_code":"USDX","exchange_rate":1.250000,"mcc_code":3000,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST with USD and zero exchange_rate + """ + {"transaction_amount":5.00,"currency_code":"USD","exchange_rate":0.000000,"mcc_code":3000,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST with USD and negative exchange_rate + """ + {"transaction_amount":5.00,"currency_code":"USD","exchange_rate":-1.230000,"mcc_code":3000,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST with CAD amount scale > 2 + """ + {"transaction_amount":10.999,"mcc_code":5999,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST a valid EUR FX at max precision + """ + {"transaction_amount":2.50,"currency_code":"EUR","exchange_rate":0.999999,"mcc_code":3000,"merchant_name":"EuroTravel","merchant_id":"ET001","transaction_type":"PURCHASE"} + """ + Then the response status should be 200 + And 'foreign_fee_amount' and 'total_cad' are correctly rounded and available_credit decremented + When I GET '/accounts//transactions?category=GIFT' + Then the response status should be 400 + When I GET '/accounts//transactions?category=PURCHASE&page=1&per_page=25' + Then the response status should be 200 + When I GET '/accounts//transactions' + Then the response status should be 403 + + @api @csrf @auth + Scenario: CSRF Regeneration After Refresh Rotation + Given I GET '/auth/csrf' and store CSRF-0 + When I POST '/accounts//transactions' with CSRF-0 + """ + {"transaction_amount":1.00,"mcc_code":5999,"merchant_name":"T","merchant_id":"T01","transaction_type":"PURCHASE"} + """ + Then the response status should be 200 + When I POST '/auth/token/refresh' + Then the response status should be 200 + When I PUT '/cards//pin' using stale CSRF-0 + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CSRF_INVALID' or 'CSRF_MISSING' + When I GET '/auth/csrf' and store CSRF-1 + Then CSRF-1 differs from CSRF-0 + When I PUT '/cards//pin' with CSRF-1 and mismatched pins + """ + {"new_pin":"1234","confirm_pin":"4321","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + When I PATCH '/cards//status' to Frozen with CSRF-1 and valid OTP + """ + {"status":"Frozen","confirm_otp":""} + """ + Then the response status should be 200 + When I POST '/auth/token/refresh' reusing the previous refresh token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I PATCH '/cards//status' to Active with CSRF-1 and valid OTP + Then the response status should be 200 + + @api @otp + Scenario: Phone OTP Verification Flow with Expiry, Resend Throttle, Attempts Remaining + Given I POST '/auth/otp/request' with + """ + {"channel":"SMS","purpose":"PHONE_VERIFY"} + """ + Then the response status should be 200 + When I POST '/auth/otp/verify' with non-numeric code + """ + {"code":"12A45B"} + """ + Then the response status should be 400 + When I POST '/auth/otp/verify' with wrong code '000000' + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I POST '/auth/otp/request' twice within 60 seconds + Then the second response status should be 429 or 400 per throttle policy + When I wait for expiry and POST '/auth/otp/verify' with an expired but correct code + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I POST '/auth/otp/request' again and then verify with the new correct code + Then the response status should be 200 + And 'phone_verified' is true + When I reuse the same OTP code again + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + + @api @pin + Scenario: Web-Based PIN Set Edge Cases: Numeric Format, Leading Zeros, OTP Expiry, Attempts Throttle, CSRF + Given card is not Blocked and I have a valid session + When I PUT '/cards//pin' without 'X-CSRF-Token' + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I PUT with whitespace in PIN + """ + {"new_pin":"12 4","confirm_pin":"12 4","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_FORMAT' + When I PUT with non-numeric PIN + """ + {"new_pin":"12A4","confirm_pin":"12A4","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_FORMAT' + When I PUT with 3-digit PIN and then 5-digit PIN + """ + {"new_pin":"123","confirm_pin":"123","session_otp":""} + """ + Then the response status should be 400 + When I PUT with 5-digit PIN + """ + {"new_pin":"12345","confirm_pin":"12345","session_otp":""} + """ + Then the response status should be 400 + When I PUT with leading zeros but expired OTP + """ + {"new_pin":"0000","confirm_pin":"0000","session_otp":""} + """ + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I request a fresh OTP and PUT with mismatched confirm_pin + """ + {"new_pin":"0000","confirm_pin":"0001","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + When I submit two more attempts with wrong OTPs until attempts throttle + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When after cooldown I PUT with valid OTP and matching '0000' + Then the response status should be 200 + And no PIN or OTP appears in any UI or storage + + @api @admin @audit + Scenario: Audit Trail for Credit Limit Changes and Access Control + Given as cardholder I GET '/accounts//summary' and record L0, A0, B0 + When as admin I PATCH '/admin/accounts//credit-limit' with + """ + {"credit_limit": ""} + """ + Then the response status should be 200 + When as cardholder I GET '/accounts//summary' + Then 'credit_limit' equals L0+1000 and 'available_credit' increased accordingly + When as admin I GET '/admin/audit?entity=credit_limit&account_id=' + Then an audit record exists with old_value L0 and new_value L0+1000 and immutable metadata + When as cardholder I GET the same audit endpoint + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + When as cardholder I try PATCH '/accounts//credit-limit' + Then the response status should be 404 or 403 + And PAN masking is enforced in any payloads + When as admin I patch credit_limit down by 500 and re-check summary and audit + Then a second immutable audit entry exists and summary reflects the new limit + + @api @rescind @realtime + Scenario: Right to Rescind Security Sweep Post-Delete + Given the account is within 14 days of issuance and Active + And I have an active WebSocket subscription to 'account::transactions' + When I POST a small PURCHASE and observe one realtime event + Then the event contains masked PAN only + When I DELETE '/accounts/' with 'X-CSRF-Token' + Then the response status should be 200 + When I GET '/accounts//summary' after rescind + Then the response status should be 404 or 403 + When I POST '/accounts//transactions' after rescind + Then the response status should be 403 + When I POST '/accounts//payments' after rescind + Then the response status should be 403 + And the WebSocket is terminated or receives an unauthorized event + When I POST '/notifications/webhook' targeting the rescinded account + """ + {"account_id":"","alert_type":"STATEMENT_READY","channel":"IN_APP","message_body":"Test","severity":"INFO","idempotency_key":""} + """ + Then the response is a safe suppression (404 or 200 no-op) and no in-portal alert appears + When I DELETE '/accounts/' again + Then the response is idempotent (404 NOT_FOUND or 409 ALREADY_CLOSED) + + # UI Tests + + @ui @session + Scenario: Session Timeout Warning - Stay Signed In Flow and Auto-Logout + Given I am on the portal and logged in with MFA with Secure, HttpOnly, SameSite=Strict cookies set + And I have an active application Step 1 draft saved + When I remain idle for 13 minutes + Then I should see a session-timeout warning modal with 2-minute countdown + When I click the 'Stay Signed In' button + Then my session should remain active and a silent token refresh should occur + And I can proceed to Step 2 without re-authenticating + When I reload the page + Then non-sensitive draft fields should be restored and ssn_last4 should not be present in localStorage or the DOM + When I ignore the warning on a subsequent cycle and remain idle past 15 minutes + Then I should be logged out automatically + And any protected API requests should result in 401 + + @ui @pci + Scenario: PAN Masking and Iframe Tokenization Compliance Across Portal Surfaces + Given I am on the account dashboard + Then I should see the card number masked as '**** **** **** 1234' + And no full PAN appears in any DOM element or data attribute + When I open browser devtools and review Network responses + Then no API response contains a full PAN and caching does not store PAN + And localStorage and sessionStorage contain no PAN, ssn_last4, or tokens + When I navigate to card management + Then any card input fields are within a third-party iframe tokenization component loaded over TLS 1.3 + And the host page never receives raw PAN + When a WebSocket transaction event arrives + Then the payload shows masked PAN only and no tokens or PII + When I open the latest statement JSON and PDF + Then no full PAN is present in either format + When a notification is rendered that originally contained a PAN-like string + Then the UI displays a masked PAN and no full PAN appears anywhere + And CSP headers restrict iframe/script sources appropriately diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.json b/functional_tests/functional-test-aegis/functional-test-aegis.json new file mode 100644 index 0000000..7725556 --- /dev/null +++ b/functional_tests/functional-test-aegis/functional-test-aegis.json @@ -0,0 +1,1027 @@ +[ + { + "type": "e2e-functional", + "title": "Applicant Journey E2E: Register -> Verify -> Login (MFA) -> Application Steps -> APPROVED -> Set PIN -> Summary -> Token Rotation, CSRF, Autosave, Session Timeout", + "description": "Covers full new-user onboarding with regulatory controls: registration validations, email verification, OAuth2 login with TOTP, multi-step credit application with autosave and session_token, approval path, masked PAN, CSRF enforcement, token refresh rotation/reuse detection, session timeout warning and logout.", + "testId": "TC-E2E-001", + "testDescription": "Validates end-to-end user lifecycle from registration through approved decision and card PIN setup while asserting security controls (TLS1.3, CSRF, cookie flags, JWT rotation) and UI masking.", + "prerequisites": "No existing account for user@example.com; test device_id available; MFA seed provisioned for TOTP; products catalog includes AEGIS_GOLD; test phone +14165550123 can receive OTP.", + "stepsToPerform": "1. Ensure browser connects over TLS 1.3 to https://portal.aegiscard.com and API https://api.aegiscard.com/v2; verify WAF and HSTS present.\n2. Call POST /v2/auth/register with valid first_name, last_name, unique email user@example.com, strong password (>=12 chars with upper/lower/digit/symbol), date_of_birth exactly 18 years ago today (boundary), phone_number +14165550123 (E.164), ssn_last4 1234, agree_terms true.\n3. Verify 201 response with user_id and verification_token; confirm password complexity not rejected and email uniqueness enforced.\n4. ASSUMPTION: Complete email verification by calling POST /v2/auth/verify with verification_token; expect 200 and account marked verified.\n5. Call POST /v2/auth/login with email user@example.com, correct password, device_id UUIDv4, remember_me true; then submit valid 6-digit TOTP mfa_code.\n6. Confirm 200 with access_token and refresh_token set in HttpOnly, Secure, SameSite=Strict cookies; verify no tokens present in localStorage or JS-accessible scope.\n7. ASSUMPTION: Obtain CSRF token via GET /v2/auth/csrf and store value for subsequent state-changing requests.\n8. Start application Step 1 by POST /v2/applications/start (with X-CSRF-Token) including full_legal_name 'Jane Q Public', email matching user@example.com, phone_number +14165550123, residential_address fields valid (Canadian postal code boundary like A1A 1A1), id_type PASSPORT, id_number 'X1234567'.\n9. Verify 201 with application_id and session_token; confirm duplicate Step 1 immediately repeated returns 409 DUPLICATE_APPLICATION.\n10. ASSUMPTION: Trigger phone OTP and verify by calling POST /v2/auth/otp/request then POST /v2/auth/otp/verify with correct code; expect 200 and phone_verified=true.\n11. Wait until the UI auto-saves after 60s; reload the page; assert draft restored for non-sensitive fields and that ssn_last4, tokens, or full PAN are not present in localStorage/DOM.\n12. Proceed to Step 2 by POST /v2/applications/{application_id}/financials with X-App-Session header set to session_token, employment_status EMPLOYED, employer_name 'Aegis Ltd', gross_annual_income 85000.00, other_income omitted, monthly_rent 1500.00, existing_debt_payments 300.00, sin_consent true; include X-CSRF-Token.\n13. Verify 200 with status PENDING_REVIEW and fico_pull_id present; assert error 400 if employer_name missing when EMPLOYED by retrying without employer_name (negative) and receiving 400 then re-submit correct data to restore positive state.\n14. Immediately POST /v2/applications/{application_id}/submit with valid card_product_id AEGIS_GOLD, e_signature Base64 of 'Jane Q Public', marketing_opt_in omitted; include X-CSRF-Token and X-App-Session.\n15. Verify decision APPROVED (FICO > 680) with credit_limit returned and card_number_masked like **** **** **** 1234; ensure no full PAN in response or DOM (REQ-014).\n16. Call POST /v2/auth/token/refresh; verify 200 new access_token and rotated refresh_token; attempt to reuse the old refresh_token and expect 401 TOKEN_INVALID (single-use rotation).\n17. Attempt PUT /v2/cards/{card_id}/pin without X-CSRF-Token and expect 403 (ASSUMPTION: error CSRF_MISSING); verify no PIN change occurred.\n18. Retry PUT /v2/cards/{card_id}/pin with X-CSRF-Token and valid session_otp, new_pin 1234, confirm_pin 1234; expect 200 and updated_at timestamp; attempt with mismatch 1234/4321 returns 400 PIN_MISMATCH (negative) then correct it.\n19. GET /v2/accounts/{account_id}/summary; verify owner-only access, balances, available_credit, credit_limit, account_status Active, and that include_rewards=false by default; retry with include_rewards=true and verify points_balance present.\n20. Idle on the portal without activity until 13 minutes; verify session timeout warning modal appears; continue idling to 15 minutes; assert auto-logout; protected API returns 401.\n21. Login again with MFA; verify all previously created artifacts (application, account) remain accessible; no sensitive data persisted in frontend storage; logout via ASSUMPTION: POST /v2/auth/logout and confirm cookies cleared.", + "expectedResult": "User registers and verifies email; logs in with MFA; completes application steps in order; receives APPROVED decision; masked PAN only shown; PIN set succeeds only with CSRF and OTP; token refresh rotates and old refresh token reuse is rejected; session timeout warning and auto-logout work; all mutating endpoints enforce CSRF; no sensitive data appears in localStorage/DOM.", + "apiPath": "/v2/auth/register, /v2/auth/verify, /v2/auth/login, /v2/auth/token/refresh, /v2/auth/logout, /v2/auth/csrf, /v2/auth/otp/request, /v2/auth/otp/verify, /v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/summary", + "httpMethod": "POST, POST, POST, POST, POST, GET, POST, POST, POST, POST, POST, PUT, GET", + "endpointGroup": "Auth, Applications, Card Management, Accounts", + "workflow": "E2E, Authentication, Credit Application, Card Management, Session Management", + "businessRuleIds": "REQ-014, NFR-05, NFR-06", + "calculationFormula": "N/A for decision logic here; decision per FICO thresholds handled by backend per SRS (APPROVED if FICO > 680).", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true", + "rateLimitBucket": "login 10 req/min per IP", + "inputFields": "first_name,last_name,email,password,date_of_birth,phone_number,ssn_last4,agree_terms,full_legal_name,residential_address,id_type,id_number,employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,card_product_id,e_signature,new_pin,confirm_pin,session_otp", + "validationRules": "Email RFC 5322 unique, Password >=12 with complexity, DOB >=18 including leap-year birthdays, Phone E.164, Postal code Canadian format, employer_name required when EMPLOYED, e_signature Base64, PIN exactly 4 digits", + "errorCodesCovered": "EMAIL_EXISTS, WEAK_PASSWORD, INVALID_CREDENTIALS, ACCOUNT_LOCKED, RATE_LIMITED, SESSION_EXPIRED, DUPLICATE_APPLICATION, SIGNATURE_REQUIRED, TOKEN_INVALID, OTP_FAILED, PIN_MISMATCH, PIN_FORMAT, CSRF_MISSING", + "stateTransitions": "Application: NEW->STEP1->STEP2->SUBMITTED->APPROVED; Card PIN: unset->set; Session: active->warning(13m)->expired(15m)", + "alertTypesCovered": "ASSUMPTION: In-app verification notices on approval", + "dataMaskingChecks": "No full PAN in responses or DOM; masked PAN **** **** **** 1234 only; no tokens or ssn_last4 in localStorage", + "auditTrailChecks": "ASSUMPTION: Verify audit record for application creation and decision, user_id/session_id/ip logged", + "piiFields": "full_legal_name,email,phone_number,residential_address,date_of_birth,ssn_last4", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/verify exists for email verification; ASSUMPTION: /v2/auth/csrf issues CSRF token; ASSUMPTION: OTP endpoints exist; ASSUMPTION: CSRF error code CSRF_MISSING with 403", + "testData": "email user@example.com, phone +14165550123, device_id 550e8400-e29b-41d4-a716-446655440000, e_signature Base64 'SmFuZSBRIFB1YmxpYw=='", + "cleanupSteps": "Logout; if needed, delete test user via admin tool (non-prod); revoke tokens", + "dependencies": "Products catalog contains AEGIS_GOLD; MFA seed provisioned; OTP delivery channel reachable" + }, + { + "type": "functional", + "title": "Foreign Currency Purchase, Rewards, Statement Accuracy, WebSocket Feed, Pagination and Rate Limit", + "description": "Validates non-CAD transaction with exchange_rate, foreign fee and Total_CAD math, rewards floor rounding by MCC, statement accuracy and grace logic, WebSocket event delivery, list transactions pagination/filtering, CSRF enforcement, and transaction rate-limit MFA trigger.", + "testId": "TC-TXN-FOREIGN-002", + "testDescription": "Ensures REQ-006 foreign fee math, REQ-012/REQ-013 rewards floor, REQ-015 statement accuracy, grace period check, and realtime feed operate correctly; covers invalid date range and CSRF missing negatives plus rate-limit behavior (>10 txns/60min).", + "prerequisites": "Approved and Active account_id for user@example.com with sufficient available_credit; authenticated session with valid access_token and X-CSRF-Token; WebSocket JWT available.", + "stepsToPerform": "1. Open WebSocket wss://realtime.aegiscard.com/v2/stream with valid JWT; subscribe to account transaction topic; expect subscription ack.\n2. POST /v2/accounts/{account_id}/transactions with X-CSRF-Token to create USD PURCHASE: transaction_amount 100.00, currency_code USD, exchange_rate 1.250000, mcc_code 3000 (Travel), merchant_name 'Aegis Air', merchant_id 'AA123', description 'Flight'.\n3. Verify 200 response includes transaction_id, available_credit updated, auth_code; assert foreign_fee_amount = (100.00 * 1.250000 * 0.03) = 3.75 and Total_CAD = (100.00 * 1.250000) * 1.03 = 128.75 with proper precision rounding (2 decimals for amounts).\n4. Validate WebSocket receives an event for the transaction with correct masked PAN and amounts; no PII leakage; unauthorized socket attempt (disconnect and reconnect without JWT) is rejected (401/403).\n5. POST another PURCHASE in non-Travel category amount 10.99 CAD, mcc_code 5999, merchant_name 'Shop', to test rewards rounding; expect approval.\n6. Compute expected rewards: Travel points = floor(128.75 * 3) = 386, Other points = floor(10.99 * 1) = 10; verify points increment on statement later (REQ-012, REQ-013). ASSUMPTION: Rewards accrue on total posted CAD amounts.\n7. Attempt a transaction without X-CSRF-Token; expect 403 CSRF_MISSING and no transaction created (negative CSRF enforcement).\n8. GET /v2/accounts/{account_id}/transactions with invalid date range (to_date earlier than from_date); expect 400 INVALID_DATE_RANGE.\n9. GET /v2/accounts/{account_id}/transactions with from_date=current cycle start, to_date=now, page=1, per_page=100 (boundary), category filter PURCHASE; expect 200 with transactions[], total_count, total_pages; verify owner-only access by requesting another account_id and expecting 403 FORBIDDEN.\n10. Rapidly initiate 10 more small PURCHASE transactions within 60 minutes (with CSRF) and ensure approvals; attempt an 11th in the same 60-min window; expect 429 FREQ_EXCEEDED with mfa_required true; verify Retry-After header present (ASSUMPTION).\n11. GET /v2/accounts/{account_id}/statements/{statement_id} after cycle close (ASSUMPTION: use latest statement id); format=JSON; validate total_spend equals sum(transaction_amount[]) within ±$0.01 tolerance (REQ-015).\n12. Validate REQ-010 grace period: if prev_statement_balance_paid_in_full=true, confirm interest_charged=0 for this cycle; else compute interest via REQ-009 using Interest = (ADB × APR / 365) × Days_in_Billing_Cycle and verify rounding to cents.\n13. Request same statement with format=PDF; expect 200 and binary/pdf content; ensure access control for non-owner is 403.", + "expectedResult": "Foreign USD transaction correctly applies 3% fee and conversion; rewards accrue with floor rounding per MCC; WebSocket delivers authenticated event; CSRF missing blocks POST; invalid date range rejected; pagination and filters work; rate-limit triggers 429 with mfa_required; statement totals and interest/grace logic are accurate; owner-only access enforced.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/statements/{statement_id}, wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "POST, GET, WebSocket", + "endpointGroup": "Transactions, Billing, Realtime", + "workflow": "Transaction Processing, Billing & Rewards, Realtime Feed", + "businessRuleIds": "REQ-006, REQ-009, REQ-010, REQ-012, REQ-013, REQ-015", + "calculationFormula": "REQ-006: Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03. REQ-012/REQ-013: Travel points = floor(amount_cad × 3), Others = floor(amount_cad × 1). REQ-009: Interest = (ADB × APR / 365) × Days_in_Billing_Cycle.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for rate-limit recovery, false for baseline", + "rateLimitBucket": "transactions >10 in 60 min => 429 with mfa_required true", + "inputFields": "account_id,transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,description,from_date,to_date,page,per_page,format", + "validationRules": "transaction_amount > 0.00, mcc_code 4 digits, exchange_rate required if currency != CAD (Decimal(8,6)), per_page <= 100, to_date >= from_date, WebSocket requires JWT", + "errorCodesCovered": "INVALID_AMOUNT, CARD_INACTIVE, INSUFFICIENT_FUNDS, FREQ_EXCEEDED, INVALID_DATE_RANGE, FORBIDDEN, CSRF_MISSING", + "stateTransitions": "Realtime subscription: unauthenticated->authenticated; Rate-limit: normal->limited (requires MFA)", + "alertTypesCovered": "ASSUMPTION: STATEMENT_READY notification visible in portal UI after statement generation", + "dataMaskingChecks": "Transaction feed and API return masked PAN only; no PII beyond necessary merchant and masked card info", + "auditTrailChecks": "ASSUMPTION: Transaction approvals logged with user_id/session_id/ip", + "piiFields": "None beyond account_id association; merchant data non-PII", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, PII minimal", + "assumptions": "ASSUMPTION: Rewards accrue on CAD-posted amounts; ASSUMPTION: Retry-After header included on 429; ASSUMPTION: Latest statement_id is discoverable via statements list.", + "testData": "mcc_code 3000 Travel, mcc_code 5999 Other, exchange_rate 1.250000, amounts 100.00 USD and 10.99 CAD", + "cleanupSteps": "None required; leave transactions for statement validation", + "dependencies": "Account has sufficient credit; Statement generation schedule known for test environment" + }, + { + "type": "functional", + "title": "Essential Service Over-Limit Buffer, Insufficient Funds, Notifications Webhook Idempotency, Owner-Only Access", + "description": "Validates 5% over-limit buffer for essential services, proper denial beyond buffer, notification webhook delivery and idempotency, in-app notification rendering, CSRF enforcement, WebSocket event, and owner-only transaction access.", + "testId": "TC-TXN-OVERLIMIT-003", + "testDescription": "Exercises REQ for essential service over-limit approval and ensures alerts are sent once via idempotency; confirms insufficient funds beyond 5% buffer, feed event, and access controls.", + "prerequisites": "Active card with available_credit set to $100.00; authenticated session with CSRF token; real-time socket connected; notification engine can call webhook.", + "stepsToPerform": "1. Verify account summary shows available_credit approximately $100.00 and account_status Active.\n2. POST /v2/accounts/{account_id}/transactions with X-CSRF-Token for an essential service merchant (ASSUMPTION: MCC 4900 Utilities) amount $104.50 CAD; expect use of 5% buffer.\n3. Confirm 200 approval with over_limit_flag true, updated available_credit reflecting buffer usage, and ISO 8583 approval mapping 00.\n4. Ensure WebSocket event received shows over_limit_flag true; UI displays over-limit banner; no PII exposure.\n5. Trigger internal notifications by POST /v2/notifications/webhook with alert_type OVER_LIMIT, channel IN_APP, severity WARNING, valid idempotency_key; expect 200 with notification_id and delivered_at.\n6. Re-send the exact same webhook payload and idempotency_key; expect 409 DUPLICATE_NOTIFICATION and that the portal UI shows only one alert (idempotency effect).\n7. Attempt another essential service transaction for $106.00 with same available_credit baseline; expect 402 INSUFFICIENT_FUNDS (exceeds 5% buffer), available_credit returned in error body.\n8. Attempt to create a transaction without X-CSRF-Token; expect 403 CSRF_MISSING; retry with CSRF to confirm success path still works for valid amounts.\n9. GET /v2/accounts/{different_account_id}/transactions as this user; expect 403 FORBIDDEN owner-only access enforcement.\n10. Validate notification message_body does not include unmasked PAN or sensitive PII; ensure timestamps are ISO 8601 UTC.\n11. Review server logs or audit (ASSUMPTION) to confirm notification delivery and transaction approvals logged with user_id/session_id/ip_address.", + "expectedResult": "Transaction within 5% buffer is approved with over_limit_flag; beyond buffer is rejected; CSRF is required; notifications webhook delivers once and rejects duplicates; in-app alert renders once; WebSocket event reflects over-limit; owner-only access enforced; PII remains masked.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/notifications/webhook, /v2/accounts/{account_id}/transactions (GET)", + "httpMethod": "POST, POST, GET", + "endpointGroup": "Transactions, Notifications", + "workflow": "Transaction Processing, Notifications & Alerts", + "businessRuleIds": "REQ-006 buffer behavior (domain rule), NFR-05 CSRF", + "calculationFormula": "Over-limit allowed up to 5% of available_credit for essential service MCC (ASSUMPTION: business rule); no foreign fee in CAD case.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard bucket", + "inputFields": "account_id,transaction_amount,merchant_name,merchant_id,mcc_code,transaction_type,alert_type,channel,severity,idempotency_key,message_body", + "validationRules": "Essential MCC eligible for 5% buffer, CSRF required on POST, idempotency_key UUID v4 unique per alert, message_body <= 500 chars", + "errorCodesCovered": "INSUFFICIENT_FUNDS, CSRF_MISSING, FORBIDDEN, DUPLICATE_NOTIFICATION", + "stateTransitions": "Credit available -> over-limit used; Notification state -> delivered -> duplicate ignored", + "alertTypesCovered": "OVER_LIMIT", + "dataMaskingChecks": "Webhook and UI messages do not display PAN; if card referenced, show **** **** **** 1234 only", + "auditTrailChecks": "ASSUMPTION: Notification and transaction approvals logged immutably with user/session/ip", + "piiFields": "None in webhook beyond account_id; ensure masking rules", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, PII Masking", + "assumptions": "ASSUMPTION: Essential service MCCs include 4900 Utilities; ASSUMPTION: Over-limit buffer precisely 5% over available_credit; ASSUMPTION: CSRF error 403.", + "testData": "available_credit $100.00, essential MCC 4900, amounts $104.50 (approve) and $106.00 (decline), idempotency_key 7b9c8b1e-2f3c-4a0d-a7f1-7d0a8c9b1234", + "cleanupSteps": "Clear in-app notifications after validation; restore available_credit via test harness if needed", + "dependencies": "Notification engine reachable; WebSocket service operational" + }, + { + "type": "functional", + "title": "Card Controls and Loss/Stolen: Freeze/Unfreeze with OTP & CSRF, Blocked Irreversibility, PIN Restrictions, Invalid Transitions", + "description": "Validates card status transitions (Active <-> Frozen), OTP validation and expiry, CSRF enforcement, reporting card lost transitioning to Blocked irreversibly, inability to set PIN on Blocked, and invalid transition handling with audit logging.", + "testId": "TC-CARD-CTRL-004", + "testDescription": "Ensures REQ-007 controls and REQ-008 PIN rules: proper OTP gating, CSRF checks, audit entries, and error handling for invalid or blocked transitions.", + "prerequisites": "Active card_id owned by user@example.com; valid authenticated session, CSRF token, and OTP delivery channel.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to confirm account_status Active and card linked.\n2. PATCH /v2/cards/{card_id}/status to Frozen with confirm_otp valid, reason 'Travel'; include X-CSRF-Token; expect 200 new_status Frozen.\n3. Verify audit trail (ASSUMPTION: admin/audit endpoint) logs user_id, session_id, ip_address, timestamp_utc for status change.\n4. Attempt a PURCHASE via POST /v2/accounts/{account_id}/transactions; expect 403 CARD_INACTIVE with card_status Frozen.\n5. PATCH /v2/cards/{card_id}/status back to Active with valid OTP and CSRF; expect 200 new_status Active; attempt with wrong/expired OTP returns 401 OTP_FAILED (negative) and attempts_remaining decremented.\n6. POST /v2/cards/{card_id}/report-lost with loss_type LOST, optional last_known_use timestamp; include X-CSRF-Token; expect 200 with blocked_card_id, new_card_eta, case_number; account reflects Blocked card.\n7. Attempt PATCH /v2/cards/{card_id}/status to Active or Frozen after Blocked; expect 400 INVALID_TRANSITION (ASSUMPTION) due to irreversibility of Blocked state.\n8. Attempt PUT /v2/cards/{card_id}/pin with valid session_otp while Blocked; expect 403 CARD_BLOCKED; confirm no PIN update.\n9. Attempt to report lost again; expect 409 ALREADY_BLOCKED; verify idempotent behaviors do not create duplicate cases.\n10. Retry PATCH /v2/cards/{card_id}/status without X-CSRF-Token; expect 403 CSRF_MISSING; confirm no status change applied.\n11. Verify UI and API always display masked PAN only; scan DOM for absence of full PAN and sensitive data.", + "expectedResult": "Card Frozen with valid OTP and CSRF; transactions fail while Frozen; Unfreeze succeeds with OTP; reporting lost moves card to Blocked irreversibly; PIN setting disallowed when Blocked; invalid transitions and missing CSRF produce appropriate errors; audit entries recorded; PAN always masked.", + "apiPath": "/v2/cards/{card_id}/status, /v2/cards/{card_id}/report-lost, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions", + "httpMethod": "PATCH, POST, PUT, GET, POST", + "endpointGroup": "Card Management, Accounts, Transactions", + "workflow": "Card Status Control, Loss/Stolen Handling", + "businessRuleIds": "REQ-007, REQ-008, REQ-014, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for OTP actions", + "rateLimitBucket": "standard", + "inputFields": "card_id,status,reason,confirm_otp,loss_type,last_known_use,new_pin,confirm_pin,session_otp", + "validationRules": "status in {Active,Frozen} only, OTP 6 digits valid and unexpired, Blocked state is irreversible, PIN exactly 4 digits and matching", + "errorCodesCovered": "INVALID_TRANSITION, OTP_FAILED, CARD_INACTIVE, CARD_BLOCKED, ALREADY_BLOCKED, CSRF_MISSING", + "stateTransitions": "Active <-> Frozen; Active/Frozen -> Blocked (irreversible)", + "alertTypesCovered": "ASSUMPTION: PIN_LOCKED or FRAUD_FLAG may be generated by backend if needed; not exercised here", + "dataMaskingChecks": "Masked PAN **** **** **** 1234 only; no full PAN in DOM", + "auditTrailChecks": "Status changes logged per NFR-04 with user_id/session_id/ip_address/timestamp_utc", + "piiFields": "None beyond card ownership linkage", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF, Audit Trail", + "assumptions": "ASSUMPTION: Audit verification available via internal endpoint; ASSUMPTION: INVALID_TRANSITION returns 400", + "testData": "OTP valid code 123456 for success, invalid code 000000 for failure", + "cleanupSteps": "None; replacement card issuance handled by operations", + "dependencies": "OTP delivery; audit log access for verification" + }, + { + "type": "functional", + "title": "Payments, Late Fee and Interest, Grace Period, Bank Account Validation, Right to Rescind Window, CSRF and Token Behaviors", + "description": "Validates payment rules (minimums, bank account linkage, scheduling), late fee trigger after due_date+2 days, interest/grace calculations across statements, right-to-rescind within 14 days, CSRF enforcement, and refresh token reuse detection in a financial context.", + "testId": "TC-BILL-PAY-005", + "testDescription": "Covers REQ-011 late fee, REQ-009 interest ADB, REQ-010 grace period, payment validations, REQ-016 rescind window, CSRF and JWT refresh reuse security.", + "prerequisites": "Active account with an open statement and a linked bank_account_id; authenticated session with CSRF; knowledge of due_date and minimum_payment_due.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/statements/{statement_id} (latest) to capture due_date, minimum_payment_due, total_balance, prev_statement_balance_paid_in_full flag.\n2. Advance system clock or use test data to be exactly due_date + 3 days; GET statement again; expect late_fee = $35.00 applied per REQ-011 (UTC handling; weekends ignored per ASSUMPTION).\n3. Attempt POST /v2/accounts/{account_id}/payments with payment_type MINIMUM and payment_amount less than minimum_payment_due; include X-CSRF-Token; expect 400 BELOW_MINIMUM with minimum_payment_due echoed.\n4. Attempt POST payment with invalid or inactive bank_account_id; expect 422 INVALID_BANK_ACCOUNT; no change to balance.\n5. Submit valid immediate POST payment with payment_type STATEMENT_BALANCE or FULL_BALANCE using active bank_account_id; expect 200 payment_id, scheduled_date (today), new_balance_estimate near $0 for FULL_BALANCE.\n6. Attempt to schedule a payment with scheduled_date in the past; expect 400 INVALID_SCHEDULED_DATE (ASSUMPTION); then resubmit with a future date (tomorrow) and receive 200 queued response.\n7. After cycle close, GET next statement; compute interest using REQ-009 Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; if previous paid in full before grace window, verify REQ-010 grace applies (no interest); else validate interest amount to cents; verify sum(transaction_amount[]) == total_spend within ±$0.01 (REQ-015).\n8. Perform POST /v2/auth/token/refresh to rotate tokens; then attempt another refresh using the already-used refresh_token; expect 401 TOKEN_INVALID; confirm new refresh works.\n9. Attempt POST /v2/accounts/{account_id}/payments without X-CSRF-Token; expect 403 CSRF_MISSING; retry with CSRF and succeed.\n10. Exercise REQ-016 Right to Rescind: within 14 days from issuance, call DELETE /v2/accounts/{id} with X-CSRF-Token; expect 200 success with audit trail entry and irreversible closure; attempt to access summary after delete returns 403/404.\n11. After day 15 (ASSUMPTION: time travel), call DELETE /v2/accounts/{id}; expect 403 RESCIND_WINDOW_CLOSED and no change to account; verify audit log records attempted rescind denial.", + "expectedResult": "Late fee $35 applies at due_date+3 days; payments enforce minimums and bank account linkage; scheduled_date must be future; interest and grace calculations are correct and statement totals reconcile; CSRF enforced on payments and DELETE; refresh token reuse is rejected; rescind allowed only within 14 days and denied after; audit records present.", + "apiPath": "/v2/accounts/{account_id}/statements/{statement_id}, /v2/accounts/{account_id}/payments, /v2/auth/token/refresh, /v2/accounts/{id}", + "httpMethod": "GET, POST, POST, DELETE", + "endpointGroup": "Billing, Payments, Auth, Account Lifecycle", + "workflow": "Billing & Financial Logic, Payments, Right to Rescind", + "businessRuleIds": "REQ-009, REQ-010, REQ-011, REQ-015, REQ-016, NFR-05", + "calculationFormula": "REQ-009: Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; REQ-011: late_fee = $35 if payment_received_date > due_date + 2 days; REQ-010: Grace period 21 days when prev_statement_balance_paid_in_full = true.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "false", + "rateLimitBucket": "payments standard", + "inputFields": "payment_amount,payment_type,bank_account_id,scheduled_date,account_id,statement_id", + "validationRules": "payment_amount min $1.00 and >= minimum_payment_due when type MINIMUM, max total_balance, bank_account_id must be active, scheduled_date future only, DELETE allowed within 14 days post-issuance", + "errorCodesCovered": "BELOW_MINIMUM, INVALID_BANK_ACCOUNT, TOKEN_INVALID, CSRF_MISSING, RESCIND_WINDOW_CLOSED", + "stateTransitions": "Account: Active->Closed (rescinded within window) or remains Active if window closed; Statement cycle progression", + "alertTypesCovered": "LATE_PAYMENT (ASSUMPTION via webhook), STATEMENT_READY", + "dataMaskingChecks": "No PAN exposure in payment confirmations; masked details only", + "auditTrailChecks": "Payment attempts and rescind actions logged with user_id/session_id/ip_address/time; credit_limit change audit not in scope here", + "piiFields": "bank_account_id association (non-PII), account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security, CSRF", + "assumptions": "ASSUMPTION: INVALID_SCHEDULED_DATE error on past scheduled_date; ASSUMPTION: RESCIND_WINDOW_CLOSED 403 after day 14; ASSUMPTION: Weekends/holidays do not alter late fee rule.", + "testData": "minimum_payment_due $50.00, total_balance $500.00, bank_account_id e.g. 11111111-2222-3333-4444-555555555555 (active), invalid bank_account_id e.g. 00000000-0000-0000-0000-000000000000", + "cleanupSteps": "If account was rescinded, create a new test account for future tests; clear scheduled payments in test env", + "dependencies": "Statement generation cadence known; APR and ADB available for calculation" + }, + { + "type": "functional", + "title": "Authentication Hardening: Lockout, Rate Limit, Remember Me TTL, Device Recognition, Logout Invalidation", + "description": "Validates login failure handling, account lockout after 5 failures, IP rate limiting (10 req/min), remember_me TTL extension, device recognition, secure cookie flags, and logout invalidating refresh token.", + "testId": "TC-AUTH-LOCKOUT-006", + "testDescription": "Focuses on negative and boundary auth behaviors with regulatory security checks: lockout vs rate limit semantics, secure cookie attributes, and refresh invalidation on logout.", + "prerequisites": "User account exists and email verified for user@example.com; MFA enabled with valid TOTP; known password; test IP can be controlled; browser over TLS 1.3.", + "stepsToPerform": "1. From the same client IP, call POST /v2/auth/login with email user@example.com and an incorrect password four times within 60 seconds.\n2. Verify each of the four attempts returns 401 INVALID_CREDENTIALS and capture any rate-limit headers (ASSUMPTION: X-RateLimit-Remaining present).\n3. Attempt a 5th invalid login within the same window; expect 403 ACCOUNT_LOCKED with unlock_at timestamp returned.\n4. Immediately attempt login with the correct password and valid 6-digit TOTP while account is locked; expect 403 ACCOUNT_LOCKED persists.\n5. Wait until unlock_at + 1 minute; attempt login again with correct password, valid TOTP, device_id set (UUID v4), and remember_me=true; expect 200 with access_token and refresh_token in HttpOnly, Secure, SameSite=Strict cookies.\n6. Validate cookie flags and that no tokens are present in window.localStorage or window.sessionStorage; confirm tokens are cookies only.\n7. Verify remember_me extended TTL: inspect refresh cookie expiry or response (ASSUMPTION: refresh_expires_in present) equals ~30 days vs default.\n8. From the same IP, send 10 additional login requests in the current minute (successful or attempted); trigger an 11th login attempt; expect 429 RATE_LIMITED with retry_after header and no tokens issued.\n9. Logout via POST /v2/auth/logout including a valid X-CSRF-Token; expect 200 and that access/refresh cookies are cleared (expired) and SameSite=Strict remains enforced.\n10. Attempt POST /v2/auth/token/refresh using the pre-logout refresh_token; expect 401 TOKEN_INVALID due to rotation/invalidation on logout.\n11. Attempt GET /v2/accounts/{account_id}/summary without fresh login; expect 401 (unauthenticated) and no data exposure.\n12. Login again with the same device_id; ASSUMPTION: response includes device_recognized=true or audit log records trusted device recognition; verify MFA still required as per policy (no bypass).", + "expectedResult": "Account locks after 5 failed attempts with unlock_at; rate limiting returns 429 on 11th request/min with retry_after; remember_me extends refresh TTL to 30 days; cookies are HttpOnly, Secure, SameSite=Strict; logout clears cookies and invalidates refresh token; accessing protected resources without auth returns 401; device recognition is recorded without reducing security.", + "apiPath": "/v2/auth/login, /v2/auth/logout, /v2/auth/token/refresh, /v2/accounts/{account_id}/summary", + "httpMethod": "POST, POST, POST, GET", + "endpointGroup": "Auth, Accounts", + "workflow": "Authentication & Session Management", + "businessRuleIds": "NFR-05, NFR-06", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "false for login, true for summary", + "csrfRequired": "true for logout only", + "mfaRequired": "true for successful login", + "rateLimitBucket": "login 10 req/min per IP", + "inputFields": "email,password,mfa_code,device_id,remember_me", + "validationRules": "Password complexity enforced at registration; login lock after 5 failures; rate limit 10/min per IP; tokens in Secure, HttpOnly, SameSite=Strict cookies only", + "errorCodesCovered": "INVALID_CREDENTIALS, ACCOUNT_LOCKED, RATE_LIMITED, TOKEN_INVALID, UNAUTHORIZED", + "stateTransitions": "Auth: unlocked->locked->unlocked; Session: logged_in->logged_out", + "dataMaskingChecks": "No tokens in localStorage/sessionStorage; cookies only; no PII in error messages", + "auditTrailChecks": "ASSUMPTION: Login attempts, lock, unlock, and logout recorded with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "email", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: refresh_expires_in or cookie expiry visible; ASSUMPTION: device_recognized flag or audit evidence available; ASSUMPTION: X-RateLimit-Remaining and retry_after headers present.", + "testData": "email user@example.com, device_id 8c4a8f1e-3c1d-4f9d-9a2c-1b2c3d4e5f60", + "cleanupSteps": "Ensure account is unlocked post-test; clear cookies/session; reset rate-limit counters if needed", + "dependencies": "MFA seed provisioned; CSRF token retrieval endpoint available" + }, + { + "type": "functional", + "title": "Credit Application: Step Order Enforcement, Session Token Validation, PENDING and DECLINED Decisions", + "description": "Verifies sequential step enforcement, X-App-Session header validation, decision thresholds for PENDING and DECLINED, marketing_opt_in default, and signature validation.", + "testId": "TC-APP-ORDER-007", + "testDescription": "Covers multi-path outcomes and error handling: invalid step order, expired/invalid session_token, e_signature missing, PENDING (FICO 600-680), DECLINED (<600), and duplicate application rule.", + "prerequisites": "Two verified users exist: userA@example.com and userB@example.com; authenticated sessions with CSRF tokens; products catalog contains a valid card_product_id.", + "stepsToPerform": "1. For userA@example.com, call POST /v2/applications/start with valid Step 1 data (full_legal_name, email matching authenticated user, phone +14165550111, valid Canadian address, id_type PASSPORT, id_number XABC12345); capture application_id and session_token.\n2. Attempt to skip Step 2 by calling POST /v2/applications/{application_id}/submit with card_product_id AEGIS_GOLD and a valid e_signature; expect 400 INVALID_STEP_ORDER (ASSUMPTION) and no decision produced.\n3. Call POST /v2/applications/{application_id}/financials with an intentionally invalid X-App-Session header (expired or tampered token); expect 401 SESSION_EXPIRED and confirm no fico_pull_id returned.\n4. Reattempt Step 2 with the correct session_token but omit employer_name while employment_status=EMPLOYED; expect 400 with field=employer_name; correct the payload by adding employer_name and sin_consent=true; expect 200 with status PENDING_REVIEW and fico_pull_id.\n5. Configure the bureau stub for userA to return FICO=620 (ASSUMPTION: test harness); call POST /v2/applications/{application_id}/submit with valid e_signature Base64 and marketing_opt_in omitted; expect 200 with decision PENDING, review_eta_hours present, and marketing_opt_in default false.\n6. Re-submit Step 3 immediately; expect idempotent behavior (200 returning same PENDING) or 409 ALREADY_SUBMITTED (ASSUMPTION); verify no duplicate application records created.\n7. For userB@example.com, call POST /v2/applications/start with valid Step 1 data; capture application_id and session_token.\n8. Call POST /v2/applications/{application_id}/financials with valid data and sin_consent=true; set bureau stub to FICO=580 (ASSUMPTION) to trigger DECLINED.\n9. Call POST /v2/applications/{application_id}/submit with malformed e_signature (not Base64 or missing); expect 400 SIGNATURE_REQUIRED; correct e_signature and resubmit; expect 200 with decision DECLINED and reason_code populated.\n10. While userB has a DECLINED outcome, attempt to start a new application again via POST /v2/applications/start; if business rule disallows concurrent active applications, expect 409 DUPLICATE_APPLICATION until prior is closed (ASSUMPTION); otherwise expect 201 for a new application and document observed behavior.\n11. Attempt POST /v2/applications/{application_id}/submit without the X-App-Session header; expect 401 SESSION_EXPIRED per requirement that Steps 2 and 3 must carry session_token.", + "expectedResult": "System enforces sequential steps; invalid session_token yields 401; e_signature required; decision outcomes adhere to thresholds (620=PENDING, 580=DECLINED with reason_code); marketing_opt_in defaults false when omitted; resubmission is idempotent; duplicate application rule enforced per spec.", + "apiPath": "/v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit", + "httpMethod": "POST, POST, POST", + "endpointGroup": "Applications", + "workflow": "Credit Application Web Flow", + "businessRuleIds": "REQ-002", + "calculationFormula": "Decision thresholds: FICO > 680 APPROVED; 600-680 PENDING; <600 DECLINED.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for account session if policy requires, not per endpoint", + "rateLimitBucket": "applications standard", + "inputFields": "full_legal_name,email,phone_number,residential_address,id_type,id_number,employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,card_product_id,e_signature", + "validationRules": "email must match authenticated user, X-App-Session required for Steps 2 and 3, employer_name required if EMPLOYED, e_signature must be Base64 encoded", + "errorCodesCovered": "INVALID_STEP_ORDER, SESSION_EXPIRED, SIGNATURE_REQUIRED, ALREADY_SUBMITTED, DUPLICATE_APPLICATION", + "stateTransitions": "Application: NEW->STEP1->STEP2(PENDING_REVIEW)->SUBMITTED->PENDING or DECLINED", + "dataMaskingChecks": "No full PAN in any application responses; no tokens exposed in UI storage", + "auditTrailChecks": "ASSUMPTION: Application creation and decision recorded with user_id, session_id, ip_address", + "piiFields": "full_legal_name,email,phone_number,residential_address", + "riskLevel": "High", + "regulatoryTags": "PII, Security", + "assumptions": "ASSUMPTION: Error INVALID_STEP_ORDER is returned when Step 3 is called before Step 2; ASSUMPTION: Bureau stub allows fixed FICO for tests; ASSUMPTION: Step 3 requires X-App-Session header though not shown in table.", + "testData": "userA@example.com FICO 620 (PENDING); userB@example.com FICO 580 (DECLINED); card_product_id AEGIS_GOLD", + "cleanupSteps": "Close or archive test applications via test harness; log out both users", + "dependencies": "Products catalog available; credit bureau stub configurable" + }, + { + "type": "functional", + "title": "Rewards Accrual Boundary Values and MCC Classification with Floor Rounding", + "description": "Validates REQ-012/REQ-013 rewards rules across MCC categories and boundary amounts (.99, .01, 1.00), ensuring floor-only rounding and correct statement totals (REQ-015).", + "testId": "TC-REWARDS-BV-008", + "testDescription": "Creates a targeted set of CAD purchases to test Travel vs Other MCC mapping and floor rounding; verifies points at account summary and statement, and rejects invalid MCC.", + "prerequisites": "Active account_id owned by user@example.com with sufficient available_credit; authenticated session with CSRF; points balance known (capture baseline).", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary with include_rewards=true to capture baseline points_balance P0 and confirm account_status Active.\n2. POST /v2/accounts/{account_id}/transactions with mcc_code 3000 (Travel), amount 1.00 CAD, merchant_name 'TravelOne', merchant_id 'T001'; expect 200 approval.\n3. POST /v2/accounts/{account_id}/transactions with mcc_code 3000 (Travel), amount 0.99 CAD, merchant_name 'TravelTwo', merchant_id 'T002'; expect 200 approval.\n4. POST /v2/accounts/{account_id}/transactions with mcc_code 5999 (Other), amount 1.00 CAD, merchant_name 'ShopOne', merchant_id 'S001'; expect 200 approval.\n5. POST /v2/accounts/{account_id}/transactions with mcc_code 5999 (Other), amount 0.01 CAD, merchant_name 'ShopTwo', merchant_id 'S002'; expect 200 approval.\n6. Attempt POST /v2/accounts/{account_id}/transactions with an invalid mcc_code '123' (3 digits) and amount 1.00; expect 400 validation error or 422 (implementation-specific) and no transaction created.\n7. GET /v2/accounts/{account_id}/transactions with category=PURCHASE, page=1, per_page=25 to confirm the four valid purchases are present with correct amounts and MCCs.\n8. Compute expected rewards per REQ-012/REQ-013: Travel points = floor(1.00*3)+floor(0.99*3)=3+2=5; Other points = floor(1.00*1)+floor(0.01*1)=1+0=1; total expected increment Δ=6.\n9. GET /v2/accounts/{account_id}/summary with include_rewards=true; verify points_balance == P0 + 6 (or that the next statement reflects +6 if accrual is statement-based; document observed accrual timing).\n10. GET /v2/accounts/{account_id}/statements/{statement_id} (latest) in JSON; verify rewards_earned includes the +6 points from these purchases and that sum(transaction_amount[]) equals total_spend within ±$0.01 (REQ-015).", + "expectedResult": "Rewards classify correctly by MCC (3000=Travel, 5999=Other); floor rounding applied so 0.99 Travel -> 2 points and 0.01 Other -> 0 points; total rewards increment equals 6; invalid MCC is rejected; statement totals reconcile within tolerance.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "POST, GET, GET", + "endpointGroup": "Transactions, Accounts, Billing", + "workflow": "Billing, Rewards & Financial Logic", + "businessRuleIds": "REQ-012, REQ-013, REQ-015", + "calculationFormula": "Travel points = floor(amount × 3); Other points = floor(amount × 1); totals must floor each line item and sum.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard", + "inputFields": "transaction_amount,mcc_code,merchant_name,merchant_id,category,page,per_page,include_rewards", + "validationRules": "mcc_code must be 4 digits; transaction_amount > 0; per_page <=100", + "errorCodesCovered": "INVALID_AMOUNT, validation error for mcc_code, FORBIDDEN (if access attempt to another account)", + "stateTransitions": "Points balance P0 -> P0+Δ after accrual", + "dataMaskingChecks": "No PAN visible; masked only **** **** **** 1234 in any UI feed related to transactions", + "auditTrailChecks": "ASSUMPTION: Transaction creation logged with user_id/session_id/ip_address", + "piiFields": "None beyond account ownership relation", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1", + "assumptions": "ASSUMPTION: Rewards accrue either realtime or at statement close; test accepts either with documentation; statement_id for latest cycle is discoverable.", + "testData": "MCC Travel=3000, Other=5999; amounts: 1.00, 0.99, 1.00, 0.01", + "cleanupSteps": "None; transactions remain for statement verification", + "dependencies": "Sufficient available_credit; statement generation schedule known" + }, + { + "type": "functional", + "title": "WebSocket Live Feed Resilience: JWT Expiry Handling, Reconnect, Unauthorized Subscription", + "description": "Validates authenticated subscription, JWT expiry during session, reconnect with refreshed token, forbidden access to other accounts, and secure transport use.", + "testId": "TC-WS-RESILIENCE-009", + "testDescription": "Exercises realtime feed auth and reliability: token expiration mid-connection, reconnection flow, subscription scoping, and absence of PII in events.", + "prerequisites": "Active account_id; valid access/refresh tokens; ability to create transactions; browser supports WebSocket over TLS 1.3.", + "stepsToPerform": "1. Connect to wss://realtime.aegiscard.com/v2/stream using Authorization: Bearer ; subscribe to topic account:{account_id}:transactions; expect subscription ack.\n2. Trigger a CAD PURCHASE via POST /v2/accounts/{account_id}/transactions; expect a WebSocket event with transaction_id, amounts, masked PAN, and no PII leakage (no full PAN, SSN, or tokens).\n3. Keep the connection open until the access token expires (15 minutes); send a ping or attempt a no-op; expect the server to close the connection or send 401/unauthorized event due to expired JWT (ASSUMPTION per implementation).\n4. Call POST /v2/auth/token/refresh to obtain a new access_token (refresh rotates); verify old refresh cannot be reused.\n5. Reconnect the WebSocket with the new access_token; resubscribe to account:{account_id}:transactions; expect ack and healthy heartbeats (ASSUMPTION: heartbeat/ping supported).\n6. Attempt to subscribe to account:{other_account_id}:transactions; expect immediate 403 FORBIDDEN or subscription error with no events delivered.\n7. Trigger another PURCHASE on own account; verify a single event is received (no duplicates) and payload integrity maintained.\n8. Simulate a transient network drop by forcibly closing the socket; within 10 seconds, reconnect and resubscribe; verify subsequent events continue streaming without duplication.\n9. Attempt to pass JWT via query string instead of Authorization header; expect connection rejected or downgraded privileges; reconnect properly using Authorization header and confirm success.\n10. Verify all frames are over TLS 1.3; confirm no downgrade; ensure no sensitive tokens are echoed in any server message.", + "expectedResult": "Authenticated subscription works; token expiry causes disconnect or 401; refresh and reconnect restore the stream; attempts to subscribe to other accounts are forbidden; events include masked PAN only; insecure token conveyance (query string) is rejected; connection remains on TLS 1.3.", + "apiPath": "wss://realtime.aegiscard.com/v2/stream, /v2/accounts/{account_id}/transactions, /v2/auth/token/refresh", + "httpMethod": "WebSocket, POST, POST", + "endpointGroup": "Realtime, Transactions, Auth", + "workflow": "Notifications & Realtime Feed", + "businessRuleIds": "REQ-014 for masking, NFR-01 TLS", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST transaction, false for WebSocket connect", + "mfaRequired": "false for socket after login", + "rateLimitBucket": "transactions standard; socket connection limits not specified", + "inputFields": "Authorization header, subscription topic account:{account_id}:transactions", + "validationRules": "WebSocket requires JWT; owner-only topic subscription; events must not include PII", + "errorCodesCovered": "FORBIDDEN (topic), TOKEN_INVALID on refresh reuse, UNAUTHORIZED on expired JWT", + "stateTransitions": "Socket: connected->expired->reconnected", + "dataMaskingChecks": "Event payloads use **** **** **** 1234; no tokens or PII in messages", + "auditTrailChecks": "ASSUMPTION: Connections and subscription attempts logged with user_id/ip", + "piiFields": "None", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Server closes socket or emits 401 on expired JWT; ASSUMPTION: heartbeat frames available; ASSUMPTION: JWT in query string is unsupported.", + "testData": "other_account_id different from owner", + "cleanupSteps": "Close WebSocket; logout; revoke refresh token", + "dependencies": "Realtime service operational; ability to create transactions in test account" + }, + { + "type": "functional", + "title": "Access Control and CSRF Enforcement Matrix: 401 vs 403, SameSite=Strict Cross-Site Protection", + "description": "Validates unauthenticated vs unauthorized responses across endpoints, CSRF enforcement on state-changing calls, and SameSite=Strict preventing cross-site cookie send.", + "testId": "TC-ACCESS-CSRF-010", + "testDescription": "Covers precise 401/403 mappings, CSRF presence and validity, cross-origin submission blocked by SameSite cookies, and secure cookie attributes.", + "prerequisites": "Two accounts exist: account_id_owned and account_id_other; user logged out initially; browser devtools access; CSRF issuance endpoint available.", + "stepsToPerform": "1. While logged out, call GET /v2/accounts/{account_id_owned}/summary without Authorization cookie; expect 401 (unauthenticated) and no body data.\n2. Login to portal; confirm access and obtain CSRF token via GET /v2/auth/csrf (ASSUMPTION) or response meta.\n3. GET /v2/accounts/{account_id_owned}/summary; expect 200 with balances and owner-only access confirmed.\n4. GET /v2/accounts/{account_id_other}/summary using current token; expect 403 FORBIDDEN (unauthorized) and no data exposure.\n5. Attempt POST /v2/accounts/{account_id_owned}/transactions with valid payload but without X-CSRF-Token; expect 403 CSRF_MISSING and no transaction created.\n6. Retry the same POST including X-CSRF-Token; expect 200 approval and transaction_id present.\n7. From a different origin (e.g., https://evil.example), attempt to submit a cross-site HTML form POST to /v2/accounts/{account_id_owned}/transactions; verify SameSite=Strict prevents auth cookies from being sent, leading to 401 UNAUTHORIZED and no side effects (ASSUMPTION: CORS blocks or cookies omitted).\n8. Inspect cookies in the browser; verify Secure, HttpOnly, SameSite=Strict flags set; ensure no tokens in localStorage/sessionStorage.\n9. Logout via POST /v2/auth/logout with current CSRF; obtain a new login session and fetch a new CSRF token; attempt POST /v2/accounts/{account_id_owned}/transactions using the old CSRF from the previous session; expect 403 CSRF_INVALID or CSRF_MISSING (implementation-specific) and no transaction created.\n10. Confirm that error responses never include PII and that rate-limiting headers appear only on applicable responses (e.g., 429) and not on access-control denials.", + "expectedResult": "Unauthenticated access yields 401; unauthorized access to other accounts yields 403; CSRF is required and validated per session; cross-site posts fail due to SameSite=Strict; secure cookie attributes enforced; no PII in errors.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/auth/logout, /v2/auth/csrf", + "httpMethod": "GET, POST, POST, GET", + "endpointGroup": "Accounts, Transactions, Auth", + "workflow": "Security & Compliance Controls", + "businessRuleIds": "NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true for owner-only resources", + "csrfRequired": "true for POST/DELETE/PATCH/PUT", + "mfaRequired": "false for this flow", + "rateLimitBucket": "N/A", + "inputFields": "X-CSRF-Token, transaction_amount, merchant_name, merchant_id, mcc_code", + "validationRules": "CSRF required on mutating requests; owner-only access on account resources; SameSite=Strict on cookies", + "errorCodesCovered": "UNAUTHORIZED, FORBIDDEN, CSRF_MISSING, CSRF_INVALID", + "stateTransitions": "Session: logged_out->logged_in->logged_out->logged_in", + "dataMaskingChecks": "No sensitive data in error payloads; PAN never present", + "auditTrailChecks": "ASSUMPTION: Access denials and logout recorded", + "piiFields": "None returned on errors", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/csrf exists; ASSUMPTION: CSRF token is session-bound; ASSUMPTION: Cross-origin request lacks credentials due to SameSite=Strict.", + "testData": "account_id_owned vs account_id_other distinct values; CSRF tokens old/new", + "cleanupSteps": "Clear cookies/session; remove any test transactions created", + "dependencies": "CORS and cookie policies configured; CSRF service available" + }, + { + "type": "functional", + "title": "Registration Validation: Email Uniqueness (Case-Insensitive), Password Complexity, DOB >=18 (Leap-Year), Phone E.164, Terms Required", + "description": "Validate registration field rules and error codes including cross-case email uniqueness, password complexity boundaries, age calculation on leap-year birthdays, E.164 phone, ssn_last4 length, and agree_terms enforcement with TLS 1.3.", + "testId": "TC-AUTH-REG-011", + "testDescription": "Ensures POST /v2/auth/register enforces all validations and returns correct status codes and fields; confirms verification email trigger and no sensitive data exposure.", + "prerequisites": "No existing account for user@example.com; network uses TLS 1.3; WAF and API gateway reachable.", + "stepsToPerform": "1. Confirm browser session negotiates TLS 1.3 with https://api.aegiscard.com/v2 (check security info/HSTS) and no mixed-content warnings.\n2. POST /v2/auth/register with agree_terms=false and otherwise valid data (email new_user@example.com, strong password, valid DOB, phone +14165550123, ssn_last4 1234); expect rejection due to terms.\n3. POST /v2/auth/register with agree_terms=true but weak password 'Short1!' (length < 12); expect 422 WEAK_PASSWORD.\n4. POST /v2/auth/register with password strong but date_of_birth set to exactly 17 years, 364 days ago (edge just under 18) including leap-year handling; expect 400 with field=date_of_birth.\n5. POST /v2/auth/register with invalid phone_number '4165550123' (missing +country code); expect 400 with field=phone_number (E.164 required).\n6. POST /v2/auth/register with ssn_last4 '123' (3 digits); expect 400 with field=ssn_last4 exact length rule enforced.\n7. POST /v2/auth/register with valid payload: first_name John, last_name Public, email user@example.com, strong password >=12 with upper/lower/digit/symbol, date_of_birth exactly 18 years ago today (boundary), phone_number +14165550123, ssn_last4 1234, agree_terms true; expect 201 with user_id and verification_token present.\n8. Immediately attempt POST /v2/auth/register again using same email but different case 'USER@EXAMPLE.COM'; expect 409 EMAIL_EXISTS confirming case-insensitive uniqueness.\n9. Validate that server indicates verification workflow triggered (verification_token in response or ASSUMPTION: an outbound email log entry exists); capture token for potential later use.\n10. Inspect browser storage to confirm no JWTs or PII stored in localStorage/sessionStorage; ensure HttpOnly cookie policy (if any set) and that no PAN or sensitive data appears in DOM.", + "expectedResult": "Registration enforces all field rules: terms required, strong password, E.164 phone, ssn_last4 exactly 4 digits, DOB >=18; email uniqueness is case-insensitive; success returns 201 with verification_token; TLS 1.3 in effect; no sensitive data stored in frontend.", + "apiPath": "/v2/auth/register", + "httpMethod": "POST", + "endpointGroup": "Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-01, Registration Rules", + "calculationFormula": "Age >= 18 computed from date_of_birth considering leap years; email uniqueness normalized case-insensitively.", + "rolesCovered": "Cardholder", + "authRequired": "false", + "csrfRequired": "false", + "mfaRequired": "false", + "rateLimitBucket": "login not exercised; registration standard", + "inputFields": "first_name,last_name,email,password,date_of_birth,phone_number,ssn_last4,agree_terms", + "validationRules": "Email RFC5322 and unique (case-insensitive), Password >=12 with upper/lower/digit/symbol, DOB >=18 with leap-year handling, Phone E.164, ssn_last4 exactly 4 digits, agree_terms true", + "errorCodesCovered": "WEAK_PASSWORD, EMAIL_EXISTS, 400 field validation errors", + "stateTransitions": "User: none->registered (pending verification)", + "dataMaskingChecks": "No PAN in DOM; no JWTs in localStorage/sessionStorage; PII only in request/response as specified", + "auditTrailChecks": "ASSUMPTION: Registration event logged with user_id/ip/timestamp", + "piiFields": "first_name,last_name,email,date_of_birth,phone_number,ssn_last4", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: Verification token or email log available in test environment; ASSUMPTION: Age calculation uses UTC date; ASSUMPTION: WAF/HSTS observable at test time.", + "testData": "email user@example.com, phone +14165550123, ssn_last4 1234, strong password e.g. 'Str0ngPass!2026'", + "cleanupSteps": "None required; optionally delete test user via admin tool (non-prod).", + "dependencies": "Email verification system or stub available for observing verification_token" + }, + { + "type": "functional", + "title": "Refresh Token Rotation Concurrency Across Tabs: Single-Use Enforcement, Session Isolation, CSRF Scope", + "description": "Validate refresh token single-use rotation when two browser tabs attempt refresh; ensure the first succeeds, the second is rejected; verify CSRF token is session-bound and cannot be re-used across tabs; logout invalidates refresh globally; cookie flags enforced.", + "testId": "TC-AUTH-ROTATE-012", + "testDescription": "Simulates multi-tab browser behavior to assert JWT refresh rotation, reuse detection (401), CSRF session binding, and logout invalidation across tabs with Secure, HttpOnly, SameSite=Strict cookies.", + "prerequisites": "Existing verified user user@example.com with MFA; ability to open two tabs (Tab A, Tab B) sharing initial session; CSRF issuance endpoint available.", + "stepsToPerform": "1. In Tab A, POST /v2/auth/login with email user@example.com, correct password, device_id UUIDv4, remember_me=false; complete MFA with valid 6-digit TOTP; expect 200 and cookies set for access_token and refresh_token (Secure, HttpOnly, SameSite=Strict).\n2. Open Tab B (same browser profile), confirm authenticated context via a protected GET /v2/accounts/{account_id}/summary returning 200 (owner-only account).\n3. In Tab A, call POST /v2/auth/token/refresh; expect 200 with new rotated refresh_token (R1-new) and new access_token; old refresh invalidated.\n4. Immediately in Tab B, call POST /v2/auth/token/refresh using the now-stale refresh cookie; expect 401 TOKEN_INVALID; verify Tab B UI forces re-auth and clears tokens.\n5. In Tab A, GET /v2/auth/csrf (ASSUMPTION endpoint) and store X-CSRF-Token CSRF-A; POST /v2/auth/logout with header X-CSRF-Token: CSRF-A; expect 200 and cookies expired in Tab A.\n6. Without logging in, in Tab B attempt POST /v2/auth/logout using CSRF-A; expect 403 CSRF_INVALID or CSRF_MISSING since CSRF tokens are session-bound; no state change.\n7. Log back in on Tab B only (MFA successful); confirm new access and refresh cookies set; GET /v2/auth/csrf to obtain CSRF-B (distinct from CSRF-A).\n8. Attempt POST /v2/auth/token/refresh in Tab B twice in quick succession: first call returns 200 rotated tokens, second call (reusing the first refresh) returns 401 TOKEN_INVALID; observe UI gracefully handles second failure.\n9. Validate cookies across both tabs are HttpOnly, Secure, SameSite=Strict; inspect storage to ensure no tokens exist in localStorage/sessionStorage.\n10. From Tab A (still logged out), try accessing a protected endpoint GET /v2/accounts/{account_id}/summary; confirm 401 UNAUTHORIZED and that re-login restores access.", + "expectedResult": "First refresh succeeds, concurrent/stale refresh attempts return 401 TOKEN_INVALID; CSRF tokens are session-bound and cannot be reused across tabs; logout invalidates tokens globally; cookies carry correct security flags; no tokens reside in web storage.", + "apiPath": "/v2/auth/login, /v2/auth/token/refresh, /v2/auth/csrf, /v2/auth/logout, /v2/accounts/{account_id}/summary", + "httpMethod": "POST, POST, GET, POST, GET", + "endpointGroup": "Auth, Accounts", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-05, NFR-06", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true for summary, false for login/refresh", + "csrfRequired": "true for logout only", + "mfaRequired": "true for successful login", + "rateLimitBucket": "login 10 req/min per IP (not exceeded in this test)", + "inputFields": "email,password,mfa_code,device_id,X-CSRF-Token", + "validationRules": "Refresh token rotation single-use, CSRF token must match active session, cookies must be Secure, HttpOnly, SameSite=Strict", + "errorCodesCovered": "TOKEN_INVALID, CSRF_INVALID, UNAUTHORIZED", + "stateTransitions": "Session: logged_in(Tab A,B)->Tab A refreshed->Tab B invalid refresh->Tab A logout->Tab B re-login", + "dataMaskingChecks": "No sensitive tokens in frontend storage; no PII in error responses", + "auditTrailChecks": "ASSUMPTION: Login, refresh, logout events logged with user_id/session_id/ip/timestamp", + "piiFields": "email", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/csrf exists and CSRF is session-bound; ASSUMPTION: UI clears tokens on 401 refresh failure; ASSUMPTION: SameSite=Strict enforced.", + "testData": "email user@example.com, device_id 550e8400-e29b-41d4-a716-446655440001", + "cleanupSteps": "Logout from all tabs; clear cookies; reset any lockouts if accidentally triggered.", + "dependencies": "MFA seed available; CSRF issuance endpoint available" + }, + { + "type": "functional", + "title": "Transaction Parameter Validation and FX Precision: Required exchange_rate, Decimal(8,6) Boundaries, INVALID_AMOUNT Zero", + "description": "Validate transaction field constraints including amount > 0, required exchange_rate when currency != CAD, precision limits for Decimal(8,6), and foreign fee calculation/rounding correctness for small amounts; confirm pagination page minimum.", + "testId": "TC-TXN-VALID-013", + "testDescription": "Covers negative and boundary validations for POST /v2/accounts/{account_id}/transactions and verifies REQ-006 math for low-value FX with two-decimal rounding; includes listing parameter boundary (page >=1).", + "prerequisites": "Active account_id owned by user@example.com with available_credit >= $50.00; authenticated session with X-CSRF-Token; merchant test IDs available.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to confirm account_status Active and available_credit >= $50.00; capture baseline available_credit.\n2. POST /v2/accounts/{account_id}/transactions with transaction_amount 0.00 CAD, valid merchant fields, mcc_code 5999, transaction_type PURCHASE, include X-CSRF-Token; expect 422 INVALID_AMOUNT.\n3. POST /v2/accounts/{account_id}/transactions with currency_code USD, transaction_amount 5.00, but omit exchange_rate; expect 400 validation error for missing exchange_rate when currency != CAD.\n4. POST /v2/accounts/{account_id}/transactions with currency_code USD, transaction_amount 1.23, exchange_rate 1.3333337 (7 decimal places); expect 400 validation error for exchange_rate precision beyond Decimal(8,6).\n5. POST valid FX transaction: currency_code USD, transaction_amount 1.23, exchange_rate 1.333333 (max precision), mcc_code 3000, merchant_name 'MiniTravel', merchant_id 'MT001', transaction_type PURCHASE, X-CSRF-Token present; expect 200 approval.\n6. Compute expected REQ-006 values: base_cad = 1.23 × 1.333333 = 1.63999959 → round to cents at final amounts only; foreign_fee_amount = base_cad × 0.03 = 0.0491999877 ≈ 0.05; Total_CAD = base_cad × 1.03 = 1.6891993777 ≈ 1.69; verify response itemises foreign_fee_amount 0.05 and Total_CAD impact reflected in available_credit.\n7. POST a non-FX transaction with transaction_type CASH_ADVANCE, transaction_amount 10.00 CAD, mcc_code 6010 (ASSUMPTION valid), merchant_name 'CashPoint', merchant_id 'CA001'; expect 200 approval or domain-appropriate handling; record category for listing.\n8. POST a BALANCE_TRANSFER transaction amount 15.00 CAD, mcc_code 6012 (ASSUMPTION valid), merchant_name 'BalanceXfer', merchant_id 'BT001'; expect 200 approval; record category.\n9. GET /v2/accounts/{account_id}/transactions with page=0 (invalid), per_page=25; expect 400 invalid page (min 1).\n10. GET /v2/accounts/{account_id}/transactions with page=1, per_page=25 and category filters PURCHASE,CASH_ADVANCE in separate calls; verify items returned match submitted types and that owner-only access is enforced (403 if querying another account).", + "expectedResult": "Transactions validate amount > 0; missing exchange_rate for non-CAD rejected; exchange_rate precision > 6 decimals rejected; valid FX applies 3% fee with correct two-decimal rounding; listing page must be >=1; owner-only access enforced.", + "apiPath": "/v2/accounts/{account_id}/transactions (POST), /v2/accounts/{account_id}/transactions (GET)", + "httpMethod": "POST, GET", + "endpointGroup": "Transactions", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-006", + "calculationFormula": "REQ-006: Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03; final monetary amounts rounded to two decimals.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard (no frequency limit reached)", + "inputFields": "transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,description,page,per_page,category", + "validationRules": "transaction_amount > 0.00, mcc_code 4 digits, exchange_rate required when currency != CAD with Decimal(8,6), page >=1, per_page <=100", + "errorCodesCovered": "INVALID_AMOUNT, INVALID_DATE_RANGE (for list if date filters used), FORBIDDEN, 400 field validation", + "stateTransitions": "Available credit decreases per approved transaction; list filters reflect transaction types", + "dataMaskingChecks": "Responses must not include full PAN; masked **** **** **** 1234 where applicable", + "auditTrailChecks": "ASSUMPTION: Transaction approvals logged with user_id/session_id/ip", + "piiFields": "None beyond account ownership linkage and merchant descriptors", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: mcc_code 6010 and 6012 acceptable for CASH_ADVANCE/BALANCE_TRANSFER; ASSUMPTION: Rounding applied at currency amount presentation, not intermediate multiplications.", + "testData": "USD exchange_rate 1.333333; amounts 1.23 USD, 10.00 CAD, 15.00 CAD; MCCs 3000, 6010, 6012", + "cleanupSteps": "None; retain transactions for later statement validation.", + "dependencies": "Sufficient available_credit; CSRF token present" + }, + { + "type": "functional", + "title": "Statement Calculations Boundary: ADB Interest Rounding, Grace Eligibility Edge, Late Fee UTC +2 Days Threshold, Not Found", + "description": "Validate REQ-009 interest calculation and rounding to cents across cycle lengths and APR, REQ-010 grace period boundary when paid-in-full exactly vs slightly under, REQ-011 late fee at due_date+2 days (UTC) boundary, and 404 for unknown statement_id.", + "testId": "TC-BILL-CALC-014", + "testDescription": "Exercises precise billing edges and time boundaries, ensuring amounts reconcile and errors are correct for missing statements.", + "prerequisites": "Account with known APR (e.g., 19.99%), recent cycles available; ability to set test data for prev_statement_balance_paid_in_full and payment_received_date times; authenticated session; statement_id list accessible.", + "stepsToPerform": "1. Retrieve latest statements list (ASSUMPTION: via a list API or portal) and pick the previous cycle; GET /v2/accounts/{account_id}/statements/{statement_id_prev} to read prev_statement_balance_paid_in_full and due_date.\n2. Configure test data so prev_statement_balance_paid_in_full=true with previous cycle fully paid on time; after current cycle close, GET /v2/accounts/{account_id}/statements/{statement_id_curr}; expect interest_charged=0 per REQ-010.\n3. Re-run with prev_statement_balance_paid_in_full=false by simulating a tiny unpaid remainder of $0.01 last cycle; after current cycle close, GET statement_id_curr2; confirm interest_charged > 0 computed by REQ-009.\n4. Validate REQ-009: using returned adb and cycle days, compute Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; compare to interest_charged rounded to cents; verify tolerance exactly matches rounding rules.\n5. Verify total_spend equals sum(transaction_amount[]) within ±$0.01 tolerance (REQ-015) using the JSON statement payload.\n6. Boundary late fee: set payment_received_date exactly at due_date + 2 days (23:59:59.000 UTC) and re-generate or fetch recalculated statement; expect late_fee=0.\n7. Set payment_received_date to due_date + 2 days + 1 second (00:00:01 UTC next moment); GET updated statement; expect late_fee=$35.00 (REQ-011) regardless of weekend/holiday (ASSUMPTION).\n8. Request the same statement in PDF by adding format=PDF; verify 200 and application/pdf content type; ensure owner-only access (403 when using another account_id).\n9. GET /v2/accounts/{account_id}/statements/{nonexistent_statement_id}; expect 404 NOT_FOUND; no sensitive data in error payload.\n10. Confirm rewards_earned field presence; if present, ensure it follows floor-only rounding for any Travel vs Other transactions recorded (consistency check with REQ-012/REQ-013, not recalculation in this test).", + "expectedResult": "Interest is zero when paid-in-full; otherwise matches REQ-009 formula to cents; late fee applies only after due_date+2 days (UTC) boundary; statement totals reconcile within tolerance; PDF/JSON retrieval works; 404 returned for unknown statement_id; owner-only access enforced.", + "apiPath": "/v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "GET", + "endpointGroup": "Billing", + "workflow": "Billing, Rewards & Financial Logic", + "businessRuleIds": "REQ-009, REQ-010, REQ-011, REQ-015", + "calculationFormula": "Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; Late fee = $35.00 if payment_received_date > due_date + 2 days (UTC).", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "false", + "mfaRequired": "false", + "rateLimitBucket": "N/A", + "inputFields": "account_id,statement_id,format", + "validationRules": "Owner-only access to statements; format JSON or PDF; nonexistent statement returns 404", + "errorCodesCovered": "NOT_FOUND, FORBIDDEN", + "stateTransitions": "Billing cycle close -> statement generated with computed fields", + "dataMaskingChecks": "No PAN exposure in statement JSON/PDF; masked PAN if present", + "auditTrailChecks": "ASSUMPTION: Statement generation and recalculations logged", + "piiFields": "Account ownership linkage only; no sensitive PII in errors", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, PII minimal", + "assumptions": "ASSUMPTION: Test harness can set payment_received_date and prev_statement_balance_paid_in_full flags; ASSUMPTION: Owner-only enforced consistently; weekends/holidays do not alter late fee logic.", + "testData": "APR 19.99%, sample adb and cycle length (e.g., 30 days), due_date known, nonexistent_statement_id a random UUID", + "cleanupSteps": "Revert any test harness overrides for payments and flags.", + "dependencies": "Statement generation schedule; access to a statements list or portal UI to obtain IDs" + }, + { + "type": "functional", + "title": "Audit Trail for Credit Limit Changes: Immutable Logging, Owner Visibility, Access Control", + "description": "Validate NFR-04 audit trail for credit_limit changes including user_id, session_id, ip_address, timestamp_utc; confirm account summary reflects new limit/available_credit; enforce 403 for non-owner audit access.", + "testId": "TC-AUDIT-LIMIT-015", + "testDescription": "Ensures credit_limit modifications are audited immutably and visible via an admin/audit interface; confirms no user-level endpoint allows limit change and that available_credit aligns with new limit.", + "prerequisites": "Active account with current_balance known; admin test credentials for audit view; authenticated cardholder session for summary checks.", + "stepsToPerform": "1. As the cardholder, GET /v2/accounts/{account_id}/summary; record credit_limit L0, current_balance B0, available_credit A0, account_status Active.\n2. As an admin (ASSUMPTION: admin role), PATCH /v2/admin/accounts/{account_id}/credit-limit with new credit_limit L1 = L0 + 1000.00 and include admin CSRF token (ASSUMPTION); expect 200 and success confirmation.\n3. As the cardholder, GET /v2/accounts/{account_id}/summary again; verify credit_limit equals L1 and available_credit updated approximately A0 + 1000.00 (accounting for current_balance), with no discrepancies.\n4. Trigger a small PURCHASE of $10.00 CAD via POST /v2/accounts/{account_id}/transactions (with X-CSRF-Token) to confirm available_credit decrements from the new limit baseline; expect 200.\n5. As admin, GET /v2/admin/audit?entity=credit_limit&account_id={account_id}; verify an audit record exists for the change showing old_value L0, new_value L1, user_id (admin), session_id, ip_address, timestamp_utc, and immutable hash or sequence ID (ASSUMPTION).\n6. As the cardholder (non-admin), attempt to access the same audit endpoint; expect 403 FORBIDDEN with no data exposure.\n7. As the cardholder, attempt to change credit_limit via a non-existent or blocked user endpoint (e.g., PATCH /v2/accounts/{account_id}/credit-limit); expect 404 NOT_FOUND or 403 FORBIDDEN, confirming no user path exists.\n8. Validate no full PAN appears in audit payloads or summaries; any card references are masked as **** **** **** 1234 (REQ-014).\n9. Repeat admin credit_limit change to L2 = L1 - 500.00 (still >= current_balance) and confirm a second audit entry appended (immutable history), then verify updated limit in summary matches L2.\n10. Confirm timestamps are ISO 8601 UTC in audit entries and that records cannot be altered by a subsequent admin call (attempted update should be rejected by design, ASSUMPTION).", + "expectedResult": "Account summary reflects new credit_limit and available_credit; audit trail captures each change with required metadata and is immutable; non-admin access to audit fails with 403; no user-level endpoint allows limit changes; PAN masking enforced.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/admin/accounts/{account_id}/credit-limit, /v2/admin/audit", + "httpMethod": "GET, POST, PATCH, GET", + "endpointGroup": "Accounts, Transactions, Admin", + "workflow": "Account Dashboard & Card Management", + "businessRuleIds": "NFR-04, REQ-014", + "calculationFormula": "available_credit = credit_limit - current_balance (simplified, excluding pending/holds).", + "rolesCovered": "Cardholder, Admin", + "authRequired": "true", + "csrfRequired": "true for PATCH/POST", + "mfaRequired": "ASSUMPTION: true for admin-sensitive actions", + "rateLimitBucket": "standard", + "inputFields": "credit_limit,new_limit,account_id,admin_csrf", + "validationRules": "Owner-only account summary; admin-only credit_limit change; audit records immutable with user_id,session_id,ip,timestamp", + "errorCodesCovered": "FORBIDDEN, NOT_FOUND", + "stateTransitions": "Credit limit L0->L1->L2; audit log length increments per change", + "dataMaskingChecks": "PAN masked everywhere; no sensitive PII in audit output beyond required metadata", + "auditTrailChecks": "Audit entries contain user_id, session_id, ip_address, timestamp_utc, old/new values, immutable identifier", + "piiFields": "user_id in audit, ip_address (considered sensitive logging metadata)", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Audit Trail, Security", + "assumptions": "ASSUMPTION: Admin endpoints exist for credit limit updates and audit retrieval; ASSUMPTION: Audit store is immutable and queryable; ASSUMPTION: Admin actions require CSRF and potentially MFA.", + "testData": "L0 current limit from summary; L1 = L0 + 1000; L2 = L1 - 500; transaction $10.00 CAD", + "cleanupSteps": "Restore original credit_limit L0 via admin endpoint; verify final audit entry documents the restoration.", + "dependencies": "Admin credentials; audit service availability; CSRF issuance for admin" + }, + { + "type": "functional", + "title": "Phone OTP Verification Flow: Request, Verify, Expiry, Resend Throttle, Attempts Remaining", + "description": "Validate phone OTP lifecycle for application phone verification including invalid code, expiry handling, resend rate limiting, attempts_remaining decrement, and successful verification flagging with no PII leakage.", + "testId": "TC-OTP-PHONE-016", + "testDescription": "Ensures OTP endpoints enforce 6-digit format, throttle resends, expire codes, track attempts_remaining, and set phone_verified=true only upon correct OTP while masking PII.", + "prerequisites": "Authenticated user user@example.com with verified email; valid access_token cookie; phone_number +14165550123 set on profile or Step 1; CSRF token available if required by implementation.", + "stepsToPerform": "1. ASSUMPTION: Call POST /v2/auth/otp/request with channel=SMS and purpose=PHONE_VERIFY; expect 200 and delivery metadata; capture request_id for correlation.\n2. Attempt POST /v2/auth/otp/verify with a non-numeric code '12A45B'; expect 400 validation error for format; confirm attempts_remaining unchanged (or decremented only on valid-shaped attempts per policy; document behavior).\n3. Submit POST /v2/auth/otp/verify with wrong 6-digit code '000000'; expect 401 OTP_FAILED and attempts_remaining decremented by 1.\n4. Immediately POST /v2/auth/otp/verify with another wrong code; expect 401 OTP_FAILED; verify attempts_remaining decremented again; ensure no phone_verified flag.\n5. Trigger POST /v2/auth/otp/request twice more within 60 seconds; the second call should return 429 RATE_LIMITED or 400 TOO_MANY_REQUESTS (ASSUMPTION) with retry_after header; ensure no new code issued on throttle.\n6. Wait until throttle window passes; POST /v2/auth/otp/request again; expect 200 and a new OTP generated; verify prior OTP becomes invalid.\n7. Wait until OTP expiry window passes (e.g., 5 minutes; ASSUMPTION); attempt POST /v2/auth/otp/verify with the expired but otherwise correct code; expect 401 OTP_FAILED with reason expired.\n8. Request a fresh OTP via POST /v2/auth/otp/request; expect 200; immediately verify via POST /v2/auth/otp/verify with the correct code; expect 200 phone_verified=true in user/application context.\n9. Validate that logs or response do not expose full phone number beyond masked format (e.g., +1******0123) and no PII like ssn_last4 or tokens are echoed; ensure HttpOnly, Secure cookies remain set; no tokens in localStorage.\n10. Attempt to reuse the same OTP after success; expect 401 OTP_FAILED (one-time use) and no change to verification state.", + "expectedResult": "OTP request and verification enforce 6-digit numeric codes; invalid format rejected; wrong codes decrement attempts_remaining; resend throttled; expired codes fail; correct code marks phone_verified=true; OTP one-time use enforced; no PII leaks; security cookies intact.", + "apiPath": "/v2/auth/otp/request, /v2/auth/otp/verify", + "httpMethod": "POST, POST", + "endpointGroup": "Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-05 for CSRF if applicable, Security OTP policy", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "ASSUMPTION: false for OTP endpoints, true if policy enforces CSRF on POST", + "mfaRequired": "true for OTP verification step itself", + "rateLimitBucket": "OTP request throttle per phone/device (ASSUMPTION)", + "inputFields": "channel,purpose,code,request_id", + "validationRules": "OTP must be exactly 6 digits; resend throttle window enforced; OTP expiry window enforced; one-time use", + "errorCodesCovered": "OTP_FAILED, RATE_LIMITED or TOO_MANY_REQUESTS, 400 field validation", + "stateTransitions": "phone_verified: false->true upon successful verification; attempts_remaining decremented on failures", + "dataMaskingChecks": "Phone masked in responses/logs; no ssn_last4, tokens, or PAN in responses; HttpOnly, Secure cookies only", + "auditTrailChecks": "ASSUMPTION: OTP requests and verifications logged with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "phone_number", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: OTP endpoints exist with purpose PHONE_VERIFY and provide attempts_remaining; ASSUMPTION: Expiry and throttle windows are configurable and visible via responses or logs.", + "testData": "phone +14165550123; invalid codes '12A45B' and '000000'; valid OTP retrieved from test harness or SMS stub", + "cleanupSteps": "None; OTPs expire automatically", + "dependencies": "SMS or OTP delivery stub available; server clock reliable" + }, + { + "type": "functional", + "title": "Application Step 1 Validation, Email Match, Address Schema, Autosave Privacy and Cross-User Isolation", + "description": "Verify Step 1 enforces email matching the authenticated user, address schema rules, ID field enumerations and lengths, duplicate application detection, and that autosave stores only non-sensitive fields and is isolated per user.", + "testId": "TC-APP-STEP1-VALID-017", + "testDescription": "Focuses on Step 1 Personal Info validations and the autosave mechanism: privacy, non-sensitive data only, namespace isolation across users, and duplicate application behavior.", + "prerequisites": "Two verified users exist: userA@example.com and userB@example.com; both can log in; browser localStorage accessible; CSRF token available.", + "stepsToPerform": "1. Login as userA@example.com; obtain CSRF token; open application form Step 1.\n2. POST /v2/applications/start with email set to userB@example.com (mismatch), valid phone +14165550111, valid address, id_type PASSPORT, id_number 'X1234567890'; expect 400 with field=email must match authenticated user.\n3. Retry with email userA@example.com but invalid province 'Ontario' instead of 2-char code; expect 400 with field=address.province.\n4. Retry with province 'ON' but invalid postal_code '12345'; expect 400 with field=address.postal_code (must be Canadian A1A 1A1).\n5. Retry with valid address but id_type 'NATIONAL_ID' (not in {PASSPORT, DRIVERS_LICENSE, PR_CARD}); expect 400 invalid enumeration.\n6. Retry with id_type DRIVERS_LICENSE but id_number length 25 chars (over 20); expect 400 field length validation.\n7. Submit a fully valid payload: email userA@example.com, proper address (e.g., A1A 1A1), id_type PASSPORT, id_number 'X1234567'; expect 201 with application_id and session_token.\n8. Wait for autosave interval (60s) to trigger; reload the page; verify draft restored for non-sensitive fields (name, address) and confirm ssn_last4 or any sensitive fields are not stored in localStorage or visible in DOM.\n9. Logout userA; login as userB; navigate to application; assert no draft from userA is visible and that localStorage keys are namespaced or cleared; start Step 1 for userB with valid data and capture application_id_B.\n10. While logged in as userB, attempt to POST /v2/applications/start again with duplicate Step 1 data immediately; expect 409 DUPLICATE_APPLICATION; ensure no second application created.\n11. Switch back to userA and call POST /v2/applications/start again; expect 409 DUPLICATE_APPLICATION for userA's active application; verify no cross-user leakage of application_id values in UI or storage.", + "expectedResult": "Step 1 rejects email mismatch, invalid province/postal code, invalid id_type, overlength id_number; valid submission returns application_id and session_token; autosave persists only non-sensitive fields; drafts are isolated per user; duplicate application attempts return 409 without creating new records.", + "apiPath": "/v2/applications/start", + "httpMethod": "POST", + "endpointGroup": "Applications", + "workflow": "Credit Application Web Flow", + "businessRuleIds": "REQ-002 step order context, NFR-05 CSRF, REQ-014 masking (no sensitive data in UI)", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true if portal policy requires", + "rateLimitBucket": "applications standard", + "inputFields": "full_legal_name,email,phone_number,address.street,address.city,address.province,address.postal_code,id_type,id_number", + "validationRules": "email must equal authenticated user email; province 2-char code; postal_code matches A1A 1A1; id_type in allowed set; id_number <= 20 chars", + "errorCodesCovered": "DUPLICATE_APPLICATION, 400 field validation errors", + "stateTransitions": "Application: none->STEP1 (active draft)", + "dataMaskingChecks": "No ssn_last4 or PAN in DOM/localStorage; masked PAN only if shown anywhere", + "auditTrailChecks": "ASSUMPTION: Application creation logged with user_id, session_id, ip_address", + "piiFields": "full_legal_name,email,phone_number,residential_address", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: Autosave uses localStorage and excludes sensitive fields by design; duplicate application detection applies per user.", + "testData": "userA@example.com, userB@example.com; postal_code 'A1A 1A1'; province 'ON'", + "cleanupSteps": "Archive or delete test applications via test harness", + "dependencies": "Products catalog not required for Step 1; Portal autosave enabled" + }, + { + "type": "functional", + "title": "Application Step 2 Soft Credit Pull: sin_consent Enforcement, Conditional Employer, Bureau Failure Retry, Session Token Expiry", + "description": "Validate Step 2 financials require sin_consent=true, enforce employer_name when EMPLOYED, handle bureau 503 retry without duplicating pulls, and reject expired or tampered session_token.", + "testId": "TC-APP-CREDITPULL-018", + "testDescription": "Focuses on Step 2 data and soft credit pull behavior including backend error handling and session_token expiry boundary.", + "prerequisites": "User logged in as user@example.com; Step 1 completed with application_id and session_token; CSRF token available; credit bureau stub controllable to simulate 503 and success.", + "stepsToPerform": "1. POST /v2/applications/{application_id}/financials with X-App-Session set to valid session_token, employment_status EMPLOYED, employer_name omitted, gross_annual_income 60000.00, monthly_rent 1200.00, existing_debt_payments 200.00, sin_consent true; expect 400 field=employer_name required when EMPLOYED.\n2. Resubmit with employer_name 'Aegis Corp' but sin_consent=false; expect 400 error indicating sin_consent must be true.\n3. Resubmit with all required fields valid and sin_consent=true; set bureau stub to return 503 TEMPORARY_FAILURE; expect 200 status=PENDING_REVIEW with fico_pull_id queued but not yet resolved, and perhaps retry_after metadata (ASSUMPTION).\n4. Immediately resubmit identical Step 2 payload; expect idempotent handling (200 with same fico_pull_id) and no duplicate bureau pulls.\n5. After a short delay, set bureau stub to return success FICO=650; trigger backend retry (ASSUMPTION: automatic) or POST a Step 2 confirm call if available (ASSUMPTION); ensure application status updates on next Step 3 to PENDING per thresholds.\n6. Attempt POST /v2/applications/{application_id}/financials with a tampered X-App-Session value; expect 401 SESSION_EXPIRED and no new fico_pull_id.\n7. Advance time 31 minutes to expire the original session_token; resubmit the valid payload with the now-expired token; expect 401 SESSION_EXPIRED.\n8. Start a fresh Step 1 for a new application (or ASSUMPTION: obtain refreshed session_token for the existing application via UI flow) and capture the new session_token; resubmit Step 2 successfully and receive 200 with status=PENDING_REVIEW and fico_pull_id.\n9. Proceed to Step 3 submit with card_product_id and e_signature; with FICO=650 expect decision PENDING; confirm marketing_opt_in default false if omitted and no full PAN returned.", + "expectedResult": "Step 2 enforces sin_consent and employer_name rules; bureau 503 yields queued PENDING_REVIEW without duplication; tampered or expired session_token returns 401; with valid token and FICO=650, Step 3 returns PENDING; no sensitive data exposed.", + "apiPath": "/v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit, /v2/applications/start", + "httpMethod": "POST, POST, POST", + "endpointGroup": "Applications", + "workflow": "Credit Application Web Flow", + "businessRuleIds": "REQ-002, NFR-05 CSRF", + "calculationFormula": "Decision thresholds per SRS: FICO > 680 APPROVED; 600-680 PENDING; <600 DECLINED.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true if portal policy requires", + "rateLimitBucket": "applications standard", + "inputFields": "employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,card_product_id,e_signature,X-App-Session", + "validationRules": "employer_name required when EMPLOYED; sin_consent must be true; session_token must be valid, unexpired, and unmodified", + "errorCodesCovered": "SESSION_EXPIRED, 400 field validation errors", + "stateTransitions": "Application: STEP1->STEP2 PENDING_REVIEW->SUBMITTED->PENDING", + "dataMaskingChecks": "No PAN or bureau raw payload in UI or responses; masked PAN only when applicable", + "auditTrailChecks": "ASSUMPTION: Credit pull initiation and decisions logged with user_id, session_id, ip_address", + "piiFields": "employment data, income amounts", + "riskLevel": "High", + "regulatoryTags": "PII, Security", + "assumptions": "ASSUMPTION: Bureau stub can be configured; ASSUMPTION: Retrying behavior is automatic or via a documented mechanism; ASSUMPTION: New session_token can be obtained if prior expired.", + "testData": "employment_status EMPLOYED; income 60000.00; rents/debts per steps; FICO=650", + "cleanupSteps": "Archive applications created during test", + "dependencies": "Credit bureau stub; time travel or wait controls; CSRF issuance" + }, + { + "type": "functional", + "title": "Notifications Webhook Validation: Invalid alert_type/channel, PII Masking in message_body, Severity Rendering", + "description": "Validate the notifications webhook rejects unknown alert_type/channel, masks PII in message_body, delivers alerts across channels, and renders proper severity in portal UI.", + "testId": "TC-NOTIFY-VALID-019", + "testDescription": "Ensures webhook input validation and PII masking, channel handling, and UI severity badge mapping operate correctly without idempotency conflicts.", + "prerequisites": "Active account_id for user@example.com; authenticated portal session open to observe in-app notifications; webhook endpoint reachable.", + "stepsToPerform": "1. POST /v2/notifications/webhook with account_id, alert_type 'UNKNOWN_EVENT', channel IN_APP, message_body 'Test', severity INFO, idempotency_key a valid UUID; expect 400 INVALID_ALERT_TYPE.\n2. POST /v2/notifications/webhook with valid alert_type LATE_PAYMENT but channel 'PAGER' (unsupported); expect 400 INVALID_ALERT_TYPE or invalid channel error per spec.\n3. POST /v2/notifications/webhook with alert_type STATEMENT_READY, channel IN_APP, severity INFO, message_body 'Your statement is ready', idempotency_key UUID1; expect 200 notification_id and delivered_at; verify portal shows INFO badge.\n4. POST /v2/notifications/webhook with alert_type OVER_LIMIT, channel EMAIL, severity WARNING, message_body 'Over-limit used on card **** **** **** 1234', idempotency_key UUID2; expect 200 queued or delivered; confirm no full PAN appears in any payload or UI.\n5. POST /v2/notifications/webhook with alert_type FRAUD_FLAG, channel IN_APP, severity CRITICAL, message_body 'Suspicious charge on card 4111 1111 1111 1111 at Merchant X', idempotency_key UUID3; expect 200; verify backend masks PAN in stored/displayed message to **** **** **** 1111 (REQ-014) and portal shows CRITICAL badge.\n6. In portal, refresh the notifications UI and verify exactly three alerts visible with correct types and severities INFO, WARNING, CRITICAL; ensure timestamps are ISO 8601 UTC and sorted by delivered_at.\n7. Attempt to send a very long message_body >500 chars; expect 400 validation error and no alert created.\n8. Confirm that in-app alert payloads contain no PII beyond masked PAN and merchant descriptors; no ssn_last4, address, or tokens present.\n9. Resend the STATEMENT_READY with a different idempotency_key UUID4 but same content; expect 200 and a second distinct alert, proving idempotency depends on key not content.", + "expectedResult": "Webhook rejects unknown alert_type and channels; enforces message_body length; masks PAN found in message_body; delivers alerts per channel; UI displays correct severities; idempotency keyed by idempotency_key; no PII leakage.", + "apiPath": "/v2/notifications/webhook", + "httpMethod": "POST", + "endpointGroup": "Notifications", + "workflow": "Notifications & Alerts", + "businessRuleIds": "REQ-014 masking, NFR-05 CSRF not required for internal webhook", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder (UI consumer), Notification Engine (caller)", + "authRequired": "false for webhook caller (internal trust), true for portal viewing", + "csrfRequired": "false for webhook (internal), true for portal state changes", + "mfaRequired": "false", + "rateLimitBucket": "webhook internal standard", + "inputFields": "account_id,alert_type,channel,severity,message_body,idempotency_key", + "validationRules": "alert_type and channel must be from enumerations; message_body <= 500 chars; idempotency_key UUID v4", + "errorCodesCovered": "INVALID_ALERT_TYPE, 400 validation error", + "stateTransitions": "Notification: none->queued/delivered; UI: unread->read (not exercised)", + "dataMaskingChecks": "PAN masking enforced in messages; no ssn_last4 or sensitive PII shown", + "auditTrailChecks": "ASSUMPTION: Notification deliveries logged with account_id, idempotency_key, timestamp_utc", + "piiFields": "account_id linking only; message must not contain unmasked PAN", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: Webhook is authenticated internally (mutual TLS or allowlist) and not CSRF-scoped; UI polls or receives push to render alerts.", + "testData": "UUID1, UUID2, UUID3, UUID4 as unique idempotency keys; sample messages containing PAN-like strings", + "cleanupSteps": "Clear test notifications from non-prod environment if needed", + "dependencies": "Portal notifications UI; backend masking filter enabled" + }, + { + "type": "functional", + "title": "Transactions Listing Date/Time Boundaries, Same-day Range, DST/UTC Handling, per_page Max Validation", + "description": "Validate transaction list filtering at time boundaries including same-day ranges, UTC handling around DST changes, stable pagination, and per_page max >100 rejection.", + "testId": "TC-TRX-LIST-DATERANGE-020", + "testDescription": "Exercises GET transactions parameters for date ranges and pagination limits without overlapping prior invalid-range tests; ensures accurate inclusion on boundary timestamps and rejection of per_page=101.", + "prerequisites": "Active account_id with at least three known transactions at controlled timestamps; authenticated session; CSRF token for creating new transactions during setup.", + "stepsToPerform": "1. Create three PURCHASE transactions via POST /v2/accounts/{account_id}/transactions with timestamps set by test harness (ASSUMPTION): T1=2026-03-14T00:00:00Z, T2=2026-03-14T23:59:59Z, T3=2026-03-15T12:00:00Z; ensure 200 approvals.\n2. GET /v2/accounts/{account_id}/transactions with from_date=2026-03-14, to_date=2026-03-14, page=1, per_page=25; expect results include exactly T1 and T2 but not T3 (same-day boundary inclusion for start and end).\n3. GET /v2/accounts/{account_id}/transactions with from_date=2026-03-15, to_date=2026-03-15, page=1, per_page=25; expect T3 present only.\n4. Around DST change window (ASSUMPTION: 2026-03-08 for North America), create two additional PURCHASE transactions at 2026-03-08T01:59:59Z and 2026-03-08T03:00:01Z; GET with from_date=2026-03-08, to_date=2026-03-08; verify both included, confirming UTC-based filtering is consistent and unaffected by DST.\n5. GET /v2/accounts/{account_id}/transactions with from_date unspecified and to_date=2026-03-14; expect default from_date billing cycle start and inclusion up to 2026-03-14 end of day.\n6. GET /v2/accounts/{account_id}/transactions with page=1, per_page=101; expect 400 validation error for per_page max 100.\n7. Populate >25 transactions (ASSUMPTION via looped POST) and GET page=1 per_page=25 then page=2 per_page=25; verify no duplicate items across pages and stable ordering (document default sort order, typically most recent first).\n8. GET /v2/accounts/{account_id}/transactions with category REFUND; if none exist, expect 200 with empty transactions[]; confirm API handles empty result gracefully.\n9. Confirm responses include total_count, page, total_pages fields; validate that sum of items across pages equals total_count for the given filter.\n10. Ensure responses contain masked PAN only where any card reference is shown and that no PII like ssn_last4 appears.", + "expectedResult": "Same-day ranges include both start and end boundary timestamps; UTC/DST boundaries do not exclude valid transactions; per_page=101 rejected; pagination stable without duplicates; empty category results return 200 with empty list; outputs include pagination metadata and mask PAN.", + "apiPath": "/v2/accounts/{account_id}/transactions", + "httpMethod": "POST, GET", + "endpointGroup": "Transactions", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-014 masking", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST, false for GET", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard", + "inputFields": "from_date,to_date,page,per_page,category,transaction_amount,merchant_name,merchant_id,mcc_code", + "validationRules": "per_page max 100; page min 1; date filters inclusive for same day; UTC timestamps used", + "errorCodesCovered": "400 validation error for per_page", + "stateTransitions": "None (read-only for GET); transactions created for setup reduce available_credit accordingly", + "dataMaskingChecks": "Masked PAN in any transaction representations; no sensitive PII in responses", + "auditTrailChecks": "ASSUMPTION: Transaction creations logged with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "None beyond account ownership linkage", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Test harness can set transaction timestamps for deterministic boundary testing; default list sort is consistent.", + "testData": "Boundary timestamps 2026-03-14T00:00:00Z and 23:59:59Z; DST window events on 2026-03-08", + "cleanupSteps": "Leave transactions for later statement validation or purge via test harness if needed", + "dependencies": "Time control or backdated posting supported in non-prod; sufficient available_credit" + }, + { + "type": "functional", + "title": "OAuth2 Authorization Code with PKCE: State Integrity, Code Verifier, Cookie Storage, Replay Defense", + "description": "Validate full OAuth2 Authorization Code with PKCE browser flow: proper state parameter handling, code_verifier -> code_challenge(S256) validation, redirect_uri checks, single-use auth code, tokens only in HttpOnly Secure cookies, and logout CSRF.", + "testId": "TC-AUTH-PKCE-021", + "testDescription": "Ensures compliance with OAuth2 + PKCE security: state matches, invalid code_verifier rejected, authorization code replay blocked, cookies have HttpOnly/Secure/SameSite=Strict; no tokens in localStorage; logout uses CSRF and invalidates refresh.", + "prerequisites": "Browser on TLS 1.3; registered and email-verified user user@example.com with MFA enabled; device_id available; portal configured for OAuth2 Authorization Code with PKCE; CSRF issuance endpoint available.", + "stepsToPerform": "1. Generate code_verifier (high-entropy 43-128 chars) and compute code_challenge=BASE64URL(SHA256(code_verifier)).\n2. Navigate to GET /v2/oauth/authorize?response_type=code&client_id=portal&redirect_uri=https://portal.aegiscard.com/callback&scope=openid%20profile&state=xyz123&code_challenge_method=S256&code_challenge= over TLS 1.3; verify HSTS and no mixed content (ASSUMPTION endpoints).\n3. On login form, enter user@example.com and correct password; complete TOTP 6-digit challenge; submit; expect redirect back with code and state=xyz123 intact.\n4. Validate state integrity: ensure returned state exactly matches xyz123; if tampered state provided in step 2 (negative retry), expect 400 INVALID_STATE and no code issued.\n5. Exchange code via POST /v2/oauth/token with grant_type=authorization_code, code=, redirect_uri matching original, client_id portal, code_verifier=; expect 200 and access/refresh tokens set as HttpOnly, Secure, SameSite=Strict cookies; no token body returned to JS (ASSUMPTION cookie delivery).\n6. Attempt token exchange again reusing the same code (replay); expect 400 or 401 AUTH_CODE_REDEEMED and no cookies changed.\n7. Attempt token exchange using wrong redirect_uri; expect 400 INVALID_REDIRECT_URI and no cookies set.\n8. Attempt token exchange using an invalid code_verifier that does not match code_challenge; expect 400 INVALID_CODE_VERIFIER and no cookies.\n9. Verify in browser devtools: cookies have HttpOnly, Secure, SameSite=Strict; window.localStorage and sessionStorage contain no access/refresh tokens; Authorization header is not persisted in JS scope.\n10. Access a protected API GET /v2/accounts/{account_id}/summary; expect 200; then POST /v2/auth/logout with X-CSRF-Token; expect 200 and cookies expired.\n11. After logout, attempt POST /v2/auth/token/refresh; expect 401 TOKEN_INVALID; accessing protected summary again returns 401 UNAUTHORIZED.", + "expectedResult": "PKCE flow succeeds only with valid state and code_verifier; authorization code is single-use; tokens are delivered via Secure, HttpOnly, SameSite=Strict cookies; no tokens in web storage; logout with CSRF clears cookies and refresh reuse is rejected.", + "apiPath": "/v2/oauth/authorize, /v2/oauth/token, /v2/auth/logout, /v2/accounts/{account_id}/summary", + "httpMethod": "GET, POST, POST, GET", + "endpointGroup": "Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-01, NFR-05, NFR-06", + "calculationFormula": "code_challenge = BASE64URL(SHA256(code_verifier))", + "rolesCovered": "Cardholder", + "authRequired": "false for authorize/token, true for summary", + "csrfRequired": "true for logout", + "mfaRequired": "true at login prompt", + "rateLimitBucket": "login 10 req/min per IP", + "inputFields": "client_id,redirect_uri,scope,state,code_verifier,code_challenge_method,code_challenge,code", + "validationRules": "state must echo back unchanged; redirect_uri must match exactly; code single-use; code_verifier must match code_challenge S256; cookies must be Secure, HttpOnly, SameSite=Strict", + "errorCodesCovered": "INVALID_STATE, INVALID_REDIRECT_URI, INVALID_CODE_VERIFIER, TOKEN_INVALID, UNAUTHORIZED", + "stateTransitions": "Auth: unauthenticated->authorized->logged_out", + "dataMaskingChecks": "No tokens visible to JS; no PII in OAuth errors", + "auditTrailChecks": "ASSUMPTION: Authorization and token issuance logged with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "email", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: /v2/oauth/authorize and /v2/oauth/token exist; tokens are cookie-delivered; portal uses PKCE S256.", + "testData": "email user@example.com, device_id 550e8400-e29b-41d4-a716-446655440010, state xyz123", + "cleanupSteps": "Logout; clear cookies; reset any lockout counters if tripped", + "dependencies": "OAuth server configured for PKCE; CSRF token endpoint available" + }, + { + "type": "functional", + "title": "Transaction Rate-Limit Recovery via MFA: 429 FREQ_EXCEEDED -> MFA Verify -> Counter Reset", + "description": "When >10 transactions in 60 minutes, API returns 429 with mfa_required=true; validate MFA challenge flow to restore ability to transact and confirm headers and counters reset.", + "testId": "TC-TXN-MFA-022", + "testDescription": "Executes rapid purchases to trigger transaction rate limit, performs MFA verification to lift restriction, and confirms subsequent transactions succeed with correct available_credit updates and proper Retry-After handling.", + "prerequisites": "Active account with sufficient available_credit; authenticated session with CSRF; OTP/TOTP delivery working; WebSocket optional.", + "stepsToPerform": "1. Confirm baseline via GET /v2/accounts/{account_id}/summary; capture available_credit and account_status Active.\n2. Perform 10 quick POST /v2/accounts/{account_id}/transactions PURCHASEs (CAD) within 60 minutes with X-CSRF-Token; expect 200 approvals and available_credit decrements.\n3. Attempt the 11th POST transaction within the same 60-minute window; expect 429 FREQ_EXCEEDED with mfa_required=true and Retry-After header present (seconds).\n4. Immediately retry the 11th transaction without completing MFA; expect 429 again until limit window or MFA passed; verify no duplicate postings created.\n5. Initiate MFA verification flow: POST /v2/auth/otp/request with purpose=TRANSACTION_RATE_LIMIT (ASSUMPTION) and channel=SMS; expect 200 with delivery metadata.\n6. Submit POST /v2/auth/otp/verify with correct 6-digit code; expect 200 and a response or flag indicating rate-limit lift (ASSUMPTION: transactions_mfa_cleared=true) and attempts_remaining unaffected on success.\n7. Retry the previously blocked transaction POST; expect 200 approval with transaction_id, auth_code, available_credit updated.\n8. Trigger one more purchase to confirm the counter effectively reset by MFA and no additional 429 occurs; verify header X-RateLimit-Remaining or equivalent reflects reset (ASSUMPTION).\n9. Negative: submit an invalid OTP before success; expect 401 OTP_FAILED and rate-limit remains; validate attempts_remaining decrements and subsequent valid OTP still works.\n10. Confirm audit/log entries exist for limit trigger and MFA clearance (ASSUMPTION); verify no PII leaks in error payloads.", + "expectedResult": "429 FREQ_EXCEEDED is returned on the 11th transaction with mfa_required=true; after successful OTP verification, subsequent transactions are permitted immediately and counters reset; invalid OTP does not lift the limit; Retry-After is provided on 429 responses.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/auth/otp/request, /v2/auth/otp/verify", + "httpMethod": "POST, POST, POST", + "endpointGroup": "Transactions, Auth", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-006 foreign fee not exercised here, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST transactions", + "mfaRequired": "true to clear rate limit", + "rateLimitBucket": "transactions >10 in 60 min -> 429 FREQ_EXCEEDED, mfa_required true", + "inputFields": "transaction_amount,merchant_name,merchant_id,mcc_code,transaction_type,channel,purpose,code", + "validationRules": "transaction_amount > 0; CSRF required; OTP must be 6 digits; MFA verification unlocks rate-limit", + "errorCodesCovered": "FREQ_EXCEEDED, OTP_FAILED", + "stateTransitions": "Rate-limit: normal->limited->cleared after MFA", + "dataMaskingChecks": "No PAN in error messages; masked PAN if present in any transaction payloads", + "auditTrailChecks": "ASSUMPTION: Limit trigger and MFA clearance logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "phone_number (masked in logs), account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: OTP purpose TRANSACTION_RATE_LIMIT supported; Retry-After header present; rate-limit can be cleared by MFA.", + "testData": "Amounts CAD $1.00 each, mcc_code 5999, merchant_id INCR001..INCR011", + "cleanupSteps": "None; keep transactions for later statement validation", + "dependencies": "OTP delivery channel; CSRF token available" + }, + { + "type": "functional", + "title": "PAN Masking and Iframe Tokenization Compliance Sweep Across Portal Surfaces", + "description": "End-to-end verification that full PAN never appears in frontend DOM, network logs, or storage; all displays show **** **** **** 1234; card fields use iframe tokenization; masking holds across API responses, WebSocket events, statements, notifications.", + "testId": "TC-PCI-MASK-023", + "testDescription": "Per REQ-014 and NFR-01, validate PAN masking everywhere and iframe tokenization integration; inspect DOM, network requests, WebSocket frames, statements JSON/PDF, notifications content, and storage for leaks.", + "prerequisites": "Active account with transactions; authenticated portal session; WebSocket subscription available; statements exist; notifications present that mention card references.", + "stepsToPerform": "1. Navigate to account dashboard; visually confirm card display shows masked PAN like **** **** **** 1234; inspect DOM elements and ensure no hidden full PAN or data attributes contain PAN.\n2. Open browser devtools Network tab and reload dashboard; filter for API calls; verify no response payload contains full PAN; any card references are masked; response caching does not store PAN.\n3. Inspect window.localStorage and sessionStorage; ensure no PAN, ssn_last4, or JWT tokens are stored; only non-sensitive UI preferences allowed.\n4. Navigate to card management UI; verify card number field (if present) is rendered via third-party iframe tokenization component (e.g., Stripe Elements) loaded over TLS 1.3; confirm iframe origin allowlisted by CSP and that host page never receives raw PAN (ASSUMPTION tokenization present).\n5. Create a CAD transaction via POST /v2/accounts/{account_id}/transactions to generate a WebSocket event; verify the event payload in the WebSocket frames contains only masked PAN and necessary merchant/amount fields; no PII or tokens included.\n6. Open latest statement JSON via GET /v2/accounts/{account_id}/statements/{statement_id}; confirm any card references are masked; download PDF format and scan text for patterns matching 16-digit PAN; expect only masked values.\n7. Trigger a notification via POST /v2/notifications/webhook with message_body containing a PAN-like string '4111 1111 1111 1111'; verify stored and rendered message masks to **** **** **** 1111 and no full PAN visible.\n8. In the application flow pages, inspect autosave keys in localStorage; confirm no ssn_last4 or card data stored; only non-sensitive application draft fields are present.\n9. Verify CSP headers in responses disallow inline scripts and restrict iframe sources; ensure all tokenization and API endpoints load over HTTPS TLS 1.3; no mixed content.\n10. Perform DOM and network search for regex patterns resembling PAN (\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b); ensure zero matches across visible content and payload bodies except masked format.", + "expectedResult": "Masked PAN is consistently used across UI, APIs, WebSocket, statements, and notifications; no full PAN appears in DOM, network, or storage; iframe tokenization is present and secure; CSP and TLS policies enforced; autosave contains no sensitive data.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/statements/{statement_id}, /v2/notifications/webhook, wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "GET, POST, GET, POST, WebSocket", + "endpointGroup": "Accounts, Transactions, Billing, Notifications, Realtime", + "workflow": "Security & Compliance Controls", + "businessRuleIds": "REQ-014, NFR-01, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true for portal and APIs; webhook internal", + "csrfRequired": "true for POST transaction; false for internal webhook", + "mfaRequired": "false", + "rateLimitBucket": "standard", + "inputFields": "message_body with PAN-like string, transaction_amount, merchant_name, mcc_code", + "validationRules": "PAN must never be transmitted to frontend; all displays are masked; tokenization via iframe required for any card inputs", + "errorCodesCovered": "N/A", + "stateTransitions": "None; read/observe-only aside from one transaction and webhook for validation", + "dataMaskingChecks": "Regex searches yield only masked PAN **** **** **** 1234; no ssn_last4 anywhere", + "auditTrailChecks": "ASSUMPTION: Security scans and masking filters logged", + "piiFields": "None beyond masked card reference and account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Iframe tokenization is used for any card entry UI; CSP headers restrict iframe origins; masking filter applies to notifications.", + "testData": "Webhook message containing 4111 1111 1111 1111; transaction CAD $5.00 at MCC 5999", + "cleanupSteps": "Delete test notification from non-prod if needed", + "dependencies": "Realtime service available; statements accessible; CSP configured" + }, + { + "type": "functional", + "title": "Web-Based PIN Set Edge Cases: Numeric Format, Leading Zeros, OTP Expiry, Attempts Throttle, CSRF", + "description": "Validate PIN endpoint enforces exactly 4 numeric digits including leading zeros, rejects non-numeric and whitespace, handles OTP expiry and attempts_remaining, rate-limits excessive attempts, and requires CSRF.", + "testId": "TC-PIN-EDGE-024", + "testDescription": "Focuses on REQ-008 PIN rules beyond mismatch: 0000 allowed, non-numeric rejected, space-trim not allowed, OTP expiry handling, attempts throttle, CSRF requirement, and successful set when all conditions met.", + "prerequisites": "Active card_id not Blocked; authenticated session; CSRF token; OTP delivery available.", + "stepsToPerform": "1. Attempt PUT /v2/cards/{card_id}/pin without X-CSRF-Token using new_pin 1234 and confirm_pin 1234 with valid session_otp; expect 403 CSRF_MISSING and no PIN change.\n2. Retry with X-CSRF-Token but provide new_pin '12 4' (contains whitespace) and confirm_pin '12 4'; expect 400 PIN_FORMAT and error message indicating exactly 4 digits required.\n3. Retry with new_pin '12A4' (non-numeric) and confirm_pin '12A4'; expect 400 PIN_FORMAT; confirm no PIN set.\n4. Retry with new_pin '123' (3 digits) and confirm_pin '123'; expect 400 PIN_FORMAT; then with new_pin '12345' and confirm_pin '12345'; expect 400 PIN_FORMAT.\n5. Attempt with leading zeros: new_pin '0000' and confirm_pin '0000' but use an expired session_otp (wait beyond expiry or use stale code); expect 401 OTP_FAILED with reason expired and attempts_remaining decremented.\n6. Request a fresh OTP via POST /v2/auth/otp/request purpose=PIN_SET (ASSUMPTION); immediately submit PUT with mismatched confirm_pin (0000 vs 0001); expect 400 PIN_MISMATCH and attempts_remaining unchanged (PIN validation precedes OTP consumption or document behavior observed).\n7. Submit two more attempts with wrong OTP codes to simulate attempts throttle; expect 401 OTP_FAILED with attempts_remaining decreasing each time; when attempts_remaining reaches 0, further attempts return 401 OTP_LOCKED or similar (ASSUMPTION) until reset window.\n8. After cooldown or with a new OTP, submit PUT with new_pin '0000' and confirm_pin '0000' and valid session_otp; expect 200 success true with updated_at timestamp.\n9. Verify audit of PIN set (ASSUMPTION) and ensure GET /v2/accounts/{account_id}/summary and UI show no PIN values anywhere; check DOM/storage for absence of PIN or OTP artifacts.\n10. Negative follow-up: attempt to reuse the same (already-used) session_otp; expect 401 OTP_FAILED (one-time use) and no change.", + "expectedResult": "PIN must be exactly 4 numeric digits; leading zeros allowed; non-numeric and whitespace rejected; CSRF required; expired or invalid OTP fails with attempts_remaining decrement and throttle; valid OTP sets PIN successfully; no PIN/OTP leakage in UI or storage.", + "apiPath": "/v2/cards/{card_id}/pin, /v2/auth/otp/request", + "httpMethod": "PUT, POST", + "endpointGroup": "Card Management, Auth", + "workflow": "Card Management", + "businessRuleIds": "REQ-008, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for PUT", + "mfaRequired": "true via session_otp", + "rateLimitBucket": "standard; OTP attempts throttle applies", + "inputFields": "new_pin,confirm_pin,session_otp,channel,purpose", + "validationRules": "PIN exactly 4 digits numeric; OTP 6 digits valid and unexpired; CSRF required; attempts throttle enforced", + "errorCodesCovered": "CSRF_MISSING, PIN_FORMAT, PIN_MISMATCH, OTP_FAILED", + "stateTransitions": "PIN: unset->set", + "dataMaskingChecks": "No PIN or OTP in responses beyond error metadata; no sensitive data in DOM/storage", + "auditTrailChecks": "ASSUMPTION: PIN set event logged with user_id/session_id/ip_address/timestamp_utc", + "piiFields": "None", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: OTP purpose PIN_SET available; attempts_remaining and lockout semantics exposed; CSRF error returns 403.", + "testData": "Valid OTP from test harness; invalid/stale OTP for expiry; CSRF token current", + "cleanupSteps": "None; PIN remains set", + "dependencies": "OTP delivery; CSRF issuance; card not in Blocked state" + }, + { + "type": "functional", + "title": "Report Card STOLEN with Delivery Address Override: OTP Gating, Address Validation, Audit, Irreversibility", + "description": "Validate high-risk Report STOLEN flow requires OTP, supports delivery_address override validation, blocks card irreversibly, prevents further status changes, and schedules replacement with masked details.", + "testId": "TC-REPORT-STOLEN-025", + "testDescription": "Focuses on nuanced lost/stolen handling beyond basic block: OTP requirement, CSRF, address schema for delivery override, audit contents, prevention of unfreeze/activate post-block, and ensuring no PIN operations permitted until replacement.", + "prerequisites": "Active card_id owned by user@example.com; authenticated session; valid CSRF token; OTP delivery available; knowledge of valid Canadian address schema.", + "stepsToPerform": "1. Confirm via GET /v2/accounts/{account_id}/summary that account_status Active and card is Active; capture masked PAN for verification later.\n2. Attempt POST /v2/cards/{card_id}/report-lost with loss_type STOLEN and a delivery_address override but omit X-CSRF-Token; expect 403 CSRF_MISSING and no state change.\n3. Retry with X-CSRF-Token but omit confirm_otp (ASSUMPTION OTP required for high-risk); if endpoint requires OTP in header or body, expect 401 OTP_FAILED or 400 missing OTP; no state change.\n4. Request an OTP via POST /v2/auth/otp/request purpose=CARD_REPORT_STOLEN; expect 200 with delivery metadata; submit POST /v2/cards/{card_id}/report-lost with loss_type STOLEN, valid confirm_otp, and invalid delivery_address fields (province 'Ontario' not 2-char, postal_code '12345'); expect 400 validation errors for address.\n5. Resubmit with valid delivery_address (street/city valid, province 'ON', postal_code 'A1A 1A1'); include last_known_use timestamp in ISO 8601 UTC; expect 200 with blocked_card_id, new_card_eta, case_number; card transitions to Blocked.\n6. Verify audit trail (ASSUMPTION admin/audit) includes user_id, session_id, ip_address, timestamp_utc, loss_type STOLEN, delivery_address override fields masked where applicable (no full PAN), and case_number.\n7. Attempt PATCH /v2/cards/{card_id}/status to Active or Frozen; expect 400 INVALID_TRANSITION; ensure status remains Blocked.\n8. Attempt PUT /v2/cards/{card_id}/pin with valid session_otp; expect 403 CARD_BLOCKED and no change; confirm UI reflects Blocked and PIN actions disabled.\n9. Attempt to report lost/stolen again for the same card; expect 409 ALREADY_BLOCKED; verify idempotent no duplicate cases created.\n10. Confirm in all responses and UI masked PAN only; scan DOM and network logs for absence of full PAN; ensure no PII leakage in errors.\n11. Optional: Verify replacement card shipment details are limited/masked and no ability to transact with the blocked card via POST /v2/accounts/{account_id}/transactions (expect 403 CARD_INACTIVE).", + "expectedResult": "Report STOLEN requires CSRF and OTP; invalid address rejected; valid request blocks card irreversibly and schedules replacement; subsequent status changes and PIN operations are disallowed; duplicate reports return 409; all card references are masked and audits recorded.", + "apiPath": "/v2/cards/{card_id}/report-lost, /v2/cards/{card_id}/status, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/auth/otp/request, /v2/admin/audit", + "httpMethod": "POST, PATCH, PUT, GET, POST, POST, GET", + "endpointGroup": "Card Management, Accounts, Auth, Admin", + "workflow": "Card Status Control", + "businessRuleIds": "REQ-007, REQ-014, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder, Admin (audit view)", + "authRequired": "true", + "csrfRequired": "true for POST/PATCH/PUT", + "mfaRequired": "true via OTP for high-risk action", + "rateLimitBucket": "standard", + "inputFields": "loss_type,confirm_otp,delivery_address.street,delivery_address.city,delivery_address.province,delivery_address.postal_code,last_known_use", + "validationRules": "delivery_address must meet address schema; OTP 6 digits valid; CSRF required; Blocked state irreversible", + "errorCodesCovered": "CSRF_MISSING, OTP_FAILED, INVALID_TRANSITION, ALREADY_BLOCKED, CARD_INACTIVE", + "stateTransitions": "Active->Blocked (irreversible)", + "dataMaskingChecks": "Masked PAN **** **** **** 1234 in all responses/UI; no full PAN in DOM/network", + "auditTrailChecks": "Audit record with user_id, session_id, ip_address, timestamp_utc, loss_type, override address", + "piiFields": "delivery_address (validated, not overexposed), phone masked in OTP context", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF, Audit Trail", + "assumptions": "ASSUMPTION: OTP required for report STOLEN; admin/audit endpoint accessible in non-prod; shipping ETA returned.", + "testData": "Valid Canadian address: 123 King St, Toronto, ON, A1A 1A1; loss_type STOLEN; valid OTP from test harness", + "cleanupSteps": "None; replacement issuance handled by operations in non-prod", + "dependencies": "OTP delivery; audit service available; CSRF issuance" + }, + { + "type": "functional", + "title": "CSRF Regeneration After Refresh Rotation: Old Token Invalid, New Token Required", + "description": "Validate that CSRF tokens are session-bound and become invalid after refresh token rotation; state-changing requests must use a newly issued CSRF token post-rotation.", + "testId": "TC-CSRF-ROTATE-026", + "testDescription": "Ensures POST/PUT/PATCH/DELETE endpoints reject an old X-CSRF-Token after /v2/auth/token/refresh rotates tokens; verifies new CSRF works, cookies flags remain secure, and no data is mutated by the rejected request.", + "prerequisites": "Verified user user@example.com with MFA enabled; active account_id and card_id owned by the user; browser over TLS 1.3; initial authenticated session established with valid access/refresh cookies; a valid CSRF token obtained.", + "stepsToPerform": "1. Confirm initial login over TLS 1.3 and obtain a fresh CSRF via GET /v2/auth/csrf; store token CSRF-0.\n2. Perform a valid state-changing call using CSRF-0, e.g., POST /v2/accounts/{account_id}/transactions with small CAD PURCHASE; expect 200 and transaction_id.\n3. Call POST /v2/auth/token/refresh to rotate tokens; verify 200 and Set-Cookie for new access_token and refresh_token (HttpOnly, Secure, SameSite=Strict); do NOT fetch a new CSRF yet.\n4. Attempt another state-changing call using the stale CSRF-0: PUT /v2/cards/{card_id}/pin with dummy payload (new_pin 1234/confirm_pin 1234 and a valid session_otp); expect 403 CSRF_INVALID or CSRF_MISSING; confirm no PIN change (no success).\n5. Obtain a new CSRF token via GET /v2/auth/csrf; store as CSRF-1; confirm CSRF-1 differs from CSRF-0.\n6. Retry PUT /v2/cards/{card_id}/pin with X-CSRF-Token: CSRF-1 and valid session_otp but with intentionally mismatched pins 1234 vs 4321 to avoid real PIN set; expect 400 PIN_MISMATCH and confirm CSRF accepted (authorization path reached).\n7. Perform a valid state change using CSRF-1, e.g., PATCH /v2/cards/{card_id}/status to Frozen with confirm_otp valid; expect 200 new_status Frozen.\n8. Inspect browser storage to verify no tokens are in localStorage/sessionStorage; check cookies retain HttpOnly, Secure, SameSite=Strict.\n9. Negative: Attempt POST /v2/auth/token/refresh again using the previously used refresh token; expect 401 TOKEN_INVALID; confirm CSRF-1 still valid for current session.\n10. Clean up by unfreezing card via PATCH /v2/cards/{card_id}/status to Active using CSRF-1; expect 200 new_status Active.", + "expectedResult": "Old CSRF token is rejected after refresh rotation; a new CSRF must be fetched and used. No unintended state changes occur with stale CSRF. Cookies remain Secure, HttpOnly, SameSite=Strict. Refresh token reuse is rejected.", + "apiPath": "/v2/auth/csrf, /v2/accounts/{account_id}/transactions, /v2/auth/token/refresh, /v2/cards/{card_id}/pin, /v2/cards/{card_id}/status", + "httpMethod": "GET, POST, POST, PUT, PATCH", + "endpointGroup": "Auth, Transactions, Card Management", + "workflow": "Security & Compliance Controls", + "businessRuleIds": "NFR-05, NFR-06", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for OTP-gated endpoints", + "rateLimitBucket": "standard", + "inputFields": "X-CSRF-Token,new_pin,confirm_pin,session_otp,confirm_otp,transaction_amount,merchant_name,merchant_id,mcc_code", + "validationRules": "CSRF tokens are session-bound and must be refreshed after token rotation; PIN must be exactly 4 digits; OTP must be 6 digits.", + "errorCodesCovered": "CSRF_INVALID, CSRF_MISSING, TOKEN_INVALID, PIN_MISMATCH", + "stateTransitions": "Session: pre-refresh->post-refresh; Card: Active->Frozen->Active", + "dataMaskingChecks": "No tokens in localStorage/sessionStorage; masked PAN only in any responses", + "auditTrailChecks": "ASSUMPTION: Status and PIN attempts logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "email (login context only)", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/csrf issues a new CSRF tied to current tokens; ASSUMPTION: CSRF_INVALID 403 is returned for token from previous session.", + "testData": "Small CAD transaction amount $1.00 at MCC 5999; valid OTPs available", + "cleanupSteps": "Return card to Active; clear cookies; revoke tokens", + "dependencies": "CSRF issuance endpoint available; OTP delivery available" + }, + { + "type": "functional", + "title": "Session Timeout Warning: Stay Signed In Flow, Timer Reset, Draft Preservation", + "description": "Validate 13-minute warning modal and user action 'Stay Signed In' prevents auto-logout, refreshes tokens, preserves CSRF and application draft, and keeps session active.", + "testId": "TC-SESSION-STAY-027", + "testDescription": "Ensures that interacting with the warning modal extends session without data loss; verifies cookies flags, CSRF continuity, and that inactivity beyond 15 min triggers auto-logout when the user ignores the warning.", + "prerequisites": "Authenticated portal session with active application draft in Step 1; autosave enabled; valid CSRF token; TLS 1.3; device_id set.", + "stepsToPerform": "1. Login with MFA; confirm Secure, HttpOnly, SameSite=Strict cookies set; fetch CSRF token CSRF-A.\n2. Start application Step 1 POST /v2/applications/start with valid payload; receive application_id and session_token; ensure autosave writes non-sensitive draft in localStorage.\n3. Idle user interactions (no API calls) for 12 minutes; verify session still active by doing a lightweight GET (e.g., a non-mutating endpoint) is not called automatically by UI.\n4. Continue idling until 13 minutes; verify warning modal appears indicating impending logout in 2 minutes.\n5. Click 'Stay Signed In' (ASSUMPTION: triggers silent refresh flow); expect a background POST /v2/auth/token/refresh 200 and cookies rotated, session timer reset; CSRF may remain valid or be transparently refreshed.\n6. Immediately POST /v2/applications/{application_id}/financials with X-App-Session and X-CSRF-Token CSRF-A; expect 200 or, if CSRF rotated, 403 prompting auto-fetch of new CSRF; fetch new CSRF if needed and re-submit successfully.\n7. Verify draft preservation: reload the page; confirm Step 1 non-sensitive fields restored; confirm ssn_last4 or sensitive data is absent in localStorage.\n8. Idle again for 14 minutes, then interact within the warning window by clicking any UI control (e.g., open notifications); confirm the timer resets (no auto-logout at 15 minutes).\n9. Negative path: Now ignore the warning modal when it appears next cycle; continue idling past 15 minutes; verify auto-logout occurs and protected API calls return 401.\n10. Re-login with MFA; confirm previous draft remains accessible; ensure cookies are Secure, HttpOnly, SameSite=Strict and no tokens in storage.", + "expectedResult": "'Stay Signed In' prevents auto-logout by extending session; tokens rotate as needed; CSRF is managed per-session; application draft remains intact and non-sensitive; ignoring the warning results in auto-logout and 401 on protected endpoints.", + "apiPath": "/v2/applications/start, /v2/applications/{application_id}/financials, /v2/auth/token/refresh, /v2/auth/csrf", + "httpMethod": "POST, POST, POST, GET", + "endpointGroup": "Applications, Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-06, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for mutating calls", + "mfaRequired": "true at initial login", + "rateLimitBucket": "standard", + "inputFields": "employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,X-App-Session", + "validationRules": "Session timeout at 15 minutes; warning at 13 minutes; Steps 2 and 3 require valid session_token; CSRF required for POST.", + "errorCodesCovered": "SESSION_EXPIRED, CSRF_MISSING, CSRF_INVALID, UNAUTHORIZED", + "stateTransitions": "Session: active->warning->extended->active or active->warning->expired", + "dataMaskingChecks": "No sensitive data (ssn_last4, PAN, tokens) in localStorage/DOM; masked PAN only when displayed", + "auditTrailChecks": "ASSUMPTION: Session extension and expiration are logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "full_legal_name,email,phone_number,residential_address", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: 'Stay Signed In' triggers token refresh and resets inactivity counter; CSRF remains valid or is reissued transparently.", + "testData": "Valid Step 1 personal info and Step 2 financials; device_id UUIDv4", + "cleanupSteps": "Logout; clear cookies; remove draft via test harness", + "dependencies": "Frontend implements 13-minute warning modal; autosave enabled; refresh endpoint available" + }, + { + "type": "functional", + "title": "Transaction Currency and Field Validation: Invalid currency_code, Negative/Zero FX Rate, Decimal Scale, Invalid Category Filter", + "description": "Validate currency and amount constraints for transactions: invalid ISO currency_code rejected, exchange_rate must be positive with Decimal(8,6), transaction_amount must have scale <= 2, and invalid category filter rejected.", + "testId": "TC-TXN-CURRENCY-VALID-028", + "testDescription": "Covers negative and non-ISO currency codes, zero/negative exchange_rate, too many decimals in transaction_amount, and invalid category enumeration in list API; includes a success case at precision boundaries.", + "prerequisites": "Active account_id with available_credit >= $50.00; authenticated session with CSRF; merchant test IDs; TLS 1.3.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to confirm Active status and available_credit baseline.\n2. POST /v2/accounts/{account_id}/transactions with currency_code 'USDX' (invalid), transaction_amount 10.00, exchange_rate 1.250000, MCC 3000; expect 400 validation error for currency_code.\n3. POST with currency_code USD but exchange_rate 0.000000; expect 400 validation error (FX rate must be positive) (ASSUMPTION: rule enforced).\n4. POST with currency_code USD and exchange_rate -1.230000; expect 400 validation error for negative rate.\n5. POST with CAD (omit currency_code) but transaction_amount 10.999 (more than 2 decimals); expect 400 or 422 validation error for Decimal(10,2) scale.\n6. POST a valid FX transaction at precision boundary: currency_code EUR, transaction_amount 2.50, exchange_rate 0.999999 (Decimal(8,6) max scale), mcc_code 3000, merchant_name 'EuroTravel', merchant_id 'ET001', transaction_type PURCHASE; expect 200 approval and correct foreign_fee_amount = (2.50×0.999999×0.03) rounded to 2 decimals; Total_CAD = (2.50×0.999999)×1.03.\n7. Verify response includes transaction_id, available_credit decreased by Total_CAD; amounts rounded to two decimals, fee itemized.\n8. GET /v2/accounts/{account_id}/transactions with category 'GIFT' (invalid); expect 400 validation error for category.\n9. GET /v2/accounts/{account_id}/transactions with valid category PURCHASE and page=1, per_page=25; expect 200 with the valid FX transaction present.\n10. Attempt GET transactions for another account_id not owned; expect 403 FORBIDDEN owner-only access.\n11. Confirm all transaction-related responses and lists show masked PAN and no PII leakage.", + "expectedResult": "Invalid currency_code, zero/negative exchange_rate, and transaction_amount with >2 decimals are rejected. Valid FX at max precision is accepted with correct 3% fee math and rounded amounts. Invalid category filter is rejected. Owner-only access enforced with masked PAN.", + "apiPath": "/v2/accounts/{account_id}/transactions (POST), /v2/accounts/{account_id}/transactions (GET), /v2/accounts/{account_id}/summary (GET)", + "httpMethod": "POST, GET, GET", + "endpointGroup": "Transactions, Accounts", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-006, NFR-05", + "calculationFormula": "Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03; monetary outputs rounded to 2 decimals.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard", + "inputFields": "transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,category,page,per_page", + "validationRules": "currency_code must be valid ISO 4217; exchange_rate Decimal(8,6) > 0 when currency!=CAD; transaction_amount Decimal(10,2) scale<=2 and > 0; category must be one of allowed enums.", + "errorCodesCovered": "400 field validation error, FORBIDDEN", + "stateTransitions": "Available_credit decremented on valid approval", + "dataMaskingChecks": "Masked PAN only; no sensitive PII in responses", + "auditTrailChecks": "ASSUMPTION: Approvals logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "None beyond account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: API validates currency_code strictly and enforces positive exchange_rate.", + "testData": "Invalid currency_code 'USDX'; valid EUR with exchange_rate 0.999999; CAD transaction_amount 10.999 for negative test", + "cleanupSteps": "None; keep valid FX transaction for billing validation", + "dependencies": "Sufficient available_credit; CSRF token present" + }, + { + "type": "functional", + "title": "Payments CUSTOM Boundary: Min $1.00, Scale Enforcement, Max = total_balance, Timezone Scheduling", + "description": "Validate payment_type CUSTOM rules including minimum/maximum amounts, decimal scale enforcement, and scheduled_date timezone boundary; ensure CSRF enforcement and owner-only access.", + "testId": "TC-PAYMENT-CUSTOM-BOUNDARY-029", + "testDescription": "Covers CUSTOM payment acceptance at exactly $1.00, rejection for >2 decimal places, rejection when exceeding total_balance, acceptance when equal to total_balance, and scheduled_date handling around UTC midnight.", + "prerequisites": "Active account with known total_balance and minimum_payment_due; at least one active bank_account_id linked; authenticated session with CSRF; TLS 1.3.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/statements/{statement_id} (latest) to capture total_balance and minimum_payment_due.\n2. GET linked bank accounts via ASSUMPTION helper or known bank_account_id; ensure bank_account_id is active.\n3. POST /v2/accounts/{account_id}/payments with payment_type CUSTOM, payment_amount 1.00, bank_account_id active, no scheduled_date; include X-CSRF-Token; expect 200 payment_id and scheduled_date today.\n4. POST a CUSTOM payment with payment_amount 0.999 (scale >2); expect 400 validation error for Decimal(10,2) scale; no balance change.\n5. POST a CUSTOM payment with payment_amount total_balance + 0.01; expect 400 validation error due to max = total_balance (ASSUMPTION: error code ABOVE_MAX or generic validation); no balance change.\n6. POST a CUSTOM payment with payment_amount exactly equal to current total_balance; expect 200 payment_id and new_balance_estimate near $0.00.\n7. Schedule a CUSTOM payment for the next day around UTC boundary: set scheduled_date to tomorrow's date (ISO 8601), verify 200 queued response and that scheduled_date is stored in UTC; ensure past date submission returns 400 INVALID_SCHEDULED_DATE.\n8. Attempt POST /v2/accounts/{account_id}/payments without X-CSRF-Token; expect 403 CSRF_MISSING and no payment created.\n9. Attempt POST payment for another user's account_id; expect 403 FORBIDDEN.\n10. Confirm cookies flags remain Secure, HttpOnly, SameSite=Strict; ensure no PAN or bank details leak in responses; only identifiers are shown.", + "expectedResult": "CUSTOM payments accept $1.00 and amounts up to total_balance, reject >2 decimal places and above-total_balance amounts, enforce CSRF and owner-only access, and accept future scheduled_date while rejecting past dates.", + "apiPath": "/v2/accounts/{account_id}/payments, /v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "POST, GET", + "endpointGroup": "Payments, Billing", + "workflow": "Billing & Financial Logic", + "businessRuleIds": "REQ-015 (statement reference), NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "false", + "rateLimitBucket": "payments standard", + "inputFields": "payment_amount,payment_type,bank_account_id,scheduled_date,account_id,statement_id", + "validationRules": "payment_amount Decimal(10,2) >= 1.00 and <= total_balance; scheduled_date must be future date; CSRF required; owner-only.", + "errorCodesCovered": "BELOW_MINIMUM (contextual), CSRF_MISSING, FORBIDDEN, INVALID_SCHEDULED_DATE, 400 generic validation", + "stateTransitions": "Balance reduced upon accepted immediate or scheduled payment posting (queued state for scheduled)", + "dataMaskingChecks": "No PAN or bank account sensitive details returned; masked or identifiers only", + "auditTrailChecks": "ASSUMPTION: Payment creation logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "bank_account_id (identifier only)", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Exceeding total_balance returns a 400 validation error; scheduled payments stored in UTC.", + "testData": "Known total_balance; active bank_account_id; minimum_payment_due value from latest statement", + "cleanupSteps": "If needed, cancel or allow scheduled payment to process in non-prod; reconcile test ledger", + "dependencies": "Linked bank account present; statement available; CSRF endpoint available" + }, + { + "type": "functional", + "title": "Right to Rescind Security Sweep: Post-Delete Access Denials, WebSocket Closure, Idempotency", + "description": "Validate that after a successful DELETE within the 14-day window, all subsequent access to account resources is denied, realtime streams are closed, and repeated DELETEs are handled idempotently.", + "testId": "TC-RESCIND-SECURITY-031", + "testDescription": "Extends REQ-016 by verifying cross-surface security behaviors post-rescind: protected APIs return 403/404, transactions and payments are blocked, WebSocket subscriptions are invalidated, notifications suppressed, and duplicate DELETE behaves safely.", + "prerequisites": "Newly issued account within 14 days; authenticated session with CSRF token; WebSocket JWT available; no critical pending payments.", + "stepsToPerform": "1. Confirm account issuance date < 14 days; GET /v2/accounts/{account_id}/summary returns 200 with Active status.\n2. Open WebSocket wss://realtime.aegiscard.com/v2/stream with valid JWT; subscribe to account:{account_id}:transactions; verify subscription ack.\n3. Initiate a small CAD PURCHASE via POST /v2/accounts/{account_id}/transactions; expect 200 and one realtime event; confirm masked PAN in event.\n4. Call DELETE /v2/accounts/{id} with X-CSRF-Token to exercise Right to Rescind; expect 200 success and audit reference (ASSUMPTION); account transitions to Closed.\n5. Immediately attempt GET /v2/accounts/{account_id}/summary; expect 404 NOT_FOUND or 403 FORBIDDEN per design; no data exposure.\n6. Attempt POST /v2/accounts/{account_id}/transactions; expect 403 FORBIDDEN or CARD_INACTIVE equivalent; confirm no transaction created.\n7. Attempt POST /v2/accounts/{account_id}/payments; expect 403 FORBIDDEN; no payment created.\n8. Verify the existing WebSocket receives a termination/unauthorized event or is closed by server; further events should not be delivered for the closed account.\n9. POST /v2/notifications/webhook targeting the rescinded account with alert_type STATEMENT_READY; expect either 404 or safe no-op (200 queued ignored) (ASSUMPTION: suppression); confirm no in-portal alert appears for the user.\n10. Attempt DELETE /v2/accounts/{id} again; expect idempotent response such as 404 NOT_FOUND or 409 ALREADY_CLOSED (ASSUMPTION); state unchanged.\n11. Ensure no PAN or PII leaked in any error responses and cookies remain Secure, HttpOnly, SameSite=Strict.", + "expectedResult": "After rescind, all account-bound APIs reject access, realtime subscription is invalidated, webhook deliveries are suppressed or no-op for the closed account, and repeated DELETE is idempotently handled. No PII leaks occur.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/payments, /v2/accounts/{id}, /v2/notifications/webhook, wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "GET, POST, POST, DELETE, POST, WebSocket", + "endpointGroup": "Account Lifecycle, Transactions, Payments, Notifications, Realtime", + "workflow": "Right to Rescind, Security & Compliance Controls", + "businessRuleIds": "REQ-016, NFR-05, REQ-014", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for DELETE/POST", + "mfaRequired": "false", + "rateLimitBucket": "standard", + "inputFields": "id (account id),alert_type,channel,severity,idempotency_key", + "validationRules": "DELETE allowed within 14 days only; post-delete access denied; WebSocket must enforce ownership and account state.", + "errorCodesCovered": "FORBIDDEN, NOT_FOUND, ALREADY_CLOSED (ASSUMPTION)", + "stateTransitions": "Account: Active->Closed (rescinded); Socket: connected->terminated", + "dataMaskingChecks": "Masked PAN in any residual events; no PII in errors", + "auditTrailChecks": "Rescind action logged with user_id/session_id/ip/timestamp_utc; attempted post-delete actions also logged", + "piiFields": "None returned post-delete", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: WebSocket disconnects on account closure; webhook suppression model returns safe status; duplicate DELETE handled idempotently.", + "testData": "account_id issued <14 days; idempotency_key a new UUID for webhook call", + "cleanupSteps": "None (account remains closed in non-prod); ensure no orphaned scheduled payments exist", + "dependencies": "WebSocket service operational; CSRF issuance; notifications engine reachable" + } +] \ No newline at end of file diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.xlsx b/functional_tests/functional-test-aegis/functional-test-aegis.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1b19c0d07ac807b3690c77f4d8c98a516062bf42 GIT binary patch literal 49257 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
    44_Tk4@72z3lcgxcsTyF}o zZ#mpXfq2t(6^H+KZOUrxqU7&2{vO+rBk@!zmf7)EfYx4F&GOuDco$5dkLOmCmPt7U zb29#Bz?~_b1<3{q82niyk;V8xO@_X%I%6!Zh}l3L>rGI}8g=`G=)eM{0v<-1QCZ4S z*tCvt8v|^_O+(_eOm1X<_Y`90r(=_QG&GZd@;b@wqn{skfMrZMTADFN51`*}=Bhhf zyntUawy4P{!;{4k@?JHJta2!nEY}YpYsAOSSwj^KHPzp|bm`e!^PyBPW;d)O-%*bd zC-^C16f)P4k6NBO9o6-Jk)DFTDK|6n7VnCzv`)`8LG?tw7Ql?Y*q%W!Uz|l*Vw9(= zRzIA)XMA}b3^E66(oWYa`=HujURoAYp}Ew!BC8GaM$!4ZT^q#0S}4x+&xG5MyzcYfh_0bh~0t7Pv~It%mc5TWn3z z7CRd9PC}6_icr6a6a!&1K!rC183yV0@g81GnHHJk*mlly234%hhW{nRj#qGS-L#$=+|tety3kO~s?lOmb@>>vVx&b*2%aNrW}#!YaGv za50b{?2c62KJj>HN=wDfn5v9OgQs1_XJfc=n(+XE76L2qINChH^bWSUh;A{41IKAj! zH(gj2u4+T|@s+xs8CE@iGdarMJaqEb;iNNd9X-6Aw%%q3lUC>cU~)J;%8wsT0^d;I z0Env9p>)XpVl-sc_anrkL`C5wM3S?X8$EIw3oaStQBdE6^ zpZbcwCv5NTxTvH#IqB<-YL}yG`*wfI_~x)J55^_=(WaT0;p5v{u>aV5cTNevU_=_< z&nw;5IV;b}T>4Zjod(`vC-j|LGS@d_mJCXsqC>Xv9H_hg?U;_0U#z88l4SDTNM#|}%Q$eJfmbhUmXOh>I*6jvx-3DJO7w&|I9mA-0TLsQI*ciVV-9TpM9s1fXv~M4SOYr1>ZNk*f*>}R0*HPeDE_4_W%UK!3(6<)g}J0@;m-^iT2*_B z=@@k0tXrG<4hS*0SJIK-(z|$uN95BfPL!lV})kcZ;rVJvg|~ z803gM?fyS&WBr;VFYpUE&wSmgL}Z%e63$;d=`Lv2cM%$Qr+$m(#I}GHn@2@iHxYx! za@tzQok4+>gMq1f?A2DNb3!1dO;T$*fGKiTqBf`Q7q&+I|6QZzxKD*R0U1D1>&It4 zCX9d^;Bl1NPRg(?@jl9$+b1$FpW_h@7C$sJB3+PU?T!OwsHR!+mcvoo)wtp7!_7#S zV>Rs;R%wVhF5Qw_%meVXG3hw$b`hqC6T_S$jO**|gWZE1Z4>-;uFBu$Yy=F;ZU4I! z@{AlY5#zHcja|k(Ue)W8R8^A7RH?#dn(v}~=V*bVX*?7^!#%{c6uP|vp(ZadZ55*S zCkpXL0siOMJ6X&Z)WmPgGyQ$8&4?)ndI~H=&)#M2k zD$av**n{C8D zhe&egvkaEr!p$QwUs6M+p)_Qw&u6Ra9?R9kQp<}kk;ZWvH?)~w0$|tJ!` ztua*zAwQSW@3=I*8Kn=;=Te#{;#3vjn{0iru~k5BT3v}QGs=%K%Rt*j=pT6Nr;ljxWOZRTFJTg zwM|g9Z|RvL@&YnsJEsxSMV#bEqvZLCujm?qP-LjCP)ieJUT)G50YNJE}qc`$X>W# zjUfTLr!+Rz)J>Y~IM1YWf_C$U*}$k}Gae+V64XS8dNiZ3z8+h0HFv7XuG9G@tkb^Af$6-_h;Hi3YpRw^Q3OI3#YO8qgo7I{qyEo7*FQPh21~DW`);gE=Nf0NfgW zzjb!l6~b5Mfl2c*zHNW=KmNO9)Cp&;sb@3^P*Sx>)>;fRP24pf2Hb>el~c5YBSrN< zgLQofjbCQxBFAmeX+d2g4=;w>Wpc>2jkMBaFfRo6LK;<4dc4EsD5lai^>jHV3Clek zeG+LWIgegvLCV?Wj)PptT4^;H95dv$eALelsYxK6=`)lEUDaexNs&v5`={pTb}(l~ zQuEFxr@L4C1a3h>8*Q^jV+}GZeFCe`v!L)-Xcl9GV637qu=Ht?pAvdjpOg0#q%x(t zKI*@gCLbt!eX*jvy^+&WwB8p|v6-EFQhJ@_3QpOpVW7TO`cjR`P`aUJTco&#_r-LY zYbQQDE8cOIqBbdcqirDU+)boE$=!MDH5#3N|YY&FFcTk+K5th+!+f}ZJD zYG|gF179!It3KYgme-Y=B`#j+bQSnT6?f;dr|`brx@$|FM^C6u-01JHwq!+e!YznG z?%dU7yyw6ltl~u1OXQ=imOUQ|5YnU*Ky=X0#W_DDh!p*pls>mN)efNqVcGT|dZ(NSTt0C1nnreki z!UYyU6VW%2o8&b)4~sSf1g6D3<`HV--L0i-7|=ihX)0}c%VK6rV<*`heed@<b3;w@a-^0nR6nL7?_i4Z6{N~eUfL#_H z2XWHztD~`KZk3L4EB_#|b|yUxg?HJOK;*hGXH1kbbIOojzRW`yBh0{g#Nav64A;VN zZ^(w5kR&b`&0fP?zeg&XMoEmsfbV~^myC*MN>R~fek60}`5G~^lId!(L`xCF^a#pL zWUcjQ+@rK>076V@uPek%1`RK^gFkjJ84$WGnCl8>4xUR4P`iX+d_*UO8Oar3c&sO( zlY&eMo`-oDYm%tNyjqM!-| zz#7)@rzbTlg=Gm5x`PqySDNA1j`9)MWHk6w+mxET0&)ZWY>s}otbLMJB8{lAPRt+R zW@sPr<0wf35o zWgvfKz>&IXRL!N6gy}=#7Gx#wGgz3PPifIV6Y^wOs(FrC2QB;rJCh3FcfH+SDOAVD zdb+wvC(?Wd&54U3k0;sre`^q$?p=9;mWPQi2Yd16a}~mkeiW0KlVwd1JArli{hI&pCDS`tApOJA zz@6=R&g8almqh+;p(UP7h?w9D9A2{BpcA;gG`92kok2}Nm6*Ddciqeeasb0kVg@>e zlBhPDQKK?4DOV4E+P;f0@|@$_4z~xiZH;4L?udOYxC_?puZFB4d*JzN#P_e&q>4>3YzLg z$!j^~Hq*D3gIGYQuRkId%LaW-^h}MCTbOiHCtf`xMyXx-XXck>k)Tz6ZjwDfJ7eFC zmG*N4#&)%D8@#u*>o^}R!G}|Z_3p&kxKT1XF%Xu}K2%Q9(ZO3C9qA0r2wMX9Ca~0@ z^!!j!RfK=KB_p2dQWr6>cN`iS=^)*xPC)OVQVrb_=U8K%zjTEYKW2a%Q>PV}I*Wc#y=oqF;v`t#v4A(4eZ6%DjCIzV_+KxC5?ow-RJ95TI>m z_ZjGSpBlsM-+lJ4cMyDv4X{MWr5c!-bW~Qc~7{hKIRILl?%EOK(P2n+eVAAqJ}j0 zDP-0pl6z|r1Fw|ulNioQG$3v&0@S6wWB+wrs%^vZl}?7zjC4mAh(99=T9htcxzIIL zgi4ZGhLRe?##}a0*083q>w47$`CJAT2>L;RCMVS4Ws{dK{z!=eZ65?>Xl&O&pkPBx za@mqa>`6%xQNg278DR%A@l&eVLXAU<01PCG+UB(gC_@K5$pJMvP5M;X^kH70ryeP% zah{cVYcMx-*+hVP!Dc!)*i311ar2Hr%sX$A##NpzTR*|f@oGYW=Ho1rH#0C3WQD87dkd2g%?#q%1d%7xYshLD-F*F@CDJel<6ZX+-vOvgqIE5G7 zy!&Fce&R-BGz-SHNRzUhkFh!A5nMZeL~t*%TOU_>K3>y2UB>{aUv-{l#n2u0ze6zn zINDBG(d&4Ts76mpOwl5BnusY@5Misi)e_i@kd}|(2v8L;)??|n^y9U((2Y|+5aHWT z5q_qHcOF7?h?1-ECyFguRIokG&UwKHwIfvtYc%scr9v866*vszU(F+fZ0-oM^({%WaClvcU+GQ%o0j{ z5kTFW?Y*?qZC1c!Ef>@`isH^@snp|6}j{?Nxv8{N{b7 z0tG=N`tN&#t8xFw0dPi@EbI&iZKAN+gd?TNiD5JV&XMQ1OEI5vhJ!DfP+*{mqS6Ty zA|i`Y*n(j69Jo4UMi2_LSu$s=xa8(oteiTCQ~Wd(Ukr4Bsn(cBLE?^boBnT)wJ`Yj#tV(yTaALg46L zo@(JJVy7$)4Eu=4of=)K+N;{r9J_(;#&yU((R58?GgWTP0SWiNYlJ)6Qsm_8Msygv z5f3X8-SPJC-oc5b`NyKmf!&F;@oNBIvA>hMOGDg};LVS@bQJ;)CEX3D>~8ZJ83G^_ zAmCK0F*}QF#eA`PhVzMzrRtWp zh8n2UG`ZpB`SIbwfwWx5iDC__KQ-6j*-=oqwh&z!Dhn06XxsUyiiIG}z*Ub@kM*V` zi22-BYY0Mj2$1C|Y}h=P*gjJ^vQ;cXLh^8rS5QSDr2x!Ux`xxe#@mn$l2^yt@%x5h zXXTc}4KLN+Wm=Sa5D)#^M(STdb-NJTHV4VhC=EZj@{I)!r5UJbvW;$>?l}c8Ebd|I zd44J;Em>CB960Qd1ZEbQFj7WG&&S$4a!g?6Qyl*!)T>^%FGAgm+7pRMuwJ$2k9gSr z;h=qR!XMW7rxXimu{3yw(v)(|&07&TwE$r^eWoDkV#Xb)5ZfUvo9jEBTx9j7lQR`C zAi9OuVu%;2dLqU(854rRPTM&YB!09?IT!;1yvKP3ciBA%))8fUw%|0zk!T0yVkT&? z_tno0STN$PFEz-H>-{LS(%2J=K-`ITLY z(nxz6i+PRRXg(i#ZL`?x(iImW938}*b_`Fq=}yojM1#NRS`dh>ZQW}cuWn|O97Z>| z__WqZgDf+pMPOqO1qI(t(uWrqB=^(L91YH#1`NAZc3ZSeoL|F_yif%p`GM@-MA}Gs zIj&(w2z6_w0)${~hMg9CEB>^gpujCsusk8`s#M6ry*@Z6Dm`C(X@PPuvwSm>o>S>R zEnJRw3F{i!U;$m zwi+_lTt->M9yy0TN+Js?R($AFv&uCt1{nu0aOIt^hV|hl430W67R(W^goNe`&bO0J zmE=Ua0!`^2>qiRme!#Z~9g={qEsT-c+^G4`pS9TV)GvXmW5$O^k>PEuJ#x ziR{wr$1Lai3&w9K8?-=oGY^?UG%S6IsIB?iHiwCqDpi3@*ELWgDUI{}^2Vmyd!!tl(&RSNI`Rxz8T|=8m;B`y5GCE@`-is z(bHL%@bODK`#1?9kyMasYaO@vfks_d@4@t7n|Vri8iqB_f1Xf349+S8^_c)kqW0L< zPCG95uQO1jk%rm@)>cSe72ZX?acH)$qL)%61ZZDHzY2HP;wt%#XYF^ zcf6LDVe{{|Ct`qf@bnuLxyXE!F^PbvU_e$(ms*qU_4YN#$L;*hQM=vB58mFljylt$ zR`zY@q;+(3a&mlp1mA>+1(GXCG8zRW^EaV%M4BzFD3!9Nbl~L=iu(g z>lUpQ!spafUBtT^yD1r_PHso^G8^erw9WPjvfn2WEMHobjD%#sRoGt^ttl^;13PID zs%GK|SQ>INqiirD4Y{pjJq^DSUb2efA+ZQ1Rf{AXDfDY!*0d$jo_OcuZwoFSh622)5H&)Ls*5Y5y_5{)BSz6ZX_ za6|QAmhUah=~i;}{2!9?P!E3@05B>OEwA{Y2xY&&2Cixj7ebQ*(n(iJ_k?|~P<;l+o;e@AaS@bov|ew+Ny|LuRp z501OZ9U=cA0*ilFhwp+Ry}`Qb-gSQSgp|-{adyr6kBBR0g5IeK4DxR0w#Go$< zqCmjBG(`uoQ06V4xI$J%8+YCteLwD9{Ls5T@5hPFvNA(SC+&{cONObr87Hputb8H} zvwGS(?`_9+WdI071ZdGm%N2or*wijgmo_+}EPYv+l6OZ|?>nxh`|%-P)zA3dE1 zPVk&H@2mi~vhzasrkGJTNy9Q+5=^Mb3H6_K&b7Jb=-`QQ(D2#F<%%})tN}GU0G?qz zLSaKs81p$qbAhV484cXW;0skuccSWHNh-*JaS4aSi02W{u?@e~Neyl$qEg5U*r~{1 z=+81a{72HTJ(%0LhG`YcM+=6?%4Do`qt@Qrkq{bq-kQ6X{v-{cu@LpJ&Xwct#8#VZ zwNS5N?>4|@5IIxx@u|n% zP+<3&ya%y&fFiJQRL%(2EKPE+sF<07d9P>`o+Knz8u}r%NHTyvQPeksFB*du z{X#vkY}Mu0L4AUlCy+U6N2s=x5>O;t%JpXXY70JrZfUjW-Mi zGsqi5O+uHV`AW_KuNEeW2~HX1FW6=zVJYe8qDH|t_@Ar~la1i$XhwbM63V$K{Wx>_ zH%O^qbWo707$)=BhJu!Q7@_s8h_^z7Iv(npvbd3X$GLSFm8L7oY0|rRmke+T(b1qg z00II-UrB!k2VSxw`cvQ_$rY~IB}hk1s`}<2xxm7KJMG++CfBedm-v)ge;SK|x4dsT z2lY(87DXYI46MPy@fgN<D=aM9iGo(;kcoQCoQvL+5#veg+l} z^LxUdsn$Us)uH_%9UvpP!Z8jUCQDTk4VZb;Jf9tNM{UN9+RV8nh>KDi^QyfYTwQ_Q za&?Y~Q}sxs3TJZ9;MWP(W{56^U^H$mm+2d0M4EvOH`QYt|5A;{bsbnt?}!VGc?Pj@ zo*k^ZR&;mX?!A5ck4=KYqkMVXA`k1wNvfd=6x?yjJ*J%URsMNw7fS#$#<*SM0wg_d zm%#hc1&RSUX3s1-`D>hN6{KL|%@XeX8Xww_8?5IFGi(=%AL-#HaM}rdj{8n@+;va=aEp zBDh6e0X0(;2ELA9rP5zId3@I2El#Ddk})v$)nh&d+srnaXp`q|n73A&xgGIq^W!k) z{Yz4EZX5^6;XmXRG&_dU!z7c%L&6;jFym8q^_4jXvpKNt`Ra5;8MEo^Ff8GsBD6F*aQ`y=PmIM!`j$@-WN$L6=5Z0ID*c@BQTP z8Y}XjnXX|f2~LT7#rcrV#Ss?CH;IaigTvF~xA1ol8xE1|;8dHIpn6k%4ug)sV?`F` zPB$z==J+{UKWA-nNsVd;v|dC8@=emw?3G5leTsiOB*m!{$@2oA(`0V6Sx#RY!XFMv zA(Cb7U`EX&*g`WxD0oP|{0~0T;H1lDV+|n`XbY2oipkEmN16TYlUoAp~&26!)>_U}D zBn^0v!)dJQCf*$0cjCs|Rk^R~z0yS*-6syRVjUryDVi+qwBfyVYT<9>AKJMaOT^<- z(`rT5)+p9SEw1gJ@=ImNmG0G9BK!o&ILgvU1V*?k?%)<&i_~3{lo?6&>YHF6)(c0` z>k9v2ae;wsLe1VvB1Hmi)Sha2;EVivBbkbwCH+brpKz7a$rqYRiHF* zkwU-4w(cce=cHwUQDqD#%(Ak{#)FG+`#0#E0|{2l*xAJ1x+`+Qo|BiSJjDe04LhW+ zIEg#uT}*z!{yk)#rivLPs8qGy3#vpt0-F=xvoCLzwBOebAw*#lAIEJzjJwV@ zHWg&4z@|NF{LHM_D@i^cV&K^8^xDyhYj!#(@ZS8Zs`)7HqRyL(q4-K%qb2cHE?*~1TOd&l-lv!}jGgg=4|e0Tm)IT;4eBGe z&kg4V+8!Ja$sdfb`)m8&B#j_{t?6ZzRcA@D6lV7Z2Gg8QlSAfi6hqXOytB?WSRV4v zc-+=6XYOhfxPTq?nL7j9XJjrXS5Ajy0W%t)*4I!ocIt~Wn~pj25DyMQ5y@|$8!Jjl zgND`Ik=L{TxG)mxDJ!2Cy@JAtIY_;V@q7{?O|J6Q7rQ<1M9iv{&2v?DS_2wvD9DEi z{x*6hoR`UBc@)n+Q5>C7mtl5_tW6b}hPjJMy6qY*o1BF#=jwa&_D^iKRIN9=AG=1T<3 zX9gBgDR7am4v_bCl2&po9D$2_I3u&l4*cHV>iL7>sU`Hz(qBGRujhDBKJaK8U4Ll|yQ?jIi1Ws}_HJ1(W7TeIkLqABep(pLSY--qk1((V0rsDZ@f+ z2SFi0YjRqK)Ozml;^_1&7%l_g#^8D!jm=z6d(g6Wcui`MJE;^UBLC#LVCX^P7})Sl zP-2}JZIxe7-$giK*OS`Lq4A1>Qbz09VBDPN#x#ms~1Y6c}h&(PE;$bbuwYq(X zdX}eJ#EuZY81s$z%#G@}G;ah>o=Zr4v2p`da*c<(V_ij>G0Z)#r6rxk&sy==C~l}w z{6+~c$15G6wX#yz%mua9e=$1m9<>Q!rS)jcMFcZz-F|;_jW{wvmZv(1aQenwGpV9u0f9MYfmp=vm6lweUV%WGX z50i&$=Wg1cnK)by>YyTURKj1l!j80JSX20HLZo7lS$jvv?*dfLn&KiRLdVk@o}kT~ zg*KQ*SSO+9N&CFR%!nJtY@g{Df(+AB8 ze}b8IkkZGOy}^}WFKpuqOh}yoK<1UjmpC~O4Tn_oe(PV^J_hlXS(pp|NE3S^?~nSf z>PeQtL!ME1NwD_Bv}*0TXkBD5ig~E85eILRA*K_P@4GMlgLZOvb2EP5yZ&hmO8$pF zg2j0)qr{K{NjK&Iv+22N=0i3kZXF6MZY`jEkKtu9R63+jv+%<&#eBN>LI_S|uGLOi zSI<9z+`0z2o{1Hj?O3i5$_u}{hsAiNAO%q|xp$nw5DRDF;|)ihG^TU3tKl?bU21gI-QamT(O|&LM0JWI%V<7SIUFIZJprr$)#T7VZ*Jx?7z_yska>mj!x(FBSswStWo=TVa zMDX1Yq+PhQ1-rWXRK}>POAF^NN!V-%mAbNPL1}E~u5}ja4H7#qY3v--gU3WYCdVA8 zCH?)usavpC9LoT8( z?hA*24D%-Gk2|!_U>%;s2%IOZVWpy3rKI#*H7WK&tuoW3!xJgW2?$F0wk3p+r`zvb z+KIeYj$Il^$v{HxR$$}PuxutdQ)nkhf3GoFy07UJXw@+KO4z@LSfwyW2O_;htRzNg zvN*~HRE&IQ*WUHrdOnEC;WoN;sHP0~P88c_M@LP3jae?dHlV6sg!81CDic@Vm(-cX zXT-0Hu{HO}Y*349-=j(`Md18%<2P}W1Z?*>#*zv%7DC|Nu4g`%5zByH6H}x4Hr^c0 z`4`x5KMe*9HL!*d95-I^0}oIjK}-W!U_coa&VVHFF~V<*$>d}1cC{101fyjztXUB& z@aj2=1RIeKSWYUvjoxF31|7ugfE!JoV8^Iq+oH_S$+#*$XA|Bk?lDDh?FMx>@OMai zM}D7Sv5Z?p@gPmI&H}qSP1Fxq(l9vXJH<#RvyGgDt<%j%N`5fMiRYSo{uF~bn>x85 zIx_*BBa;m+N9%9oj5!l_p{YveKOAN>LFV?hHM;BHb_^!Yw}CGiEIuMyvm3Yhszk8n zWB|()bSjcH;=w-WC=rVC`4lyE+>x|0Xo?gqi0@#zD4F>(b_YKf$1gpK<4dOIXtYzY z2bgR672_{G&mBVe@$l(8zpD}99%B9-dc)B^QTSF@5p;@i;y#a^y&BvQI4i>ecEd)% zT}JkGmx6k<@>W$~YV1WN=Gm^+F7mFl(+TEJ?P=%)G!>+#%BL`sR=h^a$cw4qeE|Es zy&2xoCte$zA2D8heDdv^x1r_lb)Atj=m6FQCr~P4*3zEY!4md2_}=0kGS5e6E5H2) zv_`Ncg{c;EP?NGLCXyUr5Aaphe)C2sv)L90u}1Ke~q{D~95q6 zPnolyeBqhb+_$DGh{$8KuIY}0x2bw0J#3u*$Fu=m>1Hgv@Ez#0z~mpLBFI^;t|GZ( zk`K*|G#z>f%0hjQk*3~2KIJbmSQS81Y7>n(f@n%Gs}$jGsAt&U%JDmP z6L{U{oq2glN88vnw|Jr#TQMjDSEvCsTgLR$K*Fo2X&%Qa}OEI{>f!lB`uS*@*j?bvti zQ3nQ8QaWF)_Sa?lXL=4)z;2E96rv8lH4X2H3@FPCvEDg7A?{oW_C8*SbHFYLEW=u< zr!Y;#mx7v&NkK93twyxC?86{}Mxw3~O1Bq}iG-R0>lp>Wc$Pnc=yaPCxs72|!j|h* zp0CZX6HKwMiv2Vqj#H%yLkV$cR1(^hO&d{fG%{!O2u~+*g}-L5iCF3ysf64HbEhal zVcm6Q6QY!W5Mp^+9bNRSepNpr$DDK85|Pn?$-lg#NVBm{M4phbZHe|>{_R!!a0{Xb zN^r#j9?n47nnzJ(d4yWJyVGLpv-RMT1?8Iy^Qq)S5eR!lbehb*x)W>Ll4x0At(h0m zWg*$lCTk3~R{#qT1XF849ME%qIocXL1USGZylPr^bJu%K#Wjj%rK$ z?{BYeenMv^gZhKUOhY?vA966y@N%UkO2dOmi%HFL&$#O`TmqCK*-WkiTH`@DANDT> zcefv|u7GR5dCh@vVAZ7(H$l>sa6Gm-kIf%Jl_NL(qBU_O6qD)+*6P;~Du+$>it^>3 z0#O9CdCYV-#XN5CcXej69pns~a5)xdL10;?bSk3Fa`IYs_7=odumALi{P%r_#-zFd z&O}S8RcwDzQQWDeLU7Nyt&_3z<`d(B5%$vL5p}b1HlKqy8^ca6o)J@t3qDc=D_cL! zUdEL7)j5z)wZrsyn%}R-rXEkfb`Pi5RMkkLGk-8XhL;YR93sP<(E!y~;NB*z5Bt!; zxnC_EOWi$_(qzaP1CyupI3)-gXT)lfqN1=P zDQ-wbcQDzTVQ=^o#W+3;`(6l__wekte|<5y{%$<%|1ju(RG-3RF~(zvME)O}d?UeZ zQ>?Pf@pju6JIXJ&LJ2|GbLr&aKzQi2X zFA`Hfc{5R*@Jy$u<}=w%7%((BQI)DLHqBg_FULh&tjM!lIh5sN~*wGA*@QS zunE!_zKUIBRwDSEgDfmBrakJ9MlgkO|Hs?GP!e|}Z^YdzF29+6rhG>V)E#y-1%umq zQ^)~b7bbPH!gLKC;YFSs$EGr1D=3v~YabCi94k3kMr^f8%7tU~a~g`fA&p8SV)4$J z1{Dbz&6PK0fzKu2$St^}50+xo+@yu48W$UVHQA6ynnHT2cmNOkZopb ziHO4?5y_&oWNMidlp=1>AJ{mK%ZzEbzO2A_P1jYMUYH3B6O1rRPn$x|ggGAs^vwk4 zZ@<%`sp~qAY()Z6?tqO7E@ ziju}Plhh%pv~nU+=QN}qzl;xVyUxmhrjsn?)~8Iu75$l$ptkiA6Qt)XL6N&F}&S)^PJ>`st14;upUFyOB{C0 z4{m^Ius@V#eN_@eJ^j&vr9KU)5DTv1wxPXq4TlVaoF>B{ue9$W^OMXaV?mWx4`E+B z7OrakF4fpbY}S9$bye2rt693C@$?{Rmzt~i>XCD3xnd5WAjA4OJX?JH(K!e^s=Tny zzu-9(#8nL(nII$4w@_2=ItV?MrLOv#0SUhee^?CG5Qxz{=nSi^Bg z@G^9!_;!PFV?F0$jq3={}nxqB&3vmAx++s7{g*XVybv zEDl5kgR=%S7R3;)w$ah(h3`)AUAeL-ikZPJc^9TXBR4JZ_p@sj!IZ!f5cgLl6d0Y~ zd-T3OnH79n-ssSZn8P2g|8jlv5#+-U{p&mJRiJA=xJK7->JgCKhX6);x!H-q_2o_K zuJia)j&;y{S`fm=v#SkQ_Emp)H@^Gnw(lo3Y?)K_{3TcD$G+|T^d6=`maWc#2YK83 zu0PaB_Xdb?i9420o39KgA;VwNwB(5be22#Z?(}=@Y}*Dq*bz3j#*u>b>|Oj6E_h`{ zBkTXuV&j4`Q&;b7`l4G>-2+_vj^`|JN2;2>u!ej(&TOTelJ*m3!r^%M!1?`ocAGco zc|%Ufv{UZC?+vb;8Gq~zuR(2yPWt9EmW^}mW69Z*1Y;0EE7-OW2i`)6A;3;;p1#v` zwgz2c{$g|%xDj}vY3j54YFrL`A1=n1SH17H5Wx9xa5p&bT}9Vpv?-UxgaK^{&eW{P zQK!>MI{rV|I(e_K=|65;pF_XaKt9AoF$Y;Ia^Q}vAegRJRi3uXt%vOOpIolrVZ-9abPVl zBSNGPUu7Ucw8C0a?)_p6qW2n{{c7s5Ner{K7=p?tKp~Rq9q>K%KY`^!FeK|JH6F2t z7L6!DOpR0#Nsfc*?M!n`C9w7AiT-JhCf8Yt!ltM2c1%fY64bvfljzSMvGHF6{j~6H zkUC(BHI#Gnh1f$$-VYXf$&IO{6hk-}LaIfS`tXV7QIUtZ-O_^vvx>6}MLWA}!4mZ8}%o;fH3N%w=;J)2wUUCU}>E5T;Y)nlI4PAP!7X3|A0rZGS(vZy%nxv zUs=mn$}Jr02GLB(ZXz^O72l@6i^1WTTgk9(+{!Z9Md4UTO0XcC8_%0HX2aZFQCR^c zuOOJF#(36r{9EM&%qZF(O!k>cZ6SHSfi-TZAGR2(!Z5?QFsi*ND}DUmVJboeW5n~~ zKQ?)labn#WW0~q^>LO4%e($=)C5US@--GjcP92IV1HxBR=yYIO7y1%`WIw&lDNHwvFURQG0=y|eA47% zbhnSH&G*LmHXV@OXFoeWk$N|VgXG~f9ikA)y4o7VNew7uYQiymxezD~QdgA0V&%^KAPt{=<=X4Ic}W_)b3eCoWvw(p6PTOsH+HXqS@ zvUvZ>e7#xj>>N2Kg;<7b03^NVel30^2OsUiQgpcjKkNp3Pm{aO*+Ki@q}4ubb&l?& znL+!nX>xbqexaQg@QXwH#qnL|K<+R$h}$EwB#+70_Gd(1MEJ|j2(W}O{4CPR(P-Zo zrkt6!;bsU-2@%CxKV8whP znMi()UX|ZyQL2|4pl@iup&4BR`+iD#REwQ~o;MM4{IZ~C5t4Nlr^{^ZJngRC5#pfr zalbg^-WY%7UKlm2(0&uHLi>$1>=z6HhMPF`z$by{wh-fV>TvC%bRfhf_>_VDrl^Im zM&u0n#hVAuL%$kVbx~G-K$l$`c$R3gx%u9MrME<7iEXn)y}>>V=?z&T&?Y$@es#0c zPP?<+YJXt&p%qJ=Hg`$6T`W0c4CU0pu}>0S?^$45vjwV0#JyN?!h!PuyvD0*?ooli z`1EcQ?rlyDH&~UBz%o;jC9;+Bx+zh|V8!STpyPwTPtmi&b)?DjqFigcEg&lFse#^! zuU%r(H+I;+q=qD@GGUz>#cxyB9iVw%*u~3v&cFVf_32^1E6%1Ytufcx*hTi*%8z>?0?2^ge?W!cUUq z31|Ph-tlrUEF5L3w4jP^3I`fvyv>G;RB}loScPs-P3;PLp`D%cA`b->mmL@V7xu}0 z#UWA;kV(iD*0B%)7Bx&U6%Ja{VKoigQ|-{_H7IbeFtn~T8pbx#UBAz%@Z!%ra`Nl} zIT2yFx)r+kt8aOgpT;WPlLp!LbSh-6nq8l{YLuA60ZEd^(?Wuy_by@r#guSXr&ROql76RXBcCZpc`7psc0X2ABJN`<`xbY;u(KOWn*ZQItwwr$(CZDTUAlZhs_ z?M!St|J-}ud-vhH{=Z)9oW0jN^{MLB)u*bud+)08jBg&u<1)ipQ+T0t-r8%?tN`Bx z1$u#LEIc&7M|NMUPt|D$mmfdSANp7n3DZ8|jW`XXGpLeEy*u@xNESnYB~Z?&w*$mZU)d1V4WpVgr<4$3> zLrmH3bw!AOQ`B#1N076#ZP<>vrob$2lb2-Q2RNAVq$2qKs==dIJb9mfFUtl=-C(OS z04>%#P(X9ag#FL&HxY#ZW=|-Q7c!=x?ABa+O-1k<{mu^{&Pur#m$#2zm=|~_C9U+Y z<}?I)MQg+7UZI#Q19$lFt3ceNdW1H4?{7CPlf8u9OIZgP?mcsco}q_aH}{xe1dZUB z*T8+~3@D@z(~l526afo|yJof-qJuxI0x%JXTvgV`OLB1$xoQioaEtgi3E?&fMRw4q zN!m+^DBFbnaf1pf`Y)scoA=yyTz*H|L|Dipaqvs~Y zQYQL@vbfc*9^j;K-KJwM9nB(}AC3UpQNT8L_g%mu#V?ZdbV)6e!S-^q->G+D-Gx1H-O(#x?v=jmZ}jHHaV|Gog`3W zStn^g*2_s6i4ks>i6c&%lM6bq@^!Zz#eLO1@crzf1yPVBRBuda1i$?KrYJ_>+(MWc z7s49e|M9%>)Ne2EgHA*CjMDs*JZlL;h{>-ks*~h3iP7Bq2O=)QM6SRf*t>y=NObf_ zC&;4O0$M!F>42eX?%;0=T1012;tgo2XwjCXxNPrOiWL{?!l-LKg`~j>=r@2oBLb@3 zzCiyG6GcqCd)C+I>5zIOa(ecpaBT%Xv5F%QL9NnlisMR51eM?)+mZCXVwypRQVmN@ zS5m6?I>r|x z%uW>b*mJ{?vkF2TblVjXhR8|ENRHtrtg%9tt7&h#*UZtWV#1^@p*@OH4S+b(hNFfe z%2IsBAxtNr6%>dTDRE^gj?0TbL|sWDmsV!p5TE%0d>-Rm9aB3?F&4}m(~pmEqOD3z zgAcrNjqa?(W@sU-aVa=4evTH+n`BVts9fM>dG5&-y(s*iV>y^Wqz;UyCX<6!WrZ1b zZz1BO^-Vj1wYKySIla|6OdZY5zgcAcRoX^cqv)zL3sHe-p`2Lk;F|m5O?WTs2@`>z zB;NT%hM5)0_oVhP8BL7sXCqr!N|~t6UZE)fYwUS8t+Hr#g~KgqVGfhHH7UOTlg)AP z__vBnOy@n`>#mJ0XB&s1S3-=0Hp(%fVo0HcF5goK>tSr%+A}u7%KsK?ARihMzk+3p zGG#LR&31J$&3MlA+7OK#!*anH^}twgL|{kMeE_VAK%Z8C9L6qB$mG*8%ZOu=0aWE6 zEhf$sjM>;4amJ8GLs9ls0WiI-EEv^~^Xzl1^>fbyKe-9oAvhnFh6>8he$px5c*0+Z zzi|0r9&&4Kp!HsHB`Z-?Ut}M!ABJ=aenma$sd#YXm#96)!+ug{aWbCmDG{tP#(nhG zNkMWQkkY7GJ$NJU%=cLQ8m+6Js;}DwiKC1KtNSX=H03TJFqQk7CbW=`JZ+@~kfQI? z`_vY6co@vAu96+#2dYsiMEAkx^Fpps zHML}J{U_kT#2tQz0sV$f9yja0}E9UC7VT;6-4ds-VDSh z)jW=A=v?9wus~Q7!V5H(ng&L|>RONXAu+J1dR!mWo0yn&P2;|vftx8JJXVu!tw%s_ z=I}acM@b43SgS|)&IN^Ga%T&!f05AJ$50m0^T1FGS>{PNVZIVFu`9@>qtB&laU?H0 z6i1spO_HxwN{UVh*)tQ!`CIvsT!;IamalJgt!rQN&7v%FVf%@>m46g?@-T%BCF5>4 z5%!SQX%3~?U_lx$JVpKCe0>g8!}s`6xun$Gd^Y2U@28x@QsLU5OR1g-nmHg6H;YSM zlDdd1p|o?=U?PmptaA&r0ZFVi5z2^Td8=%~;V|(1z{A_pZQdA+WLXp_nobyz+2noq z=hoLF{N^x)Iy)=oFg=jOfg=En19~eNb)`a z(|$7D^37Dv3@&N$JKD@kol=AIu9%?QRRxrtxeU76tqKHTWifGmbm0UNx5Ikssq&`3 zw^XyS5Thzy9q|NrsX4&!?}C!rU35TxKY%9x2&doD08(&XP~2wUu`L3YOk4{9^ z&a>i%U933hhv)g|!Jz}!V%kRGnY#*%LF^CB`=jXyiwbFq;aD_^v@is)2(H1#*d>ra z;mURWu#GnkaubMxGivJ`n3Mdjx&yjbyw&~w<^Hsup{#?WgQLxn?lB7eDej$Q>{~Dj z|Kh!4nrGy%iDtfVw#nZBZ;tOgfvW&(z7G(npdj~0PF4ZyDYPC6Qyb|nfjO5qR(}mf} z`x$dU9$`TCyw3xJr%@~4eAC%aG(;y4c`6fB5OCg#amY+U<2#OH4Gm7Si#UbnZAS&W zr-|3j%TrQ~9?$tLIL#YmX55Sh=%LoVDgsC4;+d@vJ4&uWeCbSEVJe#Sgm98ymK?b} zDP>JWhaFYDvFezYYby~iNb%d%Jw0F{vFs$LbQauTd;6gX_`_3{g+mSM8Pht91O@w| zfQp;+72wlAOss7(N5$dEih>WNaOwvDVJA~57p#t{x8hFJGTE7MmWvW;_`wE*+((XP z2F({8lFMJ!dWwkF(+M_J)~60?@j#fBsdzQ{kud}q{{soeaytgt1Wq)&aze1#7I{@^ zgzYWt1N+0!+<|?a%1!iu!;5?*1P0L#Qk~adSuW()HmLAd@;AW;(w;!KG#{%D0TEGu zMz{z@<$!J2$0lH0lA|;>oidemw^0BB%yjtQ5Jg>PfpBrh#td8#arvMX#Dhlr`zT_H zlSfY=<08z(I`0CkVI-+2?bsR|iH-t6bx6)qNx1;m8-zhXJ;;~mqz$?KW?^ycKtpY z?)Ch-9vK?>m#RV*J?bXNGbAd!i<(suaWAYDDGm#Ce+b_!{@SfWy>2$dHlOO9I$YYw zHMkkhG$_y*@!3J+sRGRy%5*Cz$ZkUDk`#Hl0Th%KJ?J+joC1+me17UI-jr=NoPBp<(XEQHEBq{M;u`5O?ZI1;kQ%;9DSB z?z=@8%%fOO>q;;P_PR|Z!}GN=#}=5fne8*_u>r>IR9jnlT6D5&LNB zb*=4JK&OPQ!HklPqvP_cB6o#r2>3vzjmXI{(4`6jd6OjcnM3?BtKS)kC~DgcExDV4 z+&GHuz3n0yn3(Z8drJ==&CoBbzeh1{OXmuR_9!^?uCLlD?4%5Ew_Md?eEx1U4^ogf z1*0StT|;3XC)YBqOKf7n)-7wo&n-=+uG6EcW*@MIRUM2Tx?+wIoUYVA-)DEf~t+Xu^5mM1KFfd`mz$Z!)1F|&~A1-gYjjatOaOU4arUB-3_l=fgB%lLA?ug~IWx^UZyWDz?Ynd!#E(!kXX&&DA7$VF))Q2b; z9Avp>9&EQ9epf;ca`OO|l()ry03)QhqvJ7wg0qK~6Q2Rb4EO5mJs`R=K5#o})7E*h?c&GXk z2{!oq4vz%DhmfgARoi;5vD@-s_QOVYey=;BX-2qiAEZK7s~=}H*A;$Tj2>4N18yL@ z=M|~d5-iX-XviS%(2>SsIf(DgXIyX#D?$@}{9Pp? zeF#0w3REg-5R9N?sX-F*+F?Yb6t&h%ds!4(w#eh!3;;`cL@+cGr}ovJ6smrf0-YC+ zN!1Q%OEwcD_-j#otxT;)y%sFEHPR3X-_++22(}b84Nygd0wz|XLEg}(4xs!g)oTh8 zvCM?Ji0_smGnhtOlZWpnb*$D3p+pJxn`^&nKoIf2v+c5V5T2z>lIygs)s0Tm&3bL= zL27hB;)Au-H@X*K6L{YQyn(OG)WD?#nCAZ1F z&`L3IIP>}wiDWIji$uhzW0Os8u7n(%u37ggP!JDnW4fgKg9#B+AuuRAG$Z|<(ZKum z=;M*d5!vlhDpjt_-uNCm5h^^0F8q~Wv&**F*{06uhLw86bJMYw2pmr{+p3*NuN}#U zQg7vE;$SvqZU6p>J^tkYz@(iC;WX)cpU7~88v19niJ-*~AB#;S^z?mMeZ$AAioyg5 z+z10UrNG2}?~?x1Va@vGL0g)b2zfBBimJO`KJe|#>nr_CNisIC1AS0|1WYuk0BeED zxfy%M2D^%UKXf*`!udxBt-q5c=tf^uCN@_hFv3L(XScNl%L(Mb#Jr+(CWw`Xx!;;Y zdOxliIFfchIs&b`LfaI8+6dPdUP<>c+;CD%xx4FDGqptS${ZmpP8cSvNSu<3`aEgB z_kcW}c`Mix#E=Q})b?2U`Zzt?dJC>OQQ<8!I{gAhc^~y4ZQS?!%84=G?Q6$C8Y}pi zGLVslrOz!_QB}YSE)Yh6hc9rVtb zWm+2az7>NM@iatw4^`{ypnuX_BoD_cC_*qLMk>-~f#(YyblDePT@Cg&@RHNWeyR+DIM^p_Y8c=U_l+P#)vP(8f$pD5S>AssHJcW$F zNL^XP;KU#o*$c>qDu;QTF$X-M9*w5lkCxUOg@VU-*5qIsG$4i0)w#hPx<(V@L1^M8yzIyH*&(lol^FiPy=J-N@U z9OY;30y!N}GJJO|ilcxHRT_+fLGaUl2bU0%08eIDs9V54(W@X_eK$#t{VZ|NIplV) zCLdCpj(|kkiB0v2`Wc?k&Y{;?p)F~;)Yyo@a_s6P+lG#8Jhil7*{&Z3c?VV3oQ7Fx zgM1C~eiG~(gM)IA5k?^~OtQxG3vXr#KA>55dFSgi&Z!?dz2}uUq$B8T1Xt*7v!I0- z3<8I6%roHv$IW^D`e*V{4+S)d@LSb2iRJ_j)*+z zTbu$!N_{jK~w^N#>1VuIK06{h#DJff*K8n20he*Ti?!a zGOTq9rRWdhZs!YwG;kK)6twCBpE&RN2i1HsySV2@2}iAA_cgcfhmH`pfi9l)MFF>o zfEkGPLnC);tMczVNL4A8ekI3tE&+$eZ?}Tfq~}95Lzj>EcyG;H<|q)v-jBA=E;kWD z8(X+_fO^qRmds*Zv#pA?_LDW=IcZ~A#O|}QcR)yuJFY0`w-EBGp`6*&^%gq-8K!(f z1(0a|+EzVktQS__|2!}v`t0jV@|KQA{}llp>q2-rqBOJ}MTtK1k}%PWQH>u^3*)YN z5woJFGdG!wU~I@IbnBuK96}PozguDaq$3*Fh#=odQcEB>GmJpaeHM8g4u6)ztGWLK zUYRLn)$r@&7&HUm_)s6YI4{yA{yB;resmfj96Z%r0_d(B5R1^B&=IhZgaoB^|ACGC z5>{AfQ=gXkq&9j%5EH7r6FM}22vdG4mUQL*c2X!=BnYNJy+5_uVLjYM42qVim+o6U znqJ_t0Zi5`HUq6pbA<-wH}rI4a!V*UD9(N`>sYDt6}vH@$HHevZn{kSpp<_Eu`zLNuW|s;f*Ejp{7>lA)?#E%&#o9#%4W_ zAMXvOd&2PVkAw(hD-55(@=&B>2;qBNAk7DXeSkyip%By~ZS$qBe|LVf^=4)>`Dt9@ zkA3DWb~HM-eq(RUA}OPbB!Uf0!kWI^@1d{P1a1%l=UaEyrmHe)#EAiWTK}Wf3visf zF5h@(mb;$X(lQ0%m&7<(xK8ibjs@{~1LRkMmPD^bAO=O^+}Flu3PMLdD#GTR-5X^| z5E+eXH#nn_9*C$hI4p9hsRd`=pN!$&y?p#ERYQo6r_=UVGH0Ly#3`Ubx8yjPk*xq> zzRGCxOZChu-)=$T0)==2$R!bogg8K>rE+@Wxa9Aqh5M7R@z$qarcXV2^##o1Z|yTJ z_R`(f{fDo~yJ955eav?#=;-Vshp_znsr)4QoSviH<5QG0kcK8}s9)mKWnzG4_6K$uK^|PSPbmzCfjriqPA0`dLP% zg@L~=TRzPJZ3t{n7C39VwMFLOJ({HqjKmuz8 zgz8Ff(9`m)WJuhQCXHC5}u z(6e$91aCf6?ttO96gli@WD|P*`bo+|n`Ps~Y1#SW$!FBSqnAN~D#@9hu%E6Ds*YCR zgkNB<*@nk|ka+;`9pT#a z6$?8JC}S&}O^6_am&tI2d#IN*AR1#_$&SuL`J2ABOvR7{Np+xfmKlgEECv^v+p)rG z(8w=@Z}~*rE846EL1IKFJY-LZbGnAvccr)&dyzyf$-bITG)^%0&q>(iQw&~lTz%pU&Ek2 z#HZ+U(U32qglpo`Qf|JEJ~<~UM2V(9I1s)Lnvr(#tPdWg_8}DGHysL%d510$}wkwd#Y`g0g)FR(Z%gt7`=Ed{@%4xg8l{`}*o%**AgRrK@{cha6;MJK8PobD zM?OZ6gK-y8#N#6Kb$*0S`K|t`4w*qZyM_i6a_By=^l$v$+J2QgelvSU2+>gDVZ8)} zl;nsD=)-=-O?lhN+5H32F^|(q;Xm&na~!vaaus6u=NZoN$@-&6A|DK`=Em$nDVgi<2uIz|UEhPS*{M{T4t0 zgW-f=nFWRHOdO~THYAX08EzCHME=QxhaWHHyn+^2dGPzAVs+_58g2uom1RKHsc36_ z#IhYqx+p6aLb0w9l>d65DjX<${ndePq=DaX;|EEfrX!(v>|&c(BsjG3{vY>=t+U35juW9pi%R{{3ZA0~JJVRwlsP63(z3#@IFMyxVlG z8|t#in#{_K+GmxhPjiQ(i4RhIT4}UXHCk9!L=We#+h+!VF zrZZ`xp~r771Su4z+Scm*fho@6Kn(~Vd5zD0lDV=?uN%@LBW(sLhPj(fg(^z zi2+unk3(kgQ{&wmYb1n=1crh76mSGyllCU!(Dy zh7Pa^N}!(HVI1`63&Lj>I?`fTYZou~JEg^koym&3X1O44QVCfXFi;9-NH*yXZzD%1 zC;V4wCHBo2wrghauhzRN|;+gJYiktf3V4{K?^B1b4pw4mc z@Xoib^kCqducdF5dHszAd4zKsz)sZuOqXlB1m8PU4Q<89Enumc96riDs@H)%LDYUu=-oeTj+ILZaAna`gVZ1j#ZB%uk_jETWj0gh_ zJ8w+NV#k14%W1dkyoQOdA6BsNL1c~#Q3z1D{j`GP;n^bLx2uu!Ls&|5qxJKBH2A&S zQTw^Ojp{6uhT+Py9@KPRe#V|bi3d6eVo#KT*j{H~%i>KQZDhBXj1uYyQYkZ*>TC*ly@_sU~U@xs4wc z1Zm&2)Hnqk{24J~eszl){)HrcT71YsvTv!{7{PvWeSAuMMl;5)roQZGd!T9`VMoTE zNj03y;MNUGVGF<#f7bvyk=HwZXsRkiNt%Cw`S`lFcI_=EtE%D9RJApE}|Ql=v&^Vs>xxejRb%DsE4gL24GKaPFBa{WboX!{DROz8Jbdi*zaM zV$2556!XELP4ss3HJjNvxv}&R=rIX<>9nR>C`N?CrG69oDpORIrVTlm`I?xYJfYRF z?KDW=Rt?Ktus|JFQ1rmGg{NCTeie!5AHZmY*R}$5pVPIcQOpf4m4ck{g>?@VJ%fN! znQzMP0;F!@wK1Im1MaMBu`TG`tsfu1HLF8C4ZsO^ zUUvaLYkn@u3lT&ytvyfa_4{1*lM@t*(I(ojnVs!0=c3(u!ZJI5z~@sRhohXHXQX&!UPEc@w?A70aN)P|gjVH=QKkY0cFLcqubGG$U)FMY?BzjUz zFL0MY$h9y{U=Y~lBuZRRL@?im_J+HT!0nn#^wEQf)b5Rc{@=$kPjA`WSb31xW!S3>XgvMd`pTIW7P&b6r1 z)S5<7YbZfIC9sl9j$kY!4ygtyBz!A5#~PgW+#?PtjjU~r9oj==;x9z*>n7av^Omf21!R91=`-V)bL8ysPtFA;axein!J8-g!@a01z!>kV zgPVulyCmBnmH+f?~G}D+WG@D2d zM4xNBsRbC=$s@1hKFV=XQH!y)EaVpQB+g>jmO|v-(CRBuLEs*OGQrQhX6Q1KR@O6~@cR3z+ky}A9U zPs=+6D)9=VI@6W&e4O(mFO?bW@rb>Sa4v$f8bh{{W-iK+OyocvcNgf0A>#(RgEpGB zc#|O!2YE~C*gM#2?0=7#E{#e2Ov0-`C#sE)qH_hdx);Db!3lTeo6w{!SG5_e;g-K3hk& zZz=v3_X2_tCEt(r6zOPaD7UHa0pqxVw4Doz2PHv#-vEGHkjyOroqQ6l{wPc|Az+ z+1MM+AS!w2>k3nVxuAxlP^FpFy3S=3b?(%G6yb-8-ffWp+aT!6FapRJCzn)Cez0D` zLh3oG7=UsULRXIqrB`nqm<$d|Pr;iyIlOS#IaS5i)LZ%_|L`alN+iD1u@sNfl-XC9 zTqfbsthA4#tU;nzlR5>!8R;?)B}E*TOwL6mN6zVpL+s4Q#c$YRK_(9Z};Aa}~Sfarl%pR`ga z_}iJxofrOGAR?F`EQc~>;NWo6TB)z)(-FWFTIDJQL!M270SBIyB-Br9)ORa=Q}Tef|xEL-|qd$%E0lqr?;b*i|gL{YJV`X z%ecuYNnv*&2)KrvIw)w9R8S~~L-*@E^}4N15??T&fl&f;T*z8*tSflrsj_chLyD(@4H?Q10TQTtU<9DrMh?#Z3; zEFTF?!+n?iAu({y0L5psV?{U^zsuOpir{)fgyyPY?F{P8WGp}^)twyy@o@nn3gy|} zY~0Bt*%DLsivyg#z~JM8R%8rfe#Ue&Rl;XghJRc<5jbE^538$9#1-jBr z3{lS#TwDWcHH(4fd(m3qWt)UTiF9hb0}g#{fVXoadhDaUh8lO#VX_N{N#(^N#ay7g zb-5xSpU2I!LClIH?Ka`CXItagf!gCsC&At1{#z}lik2TWD z8D87Is2gpGt%qN1yxU+R6T7H*?qw4Jg)Zd^-ri)~`Aig3d+J)b@J+a;?`Z(!$(G> zg2P#6f2h(!d~40Dm+vu59f;Z~pNG`E_--rgt_su`&4<8N{Dtu2~FQgkS&wnD_ty$p23E zCovaBaU3*BQd5A|MnLh}#FdP!CYN z4z-a+FRkkwHA3p`U_>WWWkC6P4(SbIC~V4Z*~H5Ii=|#ZSUBF(j;EAPuwSIODZ=sC z2Mj+Uu>mtJ!pE&t>9E-RGN132zN~%-gzMLLqX4=%eAtn?dW3$(-l$lWtalBz{BzEj z3aWN7-0=@bOWA8UEJ~H9qChSD#>E<&sU{XCS}^nOt;_pUrwU4t4!S2Dz0el0bI+Fe z@BOo8L7}=66-{Uq?WqS$CS7aiT&E^dN=1#Sl;^9OR#gh6jVzHyS0x<_r`0Kxv0@rp zr7%CLYrg0yY4lmR)X7Myc=Z}N&Bs@0nTuefT@whgCKnYMk`OCKqhYRz>A?IY((tIU z2w7uBFgpCtcI-(gaxHqXL9A-(Io0TgUPuPz`JBeH()hbPo^ifDXZzPt zNkiSaJRXiWPw2?EXtCZo&o5#0pU)S}caOK1di?M0fdnpn`22n!uXcJW1Szp!rGgsx z^?1DQ&(ZzfpXP(Nklc&+!dOU&j$I37A1#ocQC|i`z%LBE&6T&b z)PH}Fgsy*8D(Lc0mf0|=y;>$mRG{z}f`?$d##7Xcyo7TBP4oUfEgadNo(arcOl)Gh zQAkKLJ#S9@-IG#?8(uz*xhT-aQ9V>BMBfxeN}V#3{aR#a+L{$E1FkDbMW4mlk7`|U zyChKb5pu8m5|J{d*vn9p>6tB*&1BLamkoAIzThYT(G%S1hj7cYAbpR4b#s zJ3aptVNA0(sjEaSRx^V7m?F#BWrBA9?%J9*$u8ZpE~LnHXCkS?j9A}3deC%R^}TF4 z#bq%EDreGy%Z9f{UJHU;&uV@Ns{%A$G#!y%BU(n{r~0MW?-N#^W1us{xy}QtuIG>4 z)9jhls3@-q1h+MLFB(mZ!lKxw3l<#cHjWE*BaYBsBa9%K3>J#KE zE~4WT*0R;-`doF1rWliWB(uv%dQYN>gwQ#uKu3jKjE?EbW|p=4&kZy$i{24;8EqBT zl9<~T@51n9_)cOLL*%-!-XPUXPrQ^`8if296#L!N&V! z3CQF8T4LN2u~ikm&IY*&==+hjJIbwR9S&snu9XI8aL@pS#6H|8XjFT19S4Dp0RINj zO2ji^d9DbHa8kW0a<(Ml>Wvb``imoQ%eWT_dGiD+gy6Hv@x8hI1?&S4dFHA#VNZH z%Z`rHMui>UevdKP=FXTQ`+LC=@G}7ARvB(`_-8z0FsfK22*-OVZt6utN084BE zLGgeY`0Msy?GlvF-+;D{3gbj40cVo%XLTV3fdz{krbJ?-uDzg8Urs;L|wcsQEr!4 z2H!}Aq_%ZGqVS5_t4SXZy8r2T{_-fG-2S?*KmY*1UtR_FFOTBn>|t%<^oK7gN!GE; zVn7MKe59t?QUZphiSW0yh*Yk~`bsV7dg?Fv{RJ3vF5%njB{vunlFMr8!{|^=T;A;q zbK7yT0R1?NcGcI$2A`M=YWu!r+HOtOdX~|=dZiRqqah_LYgv>;{zmDk!sNK3(`X_wUovHIWj!7#l~Ux`4??mAjx`=PrV1 z!Dz^kwRXgyh}ZoFI~s}>CfLeHsv65a`P^Y+W+mNK>#&>-!=s5p&^GZQisK_7&Qxt2 z`RG)NI9vtnePDu)%aOWoj1YqS;xk5lw5Z-zt;i$t1$J25j)by5gm@q{fUs>d6gZuy zG3?-2(-rJl!DR9tZsYOgmzUUTo~0}ePtT3zuq{BBgDY!2V{1}`OIdk-HH*_ zaT(z+cojmwONv~VEPaad!ZsAjw;2oheF7gEpy8UdVkM+@XiRK1v#`0XW zVQs-PhyHE4``zWia_ic3C=4v#gqUC~Itq7!|C?sy6j#u@?`O|L+oLGPYhGc`?FcO>JN zpAB38oS^@AWMph-B=2Zv@AO}hkrB*VLI5BD05LEC0K(Ti{uh~nz5O3kIVVxjZjAvZ z9_Mv*r zGuk+aUbJplG;}j~^hJkKxA_sBUqlS%ClESM>aIVD?HzklY(f7p8l^O3uG9>Jn>6JJ z%Q6t{>~fhTt|4^xXlqizkLunIeLl@Q8v5He&E4D?HFX_lrF;%DIo zy5ANqYc7*m+jmJXBbgW!^V#p~*v?XuVN{?t)7~Ou;5pQs^b3Rj{VGt4tQPvCVwvPd!^O2fp zD=0}`0VRZ>3AKz!g{oY|?njruw4v6+YO>bPEDJhLJ)L!;@^t4rp96Bic+{+ zf+Oq0)XC)RtEn)&jarr%TMe|+B#~r|AjW}&MKK2EVB)h{P<-l{LA08{y_6{OfkNVY z3NllgAX@ENptMc}G^C4Qwo-EY`FvcQ5LJ~}DtoFYuW`piH@~ZKS(7U@P{9+;0?XEc zgl%q{R;hI=qILJN@JP|(TaNke61cs6fQRUK0q~*i*QJ6!WN|%Gt`3)lbu6f}&1&SG zC_`%(nlH+kZ}Row(mvL)A$RNe9$(T%Up8Es{UP3DXKjPX4}9M4IyK{YA`&GrHo``n zQF)86Nn(B0P71uaG9bQB1sfdT@fyVR6_&8j^jx z-I7hMN}AaRAfz&b9v#9Y)i0?^-ax&3J@vU0k(B9E72<{lOr6L;9G!(Ggnwkv)}e;1k!=RE zZWjPDPmuSqPL2bf81}Lk3-&n`pT&#|ub!pK@uDCVJ2=g85wvD?_1TRB#%y6XFEX~D zS+IyMuNuzs^%&&nZq%l;D%m+?av|#cL@2E)q?{otZIL1^hl*rv;_mAfFn^b>V&R&a zvzF3$eniR=peh%wnnSO|I(jO-)fnU?Rc8`h25~{SiQb`WQA4juy*q6`nG8Qi^>Ibq z{PYjL5EKo$6+mt4vog?`;DW+tP&AKV;+iu;jQnD&=V)U6 zhu!>7p*MMwtH)my(qGDl{wL(m>yLk7=>9XI6KNh)v zwLbtme*ymQ4hECdss7Qywl5;Ae}MkFKzxb)lJghr-)HRqv%hfv;`4`i%CGVM;`0Xw zCvy`M=l>lf%L$7aU?c#5kU#(cjDKPOxKa5Z?4Kf@KX(oOQ^)_kZ}7M5+JA@r zy^HX--2XdF{F?z#(_cORSJ3!3^#9f6qLzR2(|^PMlK9^a{cnk*{|@_yd;f3rfBToe v(H?F8CiI`)=D*?pZ4m#4gLnLEs{GSD%1eX(`Cmi$3OokCW@Jp~ALRcZ6OdKc literal 0 HcmV?d00001