Skip to content

Feat/offramp multi country - #356

Merged
victorisiguzoruzoma874 merged 2 commits into
kellymusk:mainfrom
Olisachukwuma1:feat/offramp-multi-country
Jul 29, 2026
Merged

Feat/offramp multi country#356
victorisiguzoruzoma874 merged 2 commits into
kellymusk:mainfrom
Olisachukwuma1:feat/offramp-multi-country

Conversation

@Olisachukwuma1

Copy link
Copy Markdown
Contributor

Stacked on #1. Base is feat/aml-compliance-controls so this diff shows only the offramp work. Merge #1 first, then this retargets to main automatically.

Problem

The offramp bank details form assumed a Nigerian customer end to end:

  • a fixed 10-digit NUBAN field (accountNumber.length !== 10)
  • a hardcoded NIGERIAN_BANKS list imported straight into the picker
  • baked into the KYC message the customer signs
  • the review step reading bank details from a Nigerian mock order regardless of input

The platform prices withdrawals in five currencies (NGN, KES, GHS, ZAR, UGX) and /api/withdrawals already screens against five jurisdictions — but a customer who picked KES on the calculator hit a Nigerian bank list on the next screen and could not complete a withdrawal.

Approach

A country registry, lib/offramp/countries.ts, is the single source of truth for each market: currency, bank directory source, whether account name resolution exists, and the account number rule (label, placeholder, length bounds, charset). The form, the validation and both API routes read from it — nothing hardcodes Nigeria any more.

The form preselects the country matching the currency chosen on the calculator, and warns when the customer changes it to one that settles in a different currency.

New

File Purpose
lib/offramp/countries.ts Country registry, validateAccountNumber, sanitizeAccountNumber
lib/offramp/bank-directory.ts Shared Bank shape, Paystack fetch, static fallback
app/api/offramp/banks/route.ts GET ?country=XX — Paystack directory proxy, cached 24h
app/api/offramp/resolve-account/route.ts POST — account name resolution
components/offramp/country-select.tsx Payout country picker

Bank lists are proxied rather than fetched from the browser: Paystack's directory needs the secret key, and caching server-side keeps every form load off Paystack. Account resolution is a POST because the account number is PII and query strings land in access logs and browser history; it is never logged.

Two deliberate constraints

Only Nigeria ships a static offline bank list. A wrong bank code routes money to the wrong institution. Nigeria's list is pre-existing reviewed NIBSS data, so it is safe as a fallback — but we hold no verified code table for GH/KE/ZA/UG. Rather than substitute a guessed list, the form asks the customer to type their bank name when Paystack is unavailable. Uganda takes this path always, since Paystack publishes no Ugandan directory.

Paystack cannot resolve account names in KE, ZA or UG. Those markets get a manual name field with an explicit "we can't verify this" warning instead of a dead end. A country that normally resolves falls back the same way if the lookup reports itself unavailable, so a gateway outage degrades the flow rather than blocking it. Without PAYSTACK_SECRET_KEY the dev mock is preserved; in production the route reports the lookup unavailable rather than returning an invented name — that name is the one thing the customer uses to check where their money is going.

Scope

Five markets: the ones that already have a FiatCurrency, a withdrawal limit in getMinMax, and a compliance jurisdiction. The other seven mobile-money markets in lib/payments/regions.ts have no offramp currency, rate or limit yet, so a bank form for them would have nothing to settle in — extending to those means adding currencies first.

Also fixed

The review step read bankDetails from MOCK_ORDER, so a Kenyan customer who completed the form still saw "Access Bank / 0123456789 / NGN". The chosen account is now carried through. Legacy saved accounts in localStorage predate the country field and are migrated to NG rather than dropped.

Testing

54 new tests, all passing:

  • countries.test.ts — per-country format rules, sanitisation bounds, currency coverage
  • bank-directory.test.ts — Paystack normalisation, de-duplication, fallback behaviour
  • bank-service.test.ts — saved-account migration, cross-country identity, resolution error paths
  • bank-account-form.test.tsx — country switching, per-country validation, resolved vs manual name, the no-directory path

npx eslint components/offramp lib/offramp app/api/offramp is clean. No type errors in any changed file.

Reviewer notes

  • The Paystack calls are unverified against a live account — I had no key to test with. The route contracts and every fallback are tested; the real API response shapes are not.
  • CI will show 4 pre-existing failures in mpesa, mtn-momo, withdrawalLimitService and helpcenter (the last is written for vitest but collected by jest). All four fail identically on a clean tree without this branch — confirmed via git stash. None touch the offramp. components/dashboard/transaction-history.tsx also has pre-existing JSX syntax errors that fail npm run type-check and next build.
  • Nothing in the offramp UI submits to /api/withdrawals yet. Passing the form's country through as jurisdiction belongs with whoever replaces MOCK_ORDER.

🤖 Generated with Claude Code

closes #330

… Uganda

The offramp bank form assumed a Nigerian customer: a fixed 10-digit NUBAN
field, a hardcoded list of Nigerian banks and a naira sign baked into the KYC
message. Customers in the other four markets the platform prices withdrawals
in could not complete a withdrawal at all.

Adds a country registry (lib/offramp/countries.ts) as the single source of
truth for each market's currency, account number format and bank directory,
and drives the form, validation and API routes from it. The form preselects
the country matching the currency chosen on the calculator, and warns when the
two diverge.

Bank lists come from Paystack via a server-side proxy so the secret key stays
on the server and the directory is cached rather than refetched per form load.
Account resolution is a POST so the account number does not land in access
logs, and it is never logged.

Two deliberate constraints:

- Only Nigeria ships a static offline bank list. A wrong bank code routes
  money to the wrong institution, and we hold no verified code table for the
  other markets, so where Paystack is unavailable the customer types their
  bank name instead of picking from a list we guessed.

- Paystack cannot resolve account names in KE, ZA or UG. Those markets get a
  manual name field with an explicit warning that the name is unverified,
  rather than a dead end. A country that normally resolves falls back the same
  way if the lookup reports itself unavailable.

Scope is the five markets that already have a FiatCurrency, a withdrawal limit
and a compliance jurisdiction. The remaining mobile-money markets in
lib/payments/regions.ts have no offramp currency to settle in yet.

Also carries the chosen account through to the review step, which previously
read bank details from the Nigerian mock order regardless of what the customer
entered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

@Olisachukwuma1 is attempting to deploy a commit to the kelly musk's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jul 26, 2026

Copy link
Copy Markdown

@Olisachukwuma1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@victorisiguzoruzoma874
victorisiguzoruzoma874 merged commit 5272af4 into kellymusk:main Jul 29, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add multi-country bank support in offramp — currently Nigeria-only

2 participants