Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,14 @@ export class BankTxService implements OnModuleInit {
.getOne();
}

async getBankTxByEndToEndId(endToEndId: string): Promise<BankTx> {
return this.bankTxRepo.findOne({
where: { endToEndId, creditDebitIndicator: BankTxIndicator.DEBIT },
relations: { transaction: true },
order: { id: 'DESC' },
});
}

async getBankTxByTransactionId(transactionId: number, relations?: FindOptionsRelations<BankTx>): Promise<BankTx> {
return this.bankTxRepo.findOne({ where: { transaction: { id: transactionId } }, relations });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('FiatOutputJobService', () => {
id: 2,
type: FiatOutputType.BANK_TX_REPEAT,
isComplete: false,
bankTx: createDefaultBankTx(),
bankTx: createCustomBankTx({}),
}),
createCustomFiatOutput({
id: 3,
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -86,9 +86,19 @@ export class FiatOutputJobService {
// --- HELPER METHODS --- //

private async getMatchingBankTx(entity: FiatOutput): Promise<BankTx> {
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 }> {
Expand Down Expand Up @@ -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 });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -654,6 +659,13 @@ export class TransactionHelper implements OnModuleInit {
return price.convert(fee);
}

private async getSolanaRentExemptionFee(asset: Asset): Promise<number> {
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<number> {
const tronService = this.blockchainRegistryService.getService(asset.blockchain) as TronService;

Expand Down
Loading