From 9f4215c9c62d9d3137f860aceaa0cd2ca856b8db Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:41:01 +0100 Subject: [PATCH] fix: accrue ref rewards in finance log before RefReward record creation Ref expenses jumped in the finance log because costs were only recognized when the batch job created RefReward records, not when refCredit was incremented on the user. Add accrued open credit (refCredit - paidRefCredit minus in-progress RefRewards) to the ref expense calculation so costs appear smoothly as they are earned. --- .../referral/reward/services/ref-reward.service.ts | 12 ++++++++++++ .../generic/user/models/user/user.service.ts | 10 ++++++++++ .../dashboard/dashboard-financial.service.ts | 1 + .../supporting/dashboard/dto/financial-log.dto.ts | 2 +- .../log/__tests__/log-job.service.spec.ts | 8 ++++++++ src/subdomains/supporting/log/dto/log.dto.ts | 1 + src/subdomains/supporting/log/log-job.module.ts | 4 ++++ src/subdomains/supporting/log/log-job.service.ts | 14 +++++++++++++- 8 files changed, 50 insertions(+), 2 deletions(-) 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 ab4b88423f..02a7c3bd4a 100644 --- a/src/subdomains/core/referral/reward/services/ref-reward.service.ts +++ b/src/subdomains/core/referral/reward/services/ref-reward.service.ts @@ -207,6 +207,18 @@ export class RefRewardService { return volume ?? 0; } + async getInProgressRefRewardVolumeEur(): Promise { + const { volume } = await this.rewardRepo + .createQueryBuilder('refReward') + .select('SUM(amountInEur)', 'volume') + .where('status NOT IN (:...status)', { + status: [RewardStatus.COMPLETE, RewardStatus.FAILED, RewardStatus.USER_SWITCH], + }) + .getRawOne<{ volume: number }>(); + + return volume ?? 0; + } + // --- HELPER METHODS --- // async updatePaidRefCredit(userIds: number[]): Promise { userIds = userIds.filter((u, j) => userIds.indexOf(u) === j).filter((i) => i); // distinct, not null diff --git a/src/subdomains/generic/user/models/user/user.service.ts b/src/subdomains/generic/user/models/user/user.service.ts index 29098ce6e2..a0fe2ecfee 100644 --- a/src/subdomains/generic/user/models/user/user.service.ts +++ b/src/subdomains/generic/user/models/user/user.service.ts @@ -632,6 +632,16 @@ export class UserService { .then((r) => r.paidRefCredit); } + public async getTotalOpenRefCredit(): Promise { + const { openCredit } = await this.userRepo + .createQueryBuilder('user') + .select('SUM(user.refCredit - user.paidRefCredit)', 'openCredit') + .where('user.refCredit - user.paidRefCredit > 0') + .getRawOne<{ openCredit: number }>(); + + return openCredit ?? 0; + } + // --- API KEY --- // async deleteApiKey(userId: number): Promise { await this.userRepo.update(userId, { apiKeyCT: null }); diff --git a/src/subdomains/supporting/dashboard/dashboard-financial.service.ts b/src/subdomains/supporting/dashboard/dashboard-financial.service.ts index 4099ddac96..656df07077 100644 --- a/src/subdomains/supporting/dashboard/dashboard-financial.service.ts +++ b/src/subdomains/supporting/dashboard/dashboard-financial.service.ts @@ -82,6 +82,7 @@ export class DashboardFinancialService { ref: { total: changes.minus?.ref?.total ?? 0, amount: changes.minus?.ref?.amount ?? 0, + accrued: changes.minus?.ref?.accrued ?? 0, fee: changes.minus?.ref?.fee ?? 0, }, binance: { diff --git a/src/subdomains/supporting/dashboard/dto/financial-log.dto.ts b/src/subdomains/supporting/dashboard/dto/financial-log.dto.ts index c0e07f7224..7b06848d04 100644 --- a/src/subdomains/supporting/dashboard/dto/financial-log.dto.ts +++ b/src/subdomains/supporting/dashboard/dto/financial-log.dto.ts @@ -25,7 +25,7 @@ export class FinancialChangesEntryDto { total: number; bank: number; kraken: { total: number; withdraw: number; trading: number }; - ref: { total: number; amount: number; fee: number }; + ref: { total: number; amount: number; accrued: number; fee: number }; binance: { total: number; withdraw: number; trading: number }; blockchain: { total: number; txIn: number; txOut: number; trading: number }; }; diff --git a/src/subdomains/supporting/log/__tests__/log-job.service.spec.ts b/src/subdomains/supporting/log/__tests__/log-job.service.spec.ts index 4023e4a9ee..fdf8d71b4b 100644 --- a/src/subdomains/supporting/log/__tests__/log-job.service.spec.ts +++ b/src/subdomains/supporting/log/__tests__/log-job.service.spec.ts @@ -17,6 +17,7 @@ import { RefRewardService } from 'src/subdomains/core/referral/reward/services/r import { BuyFiatService } from 'src/subdomains/core/sell-crypto/process/services/buy-fiat.service'; import { TradingOrderService } from 'src/subdomains/core/trading/services/trading-order.service'; import { TradingRuleService } from 'src/subdomains/core/trading/services/trading-rule.service'; +import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; 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'; @@ -24,6 +25,7 @@ import { createCustomBankTx } from '../../bank-tx/bank-tx/__mocks__/bank-tx.enti import { BankService } from '../../bank/bank/bank.service'; import { PayInService } from '../../payin/services/payin.service'; import { PayoutService } from '../../payout/services/payout.service'; +import { PricingService } from '../../pricing/services/pricing.service'; import { LogJobService } from '../log-job.service'; import { LogService } from '../log.service'; @@ -50,6 +52,8 @@ describe('LogJobService', () => { let payoutService: PayoutService; let processService: ProcessService; let paymentBalanceService: PaymentBalanceService; + let userService: UserService; + let pricingService: PricingService; beforeEach(async () => { tradingRuleService = createMock(); @@ -72,6 +76,8 @@ describe('LogJobService', () => { payoutService = createMock(); processService = createMock(); paymentBalanceService = createMock(); + userService = createMock(); + pricingService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [TestSharedModule], @@ -97,6 +103,8 @@ describe('LogJobService', () => { { provide: PayoutService, useValue: payoutService }, { provide: ProcessService, useValue: processService }, { provide: PaymentBalanceService, useValue: paymentBalanceService }, + { provide: UserService, useValue: userService }, + { provide: PricingService, useValue: pricingService }, TestUtil.provideConfig(), ], }).compile(); diff --git a/src/subdomains/supporting/log/dto/log.dto.ts b/src/subdomains/supporting/log/dto/log.dto.ts index e08d607d9b..18bce4a167 100644 --- a/src/subdomains/supporting/log/dto/log.dto.ts +++ b/src/subdomains/supporting/log/dto/log.dto.ts @@ -169,5 +169,6 @@ type ChangeBlockchainTxBalance = { type ChangeRefBalance = { total: number; amount?: number; + accrued?: number; fee?: number; }; diff --git a/src/subdomains/supporting/log/log-job.module.ts b/src/subdomains/supporting/log/log-job.module.ts index 10c25ab0f9..6172ef2318 100644 --- a/src/subdomains/supporting/log/log-job.module.ts +++ b/src/subdomains/supporting/log/log-job.module.ts @@ -8,6 +8,8 @@ import { PaymentLinkPaymentModule } from 'src/subdomains/core/payment-link/payme import { ReferralModule } from 'src/subdomains/core/referral/referral.module'; import { SellCryptoModule } from 'src/subdomains/core/sell-crypto/sell-crypto.module'; import { TradingModule } from 'src/subdomains/core/trading/trading.module'; +import { UserModule } from 'src/subdomains/generic/user/user.module'; +import { PricingModule } from '../pricing/pricing.module'; import { BankTxModule } from '../bank-tx/bank-tx.module'; import { BankModule } from '../bank/bank.module'; import { PayInModule } from '../payin/payin.module'; @@ -31,6 +33,8 @@ import { LogModule } from './log.module'; ReferralModule, PayoutModule, PaymentLinkPaymentModule, + UserModule, + PricingModule, ], controllers: [], providers: [LogJobService], diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index 6752f86cbb..369ee74b03 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -26,6 +26,8 @@ import { LiquidityManagementPipelineService } from 'src/subdomains/core/liquidit import { PaymentBalanceService } from 'src/subdomains/core/payment-link/services/payment-balance.service'; import { RefReward } from 'src/subdomains/core/referral/reward/ref-reward.entity'; import { RefRewardService } from 'src/subdomains/core/referral/reward/services/ref-reward.service'; +import { UserService } from 'src/subdomains/generic/user/models/user/user.service'; +import { PriceCurrency, PriceValidity, PricingService } from '../pricing/services/pricing.service'; import { BuyFiat } from 'src/subdomains/core/sell-crypto/process/buy-fiat.entity'; import { BuyFiatService } from 'src/subdomains/core/sell-crypto/process/services/buy-fiat.service'; import { TradingOrder } from 'src/subdomains/core/trading/entities/trading-order.entity'; @@ -82,6 +84,8 @@ export class LogJobService { private readonly payoutService: PayoutService, private readonly processService: ProcessService, private readonly paymentBalanceService: PaymentBalanceService, + private readonly userService: UserService, + private readonly pricingService: PricingService, ) {} @DfxCron(CronExpression.EVERY_MINUTE, { process: Process.TRADING_LOG, timeout: 1800 }) @@ -994,10 +998,17 @@ export class LogJobService { ); const payoutOrderFee = this.getFeeAmount(payoutOrders.filter((p) => p.context !== PayoutOrderContext.REF_PAYOUT)); + // accrued ref rewards: open credit not yet turned into RefReward records + const totalOpenRefCreditEur = await this.userService.getTotalOpenRefCredit(); + const inProgressRefRewardEur = await this.refRewardService.getInProgressRefRewardVolumeEur(); + const unprocessedCreditEur = Math.max(0, totalOpenRefCreditEur - inProgressRefRewardEur); + const eurChfPrice = await this.pricingService.getPrice(PriceCurrency.EUR, PriceCurrency.CHF, PriceValidity.PREFER_VALID); + const accruedRefRewardChf = eurChfPrice.convert(unprocessedCreditEur, 8); + const totalKrakenFee = krakenTxWithdrawFee + krakenTxTradingFee; const totalBinanceFee = binanceTxWithdrawFee + binanceTxTradingFee; - const totalRefReward = refRewards + payoutOrderRefFee; + const totalRefReward = refRewards + accruedRefRewardChf + payoutOrderRefFee; const totalTxFee = cryptoInputFee + payoutOrderFee; const totalBlockchainFee = totalTxFee + tradingOrderFee; @@ -1049,6 +1060,7 @@ export class LogJobService { ? { total: totalRefReward, amount: refRewards || undefined, + accrued: accruedRefRewardChf || undefined, fee: payoutOrderRefFee || undefined, } : undefined,