Skip to content
GitLab
Projects Groups Topics Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Register
  • Sign in
  • A Ahad Mobile
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributor statistics
    • Graph
    • Compare revisions
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 0
    • Merge requests 0
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Terraform modules
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • holatech
  • Ahad Mobile
  • Merge requests
  • !115

fix(fiat-payout): take the bank holder address from the form, not from KYC

  • Review changes

  • Download
  • Patches
  • Plain diff
Merged Khanh Pham requested to merge fix/bank-beneficiary-address into develop Oct 09, 2026
  • Overview 0
  • Commits 4
  • Pipelines 1
  • Changes 21

Why

Adding a bank account for withdrawal failed on staging (2026-10-09, user de6bc4de). The app said "The bank rejected these account details", but the bank details were fine. Airwallex answered:

POST /api/v1/beneficiaries/create → 400 validation_failed
  beneficiary.address.postcode · beneficiary.address.street_address · beneficiary.address.city

BankBeneficiariesService.create built the beneficiary address from kyc_info (loadVerifiedName). Our Sumsub level id-and-liveness does not collect an address, so line1, city and postal_code are empty for 9 of 13 KYC-completed staging users. Every 400 was mapped to BENEFICIARY_REJECTED, so the user got a wrong message and no way to fix it. Same class of defect as !114 (merged): KYC data used for a purpose KYC never collected.

What changes

  • The add-bank form asks for the account holder's address: street, apartment (optional), city, state, postcode, country. The app validates as the user types; Save stays disabled until the form is valid. The address is prefilled once from the user's last card application (GET /keysecure/cardholder → profile.address) when it exists, never from KYC.
  • CreateBankBeneficiaryDto gains a required address object (printable ASCII; ZIP / ZIP+4 and one of the 51 state codes for USA; 3–10 char postcode and an optional free-text state elsewhere; country as ISO alpha-3). The top-level state field is gone. The server sends exactly what the user typed to Airwallex (line1, line2 joined, alpha-3 → alpha-2).
  • loadVerifiedName still gates every payout call on a completed KYC and supplies the names; its address part is removed.
  • ADDRESS_INVALID (422): when Airwallex answers validation_failed and every readable error source is under beneficiary.address., the API returns the field names to fix (line1, city, postalCode, state, country). The app shows "Check the address: …", marks those inputs and keeps the values so the user edits and saves again. The state input appears for a non-US address once the bank asks for it. Other 400s stay BENEFICIARY_REJECTED. Nothing from the Airwallex body beyond the source names is logged or returned.
  • No migration: the address lives at Airwallex only.
  • The orval model for the new DTO was hand-edited (no staging BE had it yet). Run pnpm generate-api after this BE is on staging and commit the result if it differs.

Release order (important)

An old app build sends top-level state and no address; the new BE answers 400 without a code and the old app shows "Something went wrong" on every add-bank attempt. Existing beneficiaries and payouts are unaffected.

  • Staging: deploy BE, then publish the FE OTA.
  • Production: no OTA, so BE and the FE build must ship together.

Not in this MR

AirwallexClientService.describeBody (pre-existing) logs 4xx bodies with digit runs masked; an echoed address can still land in logs. Separate ticket.

Verification

  • be: yarn jest src/modules/fiat-payout src/modules/keysecure-cardholder passes (410 tests); yarn build clean. New tests mutation-checked.
  • fe: pnpm jest src/api/fiat-payout src/app src/components/withdraw passes; pnpm type-check and pnpm lint:translations clean. New tests mutation-checked; two tests that could not fail were fixed to wait for the passcode sheet.
  • Opus gate: PASS after 2 rounds, 0 blocking. Round-1 should-fixes: non-US state collection/visibility (fixed), old-build compatibility (release order above).
  • The Airwallex error shape used for the mapping is copied from the live staging log line of 2026-10-09 10:24:41 UTC.

🤖 Generated with Claude Code

Assignee
Assign to
Reviewers
Request review from
Time tracking
Source branch: fix/bank-beneficiary-address