diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx index 12c210953..be19842f1 100644 --- a/src/components/Claim/Link/views/BankFlowManager.view.tsx +++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx @@ -15,7 +15,7 @@ import useClaimLink from '../../useClaimLink' import { type AddBankAccountPayload } from '@/app/actions/types/users.types' import { useAuth } from '@/context/authContext' import { type TCreateOfframpRequest, type TCreateOfframpResponse } from '@/services/services.types' -import { getOfframpCurrencyConfig } from '@/utils/bridge.utils' +import { getOfframpConfigFromAccount } from '@/utils/bridge.utils' import { getBridgeChainName, getBridgeTokenName } from '@/utils/bridge-accounts.utils' import { generateKeysFromString, getParamsFromLink } from '@/utils/peanut-link.utils' import { getContractAddress } from '@/utils/peanut-claim.utils' @@ -239,7 +239,13 @@ export const BankFlowManager = (props: IClaimScreenProps) => { const externalAccountId = (account.bridgeAccountId ?? account.id) as string - const destination = getOfframpCurrencyConfig(account.country ?? selectedCountry!.id) + // Derive destination currency + rail from the SELECTED ACCOUNT's + // type, not from `selectedCountry`. Pairing a GB/GBP account with + // a SEPA destination is semantically impossible — Bridge rejects + // with "country is not supported for SEPA" (PEANUT-API-5P/5M/5N + // on 2026-06-02). The account's `type` already carries the right + // answer for every Bridge destination we support. + const destination = getOfframpConfigFromAccount(account) // handle offramp request creation const offrampRequestParams: TCreateOfframpRequest = { diff --git a/src/utils/__tests__/bridge.utils.test.ts b/src/utils/__tests__/bridge.utils.test.ts index 326894405..ec128da0e 100644 --- a/src/utils/__tests__/bridge.utils.test.ts +++ b/src/utils/__tests__/bridge.utils.test.ts @@ -2,6 +2,7 @@ import { BRIDGE_DEVELOPER_FEE_RATE } from '@/constants/payment.consts' import { applyBridgeCrossCurrencyFee, getCurrencyConfig, + getOfframpConfigFromAccount, getOfframpCurrencyConfig, getPaymentRailDisplayName, getMinimumAmount, @@ -142,6 +143,70 @@ describe('bridge.utils', () => { paymentRail: 'sepa', }) }) + }) + + describe('getOfframpConfigFromAccount (PEANUT-API-5P/5M/5N regression)', () => { + // The 2026-06-02 21:24 incident: a GB/GBP account got paired with + // EUR/SEPA because the picker used `account.country ?? selectedCountry` + // and the user-selected country fell through the "everything else → + // EUR/SEPA" default. These cases lock in the behavior that the account + // type alone is enough to pick the right rail. + it('GB account → GBP / faster_payments (was EUR/SEPA in the incident)', () => { + expect(getOfframpConfigFromAccount({ type: 'gb' })).toEqual({ + currency: 'gbp', + paymentRail: 'faster_payments', + }) + }) + + it('US account → USD / ach', () => { + expect(getOfframpConfigFromAccount({ type: 'us' })).toEqual({ + currency: 'usd', + paymentRail: 'ach', + }) + }) + + it('CLABE account → MXN / spei', () => { + expect(getOfframpConfigFromAccount({ type: 'clabe' })).toEqual({ + currency: 'mxn', + paymentRail: 'spei', + }) + }) + + it('IBAN account → EUR / sepa', () => { + expect(getOfframpConfigFromAccount({ type: 'iban' })).toEqual({ + currency: 'eur', + paymentRail: 'sepa', + }) + }) + + it('accepts BE Prisma-shape suffixes like BANK_IBAN / BANK_ACH_GB', () => { + expect(getOfframpConfigFromAccount({ type: 'BANK_IBAN' })).toEqual({ + currency: 'eur', + paymentRail: 'sepa', + }) + expect(getOfframpConfigFromAccount({ type: 'bank_account_gb' })).toEqual({ + currency: 'gbp', + paymentRail: 'faster_payments', + }) + }) + + it('throws on Manteca account type — must use the Manteca offramp path', () => { + expect(() => getOfframpConfigFromAccount({ type: 'manteca' })).toThrow( + 'Manteca accounts route through a separate offramp path' + ) + }) + + it('falls back to country-based picking when type is missing', () => { + expect(getOfframpConfigFromAccount({ country: 'US' })).toEqual({ + currency: 'usd', + paymentRail: 'ach', + }) + // Unknown country → EU/SEPA default, mirrors prior behavior + expect(getOfframpConfigFromAccount({ country: 'XYZ' })).toEqual({ + currency: 'eur', + paymentRail: 'sepa', + }) + }) describe('getMinimumAmount', () => { it('should return 50 for Mexico', () => { diff --git a/src/utils/bridge.utils.ts b/src/utils/bridge.utils.ts index 588e80a2e..aaa3d3655 100644 --- a/src/utils/bridge.utils.ts +++ b/src/utils/bridge.utils.ts @@ -81,6 +81,37 @@ export const getOfframpCurrencyConfig = (countryId: string): CurrencyConfig => { return getCurrencyConfig(countryId, 'offramp') } +/** + * Derive the offramp destination currency + payment rail from the bank + * account's actual `type`, falling back to country only when the type is + * unknown. + * + * Why: `getOfframpCurrencyConfig(country)` defaults *any* unknown country to + * EUR+SEPA. Pairing that default with a GBP/UK account caused Bridge to 400 + * with "country is not supported for SEPA" (PEANUT-API-5P/5M/5N, 2026-06-02 + * 21:24 UTC) — Bridge can't SEPA-credit a GBP account. The bank account's + * `type` already carries the right answer for every Bridge destination we + * support (`us`/`gb`/`clabe`/`iban`), so derive from it directly. + * + * Manteca-type accounts use a non-Bridge rail; the caller must NOT route + * those through this helper. We throw rather than silently misclassify. + */ +export const getOfframpConfigFromAccount = (account: { + type?: string | AccountType | null + country?: string | null +}): CurrencyConfig => { + const t = account.type?.toString().toLowerCase() + if (t === AccountType.US || t?.endsWith('ach') || t?.endsWith('us')) return getCurrencyConfig('US', 'offramp') + if (t === AccountType.GB || t?.endsWith('gb')) return getCurrencyConfig('GB', 'offramp') + if (t === AccountType.CLABE || t?.endsWith('clabe')) return getCurrencyConfig('MX', 'offramp') + if (t === AccountType.IBAN || t?.endsWith('iban')) return getCurrencyConfig('EU', 'offramp') + if (t === AccountType.MANTECA || t?.endsWith('manteca')) { + throw new Error('Manteca accounts route through a separate offramp path, not Bridge.') + } + // type missing / unknown — fall back to country, preserving prior behavior. + return getOfframpCurrencyConfig(account.country ?? 'EU') +} + /** * Map ISO fiat currency → Bridge bank AccountType. Used by onramp quote * + exchange-rate callers that only have the currency string in hand.