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 e92b81fddc..01c4c4b5b5 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 @@ -386,6 +386,14 @@ export class BankTxService implements OnModuleInit { .getOne(); } + async getBankTxByEndToEndId(endToEndId: string): Promise { + return this.bankTxRepo.findOne({ + where: { endToEndId, creditDebitIndicator: BankTxIndicator.DEBIT }, + relations: { transaction: true }, + order: { id: 'DESC' }, + }); + } + async getBankTxByTransactionId(transactionId: number, relations?: FindOptionsRelations): Promise { return this.bankTxRepo.findOne({ where: { transaction: { id: transactionId } }, relations }); } 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 d57ab8403e..e7b8264ef1 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 @@ -16,7 +16,7 @@ import { createCustomSell } from 'src/subdomains/core/sell-crypto/route/__mocks_ import { BankTxService } from 'src/subdomains/supporting/bank-tx/bank-tx/services/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 { createCustomBankTx } from '../../bank-tx/bank-tx/__mocks__/bank-tx.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'; @@ -107,7 +107,7 @@ describe('FiatOutputJobService', () => { id: 2, type: FiatOutputType.BANK_TX_REPEAT, isComplete: false, - bankTx: createDefaultBankTx(), + bankTx: createCustomBankTx({}), }), createCustomFiatOutput({ id: 3, @@ -474,4 +474,62 @@ describe('FiatOutputJobService', () => { expect(updateCalls[1][0]).toBe(2); }); }); + + describe('searchOutgoingBankTx', () => { + it('should match FiatOutput via remittanceInfo', async () => { + const bankTx = createCustomBankTx({ id: 100, created: new Date('2024-01-01') }); + const fiatOutput = createCustomFiatOutput({ + id: 1, + remittanceInfo: 'DFX-123', + isComplete: false, + isReadyDate: new Date('2024-01-01'), + }); + + jest.spyOn(fiatOutputRepo, 'find').mockResolvedValue([fiatOutput]); + jest.spyOn(bankTxService, 'getBankTxByRemittanceInfo').mockResolvedValue(bankTx); + + await service['searchOutgoingBankTx'](); + + expect(bankTxService.getBankTxByRemittanceInfo).toHaveBeenCalledWith('DFX-123'); + expect(fiatOutputRepo.update).toHaveBeenCalledWith(1, expect.objectContaining({ isComplete: true, bankTx })); + }); + + it('should match FiatOutput via endToEndId when remittanceInfo is not set', async () => { + const bankTx = createCustomBankTx({ id: 200, created: new Date('2024-01-01') }); + const fiatOutput = createCustomFiatOutput({ + id: 2, + endToEndId: 'E2E-79057', + remittanceInfo: undefined, + isComplete: false, + isReadyDate: new Date('2024-01-01'), + type: FiatOutputType.LIQ_MANAGEMENT, + }); + + jest.spyOn(fiatOutputRepo, 'find').mockResolvedValue([fiatOutput]); + jest.spyOn(bankTxService, 'getBankTxByRemittanceInfo').mockResolvedValue(null); + jest.spyOn(bankTxService, 'getBankTxByEndToEndId').mockResolvedValue(bankTx); + + await service['searchOutgoingBankTx'](); + + expect(bankTxService.getBankTxByEndToEndId).toHaveBeenCalledWith('E2E-79057'); + expect(fiatOutputRepo.update).toHaveBeenCalledWith(2, expect.objectContaining({ isComplete: true, bankTx })); + }); + + it('should not match if BankTx created before FiatOutput isReadyDate', async () => { + const bankTx = createCustomBankTx({ id: 300, created: new Date('2024-01-01') }); + const fiatOutput = createCustomFiatOutput({ + id: 3, + endToEndId: 'E2E-79058', + isComplete: false, + isReadyDate: new Date('2024-01-02'), // after BankTx.created + }); + + jest.spyOn(fiatOutputRepo, 'find').mockResolvedValue([fiatOutput]); + jest.spyOn(bankTxService, 'getBankTxByEndToEndId').mockResolvedValue(bankTx); + + await service['searchOutgoingBankTx'](); + + expect(fiatOutputRepo.update).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts b/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts index b6efc350fd..44b5d9b6ff 100644 --- a/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts +++ b/src/subdomains/supporting/fiat-output/fiat-output-job.service.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { CronExpression } from '@nestjs/schedule'; -import { Config } from 'src/config/config'; import { isLiechtensteinBankHoliday } from 'src/config/bank-holiday.config'; +import { Config } from 'src/config/config'; import { Pain001Payment } from 'src/integration/bank/services/iso20022.service'; import { YapealService } from 'src/integration/bank/services/yapeal.service'; import { AzureStorageService } from 'src/integration/infrastructure/azure-storage.service'; @@ -86,9 +86,19 @@ export class FiatOutputJobService { // --- HELPER METHODS --- // private async getMatchingBankTx(entity: FiatOutput): Promise { - if (!entity.remittanceInfo) return undefined; + // Try remittanceInfo first + if (entity.remittanceInfo) { + const bankTx = await this.bankTxService.getBankTxByRemittanceInfo(entity.remittanceInfo); + if (bankTx) return bankTx; + } - return this.bankTxService.getBankTxByRemittanceInfo(entity.remittanceInfo); + // Fallback to endToEndId (used for Yapeal LiqManagement payments) + if (entity.endToEndId) { + const bankTx = await this.bankTxService.getBankTxByEndToEndId(entity.endToEndId); + if (bankTx) return bankTx; + } + + return undefined; } private async getPayoutAccount(entity: FiatOutput, country: Country): Promise<{ accountIban: string; bank: Bank }> { @@ -435,6 +445,9 @@ export class FiatOutputJobService { case FiatOutputType.BANK_TX_RETURN: return this.bankTxService.updateInternal(bankTx, { type: BankTxType.BANK_TX_RETURN_CHARGEBACK }); + + case FiatOutputType.LIQ_MANAGEMENT: + return this.bankTxService.updateInternal(bankTx, { type: BankTxType.INTERNAL }); } } } diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index cd00bdf80b..1b7f00094b 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -427,7 +427,12 @@ export class TransactionHelper implements OnModuleInit { }); const dfxFeeAmount = inputAmount * chargebackFee.rate + price.convert(chargebackFee.fixed); - const networkFeeAmount = price.convert(chargebackFee.network); + + let networkFeeAmount = price.convert(chargebackFee.network); + + if (isAsset(inputCurrency) && inputCurrency.blockchain === Blockchain.SOLANA) + networkFeeAmount += await this.getSolanaRentExemptionFee(inputCurrency); + const bankFeeAmount = refundEntity.paymentMethodIn === FiatPaymentMethod.BANK ? price.convert( @@ -654,6 +659,13 @@ export class TransactionHelper implements OnModuleInit { return price.convert(fee); } + private async getSolanaRentExemptionFee(asset: Asset): Promise { + if (asset.type !== AssetType.COIN) return 0; + + const price = await this.pricingService.getPrice(asset, PriceCurrency.CHF, PriceValidity.ANY); + return price.convert(Config.blockchain.solana.minimalCoinAccountRent) * 1.05; // 5% buffer for rounding + } + private async getTronCreateAccountFee(user: User, asset: Asset): Promise { const tronService = this.blockchainRegistryService.getService(asset.blockchain) as TronService;