diff --git a/migration/1761754328324-AddBuyCryptoBuyFiatPlatformFeeAmount.js b/migration/1761754328324-AddBuyCryptoBuyFiatPlatformFeeAmount.js new file mode 100644 index 0000000000..5820cabaa0 --- /dev/null +++ b/migration/1761754328324-AddBuyCryptoBuyFiatPlatformFeeAmount.js @@ -0,0 +1,21 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddBuyCryptoBuyFiatPlatformFeeAmount1761754328324 { + name = 'AddBuyCryptoBuyFiatPlatformFeeAmount1761754328324' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "buy_fiat" ADD "partnerFeeAmount" float`); + await queryRunner.query(`ALTER TABLE "buy_crypto" ADD "partnerFeeAmount" float`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "buy_crypto" DROP COLUMN "partnerFeeAmount"`); + await queryRunner.query(`ALTER TABLE "buy_fiat" DROP COLUMN "partnerFeeAmount"`); + } +} diff --git a/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts b/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts index 5fbe905116..6aa4620cb3 100644 --- a/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts +++ b/src/subdomains/core/buy-crypto/process/entities/buy-crypto.entity.ts @@ -23,12 +23,13 @@ import { FiatOutput } from 'src/subdomains/supporting/fiat-output/fiat-output.en import { CheckoutTx } from 'src/subdomains/supporting/fiat-payin/entities/checkout-tx.entity'; import { MailTranslationKey } from 'src/subdomains/supporting/notification/factories/mail.factory'; import { CryptoInput } from 'src/subdomains/supporting/payin/entities/crypto-input.entity'; -import { FeeDto, InternalFeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; +import { InternalFeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { CryptoPaymentMethod, FiatPaymentMethod, PaymentMethod, } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; +import { FeeType } from 'src/subdomains/supporting/payment/entities/fee.entity'; import { SpecialExternalAccount } from 'src/subdomains/supporting/payment/entities/special-external-account.entity'; import { Transaction } from 'src/subdomains/supporting/payment/entities/transaction.entity'; import { Price, PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; @@ -121,6 +122,9 @@ export class BuyCrypto extends IEntity { @Column({ length: 256, nullable: true }) usedRef?: string; + @Column({ length: 256, nullable: true }) + usedPartnerFeeRef?: string; + @Column({ type: 'float', nullable: true }) refProvision?: number; @@ -150,6 +154,9 @@ export class BuyCrypto extends IEntity { @Column({ type: 'float', nullable: true }) bankFeeAmount?: number; //inputReferenceAsset + @Column({ type: 'float', nullable: true }) + partnerFeeAmount?: number; //inputReferenceAsset + @Column({ type: 'float', nullable: true }) percentFeeAmount?: number; //inputReferenceAsset @@ -475,11 +482,11 @@ export class BuyCrypto extends IEntity { } setFeeAndFiatReference( - fee: InternalFeeDto & FeeDto, + fee: InternalFeeDto, minFeeAmountFiat: number, totalFeeAmountChf: number, ): UpdateResult { - const { usedRef, refProvision } = this.user.specifiedRef; + const partnerFee = fee.partner ? fee.fees.find((f) => f.type === FeeType.PARTNER) : undefined; const inputReferenceAmountMinusFee = this.inputReferenceAmount - fee.total; const update: Partial = @@ -495,10 +502,12 @@ export class BuyCrypto extends IEntity { totalFeeAmountChf, blockchainFee: fee.network, bankFeeAmount: fee.bank, + partnerFeeAmount: fee.partner, + usedPartnerFeeRef: fee.partner ? partnerFee.wallet.owner.ref : undefined, inputReferenceAmountMinusFee, - usedRef, - refProvision, - refFactor: !fee.payoutRefBonus || usedRef === Config.defaultRef ? 0 : 1, + usedRef: this.user.usedRef, + refProvision: this.user.refFeePercent, + refFactor: !fee.payoutRefBonus || this.user.usedRef === Config.defaultRef ? 0 : 1, usedFees: fee.fees?.map((fee) => fee.id).join(';'), networkStartFeeAmount: fee.networkStart, status: this.status === BuyCryptoStatus.WAITING_FOR_LOWER_FEE ? BuyCryptoStatus.CREATED : undefined, @@ -595,6 +604,8 @@ export class BuyCrypto extends IEntity { chargebackAllowedBy: null, chargebackOutput: null, priceDefinitionAllowedDate: null, + partnerFeeAmount: null, + usedPartnerFeeRef: null, }; Object.assign(this, update); 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 3544f48823..61ed813835 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 @@ -883,13 +883,18 @@ export class BuyCryptoService { for (const ref of refs) { const { volume: buyCryptoVolume, credit: buyCryptoCredit } = await this.getRefVolume(ref); + const { volume: buyCryptoPartnerVolume, credit: buyCryptoPartnerCredit } = await this.getPartnerFeeRefVolume(ref); const { volume: buyFiatVolume, credit: buyFiatCredit } = await this.buyFiatService.getRefVolume(ref); + const { volume: buyFiatPartnerVolume, credit: buyFiatPartnerCredit } = + await this.buyFiatService.getPartnerFeeRefVolume(ref); const { volume: manualVolume, credit: manualCredit } = await this.transactionService.getManualRefVolume(ref); await this.userService.updateRefVolume( ref, buyCryptoVolume + buyFiatVolume + manualVolume, buyCryptoCredit + buyFiatCredit + manualCredit, + buyCryptoPartnerVolume + buyFiatPartnerVolume, + buyCryptoPartnerCredit + buyFiatPartnerCredit, ); } } @@ -906,6 +911,18 @@ export class BuyCryptoService { return { volume: volume ?? 0, credit: credit ?? 0 }; } + async getPartnerFeeRefVolume(ref: string): Promise<{ volume: number; credit: number }> { + const { volume, credit } = await this.buyCryptoRepo + .createQueryBuilder('buyCrypto') + .select('SUM(amountInEur)', 'volume') + .addSelect('SUM(partnerFeeAmount * (amountInEur/amountInChf ))', 'credit') + .where('usedPartnerFeeRef = :ref', { ref }) + .andWhere('amlCheck = :check', { check: CheckStatus.PASS }) + .getRawOne<{ volume: number; credit: number }>(); + + return { volume: volume ?? 0, credit: credit ?? 0 }; + } + // Admin Support Tool methods async getAllRefTransactions(refCodes: string[]): Promise { diff --git a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts index 7d7e7dbbc6..b899cd5eef 100644 --- a/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/buy/buy.controller.ts @@ -283,7 +283,7 @@ export class BuyController { annualVolume: buy.annualVolume, bankUsage: buy.active ? buy.bankUsage : undefined, asset: AssetDtoMapper.toDto(buy.asset), - fee: Util.round(fee.rate * 100, Config.defaultPercentageDecimal), + fee: Util.round(fee.dfx.rate * 100, Config.defaultPercentageDecimal), minDeposits: [minDeposit], minFee: { amount: fee.network, asset: 'CHF' }, }; diff --git a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts index 58ed4f7bca..22a210ae62 100644 --- a/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts +++ b/src/subdomains/core/buy-crypto/routes/swap/swap.controller.ts @@ -217,7 +217,7 @@ export class SwapController { deposit: swap.active ? DepositDtoMapper.entityToDto(swap.deposit) : undefined, asset: AssetDtoMapper.toDto(swap.asset), blockchain: swap.deposit.blockchainList[0], - fee: Util.round(fee.rate * 100, Config.defaultPercentageDecimal), + fee: Util.round(fee.dfx.rate * 100, Config.defaultPercentageDecimal), minDeposits: [minDeposit], minFee: { amount: fee.network, asset: 'CHF' }, }; diff --git a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts index ea582f0784..edb7130945 100644 --- a/src/subdomains/core/history/__tests__/transaction-helper.spec.ts +++ b/src/subdomains/core/history/__tests__/transaction-helper.spec.ts @@ -16,9 +16,9 @@ import { CardBankName, IbanBankName } from 'src/subdomains/supporting/bank/bank/ 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 { - createCustomInternalChargebackFeeDto, - createInternalChargebackFeeDto, -} from 'src/subdomains/supporting/payment/__mocks__/internal-fee.dto.mock'; + createChargebackFeeInfo, + createCustomChargebackFeeInfo, +} from 'src/subdomains/supporting/payment/__mocks__/fee.dto.mock'; import { createCustomTransaction } from 'src/subdomains/supporting/payment/__mocks__/transaction.entity.mock'; import { TransactionSpecificationRepository } from 'src/subdomains/supporting/payment/repositories/transaction-specification.repository'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; @@ -97,7 +97,7 @@ describe('TransactionHelper', () => { }); jest.spyOn(fiatService, 'getFiatByName').mockResolvedValue(createCustomFiat({ name: 'CHF' })); - jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createInternalChargebackFeeDto()); + jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createChargebackFeeInfo()); jest .spyOn(pricingService, 'getPrice') .mockResolvedValue(createCustomPrice({ source: 'CHF', target: 'CHF', price: 1 })); @@ -156,7 +156,7 @@ describe('TransactionHelper', () => { jest.spyOn(feeService, 'getBlockchainFee').mockResolvedValue(0.01); jest.spyOn(fiatService, 'getFiatByName').mockResolvedValue(createDefaultFiat()); - jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createInternalChargebackFeeDto()); + jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createChargebackFeeInfo()); jest.spyOn(pricingService, 'getPrice').mockResolvedValue(createCustomPrice({ price: 1 })); await expect( @@ -184,9 +184,7 @@ describe('TransactionHelper', () => { jest.spyOn(feeService, 'getBlockchainFee').mockResolvedValue(0.01); jest.spyOn(fiatService, 'getFiatByName').mockResolvedValue(createDefaultFiat()); - jest - .spyOn(feeService, 'getChargebackFee') - .mockResolvedValue(createCustomInternalChargebackFeeDto({ network: 0.01 })); + jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createCustomChargebackFeeInfo({ network: 0.01 })); jest.spyOn(pricingService, 'getPrice').mockResolvedValue(createCustomPrice({ price: 1 })); await expect( @@ -215,9 +213,7 @@ describe('TransactionHelper', () => { jest.spyOn(feeService, 'getBlockchainFee').mockResolvedValue(0.01); jest.spyOn(fiatService, 'getFiatByName').mockResolvedValue(createDefaultFiat()); - jest - .spyOn(feeService, 'getChargebackFee') - .mockResolvedValue(createCustomInternalChargebackFeeDto({ network: 0.01 })); + jest.spyOn(feeService, 'getChargebackFee').mockResolvedValue(createCustomChargebackFeeInfo({ network: 0.01 })); jest.spyOn(pricingService, 'getPrice').mockResolvedValue(createCustomPrice({ price: 1 })); await expect( diff --git a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts index d6239f1501..203e1307a9 100644 --- a/src/subdomains/core/history/mappers/transaction-dto.mapper.ts +++ b/src/subdomains/core/history/mappers/transaction-dto.mapper.ts @@ -357,6 +357,10 @@ export class TransactionDtoMapper { feeAmountType(entity.inputAssetEntity), ) : null, + platform: + entity.partnerFeeAmount != null + ? Util.roundReadable(entity.partnerFeeAmount * referencePrice, feeAmountType(entity.inputAssetEntity)) + : null, total: entity.totalFeeAmount != null ? Util.roundReadable(totalFee * referencePrice, feeAmountType(entity.inputAssetEntity)) diff --git a/src/subdomains/core/referral/reward/services/ref-reward.service.ts b/src/subdomains/core/referral/reward/services/ref-reward.service.ts index 318e7907a4..0fafde7569 100644 --- a/src/subdomains/core/referral/reward/services/ref-reward.service.ts +++ b/src/subdomains/core/referral/reward/services/ref-reward.service.ts @@ -147,7 +147,7 @@ export class RefRewardService { : await this.assetService.getNativeAsset(blockchain); for (const user of users) { - const refCreditEur = user.refCredit - user.paidRefCredit; + const refCreditEur = user.completeRefCredit - user.paidRefCredit; const minCredit = PayoutLimits[blockchain]; if (!(refCreditEur >= minCredit)) continue; 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 a338bbc89a..ed83e0ecd5 100644 --- a/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts +++ b/src/subdomains/core/sell-crypto/process/buy-fiat.entity.ts @@ -14,12 +14,13 @@ import { Wallet } from 'src/subdomains/generic/user/models/wallet/wallet.entity' import { BankTx } from 'src/subdomains/supporting/bank-tx/bank-tx/entities/bank-tx.entity'; import { MailTranslationKey } from 'src/subdomains/supporting/notification/factories/mail.factory'; import { CryptoInput } from 'src/subdomains/supporting/payin/entities/crypto-input.entity'; -import { FeeDto, InternalFeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; +import { InternalFeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { CryptoPaymentMethod, FiatPaymentMethod, PaymentMethod, } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; +import { FeeType } from 'src/subdomains/supporting/payment/entities/fee.entity'; import { SpecialExternalAccount } from 'src/subdomains/supporting/payment/entities/special-external-account.entity'; import { Price, PriceStep } from 'src/subdomains/supporting/pricing/domain/entities/price'; import { PriceCurrency } from 'src/subdomains/supporting/pricing/services/pricing.service'; @@ -87,6 +88,9 @@ export class BuyFiat extends IEntity { @Column({ length: 256, nullable: true }) usedRef?: string; + @Column({ length: 256, nullable: true }) + usedPartnerFeeRef?: string; + @Column({ type: 'float', nullable: true }) refProvision?: number; @@ -116,6 +120,9 @@ export class BuyFiat extends IEntity { @Column({ type: 'float', nullable: true }) bankFeeAmount?: number; //inputAsset + @Column({ type: 'float', nullable: true }) + partnerFeeAmount?: number; //inputAsset + @Column({ type: 'float', nullable: true }) percentFeeAmount?: number; //inputAsset @@ -283,11 +290,11 @@ export class BuyFiat extends IEntity { } setFeeAndFiatReference( - fee: InternalFeeDto & FeeDto, + fee: InternalFeeDto, minFeeAmountFiat: number, totalFeeAmountChf: number, ): UpdateResult { - const { usedRef, refProvision } = this.user.specifiedRef; + const partnerFee = fee.partner ? fee.fees.find((f) => f.type === FeeType.PARTNER) : undefined; const inputReferenceAmountMinusFee = this.inputReferenceAmount - fee.total; const update: Partial = @@ -303,10 +310,12 @@ export class BuyFiat extends IEntity { totalFeeAmountChf, blockchainFee: fee.network, bankFeeAmount: fee.bank, + partnerFeeAmount: fee.partner, + usedPartnerFeeRef: fee.partner ? partnerFee.wallet.owner.ref : undefined, inputReferenceAmountMinusFee, - usedRef, - refProvision, - refFactor: !fee.payoutRefBonus || usedRef === Config.defaultRef ? 0 : 1, + usedRef: this.user.usedRef, + refProvision: this.user.refFeePercent, + refFactor: !fee.payoutRefBonus || this.user.usedRef === Config.defaultRef ? 0 : 1, usedFees: fee.fees?.map((fee) => fee.id).join(';'), }; @@ -469,6 +478,8 @@ export class BuyFiat extends IEntity { chargebackAllowedDateUser: null, chargebackAmount: null, chargebackAllowedBy: null, + partnerFeeAmount: null, + usedPartnerFeeRef: 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 386b64aaf2..68fce85286 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 @@ -483,13 +483,18 @@ export class BuyFiatService { for (const ref of refs) { const { volume: buyFiatVolume, credit: buyFiatCredit } = await this.getRefVolume(ref); + const { volume: buyFiatPartnerVolume, credit: buyFiatPartnerCredit } = await this.getPartnerFeeRefVolume(ref); const { volume: buyCryptoVolume, credit: buyCryptoCredit } = await this.buyCryptoService.getRefVolume(ref); + const { volume: buyCryptoPartnerVolume, credit: buyCryptoPartnerCredit } = + await this.buyCryptoService.getPartnerFeeRefVolume(ref); const { volume: manualVolume, credit: manualCredit } = await this.transactionService.getManualRefVolume(ref); await this.userService.updateRefVolume( ref, buyFiatVolume + buyCryptoVolume + manualVolume, buyFiatCredit + buyCryptoCredit + manualCredit, + buyFiatPartnerVolume + buyCryptoPartnerVolume, + buyFiatPartnerCredit + buyCryptoPartnerCredit, ); } } @@ -506,6 +511,18 @@ export class BuyFiatService { return { volume: volume ?? 0, credit: credit ?? 0 }; } + async getPartnerFeeRefVolume(ref: string): Promise<{ volume: number; credit: number }> { + const { volume, credit } = await this.buyFiatRepo + .createQueryBuilder('buyFiat') + .select('SUM(amountInEur)', 'volume') + .addSelect('SUM(partnerFeeAmount * (amountInEur/amountInChf ))', 'credit') + .where('usedPartnerFeeRef = :ref', { ref }) + .andWhere('amlCheck = :check', { check: CheckStatus.PASS }) + .getRawOne<{ volume: number; credit: number }>(); + + return { volume: volume ?? 0, credit: credit ?? 0 }; + } + // Statistics async getTransactions(dateFrom: Date = new Date(0), dateTo: Date = new Date()): Promise { diff --git a/src/subdomains/core/sell-crypto/route/sell.controller.ts b/src/subdomains/core/sell-crypto/route/sell.controller.ts index 5eae1d23c5..a20af30530 100644 --- a/src/subdomains/core/sell-crypto/route/sell.controller.ts +++ b/src/subdomains/core/sell-crypto/route/sell.controller.ts @@ -246,7 +246,7 @@ export class SellController { fiat: FiatDtoMapper.toDto(sell.fiat), currency: FiatDtoMapper.toDto(sell.fiat), deposit: sell.active ? DepositDtoMapper.entityToDto(sell.deposit) : undefined, - fee: Util.round(fee.rate * 100, Config.defaultPercentageDecimal), + fee: Util.round(fee.dfx.rate * 100, Config.defaultPercentageDecimal), blockchain: sell.deposit.blockchainList[0], minFee: { amount: fee.network, asset: 'CHF' }, minDeposits: [minDeposit], diff --git a/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts b/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts index 0efdbab538..07f1774464 100644 --- a/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts +++ b/src/subdomains/generic/user/models/user/dto/user-dto.mapper.ts @@ -74,8 +74,8 @@ export class UserDtoMapper { const dto: ReferralDto = { code: user.ref, commission: Util.round(user.refFeePercent / 100, 4), - volume: user.refVolume, - credit: user.refCredit, + volume: user.completeRefVolume, + credit: user.completeRefCredit, paidCredit: user.paidRefCredit, userCount: userCount, activeUserCount: activeUserCount, diff --git a/src/subdomains/generic/user/models/user/user.entity.ts b/src/subdomains/generic/user/models/user/user.entity.ts index 8ddea97a29..2311b20b62 100644 --- a/src/subdomains/generic/user/models/user/user.entity.ts +++ b/src/subdomains/generic/user/models/user/user.entity.ts @@ -1,4 +1,3 @@ -import { Config } from 'src/config/config'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; import { CryptoService } from 'src/integration/blockchain/shared/services/crypto.service'; import { UserRole } from 'src/shared/auth/user-role.enum'; @@ -123,6 +122,12 @@ export class User extends IEntity { @Column({ type: 'float', default: 0 }) refCredit: number; // EUR + @Column({ type: 'float', default: 0 }) + partnerRefVolume: number; // EUR + + @Column({ type: 'float', default: 0 }) + partnerRefCredit: number; // EUR + @Column({ type: 'float', default: 0 }) paidRefCredit: number; // EUR @@ -191,12 +196,6 @@ export class User extends IEntity { return [this.id, update]; } - get specifiedRef(): { usedRef: string; refProvision: number } { - return this.wallet?.name === 'CakeWallet' - ? { usedRef: '160-195', refProvision: 2 } - : { usedRef: this.usedRef, refProvision: this.usedRef === Config.defaultRef ? 0 : this.refFeePercent }; - } - get blockchains(): Blockchain[] { // wallet name / blockchain map const customChains = { @@ -217,6 +216,14 @@ export class User extends IEntity { get isDeleted(): boolean { return this.status === UserStatus.DELETED; } + + get completeRefVolume(): number { + return this.refVolume + this.partnerRefVolume; + } + + get completeRefCredit(): number { + return this.refCredit + this.partnerRefCredit; + } } export const UserSupportUpdateCols = ['status', 'setRef']; diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index 16c75e928a..2748840151 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -26,7 +26,7 @@ import { HistoryFilter, HistoryFilterKey } from 'src/subdomains/core/history/dto import { KycInputDataDto } from 'src/subdomains/generic/kyc/dto/input/kyc-data.dto'; import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; import { CardBankName, IbanBankName } from 'src/subdomains/supporting/bank/bank/dto/bank.dto'; -import { InternalFeeDto } from 'src/subdomains/supporting/payment/dto/fee.dto'; +import { FeeInfo } from 'src/subdomains/supporting/payment/dto/fee.dto'; import { PaymentMethod } from 'src/subdomains/supporting/payment/dto/payment-method.enum'; import { FeeService } from 'src/subdomains/supporting/payment/services/fee.service'; import { Between, FindOptionsRelations, Not } from 'typeorm'; @@ -447,7 +447,7 @@ export class UserService { bankOut: CardBankName | IbanBankName, from: Active, to: Active, - ): Promise { + ): Promise { const user = await this.getUser(userId, { userData: true }); if (!user) throw new NotFoundException('User not found'); @@ -521,12 +521,20 @@ export class UserService { }; } - async updateRefVolume(ref: string, volume: number, credit: number): Promise { + async updateRefVolume( + ref: string, + volume: number, + credit: number, + partnerVolume?: number, + partnerCredit?: number, + ): Promise { await this.userRepo.update( { ref }, { refVolume: Util.round(volume, Config.defaultVolumeDecimal), refCredit: Util.round(credit, Config.defaultVolumeDecimal), + partnerRefVolume: Util.round(partnerVolume, Config.defaultVolumeDecimal), + partnerRefCredit: Util.round(partnerCredit, Config.defaultVolumeDecimal), }, ); } @@ -625,8 +633,8 @@ export class UserService { return { ref: user.ref, refFeePercent: user.refFeePercent, - refVolume: user.refVolume, - refCredit: user.refCredit, + refVolume: user.completeRefVolume, + refCredit: user.completeRefCredit, paidRefCredit: user.paidRefCredit, ...(await this.getRefUserCounts(user)), }; diff --git a/src/subdomains/generic/user/models/wallet/wallet.entity.ts b/src/subdomains/generic/user/models/wallet/wallet.entity.ts index 880b9a909c..ec7262382c 100644 --- a/src/subdomains/generic/user/models/wallet/wallet.entity.ts +++ b/src/subdomains/generic/user/models/wallet/wallet.entity.ts @@ -3,7 +3,7 @@ import { AmlRule } from 'src/subdomains/core/aml/enums/aml-rule.enum'; import { KycStepType } from 'src/subdomains/generic/kyc/enums/kyc.enum'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; import { MailContextType } from 'src/subdomains/supporting/notification/enums'; -import { Column, Entity, Index, OneToMany } from 'typeorm'; +import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm'; import { WebhookType } from '../../services/webhook/dto/webhook.dto'; import { KycType } from '../user-data/user-data.enum'; @@ -21,6 +21,9 @@ export enum WebhookConfigOption { @Entity() export class Wallet extends IEntity { + @ManyToOne(() => User, { nullable: true }) + owner?: User; + @Column({ length: 256, nullable: true }) @Index({ unique: true, where: 'address IS NOT NULL' }) address?: string; diff --git a/src/subdomains/supporting/payment/__mocks__/fee.dto.mock.ts b/src/subdomains/supporting/payment/__mocks__/fee.dto.mock.ts new file mode 100644 index 0000000000..25201d62a8 --- /dev/null +++ b/src/subdomains/supporting/payment/__mocks__/fee.dto.mock.ts @@ -0,0 +1,35 @@ +import { FeeInfo } from '../dto/fee.dto'; +import { createDefaultFee } from './fee.entity.mock'; + +const defaultFeeInfo: FeeInfo = { + fees: [createDefaultFee()], + dfx: { fixed: 0, rate: 0.01 }, + bank: { fixed: 0, rate: 0 }, + partner: { fixed: 0, rate: 0 }, + network: 0, + payoutRefBonus: false, +}; + +const defaultChargebackFeeInfo: FeeInfo = { + fees: [createDefaultFee()], + dfx: { fixed: 0, rate: 0 }, + bank: { fixed: 0, rate: 0 }, + partner: { fixed: 0, rate: 0 }, + network: 0, + payoutRefBonus: false, +}; + +export function createFeeInfo(): FeeInfo { + return createCustomFeeInfo({}); +} + +export function createCustomFeeInfo(customValues: Partial): FeeInfo { + return { ...defaultFeeInfo, ...customValues }; +} +export function createChargebackFeeInfo(): FeeInfo { + return createCustomChargebackFeeInfo({}); +} + +export function createCustomChargebackFeeInfo(customValues: Partial): FeeInfo { + return { ...defaultChargebackFeeInfo, ...customValues }; +} diff --git a/src/subdomains/supporting/payment/__mocks__/internal-fee.dto.mock.ts b/src/subdomains/supporting/payment/__mocks__/internal-fee.dto.mock.ts deleted file mode 100644 index d8c1612b2b..0000000000 --- a/src/subdomains/supporting/payment/__mocks__/internal-fee.dto.mock.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { InternalChargebackFeeDto, InternalFeeDto } from '../dto/fee.dto'; -import { createDefaultFee } from './fee.entity.mock'; - -const defaultInternalFeeDto: Partial = { - fees: [createDefaultFee()], - bankFixed: 0, - bankRate: 0, - fixed: 0, - network: 0, - payoutRefBonus: false, - rate: 0.01, -}; - -const defaultInternalChargebackFeeDto: Partial = { - fees: [createDefaultFee()], - fixed: 0, - network: 0, - rate: 0, - bankFixed: 0, - bankRate: 0, -}; - -export function createInternalFeeDto(): InternalFeeDto { - return createCustomInternalFeeDto({}); -} - -export function createCustomInternalFeeDto(customValues: Partial): InternalFeeDto { - return Object.assign(new InternalFeeDto(), { ...defaultInternalFeeDto, ...customValues }); -} - -export function createInternalChargebackFeeDto(): InternalChargebackFeeDto { - return createCustomInternalChargebackFeeDto({}); -} - -export function createCustomInternalChargebackFeeDto(customValues: Partial): InternalChargebackFeeDto { - return Object.assign(new InternalChargebackFeeDto(), { ...defaultInternalChargebackFeeDto, ...customValues }); -} diff --git a/src/subdomains/supporting/payment/dto/fee.dto.ts b/src/subdomains/supporting/payment/dto/fee.dto.ts index d39f676cd9..1120ccf19b 100644 --- a/src/subdomains/supporting/payment/dto/fee.dto.ts +++ b/src/subdomains/supporting/payment/dto/fee.dto.ts @@ -1,42 +1,80 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Fee } from '../entities/fee.entity'; +import { TxSpec } from './transaction-helper/tx-spec.dto'; + +export class FeeDto { + @ApiProperty({ description: 'Minimum fee amount' }) + min: number; -export class BaseFeeDto { @ApiProperty({ description: 'Fee rate' }) rate: number; // final fee rate @ApiProperty({ description: 'Fixed fee amount' }) fixed: number; // final fixed fee + @ApiProperty({ description: 'DFX fee amount' }) + dfx: number; + @ApiProperty({ description: 'Network fee amount' }) network: number; // final network fee -} -export class FeeDto extends BaseFeeDto { - @ApiProperty({ description: 'Minimum fee amount' }) - min: number; + @ApiPropertyOptional({ description: 'Network start fee' }) + networkStart?: number; - @ApiProperty({ description: 'DFX fee amount' }) - dfx: number; + @ApiProperty({ description: 'Platform fee amount' }) + platform: number; @ApiProperty({ description: 'Bank fee amount' }) bank: number; // final bank fee addition @ApiProperty({ description: 'Total fee amount (DFX + bank + network fee)' }) total: number; +} - @ApiPropertyOptional({ description: 'Network start fee' }) +export interface InternalFeeDto { + fees: Fee[]; + min: number; + rate: number; + fixed: number; + bank: number; + partner: number; + network: number; networkStart?: number; + total: number; + payoutRefBonus: boolean; } -export class InternalBaseFeeDto extends BaseFeeDto { - fees: Fee[]; - bankRate: number; // bank fee rate - bankFixed: number; // bank fixed fee +export interface FeeAmountsDto { + dfx: number; + bank: number; + partner: number; + total: number; } -export class InternalFeeDto extends InternalBaseFeeDto { +export interface FeeInfo { + fees: Fee[]; + dfx: FeeSpec; + bank: FeeSpec; + partner: FeeSpec; + network: number; payoutRefBonus: boolean; } -export class InternalChargebackFeeDto extends InternalBaseFeeDto {} +export interface FeeSpec { + rate: number; + fixed: number; +} + +export function toFeeDto(amounts: FeeAmountsDto, spec: TxSpec): FeeDto { + return Object.assign(new FeeDto(), { + min: spec.fee.min, + rate: spec.fee.dfx.rate, + fixed: spec.fee.dfx.fixed, + network: spec.fee.network, + networkStart: spec.fee.networkStart, + dfx: amounts.dfx, + platform: amounts.partner, + bank: amounts.bank, + total: amounts.total, + }); +} diff --git a/src/subdomains/supporting/payment/dto/transaction-helper/tx-spec.dto.ts b/src/subdomains/supporting/payment/dto/transaction-helper/tx-spec.dto.ts index 43f09232c2..a78a76594d 100644 --- a/src/subdomains/supporting/payment/dto/transaction-helper/tx-spec.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction-helper/tx-spec.dto.ts @@ -1,3 +1,5 @@ +import { FeeSpec } from '../fee.dto'; + export interface TxMinSpec { minVolume: number; minFee: number; @@ -10,8 +12,9 @@ export interface TxSpec { }; fee: { min: number; - fixed: number; - bankFixed: number; + dfx: FeeSpec; + partner: FeeSpec; + bank: FeeSpec; network: number; networkStart: number; }; diff --git a/src/subdomains/supporting/payment/entities/fee.entity.ts b/src/subdomains/supporting/payment/entities/fee.entity.ts index d0fb27e106..3798043b15 100644 --- a/src/subdomains/supporting/payment/entities/fee.entity.ts +++ b/src/subdomains/supporting/payment/entities/fee.entity.ts @@ -15,6 +15,7 @@ export enum FeeType { BASE = 'Base', // Single use only, absolute base fee DISCOUNT = 'Discount', // Single use only, absolute discount RELATIVE_DISCOUNT = 'RelativeDiscount', // Single use only, relative discount + PARTNER = 'Partner', // Single use only, additive partner fee ADDITION = 'Addition', // Multiple use possible, additive fee CHARGEBACK_BASE = 'ChargebackBase', // Single use only, absolute base fee CHARGEBACK_SPECIAL = 'ChargebackSpecial', // Single use only, highest prio applies to all diff --git a/src/subdomains/supporting/payment/services/fee.service.ts b/src/subdomains/supporting/payment/services/fee.service.ts index d67f38de0a..d75c8c18d4 100644 --- a/src/subdomains/supporting/payment/services/fee.service.ts +++ b/src/subdomains/supporting/payment/services/fee.service.ts @@ -29,7 +29,7 @@ import { BankService } from '../../bank/bank/bank.service'; import { CardBankName, IbanBankName } from '../../bank/bank/dto/bank.dto'; import { PayoutService } from '../../payout/services/payout.service'; import { PriceCurrency, PriceValidity, PricingService } from '../../pricing/services/pricing.service'; -import { InternalChargebackFeeDto, InternalFeeDto } from '../dto/fee.dto'; +import { FeeInfo } from '../dto/fee.dto'; import { CreateFeeDto } from '../dto/input/create-fee.dto'; import { FiatPaymentMethod, PaymentMethod } from '../dto/payment-method.enum'; import { Fee, FeeType } from '../entities/fee.entity'; @@ -225,7 +225,7 @@ export class FeeService { return fee; } - async getChargebackFee(request: OptionalFeeRequest): Promise { + async getChargebackFee(request: OptionalFeeRequest): Promise { const userFees = await this.getValidFees(request); try { @@ -241,7 +241,7 @@ export class FeeService { } } - async getUserFee(request: UserFeeRequest): Promise { + async getUserFee(request: UserFeeRequest): Promise { const userFees = await this.getValidFees(request); try { @@ -259,7 +259,7 @@ export class FeeService { } } - async getDefaultFee(request: FeeRequestBase, accountType = AccountType.PERSONAL): Promise { + async getDefaultFee(request: FeeRequestBase, accountType = AccountType.PERSONAL): Promise { const defaultFees = await this.getValidFees({ ...request, accountType }); try { @@ -312,7 +312,7 @@ export class FeeService { } private async getAllFees(): Promise { - return this.feeRepo.findCached('all'); + return this.feeRepo.findCached('all', { relations: { wallet: { owner: true } } }); } private async calculateFee( @@ -322,11 +322,18 @@ export class FeeService { allowCachedBlockchainFee: boolean, paymentMethodIn: PaymentMethod, userDataId?: number, - ): Promise { + ): Promise { const blockchainFee = (await this.getBlockchainFeeInChf(from, allowCachedBlockchainFee)) + (await this.getBlockchainFeeInChf(to, allowCachedBlockchainFee)); + // get partner fee + const partnerFee = Util.minObj( + fees.filter((fee) => fee.type === FeeType.PARTNER), + 'rate', + ); + const partnerFeeSpec = { rate: partnerFee?.rate ?? 0, fixed: partnerFee?.fixed ?? 0 }; + // get min special fee const specialFee = Util.minObj( fees.filter((fee) => fee.type === FeeType.SPECIAL), @@ -336,10 +343,9 @@ export class FeeService { if (specialFee) return { fees: [specialFee], - rate: specialFee.rate, - fixed: specialFee.fixed ?? 0, - bankRate: 0, - bankFixed: 0, + dfx: { rate: specialFee.rate, fixed: specialFee.fixed ?? 0 }, + bank: { rate: 0, fixed: 0 }, + partner: partnerFeeSpec, payoutRefBonus: specialFee.payoutRefBonus, network: Math.min(specialFee.blockchainFactor * blockchainFee, Config.maxBlockchainFee), }; @@ -353,12 +359,11 @@ export class FeeService { if (customFee) return { fees: [customFee], - rate: customFee.rate, - fixed: customFee.fixed ?? 0, - bankRate: 0, - bankFixed: 0, - payoutRefBonus: customFee.payoutRefBonus, + dfx: { rate: customFee.rate, fixed: customFee.fixed ?? 0 }, + bank: { rate: 0, fixed: 0 }, + partner: partnerFeeSpec, network: Math.min(customFee.blockchainFactor * blockchainFee, Config.maxBlockchainFee), + payoutRefBonus: customFee.payoutRefBonus, }; // get min base fee @@ -386,6 +391,8 @@ export class FeeService { const combinedBankFeeRate = Util.sumObjValue(bankFees, 'rate'); const combinedBankFixedFee = Util.sumObjValue(bankFees, 'fixed'); + const bankFeeSpec = { rate: combinedBankFeeRate, fixed: combinedBankFixedFee }; + const combinedExtraFeeRate = Util.sumObjValue(additiveFees, 'rate') - (discountFee?.rate ?? 0); const combinedExtraFixedFee = Util.sumObjValue(additiveFees, 'fixed') - (discountFee?.fixed ?? 0); @@ -394,10 +401,9 @@ export class FeeService { this.logger.warn(`Discount is higher than base fee for user data ${userDataId}`); return { fees: [baseFee], - rate: baseFee.rate, - fixed: baseFee.fixed, - bankRate: combinedBankFeeRate, - bankFixed: combinedBankFixedFee, + dfx: { rate: baseFee.rate, fixed: baseFee.fixed }, + bank: bankFeeSpec, + partner: partnerFeeSpec, payoutRefBonus: true, network: Math.min(baseFee.blockchainFactor * blockchainFee, Config.maxBlockchainFee), }; @@ -405,10 +411,9 @@ export class FeeService { return { fees: [baseFee, discountFee, ...additiveFees].filter((e) => e != null), - rate: baseFee.rate + combinedExtraFeeRate, - fixed: Math.max(baseFee.fixed + combinedExtraFixedFee, 0), - bankRate: combinedBankFeeRate, - bankFixed: combinedBankFixedFee, + dfx: { rate: baseFee.rate + combinedExtraFeeRate, fixed: Math.max(baseFee.fixed + combinedExtraFixedFee, 0) }, + bank: bankFeeSpec, + partner: partnerFeeSpec, payoutRefBonus: baseFee.payoutRefBonus && (discountFee?.payoutRefBonus ?? true) && @@ -431,7 +436,7 @@ export class FeeService { from: Active, allowCachedBlockchainFee: boolean, paymentMethodIn: PaymentMethod, - ): Promise { + ): Promise { const blockchainFee = await this.getBlockchainFeeInChf(from, allowCachedBlockchainFee); // get min special fee @@ -443,11 +448,11 @@ export class FeeService { if (specialFee) return { fees: [specialFee], - rate: specialFee.rate, - fixed: specialFee.fixed ?? 0, - bankRate: 0, - bankFixed: 0, + dfx: { rate: specialFee.rate, fixed: specialFee.fixed ?? 0 }, + bank: { rate: 0, fixed: 0 }, + partner: { rate: 0, fixed: 0 }, network: Math.min(specialFee.blockchainFactor * blockchainFee, Config.maxBlockchainFee), + payoutRefBonus: false, }; // get min custom fee @@ -459,11 +464,11 @@ export class FeeService { if (customFee) return { fees: [customFee], - rate: customFee.rate, - fixed: customFee.fixed ?? 0, - bankRate: 0, - bankFixed: 0, + dfx: { rate: customFee.rate, fixed: customFee.fixed ?? 0 }, + bank: { rate: 0, fixed: 0 }, + partner: { rate: 0, fixed: 0 }, network: Math.min(customFee.blockchainFactor * blockchainFee, Config.maxBlockchainFee), + payoutRefBonus: false, }; // get chargeback fees @@ -491,14 +496,17 @@ export class FeeService { if (!baseFee) throw new InternalServerErrorException('Chargeback base fee is missing'); return { fees: [baseFee, ...additiveFees], - rate: baseFee.rate + combinedAdditiveChargebackFeeRate, - fixed: (baseFee.fixed ?? 0) + (combinedAdditiveChargebackFixedFee ?? 0), - bankRate: combinedBankFeeRate, - bankFixed: combinedBankFixedFee ?? 0, + dfx: { + rate: baseFee.rate + combinedAdditiveChargebackFeeRate, + fixed: (baseFee.fixed ?? 0) + (combinedAdditiveChargebackFixedFee ?? 0), + }, + bank: { rate: combinedBankFeeRate, fixed: combinedBankFixedFee ?? 0 }, + partner: { rate: 0, fixed: 0 }, network: Math.min( (baseFee.blockchainFactor + combinedAdditiveChargebackBlockchainFee) * blockchainFee, Config.maxBlockchainFee, ), + payoutRefBonus: false, }; } @@ -541,6 +549,7 @@ export class FeeService { FeeType.CHARGEBACK_BANK, FeeType.BANK, FeeType.SPECIAL, + FeeType.PARTNER, FeeType.CHARGEBACK_SPECIAL, ].includes(f.type) && !f.specialCode) || diff --git a/src/subdomains/supporting/payment/services/transaction-helper.ts b/src/subdomains/supporting/payment/services/transaction-helper.ts index 2b5c5e037a..37cbbff139 100644 --- a/src/subdomains/supporting/payment/services/transaction-helper.ts +++ b/src/subdomains/supporting/payment/services/transaction-helper.ts @@ -1,4 +1,4 @@ -import { BadRequestException, ForbiddenException, Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { CronExpression } from '@nestjs/schedule'; import { Config, Environment } from 'src/config/config'; import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum'; @@ -40,7 +40,7 @@ import { BankTx } from '../../bank-tx/bank-tx/entities/bank-tx.entity'; import { CardBankName, IbanBankName } from '../../bank/bank/dto/bank.dto'; import { CryptoInput, PayInConfirmationType } from '../../payin/entities/crypto-input.entity'; import { PriceCurrency, PriceValidity, PricingService } from '../../pricing/services/pricing.service'; -import { FeeDto, InternalFeeDto } from '../dto/fee.dto'; +import { FeeAmountsDto, FeeInfo, FeeSpec, InternalFeeDto, toFeeDto } from '../dto/fee.dto'; import { FiatPaymentMethod, PaymentMethod } from '../dto/payment-method.enum'; import { QuoteError } from '../dto/transaction-helper/quote-error.enum'; import { TargetEstimation, TransactionDetails } from '../dto/transaction-helper/transaction-details.dto'; @@ -197,7 +197,7 @@ export class TransactionHelper implements OnModuleInit { bankIn: CardBankName | IbanBankName | undefined, bankOut: CardBankName | IbanBankName | undefined, user: User, - ): Promise { + ): Promise { // get fee const [fee, networkStartFee] = await this.getAllFees( user, @@ -219,30 +219,26 @@ export class TransactionHelper implements OnModuleInit { const specs: TxSpec = { fee: { min: minSpecs.minFee, - fixed: fee.fixed, network: fee.network, networkStart: networkStartFee, - bankFixed: fee.bankFixed, + dfx: fee.dfx, + bank: fee.bank, + partner: fee.partner, }, volume: { min: minSpecs.minVolume, max: Number.MAX_VALUE }, }; const sourceSpecs = await this.getSourceSpecs(fromReference, specs, PriceValidity.VALID_ONLY); - const { dfx, bank, total } = this.calculateTotalFee( - inputReferenceAmount, - fee.rate, - fee.bankRate, - sourceSpecs, - from, - ); + const amounts = this.calculateTotalFee(inputReferenceAmount, sourceSpecs, from); + + const feeDto = toFeeDto(amounts, sourceSpecs); return { - ...fee, - ...sourceSpecs.fee, - total, - dfx, - bank, + ...feeDto, + fees: fee.fees, + partner: amounts.partner, + payoutRefBonus: fee.payoutRefBonus, }; } @@ -308,11 +304,12 @@ export class TransactionHelper implements OnModuleInit { // target estimation const extendedSpecs: TxSpec = { fee: { - network: fee.network, - fixed: fee.fixed, min: specs.minFee, + network: fee.network, networkStart: networkStartFee, - bankFixed: fee.bankFixed, + dfx: fee.dfx, + bank: fee.bank, + partner: fee.partner, }, volume: { min: specs.minVolume, @@ -326,8 +323,6 @@ export class TransactionHelper implements OnModuleInit { const target = await this.getTargetEstimation( sourceAmount, targetAmount, - fee.rate, - fee.bankRate, sourceSpecs, targetSpecs, from, @@ -419,12 +414,12 @@ export class TransactionHelper implements OnModuleInit { userData, }); - const dfxFeeAmount = inputAmount * chargebackFee.rate + price.convert(chargebackFee.fixed); + const dfxFeeAmount = inputAmount * chargebackFee.dfx.rate + price.convert(chargebackFee.dfx.fixed); const networkFeeAmount = price.convert(chargebackFee.network); const bankFeeAmount = refundEntity.paymentMethodIn === FiatPaymentMethod.BANK ? price.convert( - chargebackFee.bankRate * inputAmount + chargebackFee.bankFixed + refundEntity.chargebackBankFee * 1.01, + chargebackFee.bank.rate * inputAmount + chargebackFee.bank.fixed + refundEntity.chargebackBankFee * 1.01, ) : 0; // Bank fee buffer 1% @@ -537,7 +532,7 @@ export class TransactionHelper implements OnModuleInit { specialCodes: string[], exactPrice: boolean, allowCachedBlockchainFee: boolean, - ): Promise<[InternalFeeDto, number]> { + ): Promise<[FeeInfo, number]> { const [fee, networkStartFee] = await Promise.all([ this.getTxFee( user, @@ -624,7 +619,7 @@ export class TransactionHelper implements OnModuleInit { txVolumeChf: number, specialCodes: string[], allowCachedBlockchainFee: boolean, - ): Promise { + ): Promise { const feeRequest: UserFeeRequest = { user, wallet, @@ -645,8 +640,6 @@ export class TransactionHelper implements OnModuleInit { private async getTargetEstimation( inputAmount: number | undefined, outputAmount: number | undefined, - feeRate: number, - bankFeeRate: number, sourceSpecs: TxSpec, targetSpecs: TxSpec, from: Active, @@ -656,14 +649,15 @@ export class TransactionHelper implements OnModuleInit { const price = await this.pricingService.getPrice(from, to, priceValidity); const outputAmountSource = outputAmount && price.invert().convert(outputAmount); - const sourceAmount = inputAmount ?? this.getInputAmount(outputAmountSource, feeRate, bankFeeRate, sourceSpecs); - const sourceFees = this.calculateTotalFee(sourceAmount, feeRate, bankFeeRate, sourceSpecs, from); + const sourceAmount = inputAmount ?? this.getInputAmount(outputAmountSource, sourceSpecs); + const sourceFees = this.calculateTotalFee(sourceAmount, sourceSpecs, from); const targetAmount = outputAmount ?? price.convert(Math.max(inputAmount - sourceFees.total, 0)); - const targetFees = { + const targetFees: FeeAmountsDto = { dfx: this.convertFee(sourceFees.dfx, price, to), - total: this.convertFee(sourceFees.total, price, to), bank: this.convertFee(sourceFees.bank, price, to), + partner: this.convertFee(sourceFees.partner, price, to), + total: this.convertFee(sourceFees.total, price, to), }; return { @@ -674,27 +668,19 @@ export class TransactionHelper implements OnModuleInit { estimatedAmount: Util.roundReadable(targetAmount, amountType(to)), exactPrice: price.isValid, priceSteps: price.steps, - feeSource: { - rate: feeRate, - ...sourceSpecs.fee, - ...sourceFees, - }, - feeTarget: { - rate: feeRate, - ...targetSpecs.fee, - ...targetFees, - }, + feeSource: toFeeDto(sourceFees, sourceSpecs), + feeTarget: toFeeDto(targetFees, targetSpecs), }; } private getInputAmount( outputAmount: number, - rate: number, - bankRate: number, - { fee: { min, fixed, network, bankFixed, networkStart } }: TxSpec, + { fee: { min, network, dfx, bank, partner, networkStart } }: TxSpec, ): number { - const inputAmountNormal = (outputAmount + fixed + network + bankFixed + networkStart) / (1 - (rate + bankRate)); - const inputAmountWithMinFee = outputAmount + network + bankFixed + networkStart + min; + const inputAmountNormal = + (outputAmount + dfx.fixed + bank.fixed + partner.fixed + network + networkStart) / + (1 - (dfx.rate + bank.rate + partner.rate)); + const inputAmountWithMinFee = outputAmount + network + bank.fixed + partner.fixed + networkStart + min; return Math.max(inputAmountNormal, inputAmountWithMinFee); } @@ -752,27 +738,35 @@ export class TransactionHelper implements OnModuleInit { private calculateTotalFee( amount: number, - rate: number, - bankRate: number, - { fee: { fixed, min, network, networkStart, bankFixed } }: TxSpec, + { fee: { min, network, networkStart, dfx, bank, partner } }: TxSpec, roundingActive: Active, - ): { dfx: number; bank: number; total: number } { - const bank = amount * bankRate + bankFixed; - const dfx = Math.max(amount * rate + fixed, min); - const total = dfx + bank + network + (networkStart ?? 0); + ): FeeAmountsDto { + const dfxAmount = Math.max(this.calculateFee(amount, dfx), min); + const bankAmount = this.calculateFee(amount, bank); + const partnerAmount = this.calculateFee(amount, partner); + const totalAmount = dfxAmount + partnerAmount + bankAmount + network + (networkStart ?? 0); return { - dfx: Util.roundReadable(dfx, feeAmountType(roundingActive)), - bank: Util.roundReadable(bank, feeAmountType(roundingActive)), - total: Util.roundReadable(total, feeAmountType(roundingActive)), + dfx: Util.roundReadable(dfxAmount, feeAmountType(roundingActive)), + bank: Util.roundReadable(bankAmount, feeAmountType(roundingActive)), + partner: Util.roundReadable(partnerAmount, feeAmountType(roundingActive)), + total: Util.roundReadable(totalAmount, feeAmountType(roundingActive)), }; } + private calculateFee(amount: number, spec: FeeSpec): number { + return amount * spec.rate + spec.fixed; + } + private convert(amount: number, price: Price, roundingActive: Active): number { const targetAmount = price.convert(amount); return Util.roundReadable(targetAmount, amountType(roundingActive)); } + private convertFeeSpec(spec: FeeSpec, price: Price, roundingActive: Active): FeeSpec { + return { rate: spec.rate, fixed: this.convertFee(spec.fixed, price, roundingActive) }; + } + private convertFee(amount: number, price: Price, roundingActive: Active): number { const targetAmount = price.convert(amount); return Util.roundReadable(targetAmount, feeAmountType(roundingActive));