From db680d0c0638ab455d4ac0e65aa5424d0ae400b6 Mon Sep 17 00:00:00 2001 From: David May <85513542+davidleomay@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:41:02 +0100 Subject: [PATCH 1/8] fix: use correct refund currency (CHF only for CH/LI IBANs) (#2874) * fix: use correct refund currency (CHF only for CH/LI IBANs) * fix: fix test failures for isDomesticIban and refundPrice - Use Object.assign in TestUtil.provideConfig to preserve class methods (spread operator only copies enumerable properties, not methods) - Add pricingService.getPrice mock to manualPrice test (required since new code calls getPrice for refundPrice conversion) - Update expected refundAmount from 99.87 to 99.88 (rounding adjustment) * fix: valid only price --------- Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> --- src/config/config.ts | 4 +++ src/shared/services/payment-info.service.ts | 2 +- src/shared/utils/test.util.ts | 2 +- src/shared/utils/util.ts | 2 +- .../core/aml/services/aml-helper.service.ts | 2 +- .../core/aml/services/aml.service.ts | 2 +- .../process/services/buy-crypto.service.ts | 2 +- .../__tests__/transaction-helper.spec.ts | 5 +++- .../controllers/transaction.controller.ts | 4 +++ .../core/history/dto/refund-internal.dto.ts | 1 + .../transaction/transaction-util.service.ts | 5 ++-- .../bank-tx-return/bank-tx-return.service.ts | 2 +- .../bank-account/is-dfx-iban.validator.ts | 8 ++--- .../payment/services/transaction-helper.ts | 30 ++++++++++++++----- 14 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index eb2b248aed..460c034c0f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -56,6 +56,10 @@ export class Configuration { priceSourceManual = 'DFX'; // source name for priceStep if price is set manually in buy-crypto priceSourcePayment = 'Payment'; // source name for priceStep if price is defined by payment quote + isDomesticIban(iban: string): boolean { + return ['CH', 'LI'].includes(iban?.substring(0, 2)); + } + defaults = { currency: 'EUR', language: 'EN', diff --git a/src/shared/services/payment-info.service.ts b/src/shared/services/payment-info.service.ts index 472a7e955c..cd7f3e57c8 100644 --- a/src/shared/services/payment-info.service.ts +++ b/src/shared/services/payment-info.service.ts @@ -90,7 +90,7 @@ export class PaymentInfoService { if (!dto.currency) throw new NotFoundException('Currency not found'); if (!dto.currency.buyable) throw new BadRequestException('Currency not buyable'); - if ('iban' in dto && dto.currency?.name === 'CHF' && !dto.iban.startsWith('CH') && !dto.iban.startsWith('LI')) + if ('iban' in dto && dto.currency?.name === 'CHF' && !Config.isDomesticIban(dto.iban)) throw new BadRequestException( 'CHF transactions are only permitted to Liechtenstein or Switzerland. Use EUR for other countries.', ); diff --git a/src/shared/utils/test.util.ts b/src/shared/utils/test.util.ts index e0227a5394..5f18e7ecf6 100644 --- a/src/shared/utils/test.util.ts +++ b/src/shared/utils/test.util.ts @@ -4,7 +4,7 @@ import { DeepPartial } from 'typeorm'; export class TestUtil { static provideConfig(config: DeepPartial = {}): Provider { - const conf = { ...new Configuration(), ...config } as Configuration; + const conf = Object.assign(new Configuration(), config); return { provide: ConfigService, useValue: new ConfigService(conf) }; } } diff --git a/src/shared/utils/util.ts b/src/shared/utils/util.ts index 46b766e4e1..b935bfcbe1 100644 --- a/src/shared/utils/util.ts +++ b/src/shared/utils/util.ts @@ -67,7 +67,7 @@ export class Util { } static roundByPrecision(amount: number, precision: number): number { - return new BigNumber(amount).precision(precision).toNumber(); + return new BigNumber(amount).precision(precision, BigNumber.ROUND_HALF_UP).toNumber(); } static floorByPrecision(amount: number, precision: number): number { diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index 243f560f58..e8cd420bc0 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -305,7 +305,7 @@ export class AmlHelperService { if (entity.inputAmount > entity.cryptoInput.asset.liquidityCapacity) errors.push(AmlError.LIQUIDITY_LIMIT_EXCEEDED); if (nationality && !nationality.cryptoEnable) errors.push(AmlError.TX_COUNTRY_NOT_ALLOWED); - if (entity.sell.fiat.name === 'CHF' && !entity.sell.iban.startsWith('CH') && !entity.sell.iban.startsWith('LI')) + if (entity.sell.fiat.name === 'CHF' && !Config.isDomesticIban(entity.sell.iban)) errors.push(AmlError.ABROAD_CHF_NOT_ALLOWED); if ( blacklist.some((b) => diff --git a/src/subdomains/core/aml/services/aml.service.ts b/src/subdomains/core/aml/services/aml.service.ts index 9b73b52b53..76f8a7ec49 100644 --- a/src/subdomains/core/aml/services/aml.service.ts +++ b/src/subdomains/core/aml/services/aml.service.ts @@ -60,7 +60,7 @@ export class AmlService { if ( !entity.userData.bankTransactionVerification && entity instanceof BuyFiat && - (entity.sell.iban.startsWith('LI') || entity.sell.iban.startsWith('CH')) + Config.isDomesticIban(entity.sell.iban) ) entity.userData = await this.userDataService.updateUserDataInternal(entity.userData, { bankTransactionVerification: CheckStatus.GSHEET, diff --git a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts index 040f8a53fc..02a4b4d545 100644 --- a/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts +++ b/src/subdomains/core/buy-crypto/process/services/buy-crypto.service.ts @@ -558,7 +558,7 @@ export class BuyCryptoService { { iban: chargebackIban, amount: chargebackAmount, - currency: buyCrypto.bankTx?.currency, + currency: dto.chargebackCurrency ?? buyCrypto.bankTx?.currency, ...creditorData, }, ); diff --git a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts index ea582f0784..6805787f25 100644 --- a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts +++ b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts @@ -130,6 +130,9 @@ describe('TransactionHelper', () => { jest.spyOn(fiatService, 'getFiatByName').mockResolvedValue(createCustomFiat({ name: 'CHF' })); jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createInternalChargebackFeeDto()); + jest + .spyOn(pricingService, 'getPrice') + .mockResolvedValue(createCustomPrice({ source: 'CHF', target: 'CHF', price: 1 })); await expect( txHelper.getRefundData( @@ -141,7 +144,7 @@ describe('TransactionHelper', () => { ), ).resolves.toMatchObject({ fee: { network: 0, bank: 1.13 }, - refundAmount: 99.87, + refundAmount: 99.88, refundTarget: 'DE12500105170648489890', }); }); diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index 99c36c058f..9b8abb4298 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -454,11 +454,14 @@ export class TransactionController { } : undefined; + const chargebackCurrency = refundData.refundAsset.name; + if (transaction.targetEntity instanceof BankTxReturn) { if (!dto.creditorData) throw new BadRequestException('Creditor data is required for bank refunds'); return this.bankTxReturnService.refundBankTx(transaction.targetEntity, { refundIban: refundData.refundTarget ?? dto.refundTarget, + chargebackCurrency, creditorData, ...refundDto, }); @@ -487,6 +490,7 @@ export class TransactionController { return this.buyCryptoService.refundBankTx(transaction.targetEntity, { refundIban: refundData.refundTarget ?? dto.refundTarget, + chargebackCurrency, creditorData, ...refundDto, }); diff --git a/src/subdomains/core/history/dto/refund-internal.dto.ts b/src/subdomains/core/history/dto/refund-internal.dto.ts index 021ec5687f..357bcc187e 100644 --- a/src/subdomains/core/history/dto/refund-internal.dto.ts +++ b/src/subdomains/core/history/dto/refund-internal.dto.ts @@ -42,6 +42,7 @@ export class BaseRefund { export class BankTxRefund extends BaseRefund { refundIban?: string; + chargebackCurrency?: string; chargebackOutput?: FiatOutput; creditorData?: CreditorData; } diff --git a/src/subdomains/core/transaction/transaction-util.service.ts b/src/subdomains/core/transaction/transaction-util.service.ts index 420bdab186..9fb2eed606 100644 --- a/src/subdomains/core/transaction/transaction-util.service.ts +++ b/src/subdomains/core/transaction/transaction-util.service.ts @@ -8,6 +8,7 @@ import { } from '@nestjs/common'; import { BigNumber } from 'ethers/lib/ethers'; import * as IbanTools from 'ibantools'; +import { Config } from 'src/config/config'; import { BlockchainRegistryService } from 'src/integration/blockchain/shared/services/blockchain-registry.service'; import { TxValidationService } from 'src/integration/blockchain/shared/services/tx-validation.service'; import { CheckoutPaymentStatus } from 'src/integration/checkout/dto/checkout.dto'; @@ -122,9 +123,7 @@ export class TransactionUtilService { throw new BadRequestException('BIC not allowed'); return ( - bankAccount && - (bankAccount.bic || iban.startsWith('CH') || iban.startsWith('LI')) && - IbanTools.validateIBAN(bankAccount.iban).valid + bankAccount && (bankAccount.bic || Config.isDomesticIban(iban)) && IbanTools.validateIBAN(bankAccount.iban).valid ); } diff --git a/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts b/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts index fe031718c5..2992519413 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx-return/bank-tx-return.service.ts @@ -213,7 +213,7 @@ export class BankTxReturnService { { iban: chargebackIban, amount: chargebackAmount, - currency: bankTxReturn.bankTx?.currency, + currency: dto.chargebackCurrency ?? bankTxReturn.bankTx?.currency, ...creditorData, }, ); diff --git a/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts b/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts index 003f9f4b6e..6c9e19f331 100644 --- a/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts +++ b/src/subdomains/supporting/bank/bank-account/is-dfx-iban.validator.ts @@ -7,6 +7,7 @@ import { ValidatorConstraintInterface, } from 'class-validator'; import * as IbanTools from 'ibantools'; +import { Config } from 'src/config/config'; import { SpecialExternalAccountType } from '../../payment/entities/special-external-account.entity'; import { SpecialExternalAccountService } from '../../payment/services/special-external-account.service'; import { Bank } from '../bank/bank.entity'; @@ -79,8 +80,7 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface { if (!valid) return `${args.property} not valid`; // BIC lookup required for non-CH/LI IBANs - if (!this.currentBIC && !iban.startsWith('CH') && !iban.startsWith('LI')) - return `${args.property} BIC could not be determined`; + if (!this.currentBIC && !Config.isDomesticIban(iban)) return `${args.property} BIC could not be determined`; // check blocked IBANs const isBlocked = this.blockedIbans.some((i) => new RegExp(i.toLowerCase()).test(iban.toLowerCase())); @@ -92,8 +92,8 @@ export class IsDfxIbanValidator implements ValidatorConstraintInterface { if (this.dfxBanks.some((b) => b.iban.toLowerCase() === iban.toLowerCase())) return `${args.property} DFX IBAN not allowed`; - // check if QR IBAN - if (iban.startsWith('CH') || iban.startsWith('LI')) { + // check if QR IBAN (CH/LI only) + if (Config.isDomesticIban(iban)) { const iid = +iban.substring(4, 9); if (iid >= 30000 && iid <= 31999) return `${args.property} QR IBAN not allowed`; } diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 3cb726fb17..99c0aa2d58 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -401,6 +401,12 @@ export class TransactionHelper implements OnModuleInit { const inputCurrency = await this.getRefundActive(refundEntity); if (!inputCurrency.refundEnabled) throw new BadRequestException(`Refund for ${inputCurrency.name} not allowed`); + // CHF refunds only to domestic IBANs + const refundCurrency = + isFiat && inputCurrency.name === 'CHF' && refundTarget && !Config.isDomesticIban(refundTarget) + ? await this.fiatService.getFiatByName('EUR') + : inputCurrency; + const price = refundEntity.manualChfPrice ?? (await this.pricingService.getPrice(PriceCurrency.CHF, inputCurrency, PriceValidity.PREFER_VALID)); @@ -429,11 +435,21 @@ export class TransactionHelper implements OnModuleInit { ) : 0; // Bank fee buffer 1% - const totalFeeAmount = Util.roundReadable(dfxFeeAmount + networkFeeAmount + bankFeeAmount, feeAmountType); + const totalFeeAmount = dfxFeeAmount + networkFeeAmount + bankFeeAmount; if (totalFeeAmount >= inputAmount) throw new BadRequestException('Transaction fee is too expensive'); - const refundAsset = + const inputAsset = inputCurrency instanceof Asset ? AssetDtoMapper.toDto(inputCurrency) : FiatDtoMapper.toDto(inputCurrency); + const refundAsset = + refundCurrency instanceof Asset ? AssetDtoMapper.toDto(refundCurrency) : FiatDtoMapper.toDto(refundCurrency); + + // convert to refund currency + const refundPrice = await this.pricingService.getPrice(inputCurrency, refundCurrency, PriceValidity.VALID_ONLY); + + const refundAmount = Util.roundReadable(refundPrice.convert(inputAmount - totalFeeAmount), amountType); + const feeDfx = Util.roundReadable(refundPrice.convert(dfxFeeAmount), feeAmountType); + const feeNetwork = Util.roundReadable(refundPrice.convert(networkFeeAmount), feeAmountType); + const feeBank = Util.roundReadable(refundPrice.convert(bankFeeAmount), feeAmountType); // Get bank details from the refund entity const bankTx = this.getBankTxFromRefundEntity(refundEntity); @@ -441,12 +457,12 @@ export class TransactionHelper implements OnModuleInit { return { expiryDate: Util.secondsAfter(Config.transactionRefundExpirySeconds), inputAmount: Util.roundReadable(inputAmount, amountType), - inputAsset: refundAsset, - refundAmount: Util.roundReadable(inputAmount - totalFeeAmount, amountType), + inputAsset, + refundAmount, fee: { - dfx: Util.roundReadable(dfxFeeAmount, feeAmountType), - network: Util.roundReadable(networkFeeAmount, feeAmountType), - bank: Util.roundReadable(bankFeeAmount, feeAmountType), + dfx: feeDfx, + network: feeNetwork, + bank: feeBank, }, refundAsset, refundTarget, From d630fee8c89c13c410cffc85da020f37994e5c60 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:28:10 +0100 Subject: [PATCH 2/8] feat(liquidity-management): add Lightning withdraw support via Binance (#2883) --- .../exchange/services/binance.service.ts | 2 +- src/integration/lightning/dto/lnbits.dto.ts | 4 +- src/integration/lightning/dto/lnd.dto.ts | 18 ++++ src/integration/lightning/lightning-client.ts | 8 ++ .../actions/base/ccxt-exchange.adapter.ts | 2 +- .../adapters/actions/binance.adapter.ts | 98 +++++++++++++++++++ 6 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/integration/exchange/services/binance.service.ts b/src/integration/exchange/services/binance.service.ts index b1b69bbb63..73ca59ccae 100644 --- a/src/integration/exchange/services/binance.service.ts +++ b/src/integration/exchange/services/binance.service.ts @@ -13,7 +13,7 @@ export class BinanceService extends ExchangeService { Arbitrum: 'ARBITRUM', BinanceSmartChain: 'BSC', Bitcoin: 'BTC', - Lightning: undefined, + Lightning: 'LIGHTNING', Spark: undefined, Monero: 'XMR', Zano: undefined, diff --git a/src/integration/lightning/dto/lnbits.dto.ts b/src/integration/lightning/dto/lnbits.dto.ts index 99b0c6acd6..036453885b 100644 --- a/src/integration/lightning/dto/lnbits.dto.ts +++ b/src/integration/lightning/dto/lnbits.dto.ts @@ -15,8 +15,8 @@ export interface LnBitsWalletPaymentParamsDto { amount: number; memo: string; expirySec: number; - webhook: string; - extra: { + webhook?: string; + extra?: { link: string; signature: string; }; diff --git a/src/integration/lightning/dto/lnd.dto.ts b/src/integration/lightning/dto/lnd.dto.ts index 7e3046c93c..42b884a512 100644 --- a/src/integration/lightning/dto/lnd.dto.ts +++ b/src/integration/lightning/dto/lnd.dto.ts @@ -27,6 +27,24 @@ export enum LndPaymentStatus { FAILED = 'FAILED', } +export enum LndInvoiceState { + OPEN = 'OPEN', + SETTLED = 'SETTLED', + CANCELED = 'CANCELED', + ACCEPTED = 'ACCEPTED', +} + +export interface LndInvoiceDto { + memo: string; + r_hash: string; + payment_request: string; + value_sat: string; + state: LndInvoiceState; + settled: boolean; + settle_date: string; + amt_paid_sat: string; +} + export interface LndPaymentDto { payment_hash: string; value_sat: number; diff --git a/src/integration/lightning/lightning-client.ts b/src/integration/lightning/lightning-client.ts index 94353796a0..0649f33d91 100644 --- a/src/integration/lightning/lightning-client.ts +++ b/src/integration/lightning/lightning-client.ts @@ -8,6 +8,7 @@ import { LndChannelBalanceDto, LndChannelDto, LndInfoDto, + LndInvoiceDto, LndPaymentDto, LndRouteDto, LndSendPaymentResponseDto, @@ -115,6 +116,13 @@ export class LightningClient { .then((p) => p.payments); } + async lookupInvoice(paymentHashHex: string): Promise { + return this.http.get( + `${Config.blockchain.lightning.lnd.apiUrl}/invoice/${paymentHashHex}`, + this.httpLndConfig(), + ); + } + async sendPaymentByInvoice(invoice: string): Promise { return this.http.post( `${Config.blockchain.lightning.lnd.apiUrl}/channels/transactions`, diff --git a/src/subdomains/core/liquidity-management/adapters/actions/base/ccxt-exchange.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/base/ccxt-exchange.adapter.ts index e257f109a1..8c9894b866 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/base/ccxt-exchange.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/base/ccxt-exchange.adapter.ts @@ -36,7 +36,7 @@ export abstract class CcxtExchangeAdapter extends LiquidityActionAdapter { constructor( system: LiquidityManagementSystem, - private readonly exchangeService: ExchangeService, + protected readonly exchangeService: ExchangeService, private readonly exchangeRegistry: ExchangeRegistryService, private readonly dexService: DexService, private readonly orderRepo: LiquidityManagementOrderRepository, diff --git a/src/subdomains/core/liquidity-management/adapters/actions/binance.adapter.ts b/src/subdomains/core/liquidity-management/adapters/actions/binance.adapter.ts index 2037b64aa9..7677d2d9d4 100644 --- a/src/subdomains/core/liquidity-management/adapters/actions/binance.adapter.ts +++ b/src/subdomains/core/liquidity-management/adapters/actions/binance.adapter.ts @@ -1,15 +1,32 @@ import { Injectable } from '@nestjs/common'; import { BinanceService } from 'src/integration/exchange/services/binance.service'; import { ExchangeRegistryService } from 'src/integration/exchange/services/exchange-registry.service'; +import { LndInvoiceState } from 'src/integration/lightning/dto/lnd.dto'; +import { LightningClient } from 'src/integration/lightning/lightning-client'; +import { LightningHelper } from 'src/integration/lightning/lightning-helper'; +import { LightningService } from 'src/integration/lightning/services/lightning.service'; import { AssetService } from 'src/shared/models/asset/asset.service'; +import { Util } from 'src/shared/utils/util'; import { DexService } from 'src/subdomains/supporting/dex/services/dex.service'; import { PricingService } from 'src/subdomains/supporting/pricing/services/pricing.service'; +import { LiquidityManagementOrder } from '../../entities/liquidity-management-order.entity'; import { LiquidityManagementSystem } from '../../enums'; +import { OrderFailedException } from '../../exceptions/order-failed.exception'; +import { OrderNotProcessableException } from '../../exceptions/order-not-processable.exception'; +import { CorrelationId } from '../../interfaces'; import { LiquidityManagementOrderRepository } from '../../repositories/liquidity-management-order.repository'; import { CcxtExchangeAdapter } from './base/ccxt-exchange.adapter'; +export enum BinanceAdapterCommands { + LIGHTNING_WITHDRAW = 'lightning-withdraw', +} + +const BINANCE_LIGHTNING_MAX_WITHDRAWAL_BTC = 0.00999; + @Injectable() export class BinanceAdapter extends CcxtExchangeAdapter { + private readonly lightningClient: LightningClient; + constructor( binanceService: BinanceService, exchangeRegistry: ExchangeRegistryService, @@ -17,6 +34,7 @@ export class BinanceAdapter extends CcxtExchangeAdapter { liquidityOrderRepo: LiquidityManagementOrderRepository, pricingService: PricingService, assetService: AssetService, + lightningService: LightningService, ) { super( LiquidityManagementSystem.BINANCE, @@ -27,5 +45,85 @@ export class BinanceAdapter extends CcxtExchangeAdapter { pricingService, assetService, ); + + this.lightningClient = lightningService.getDefaultClient(); + this.commands.set(BinanceAdapterCommands.LIGHTNING_WITHDRAW, this.lightningWithdraw.bind(this)); + } + + // --- LIGHTNING WITHDRAW --- // + + private async lightningWithdraw(order: LiquidityManagementOrder): Promise { + const asset = order.pipeline.rule.targetAsset.dexName; + const balance = await this.exchangeService.getAvailableBalance(asset); + + const amount = Util.floor(Math.min(order.maxAmount, balance, BINANCE_LIGHTNING_MAX_WITHDRAWAL_BTC), 8); + + if (amount <= 0) + throw new OrderNotProcessableException( + `${this.exchangeService.name}: not enough balance for ${asset} (balance: ${balance}, min. requested: ${order.minAmount}, max. requested: ${order.maxAmount})`, + ); + const amountSats = LightningHelper.btcToSat(amount); + + // Generate invoice via LnBits + const invoice = await this.lightningClient.getLnBitsWalletPayment({ + amount: amountSats, + memo: `LM Order ${order.id}`, + expirySec: 1800, // 30 min (Binance limit) + }); + + order.inputAmount = amount; + order.inputAsset = asset; + order.outputAsset = asset; + + // Send invoice to Binance for withdrawal + const response = await this.exchangeService.withdrawFunds(asset, amount, invoice.pr, undefined, 'LIGHTNING'); + + return response.id; + } + + // --- COMPLETION CHECK --- // + + async checkCompletion(order: LiquidityManagementOrder): Promise { + if (order.action.command === BinanceAdapterCommands.LIGHTNING_WITHDRAW) { + return this.checkLightningWithdrawCompletion(order); + } + return super.checkCompletion(order); + } + + private async checkLightningWithdrawCompletion(order: LiquidityManagementOrder): Promise { + const asset = order.pipeline.rule.targetAsset.dexName; + const withdrawal = await this.exchangeService.getWithdraw(order.correlationId, asset); + if (!withdrawal) return false; + + if (withdrawal.status === 'failed') { + throw new OrderFailedException(`Lightning withdrawal ${order.correlationId} failed on Binance`); + } + + // For Lightning, txid = payment_hash (hex) + const paymentHash = withdrawal.txid; + if (!paymentHash) return false; + + try { + const invoice = await this.lightningClient.lookupInvoice(paymentHash); + const isComplete = invoice.state === LndInvoiceState.SETTLED; + + if (isComplete) { + order.outputAmount = LightningHelper.satToBtc(+invoice.amt_paid_sat); + } + + return isComplete; + } catch { + // Invoice not found = not yet received + return false; + } + } + + // --- VALIDATION --- // + + validateParams(command: string, params: Record): boolean { + if (command === BinanceAdapterCommands.LIGHTNING_WITHDRAW) { + return true; // No params needed for lightning-withdraw + } + return super.validateParams(command, params); } } From 92681d6cc0194a969f3b8cf402ce11facf83cf76 Mon Sep 17 00:00:00 2001 From: David May Date: Thu, 8 Jan 2026 23:04:50 +0100 Subject: [PATCH 3/8] chore: small cleanup --- .../bank-tx/bank-tx/services/bank-tx.service.ts | 2 +- .../bank/virtual-iban/virtual-iban.service.ts | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts b/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts index e64006dd2e..5c217f21d5 100644 --- a/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts +++ b/src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts @@ -207,7 +207,7 @@ export class BankTxService implements OnModuleInit { if (tx.creditDebitIndicator === BankTxIndicator.CREDIT) { // check for dedicated asset vIBAN if (tx.virtualIban) { - const virtualIban = await this.virtualIbanService.getByIbanWithBuy(tx.virtualIban); + const virtualIban = await this.virtualIbanService.getByIban(tx.virtualIban); if (virtualIban?.buy) { await this.updateInternal(tx, { type: BankTxType.BUY_CRYPTO, buyId: virtualIban.buy.id }); continue; 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 7f77616595..30d1e19bff 100644 --- a/src/subdomains/supporting/bank/virtual-iban/virtual-iban.service.ts +++ b/src/subdomains/supporting/bank/virtual-iban/virtual-iban.service.ts @@ -103,10 +103,10 @@ export class VirtualIbanService { }); } - async getByIbanWithBuy(iban: string): Promise { - return this.virtualIbanRepo.findOne({ + async getByIban(iban: string): Promise { + return this.virtualIbanRepo.findOneCached(iban, { where: { iban }, - relations: { userData: true, bank: true, buy: { user: { userData: true } } }, + relations: { userData: true, bank: true, buy: true }, }); } @@ -122,13 +122,6 @@ export class VirtualIbanService { }); } - async getByIban(iban: string): Promise { - return this.virtualIbanRepo.findOneCached(iban, { - where: { iban }, - relations: { userData: true, bank: true }, - }); - } - async getBaseAccountIban(iban: string): Promise { return this.getByIban(iban).then((viban) => viban?.bank.iban); } From ba2db4245f90504afc22de629c2cdff175f47b87 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:48:44 +0100 Subject: [PATCH 4/8] feat(bitcoin): accept unconfirmed withdrawals for faster buy payouts (#2884) Remove the confirmation requirement from checkTransferCompletion(). Previously, Bitcoin withdrawals from exchanges (e.g., Binance) had to wait for 1 blockchain confirmation before the liquidity pipeline could continue and trigger buy payouts. Now, as soon as the withdrawal TX is visible in the mempool, the pipeline completes and buy payouts can be created immediately. This works because: - getBalance() already includes untrusted_pending (unconfirmed external) - sendMany() already uses include_unsafe: true for coin selection - CPFP fee multiplier is active for unconfirmed UTXOs Expected improvement: Buy payouts can occur in the same block as the exchange withdrawal instead of the next block (~10-15 min faster). --- src/subdomains/supporting/dex/services/dex-bitcoin.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts b/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts index c5180b7c8a..50e428ac3c 100644 --- a/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts +++ b/src/subdomains/supporting/dex/services/dex-bitcoin.service.ts @@ -34,7 +34,7 @@ export class DexBitcoinService { async checkTransferCompletion(transferTxId: string): Promise { const transaction = await this.client.getTx(transferTxId); - return transaction && transaction.blockhash && transaction.confirmations > 0; + return transaction != null; } async getRecentHistory(txCount: number): Promise { From 59b0b98dcb3b7be7697c4ed6c3931ae654825554 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:49:05 +0100 Subject: [PATCH 5/8] feat: enhance resetAmlCheck with full reset and audit logging (#2878) * feat: enhance resetAmlCheck with full reset and audit logging - Add priceSteps, priceDefinitionAllowedDate, usedFees, bankFeeAmount to resetAmlCheck() in BuyFiat entity for complete price recalculation - Create SupportLog entry when resetAmlCheck is called, documenting: - Previous amlCheck and amlReason - Output amount and asset - FiatOutput ID and transmission status - Price definition date - Export SupportLogService from SupportIssueModule - Import SupportIssueModule in SellCryptoModule This ensures full traceability when admin resets a BuyFiat transaction, e.g. when bank (Yapeal) rejects a payout. * fix: add null check for userData in resetAmlCheck * fix: add outputAsset to relations for complete audit log * test: add SupportLogService mock to buy-fiat.service.spec --- .../__tests__/buy-fiat.service.spec.ts | 4 +++ .../sell-crypto/process/buy-fiat.entity.ts | 4 +++ .../process/services/buy-fiat.service.ts | 27 ++++++++++++++++++- .../core/sell-crypto/sell-crypto.module.ts | 2 ++ .../support-issue/support-issue.module.ts | 2 +- 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts b/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts index 003848c156..a836ab9768 100644 --- a/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts +++ b/src/subdomains/core/sell-crypto/process/__tests__/buy-fiat.service.spec.ts @@ -18,6 +18,7 @@ import { PayInService } from 'src/subdomains/supporting/payin/services/payin.ser import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; +import { SupportLogService } from 'src/subdomains/supporting/support-issue/services/support-log.service'; import { createCustomSellHistory } from '../../route/dto/__mocks__/sell-history.dto.mock'; import { SellRepository } from '../../route/sell.repository'; import { SellService } from '../../route/sell.service'; @@ -55,6 +56,7 @@ describe('BuyFiatService', () => { let amlService: AmlService; let transactionHelper: TransactionHelper; let custodyOrderService: CustodyOrderService; + let supportLogService: SupportLogService; beforeEach(async () => { buyFiatRepo = createMock(); @@ -75,6 +77,7 @@ describe('BuyFiatService', () => { amlService = createMock(); transactionHelper = createMock(); custodyOrderService = createMock(); + supportLogService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [TestSharedModule], @@ -98,6 +101,7 @@ describe('BuyFiatService', () => { { provide: AmlService, useValue: amlService }, { provide: TransactionHelper, useValue: transactionHelper }, { provide: CustodyOrderService, useValue: custodyOrderService }, + { provide: SupportLogService, useValue: supportLogService }, ], }).compile(); diff --git a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts index 22314def7a..c4af45bd30 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts @@ -469,6 +469,10 @@ export class BuyFiat extends IEntity { chargebackAllowedDateUser: null, chargebackAmount: null, chargebackAllowedBy: null, + priceSteps: null, + priceDefinitionAllowedDate: null, + usedFees: null, + bankFeeAmount: null, }; Object.assign(this, update); diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts index 4a896b0082..e88bedeac0 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat.service.ts @@ -21,6 +21,8 @@ import { TransactionTypeInternal } from 'src/subdomains/supporting/payment/entit import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper'; import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service'; import { TransactionService } from 'src/subdomains/supporting/payment/services/transaction.service'; +import { SupportLogType } from 'src/subdomains/supporting/support-issue/enums/support-log.enum'; +import { SupportLogService } from 'src/subdomains/supporting/support-issue/services/support-log.service'; import { Between, FindOptionsRelations, In, MoreThan } from 'typeorm'; import { FiatOutputService } from '../../../../supporting/fiat-output/fiat-output.service'; import { CheckStatus } from '../../../aml/enums/check-status.enum'; @@ -65,6 +67,7 @@ export class BuyFiatService { private readonly transactionHelper: TransactionHelper, @Inject(forwardRef(() => CustodyOrderService)) private readonly custodyOrderService: CustodyOrderService, + private readonly supportLogService: SupportLogService, ) {} async createFromCryptoInput(cryptoInput: CryptoInput, sell: Sell, request?: TransactionRequest): Promise { @@ -324,7 +327,10 @@ export class BuyFiatService { } async resetAmlCheck(id: number): Promise { - const entity = await this.buyFiatRepo.findOne({ where: { id }, relations: { fiatOutput: true } }); + const entity = await this.buyFiatRepo.findOne({ + where: { id }, + relations: { fiatOutput: true, transaction: { userData: true }, outputAsset: true }, + }); if (!entity) throw new NotFoundException('BuyFiat not found'); if (entity.isComplete || entity.fiatOutput?.isComplete) throw new BadRequestException('BuyFiat is already complete'); @@ -332,8 +338,27 @@ export class BuyFiatService { const fiatOutputId = entity.fiatOutput?.id; + const resetDetails = { + buyFiatId: entity.id, + amlCheck: entity.amlCheck, + amlReason: entity.amlReason, + outputAmount: entity.outputAmount, + outputAsset: entity.outputAsset?.name, + fiatOutputId: fiatOutputId, + fiatOutputTransmitted: entity.fiatOutput?.isTransmittedDate, + priceDefinitionAllowedDate: entity.priceDefinitionAllowedDate, + }; + await this.buyFiatRepo.update(...entity.resetAmlCheck()); if (fiatOutputId) await this.fiatOutputService.delete(fiatOutputId); + + if (entity.transaction.userData) { + await this.supportLogService.createSupportLog(entity.transaction.userData, { + type: SupportLogType.SUPPORT, + message: `BuyFiat ${entity.id} reset`, + comment: `AML check reset. Previous state: ${JSON.stringify(resetDetails)}`, + }); + } } async updateVolumes(start = 1, end = 100000): Promise { diff --git a/src/subdomains/core/sell-crypto/sell-crypto.module.ts b/src/subdomains/core/sell-crypto/sell-crypto.module.ts index 3db81713de..a30334ba74 100644 --- a/src/subdomains/core/sell-crypto/sell-crypto.module.ts +++ b/src/subdomains/core/sell-crypto/sell-crypto.module.ts @@ -12,6 +12,7 @@ import { PayInModule } from 'src/subdomains/supporting/payin/payin.module'; import { PaymentModule } from 'src/subdomains/supporting/payment/payment.module'; import { TransactionModule } from 'src/subdomains/supporting/payment/transaction.module'; import { PricingModule } from 'src/subdomains/supporting/pricing/pricing.module'; +import { SupportIssueModule } from 'src/subdomains/supporting/support-issue/support-issue.module'; import { AmlModule } from '../aml/aml.module'; import { BuyCryptoModule } from '../buy-crypto/buy-crypto.module'; import { CustodyModule } from '../custody/custody.module'; @@ -50,6 +51,7 @@ import { SellService } from './route/sell.service'; forwardRef(() => TransactionUtilModule), RouteModule, forwardRef(() => CustodyModule), + SupportIssueModule, ], controllers: [BuyFiatController, SellController], providers: [ diff --git a/src/subdomains/supporting/support-issue/support-issue.module.ts b/src/subdomains/supporting/support-issue/support-issue.module.ts index 48f509275c..6f4be733b7 100644 --- a/src/subdomains/supporting/support-issue/support-issue.module.ts +++ b/src/subdomains/supporting/support-issue/support-issue.module.ts @@ -55,6 +55,6 @@ import { SupportIssueController } from './support-issue.controller'; SupportLogRepository, SupportLogService, ], - exports: [SupportIssueService, LimitRequestService], + exports: [SupportIssueService, LimitRequestService, SupportLogService], }) export class SupportIssueModule {} From 1394287a855063eade653cdc73535f1cff3b86d7 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:49:29 +0100 Subject: [PATCH 6/8] Remove Maerki Baumann integration and set Yapeal as default bank (#2747) - Remove maerkiBaumannEnable flag from Country entity - Remove Maerki-specific country check logic from Bank entity - Update default bank for SEPA payments from Maerki to Yapeal - Remove Maerki from blockchain-to-bank mapping - Update all tests to use Yapeal instead of Maerki - Replace maerkiEUR/maerkiCHF mocks with yapealEUR/yapealCHF - Preserve IbanBankName.MAERKI and Blockchain.MAERKI_BAUMANN enums for database compatibility --- .../country/__mocks__/country.entity.mock.ts | 1 - src/shared/models/country/country.entity.ts | 3 --- .../__tests__/transaction-helper.spec.ts | 4 ++-- .../services/buy-fiat-preparation.service.ts | 2 +- .../bank/bank/__mocks__/bank.entity.mock.ts | 20 +++++++++---------- .../bank/bank/__tests__/bank.service.spec.ts | 20 +++++++++---------- .../supporting/bank/bank/bank.entity.ts | 5 +---- .../supporting/bank/bank/bank.service.ts | 2 -- .../__tests__/fiat-output-job.service.spec.ts | 18 ++++++++--------- .../payment/services/transaction-helper.ts | 2 +- 10 files changed, 34 insertions(+), 43 deletions(-) diff --git a/src/shared/models/country/__mocks__/country.entity.mock.ts b/src/shared/models/country/__mocks__/country.entity.mock.ts index 95f3553063..5f4f298c50 100644 --- a/src/shared/models/country/__mocks__/country.entity.mock.ts +++ b/src/shared/models/country/__mocks__/country.entity.mock.ts @@ -7,7 +7,6 @@ const defaultCountry: Partial = { dfxEnable: true, lockEnable: true, ipEnable: true, - maerkiBaumannEnable: true, yapealEnable: true, updated: undefined, created: undefined, diff --git a/src/shared/models/country/country.entity.ts b/src/shared/models/country/country.entity.ts index 77fe3d061b..2d9943b0e9 100644 --- a/src/shared/models/country/country.entity.ts +++ b/src/shared/models/country/country.entity.ts @@ -30,9 +30,6 @@ export class Country extends IEntity { @Column({ default: true }) ipEnable: boolean; - @Column({ default: false }) - maerkiBaumannEnable: boolean; - @Column({ default: false }) yapealEnable: boolean; diff --git a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts index 6805787f25..1d209f5459 100644 --- a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts +++ b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts @@ -106,7 +106,7 @@ describe('TransactionHelper', () => { txHelper.getRefundData( transaction.refundTargetEntity, defaultUserData, - IbanBankName.MAERKI, + IbanBankName.YAPEAL, 'DE12500105170648489890', !transaction.cryptoInput, ), @@ -138,7 +138,7 @@ describe('TransactionHelper', () => { txHelper.getRefundData( transaction.refundTargetEntity, defaultUserData, - IbanBankName.MAERKI, + IbanBankName.YAPEAL, 'DE12500105170648489890', !transaction.cryptoInput, ), diff --git a/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts b/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts index 3ee44057ef..04fbc82b5f 100644 --- a/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts +++ b/src/subdomains/core/sell-crypto/process/services/buy-fiat-preparation.service.ts @@ -201,7 +201,7 @@ export class BuyFiatPreparationService { CryptoPaymentMethod.CRYPTO, FiatPaymentMethod.BANK, undefined, - IbanBankName.MAERKI, + IbanBankName.YAPEAL, entity.user, ); diff --git a/src/subdomains/supporting/bank/bank/__mocks__/bank.entity.mock.ts b/src/subdomains/supporting/bank/bank/__mocks__/bank.entity.mock.ts index df73b715d0..cb54d1de94 100644 --- a/src/subdomains/supporting/bank/bank/__mocks__/bank.entity.mock.ts +++ b/src/subdomains/supporting/bank/bank/__mocks__/bank.entity.mock.ts @@ -19,19 +19,19 @@ export const olkyEUR = createCustomBank({ sctInst: true, }); -export const maerkiEUR = createCustomBank({ - name: IbanBankName.MAERKI, +export const yapealEUR = createCustomBank({ + name: IbanBankName.YAPEAL, currency: 'EUR', - iban: 'CH6808573177975201814', - bic: 'MAEBCHZZ', + iban: 'CH1234567890123456789', + bic: 'YAPECHCHXXX', receive: true, }); -export const maerkiCHF = createCustomBank({ - name: IbanBankName.MAERKI, +export const yapealCHF = createCustomBank({ + name: IbanBankName.YAPEAL, currency: 'CHF', - iban: 'CH3408573177975200001', - bic: 'MAEBCHZZ', + iban: 'CH9876543210987654321', + bic: 'YAPECHCHXXX', receive: true, }); @@ -44,10 +44,10 @@ export function createCustomBank(customValues: Partial): Bank { } export function createDefaultBanks(): Bank[] { - return [olkyEUR, maerkiEUR, maerkiCHF]; + return [olkyEUR, yapealEUR, yapealCHF]; } export function createDefaultDisabledBanks(): Bank[] { olkyEUR.receive = false; - return [olkyEUR, maerkiEUR, maerkiCHF]; + return [olkyEUR, yapealEUR, yapealCHF]; } diff --git a/src/subdomains/supporting/bank/bank/__tests__/bank.service.spec.ts b/src/subdomains/supporting/bank/bank/__tests__/bank.service.spec.ts index 95ae45d606..5dba56f986 100644 --- a/src/subdomains/supporting/bank/bank/__tests__/bank.service.spec.ts +++ b/src/subdomains/supporting/bank/bank/__tests__/bank.service.spec.ts @@ -14,8 +14,8 @@ import { FiatPaymentMethod } from 'src/subdomains/supporting/payment/dto/payment import { createDefaultBanks, createDefaultDisabledBanks, - maerkiCHF, - maerkiEUR, + yapealCHF, + yapealEUR, olkyEUR, } from '../__mocks__/bank.entity.mock'; import { BankRepository } from '../bank.repository'; @@ -70,10 +70,10 @@ describe('BankService', () => { service = module.get(BankService); }); - function defaultSetup(maerkiBaumannEnable = true, disabledBank = false) { + function defaultSetup(yapealEnable = true, disabledBank = false) { jest .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ maerkiBaumannEnable: maerkiBaumannEnable })); + .mockResolvedValue(createCustomCountry({ yapealEnable: yapealEnable })); const allBanks = disabledBank ? createDefaultDisabledBanks() : createDefaultBanks(); jest.spyOn(bankRepo, 'findCachedBy').mockImplementation(async (_key: string, filter?: any) => { @@ -91,8 +91,8 @@ describe('BankService', () => { it('should return first matching bank for CHF currency', async () => { defaultSetup(); const result = await service.getBank(createBankSelectorInput('CHF', 10000)); - expect(result.iban).toBe(maerkiCHF.iban); - expect(result.bic).toBe(maerkiCHF.bic); + expect(result.iban).toBe(yapealCHF.iban); + expect(result.bic).toBe(yapealCHF.bic); }); it('should return matching bank for EUR currency', async () => { @@ -112,8 +112,8 @@ describe('BankService', () => { it('should return first matching bank for CHF currency with standard payment', async () => { defaultSetup(true); const result = await service.getBank(createBankSelectorInput('CHF')); - expect(result.iban).toBe(maerkiCHF.iban); - expect(result.bic).toBe(maerkiCHF.bic); + expect(result.iban).toBe(yapealCHF.iban); + expect(result.bic).toBe(yapealCHF.bic); }); it('should fallback to EUR for unsupported currency', async () => { @@ -126,7 +126,7 @@ describe('BankService', () => { it('should fallback to first EUR bank when sctInst bank is disabled', async () => { defaultSetup(true, true); const result = await service.getBank(createBankSelectorInput('EUR', undefined, FiatPaymentMethod.INSTANT)); - expect(result.iban).toBe(maerkiEUR.iban); - expect(result.bic).toBe(maerkiEUR.bic); + expect(result.iban).toBe(yapealEUR.iban); + expect(result.bic).toBe(yapealEUR.bic); }); }); diff --git a/src/subdomains/supporting/bank/bank/bank.entity.ts b/src/subdomains/supporting/bank/bank/bank.entity.ts index de2464461b..61e9fbbfe1 100644 --- a/src/subdomains/supporting/bank/bank/bank.entity.ts +++ b/src/subdomains/supporting/bank/bank/bank.entity.ts @@ -38,9 +38,6 @@ export class Bank extends IEntity { // --- ENTITY METHODS --- // isCountryEnabled(country: Country): boolean { - return ( - (this.name === IbanBankName.YAPEAL && country.yapealEnable) || - (this.name === IbanBankName.MAERKI && country.maerkiBaumannEnable) - ); + return this.name === IbanBankName.YAPEAL && country.yapealEnable; } } diff --git a/src/subdomains/supporting/bank/bank/bank.service.ts b/src/subdomains/supporting/bank/bank/bank.service.ts index d81c9ec7fc..02b43d3075 100644 --- a/src/subdomains/supporting/bank/bank/bank.service.ts +++ b/src/subdomains/supporting/bank/bank/bank.service.ts @@ -107,8 +107,6 @@ export class BankService implements OnModuleInit { private static blockchainToBankName(blockchain: Blockchain): IbanBankName | undefined { switch (blockchain) { - case Blockchain.MAERKI_BAUMANN: - return IbanBankName.MAERKI; case Blockchain.OLKYPAY: return IbanBankName.OLKY; case Blockchain.YAPEAL: diff --git a/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts b/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts index cd36282d25..249856da12 100644 --- a/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts +++ b/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts @@ -17,7 +17,7 @@ import { BankTxService } from 'src/subdomains/supporting/bank-tx/bank-tx/service import { BankTxRepeatService } from '../../bank-tx/bank-tx-repeat/bank-tx-repeat.service'; import { BankTxReturnService } from '../../bank-tx/bank-tx-return/bank-tx-return.service'; import { createDefaultBankTx } from '../../bank-tx/bank-tx/__mocks__/bank-tx.entity.mock'; -import { createCustomBank, maerkiEUR } from '../../bank/bank/__mocks__/bank.entity.mock'; +import { createCustomBank, yapealEUR } from '../../bank/bank/__mocks__/bank.entity.mock'; import { BankService } from '../../bank/bank/bank.service'; import { IbanBankName } from '../../bank/bank/dto/bank.dto'; import { createCustomVirtualIban } from '../../bank/virtual-iban/__mocks__/virtual-iban.entity.mock'; @@ -119,18 +119,18 @@ describe('FiatOutputJobService', () => { jest .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ maerkiBaumannEnable: true })); + .mockResolvedValue(createCustomCountry({ yapealEnable: true })); - jest.spyOn(bankService, 'getSenderBank').mockResolvedValue(maerkiEUR); + jest.spyOn(bankService, 'getSenderBank').mockResolvedValue(yapealEUR); await service['assignBankAccount'](); const updateCalls = (fiatOutputRepo.update as jest.Mock).mock.calls; expect(updateCalls[0][0]).toBe(1); - expect(updateCalls[0][1]).toMatchObject({ originEntityId: 100, accountIban: maerkiEUR.iban }); + expect(updateCalls[0][1]).toMatchObject({ originEntityId: 100, accountIban: yapealEUR.iban }); expect(updateCalls[1][0]).toBe(3); - expect(updateCalls[1][1]).toMatchObject({ originEntityId: 102, accountIban: maerkiEUR.iban }); + expect(updateCalls[1][1]).toMatchObject({ originEntityId: 102, accountIban: yapealEUR.iban }); }); it('should use virtual IBAN when user has one for BuyFiat', async () => { @@ -147,12 +147,12 @@ describe('FiatOutputJobService', () => { jest .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ maerkiBaumannEnable: true })); + .mockResolvedValue(createCustomCountry({ yapealEnable: true })); // Mock virtual IBAN for user jest .spyOn(virtualIbanService, 'getActiveForUserAndCurrency') - .mockResolvedValue(createCustomVirtualIban({ iban: virtualIban, bank: maerkiEUR })); + .mockResolvedValue(createCustomVirtualIban({ iban: virtualIban, bank: yapealEUR })); await service['assignBankAccount'](); @@ -175,12 +175,12 @@ describe('FiatOutputJobService', () => { jest .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ maerkiBaumannEnable: true })); + .mockResolvedValue(createCustomCountry({ yapealEnable: true })); // Mock virtual IBAN for user jest .spyOn(virtualIbanService, 'getActiveForUserAndCurrency') - .mockResolvedValue(createCustomVirtualIban({ iban: virtualIban, bank: maerkiEUR })); + .mockResolvedValue(createCustomVirtualIban({ iban: virtualIban, bank: yapealEUR })); await service['assignBankAccount'](); diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 99c0aa2d58..3d0a4d2f6e 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -758,7 +758,7 @@ export class TransactionHelper implements OnModuleInit { static getDefaultBankByPaymentMethod(paymentMethod: PaymentMethod): CardBankName | IbanBankName { switch (paymentMethod) { case FiatPaymentMethod.BANK: - return IbanBankName.MAERKI; + return IbanBankName.YAPEAL; case FiatPaymentMethod.CARD: return CardBankName.CHECKOUT; case FiatPaymentMethod.INSTANT: From 004b1325cd810dcd3468e7c071d83572c32b4dd6 Mon Sep 17 00:00:00 2001 From: bernd2022 <104787072+bernd2022@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:35:04 +0100 Subject: [PATCH 7/8] fix: prettier formatting in fiat-output-job.service.spec.ts (#2891) --- .../__tests__/fiat-output-job.service.spec.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts b/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts index 249856da12..d57ab8403e 100644 --- a/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts +++ b/src/subdomains/supporting/fiat-output/__tests__/fiat-output-job.service.spec.ts @@ -117,9 +117,7 @@ describe('FiatOutputJobService', () => { }), ]); - jest - .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ yapealEnable: true })); + jest.spyOn(countryService, 'getCountryWithSymbol').mockResolvedValue(createCustomCountry({ yapealEnable: true })); jest.spyOn(bankService, 'getSenderBank').mockResolvedValue(yapealEUR); @@ -145,9 +143,7 @@ describe('FiatOutputJobService', () => { }), ]); - jest - .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ yapealEnable: true })); + jest.spyOn(countryService, 'getCountryWithSymbol').mockResolvedValue(createCustomCountry({ yapealEnable: true })); // Mock virtual IBAN for user jest @@ -173,9 +169,7 @@ describe('FiatOutputJobService', () => { }), ]); - jest - .spyOn(countryService, 'getCountryWithSymbol') - .mockResolvedValue(createCustomCountry({ yapealEnable: true })); + jest.spyOn(countryService, 'getCountryWithSymbol').mockResolvedValue(createCustomCountry({ yapealEnable: true })); // Mock virtual IBAN for user jest From e3eb43e0cb389921f734520027fbe7ce44df1a18 Mon Sep 17 00:00:00 2001 From: David May Date: Fri, 9 Jan 2026 10:55:44 +0100 Subject: [PATCH 8/8] feat: added migration --- migration/1767952500437-MaerkiRemoved.js | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 migration/1767952500437-MaerkiRemoved.js diff --git a/migration/1767952500437-MaerkiRemoved.js b/migration/1767952500437-MaerkiRemoved.js new file mode 100644 index 0000000000..2a96ef6e5e --- /dev/null +++ b/migration/1767952500437-MaerkiRemoved.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class MaerkiRemoved1767952500437 { + name = 'MaerkiRemoved1767952500437' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "country" DROP CONSTRAINT "DF_687dc858f7aff3f03ffbb214f2c"`); + await queryRunner.query(`ALTER TABLE "country" DROP COLUMN "maerkiBaumannEnable"`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "country" ADD "maerkiBaumannEnable" bit NOT NULL`); + await queryRunner.query(`ALTER TABLE "country" ADD CONSTRAINT "DF_687dc858f7aff3f03ffbb214f2c" DEFAULT 0 FOR "maerkiBaumannEnable"`); + } +}