diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index 9a20877..f48384c 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -1,9 +1,11 @@ import { Injectable, NotFoundException } from '@nestjs/common' import { Affiliate, Prisma } from '@prisma/client' +import { bnOrZero } from '@shapeshiftoss/chain-adapters' + import { PrismaService } from '../prisma/prisma.service' import { SHAPESHIFT_BPS } from '../swaps/constants' -import { calculateFeeForSwap, getPartnerFeeRate, toSwap } from '../swaps/utils' +import { calculateFeeForSwap, getPartnerFeeUsd, toSwap } from '../swaps/utils' import { getNextCursor, swapCursorArgs } from '../utils/pagination' import type { AffiliateStatsResult, CreateAffiliateDto, UpdateAffiliateDto } from './types' @@ -84,8 +86,8 @@ export class AffiliateService { }) let totalSwaps = 0 - let totalVolumeUsd = 0 - let totalFeesEarnedUsd = 0 + let totalVolumeUsd = bnOrZero(0) + let totalFeesEarnedUsd = bnOrZero(0) for (const item of items) { const swap = toSwap(item) @@ -93,11 +95,9 @@ export class AffiliateService { const fee = calculateFeeForSwap(swap) if (!fee) continue - const rate = getPartnerFeeRate(fee.verifiedBps, swap.partnerBps) - totalSwaps++ - totalVolumeUsd += fee.volumeUsd - totalFeesEarnedUsd += fee.feeUsd * rate + totalVolumeUsd = totalVolumeUsd.plus(fee.volumeUsd) + totalFeesEarnedUsd = totalFeesEarnedUsd.plus(getPartnerFeeUsd(fee.feeUsd, fee.verifiedBps, swap.partnerBps)) } return { @@ -133,7 +133,7 @@ export class AffiliateService { const fee = calculateFeeForSwap(swap) const feeUsd = fee ? fee.feeUsd.toString() : null const volumeUsd = fee ? fee.volumeUsd.toString() : null - const partnerFeeUsd = fee ? (fee.feeUsd * getPartnerFeeRate(fee.verifiedBps, swap.partnerBps)).toString() : null + const partnerFeeUsd = fee ? getPartnerFeeUsd(fee.feeUsd, fee.verifiedBps, swap.partnerBps) : null return { ...swap, feeUsd, partnerFeeUsd, volumeUsd } }) diff --git a/apps/swap-service/src/swaps/utils.ts b/apps/swap-service/src/swaps/utils.ts index e6e0b2a..098957d 100644 --- a/apps/swap-service/src/swaps/utils.ts +++ b/apps/swap-service/src/swaps/utils.ts @@ -56,9 +56,13 @@ export const formatAmount = (amount: string | number): string => { .replace(/\.?0+$/, '') } -export const getPartnerFeeRate = (verifiedBps: number, partnerBps: number): number => { - if (verifiedBps <= 0) return 0 - return Math.min(partnerBps / verifiedBps, 1) +// The partner's share of the affiliate fee in USD, as an exact string. Multiplies before dividing +// (feeUsd × partnerBps ÷ verifiedBps) so the result stays precise — computing a partnerBps/verifiedBps +// rate first (e.g. 50/60) would introduce lossy-float artifacts. Capped at the whole fee. +export const getPartnerFeeUsd = (feeUsd: number, verifiedBps: number, partnerBps: number): string => { + if (verifiedBps <= 0) return '0' + const share = bnOrZero(feeUsd).times(partnerBps).div(verifiedBps) + return (share.gt(feeUsd) ? bnOrZero(feeUsd) : share).toString() } export const computeSellAmountUsd = ( diff --git a/scripts/affiliate-payouts/affiliate-payouts.test.ts b/scripts/affiliate-payouts/affiliate-payouts.test.ts index edc4010..f552676 100644 --- a/scripts/affiliate-payouts/affiliate-payouts.test.ts +++ b/scripts/affiliate-payouts/affiliate-payouts.test.ts @@ -36,7 +36,8 @@ const stubDeps: FeeDeps = { if (r.priceable === false) return null return { feeUsd: 12, volumeUsd: 2000, verifiedBps: 60, actualFeeUsd: 12, impliedFeeUsd: 12, ...r.fee } }, - getPartnerFeeRate: (verifiedBps, partnerBps) => (verifiedBps <= 0 ? 0 : Math.min(partnerBps / verifiedBps, 1)), + getPartnerFeeUsd: (feeUsd, verifiedBps, partnerBps) => + verifiedBps <= 0 ? '0' : BigNumber.min(new BigNumber(feeUsd).times(partnerBps).div(verifiedBps), feeUsd).toString(), } const accrual = (over: Partial & Pick): PartnerAccrual => ({ diff --git a/scripts/affiliate-payouts/affiliate-payouts.ts b/scripts/affiliate-payouts/affiliate-payouts.ts index f176e4b..497e9aa 100644 --- a/scripts/affiliate-payouts/affiliate-payouts.ts +++ b/scripts/affiliate-payouts/affiliate-payouts.ts @@ -2,7 +2,7 @@ import { PrismaClient } from '@prisma/client' import * as fs from 'fs' import * as path from 'path' -import { calculateFeeForSwap, getPartnerFeeRate, toSwap } from '../../apps/swap-service/src/swaps/utils' +import { calculateFeeForSwap, getPartnerFeeUsd, toSwap } from '../../apps/swap-service/src/swaps/utils' import type { PartnerPayout, PayoutRecord } from './types' import { aggregateByPartner, buildPayouts, buildRecord, resolveWindow, toCsv } from './utils' @@ -88,7 +88,7 @@ async function generate(monthArg: string | undefined, force: boolean): Promise = { toSwap: (row: PrismaSwap) => S calculateFeeForSwap: (swap: S) => FeeResult | null - getPartnerFeeRate: (verifiedBps: number, partnerBps: number) => number + getPartnerFeeUsd: (feeUsd: number, verifiedBps: number, partnerBps: number) => string } export type PayoutWarning = { diff --git a/scripts/affiliate-payouts/utils.ts b/scripts/affiliate-payouts/utils.ts index 91fcbd9..b71cc5d 100644 --- a/scripts/affiliate-payouts/utils.ts +++ b/scripts/affiliate-payouts/utils.ts @@ -141,8 +141,7 @@ export function aggregateByPartner( continue } - const rate = deps.getPartnerFeeRate(fee.verifiedBps, row.partnerBps) - if (rate <= 0) { + if (row.partnerBps <= 0) { partnerBpsUnset.push({ swapId: row.swapId, partnerCode, @@ -159,9 +158,10 @@ export function aggregateByPartner( feesEarnedUsd: new BigNumber(0), } + // Pay only on the verified on-chain fee, via the shared exact partner-share helper. accrual.swapCount += 1 accrual.volumeUsd = accrual.volumeUsd.plus(fee.volumeUsd) - accrual.feesEarnedUsd = accrual.feesEarnedUsd.plus(new BigNumber(fee.actualFeeUsd).times(rate)) + accrual.feesEarnedUsd = accrual.feesEarnedUsd.plus(deps.getPartnerFeeUsd(fee.actualFeeUsd, fee.verifiedBps, row.partnerBps)) partners.set(partnerCode.toLowerCase(), accrual) }