Feat/offramp multi country - #356
Merged
victorisiguzoruzoma874 merged 2 commits intoJul 29, 2026
Merged
Conversation
… 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>
|
@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. |
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The offramp bank details form assumed a Nigerian customer end to end:
accountNumber.length !== 10)NIGERIAN_BANKSlist imported straight into the picker₦baked into the KYC message the customer signsThe platform prices withdrawals in five currencies (
NGN,KES,GHS,ZAR,UGX) and/api/withdrawalsalready 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
lib/offramp/countries.tsvalidateAccountNumber,sanitizeAccountNumberlib/offramp/bank-directory.tsBankshape, Paystack fetch, static fallbackapp/api/offramp/banks/route.tsGET ?country=XX— Paystack directory proxy, cached 24happ/api/offramp/resolve-account/route.tsPOST— account name resolutioncomponents/offramp/country-select.tsxBank 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
POSTbecause 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_KEYthe 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 ingetMinMax, and a compliance jurisdiction. The other seven mobile-money markets inlib/payments/regions.tshave 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
bankDetailsfromMOCK_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 inlocalStoragepredate thecountryfield and are migrated toNGrather than dropped.Testing
54 new tests, all passing:
countries.test.ts— per-country format rules, sanitisation bounds, currency coveragebank-directory.test.ts— Paystack normalisation, de-duplication, fallback behaviourbank-service.test.ts— saved-account migration, cross-country identity, resolution error pathsbank-account-form.test.tsx— country switching, per-country validation, resolved vs manual name, the no-directory pathnpx eslint components/offramp lib/offramp app/api/offrampis clean. No type errors in any changed file.Reviewer notes
mpesa,mtn-momo,withdrawalLimitServiceandhelpcenter(the last is written for vitest but collected by jest). All four fail identically on a clean tree without this branch — confirmed viagit stash. None touch the offramp.components/dashboard/transaction-history.tsxalso has pre-existing JSX syntax errors that failnpm run type-checkandnext build./api/withdrawalsyet. Passing the form's country through asjurisdictionbelongs with whoever replacesMOCK_ORDER.🤖 Generated with Claude Code
closes #330