diff --git a/src/subdomains/core/buy-crypto/routes/buy/buy.service.ts b/src/subdomains/core/buy-crypto/routes/buy/buy.service.ts index 6e4d01ca14..2a653d1ee4 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.service.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.service.ts @@ -384,8 +384,8 @@ export class BuyService { // user-level vIBAN let virtualIban = await this.virtualIbanService.getActiveForUserAndCurrency(selector.userData, selector.currency); - // EUR/CHF: create vIBAN for KYC 50+ - if (!virtualIban && ['EUR', 'CHF'].includes(selector.currency) && selector.userData.kycLevel >= KycLevel.LEVEL_50) { + // CHF: create vIBAN for KYC 50+ + if (!virtualIban && VirtualIbanService.isUserEligible(selector.currency, selector.userData)) { virtualIban = await this.virtualIbanService.createForUser(selector.userData, selector.currency).catch(() => null); } diff --git a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts index c614eb0ac0..65df3d27f1 100644 --- a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts +++ b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts @@ -10,10 +10,18 @@ import { TestSharedModule } from 'src/shared/utils/test.shared.module'; import { TestUtil } from 'src/shared/utils/test.util'; import { BuyCryptoService } from 'src/subdomains/core/buy-crypto/process/services/buy-crypto.service'; import { BuyService } from 'src/subdomains/core/buy-crypto/routes/buy/buy.service'; -import { createDefaultUserData } from 'src/subdomains/generic/user/models/user-data/__mocks__/user-data.entity.mock'; +import { + createCustomUserData, + createDefaultUserData, +} from 'src/subdomains/generic/user/models/user-data/__mocks__/user-data.entity.mock'; +import { KycLevel } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { WalletService } from 'src/subdomains/generic/user/models/wallet/wallet.service'; import { createDefaultBankTx } from 'src/subdomains/supporting/bank-tx/bank-tx/__mocks__/bank-tx.entity.mock'; +import { olkyEUR, yapealEUR } from 'src/subdomains/supporting/bank/bank/__mocks__/bank.entity.mock'; +import { BankService } from 'src/subdomains/supporting/bank/bank/bank.service'; import { CardBankName, IbanBankName } from 'src/subdomains/supporting/bank/bank/dto/bank.dto'; +import { createCustomVirtualIban } from 'src/subdomains/supporting/bank/virtual-iban/__mocks__/virtual-iban.entity.mock'; +import { VirtualIbanService } from 'src/subdomains/supporting/bank/virtual-iban/virtual-iban.service'; import { createDefaultCheckoutTx } from 'src/subdomains/supporting/fiat-payin/__mocks__/checkout-tx.entity.mock'; import { createDefaultCryptoInput } from 'src/subdomains/supporting/payin/entities/__mocks__/crypto-input.entity.mock'; import { @@ -21,6 +29,7 @@ import { createCustomChargebackFeeInfo, } from 'src/subdomains/supporting/payment/__mocks__/fee.dto.mock'; import { createCustomTransaction } from 'src/subdomains/supporting/payment/__mocks__/transaction.entity.mock'; +import { FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; import { TransactionSpecificationRepository } from 'src/subdomains/supporting/payment/repositories/transaction-specification.repository'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; @@ -46,6 +55,8 @@ describe('TransactionHelper', () => { let buyService: BuyService; let assetService: AssetService; let countryService: CountryService; + let bankService: BankService; + let virtualIbanService: VirtualIbanService; beforeEach(async () => { specRepo = createMock(); @@ -60,6 +71,8 @@ describe('TransactionHelper', () => { buyService = createMock(); assetService = createMock(); countryService = createMock(); + bankService = createMock(); + virtualIbanService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [TestSharedModule], @@ -77,6 +90,8 @@ describe('TransactionHelper', () => { { provide: BuyService, useValue: buyService }, { provide: AssetService, useValue: assetService }, { provide: CountryService, useValue: countryService }, + { provide: BankService, useValue: bankService }, + { provide: VirtualIbanService, useValue: virtualIbanService }, TestUtil.provideConfig(), ], }).compile(); @@ -237,4 +252,51 @@ describe('TransactionHelper', () => { refundTarget: undefined, }); }); + describe('getBankIn', () => { + const eur = createCustomFiat({ name: 'EUR' }); + + it('should return the deposit bank for bank transfers', async () => { + jest.spyOn(virtualIbanService, 'getActiveForUserAndCurrency').mockResolvedValue(null); + jest.spyOn(bankService, 'getBank').mockResolvedValue(olkyEUR); + + await expect( + txHelper['getBankIn'](eur, FiatPaymentMethod.BANK, createCustomUserData({ kycLevel: KycLevel.LEVEL_30 })), + ).resolves.toBe(IbanBankName.OLKY); + }); + + it('should return the vIBAN bank for users with an active vIBAN', async () => { + jest + .spyOn(virtualIbanService, 'getActiveForUserAndCurrency') + .mockResolvedValue(createCustomVirtualIban({ bank: yapealEUR })); + + await expect( + txHelper['getBankIn'](eur, FiatPaymentMethod.BANK, createCustomUserData({ kycLevel: KycLevel.LEVEL_50 })), + ).resolves.toBe(IbanBankName.YAPEAL); + }); + + it('should return the deposit bank for users without an active vIBAN', async () => { + jest.spyOn(virtualIbanService, 'getActiveForUserAndCurrency').mockResolvedValue(null); + jest.spyOn(bankService, 'getBank').mockResolvedValue(olkyEUR); + + await expect( + txHelper['getBankIn'](eur, FiatPaymentMethod.BANK, createCustomUserData({ kycLevel: KycLevel.LEVEL_50 })), + ).resolves.toBe(IbanBankName.OLKY); + }); + + it('should return the instant bank for instant transfers', async () => { + jest.spyOn(bankService, 'getBank').mockResolvedValue(olkyEUR); + + await expect(txHelper['getBankIn'](eur, FiatPaymentMethod.INSTANT, undefined)).resolves.toBe(IbanBankName.OLKY); + }); + + it('should return the default bank for card payments', async () => { + await expect(txHelper['getBankIn'](eur, FiatPaymentMethod.CARD, undefined)).resolves.toBe(CardBankName.CHECKOUT); + }); + + it('should fall back to the default bank if no deposit bank is found', async () => { + jest.spyOn(bankService, 'getBank').mockResolvedValue(undefined); + + await expect(txHelper['getBankIn'](eur, FiatPaymentMethod.BANK, undefined)).resolves.toBe(IbanBankName.YAPEAL); + }); + }); }); diff --git a/src/subdomains/supporting/bank/bank/bank.service.ts b/src/subdomains/supporting/bank/bank/bank.service.ts index 47460d8bca..0add97c4fa 100644 --- a/src/subdomains/supporting/bank/bank/bank.service.ts +++ b/src/subdomains/supporting/bank/bank/bank.service.ts @@ -8,7 +8,7 @@ import { BankRepository } from './bank.repository'; import { IbanBankName } from './dto/bank.dto'; export interface BankSelectorInput { - amount: number; + amount?: number; currency: string; paymentMethod: FiatPaymentMethod; userData?: UserData; diff --git a/src/subdomains/supporting/bank/virtual-iban/virtual-iban.service.ts b/src/subdomains/supporting/bank/virtual-iban/virtual-iban.service.ts index bdebbd97ae..7c8ca82ab6 100644 --- a/src/subdomains/supporting/bank/virtual-iban/virtual-iban.service.ts +++ b/src/subdomains/supporting/bank/virtual-iban/virtual-iban.service.ts @@ -3,6 +3,7 @@ import { YapealService } from 'src/integration/bank/services/yapeal.service'; import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { Buy } from 'src/subdomains/core/buy-crypto/routes/buy/buy.entity'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; +import { KycLevel } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { BankService } from '../bank/bank.service'; import { IbanBankName } from '../bank/dto/bank.dto'; import { VirtualIban, VirtualIbanStatus } from './virtual-iban.entity'; @@ -10,6 +11,13 @@ import { VirtualIbanRepository } from './virtual-iban.repository'; @Injectable() export class VirtualIbanService { + static readonly bankName = IbanBankName.YAPEAL; + static readonly currencies = ['CHF']; + + static isUserEligible(currencyName: string, userData: UserData): boolean { + return VirtualIbanService.currencies.includes(currencyName) && userData.kycLevel >= KycLevel.LEVEL_50; + } + constructor( private readonly virtualIbanRepo: VirtualIbanRepository, private readonly bankService: BankService, @@ -44,7 +52,7 @@ export class VirtualIbanService { const currency = await this.fiatService.getFiatByName(currencyName); if (!currency) throw new BadRequestException('Currency not found'); - const bank = await this.bankService.getBankInternal(IbanBankName.YAPEAL, currencyName); + const bank = await this.bankService.getBankInternal(VirtualIbanService.bankName, currencyName); if (!bank?.receive) throw new BadRequestException('No bank available for this currency'); const { iban, bban, accountUid } = await this.reserveVibanFromYapeal(bank.iban); diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 23121ba12c..39b8d65d17 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -37,7 +37,9 @@ import { FeeService, UserFeeRequest } from 'src/subdomains/supporting/payment/se import { Price } from 'src/subdomains/supporting/pricing/domain/entities/price'; import { BankTxReturn } from '../../bank-tx/bank-tx-return/bank-tx-return.entity'; import { BankTx } from '../../bank-tx/bank-tx/entities/bank-tx.entity'; +import { BankService } from '../../bank/bank/bank.service'; import { CardBankName, IbanBankName } from '../../bank/bank/dto/bank.dto'; +import { VirtualIbanService } from '../../bank/virtual-iban/virtual-iban.service'; import { CryptoInput, PayInConfirmationType } from '../../payin/entities/crypto-input.entity'; import { PriceCurrency, PriceValidity, PricingService } from '../../pricing/services/pricing.service'; import { FeeAmountsDto, FeeInfo, FeeSpec, InternalFeeDto, toFeeDto } from '../dto/fee.dto'; @@ -77,6 +79,8 @@ export class TransactionHelper implements OnModuleInit { private readonly buyService: BuyService, private readonly assetService: AssetService, private readonly countryService: CountryService, + private readonly bankService: BankService, + private readonly virtualIbanService: VirtualIbanService, ) {} onModuleInit() { @@ -269,7 +273,7 @@ export class TransactionHelper implements OnModuleInit { const chfPrice = await this.pricingService.getPrice(txAsset, PriceCurrency.CHF, PriceValidity.ANY); const txAmountChf = chfPrice.convert(txAmount); - const bankIn = TransactionHelper.getDefaultBankByPaymentMethod(paymentMethodIn); + const bankIn = await this.getBankIn(from, paymentMethodIn, user?.userData); const bankOut = TransactionHelper.getDefaultBankByPaymentMethod(paymentMethodOut); const wallet = walletName ? await this.walletService.getByIdOrName(undefined, walletName) : undefined; @@ -851,6 +855,31 @@ export class TransactionHelper implements OnModuleInit { // --- HELPER METHODS --- // + private async getBankIn( + from: Active, + paymentMethodIn: PaymentMethod, + userData?: UserData, + ): Promise { + const isBankTransfer = + isFiat(from) && + [FiatPaymentMethod.BANK, FiatPaymentMethod.INSTANT].includes(paymentMethodIn as FiatPaymentMethod); + if (!isBankTransfer) return TransactionHelper.getDefaultBankByPaymentMethod(paymentMethodIn); + + // vIBAN deposits are received at the vIBAN bank + if (userData) { + const virtualIban = await this.virtualIbanService.getActiveForUserAndCurrency(userData, from.name); + if (virtualIban?.bank.receive) return virtualIban.bank.name; + } + + const bank = await this.bankService.getBank({ + currency: from.name, + paymentMethod: paymentMethodIn as FiatPaymentMethod, + userData, + }); + + return bank?.name ?? TransactionHelper.getDefaultBankByPaymentMethod(paymentMethodIn); + } + static getDefaultBankByPaymentMethod(paymentMethod: PaymentMethod): CardBankName | IbanBankName { switch (paymentMethod) { case FiatPaymentMethod.BANK: