Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,60 @@ describe('AffiliateService attribution reads', () => {
expect(result).toEqual({ totalSwaps: 0, totalVolumeUsd: '0.00', totalFeesEarnedUsd: '0.00' })
})
})

describe('AffiliateService.getAffiliateSwaps fee-split enrichment', () => {
const swapRow = (over: Record<string, unknown> = {}) => ({
swapId: 's1',
partnerCode: 'alpha',
swapperName: 'THORChain',
sellTxHash: '0xAAA',
buyTxHash: null,
partnerBps: 50,
shapeshiftBps: 10,
affiliateBps: 55,
status: 'SUCCESS',
isAffiliateVerified: true,
sellAsset: { precision: 8 },
buyAsset: {},
metadata: {},
sellAmountCryptoBaseUnit: '100000000',
sellAssetUsd: '10',
actualAffiliateFeeAmountCryptoBaseUnit: null,
affiliateFeeAssetId: null,
affiliateAssetUsd: null,
affiliateVerificationDetails: {
hasAffiliate: true,
affiliateBps: 60,
verifiedSellAmountCryptoBaseUnit: '100000000',
},
createdAt: new Date('2026-06-01T12:00:00.000Z'),
updatedAt: new Date('2026-06-01T12:00:00.000Z'),
...over,
})

it('derives feeUsd/partnerFeeUsd/volumeUsd from the verified fee and preserves stored affiliateBps', async () => {
const findMany = jest.fn().mockResolvedValue([swapRow()])
const service = new AffiliateService(makePrismaMock(undefined, findMany))

const { swaps } = await service.getAffiliateSwaps(undefined, { limit: 50 })

// verifiedBps 60, sell 1.0 unit @ $10 => feeUsd = 10 * 60/10000 = 0.06 (full-precision string, no rounding)
// partner share = feeUsd * partnerBps/verifiedBps = 0.06 * 50/60 = 0.05
expect(swaps[0].feeUsd).toBe('0.06')
expect(swaps[0].partnerFeeUsd).toBe('0.05')
expect(swaps[0].volumeUsd).toBe('10')
// stored affiliateBps (55) passes through untouched — not overwritten with verifiedBps (60)
expect(swaps[0].affiliateBps).toBe(55)
})

it('nulls the fee fields when the swap is unpriceable (no verified fee)', async () => {
const findMany = jest.fn().mockResolvedValue([swapRow({ affiliateVerificationDetails: null })])
const service = new AffiliateService(makePrismaMock(undefined, findMany))

const { swaps } = await service.getAffiliateSwaps(undefined, { limit: 50 })

expect(swaps[0]).toMatchObject({ feeUsd: null, partnerFeeUsd: null, volumeUsd: null })
// stored affiliateBps is untouched even when the fee can't be computed
expect(swaps[0].affiliateBps).toBe(55)
})
})
5 changes: 5 additions & 0 deletions apps/swap-service/src/affiliate/affiliate.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export class AffiliateController {
return this.affiliateService.getAffiliateStats(query.partnerCode, query)
}

@Get()
async getAffiliates() {
return this.affiliateService.getAffiliates()
}

@Get(':address')
async getAffiliate(@Param('address') address: string) {
const affiliate = await this.affiliateService.getAffiliateByWalletAddress(address)
Expand Down
22 changes: 17 additions & 5 deletions apps/swap-service/src/affiliate/affiliate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Affiliate, Prisma } from '@prisma/client'

import { PrismaService } from '../prisma/prisma.service'
import { SHAPESHIFT_BPS } from '../swaps/constants'
import { PaginatedSwaps } from '../swaps/types'
import { calculateFeeForSwap, getPartnerFeeRate, toSwap } from '../swaps/utils'
import { getNextCursor, swapCursorArgs } from '../utils/pagination'

Expand All @@ -14,6 +13,10 @@ import { isReservedPartnerCode } from './utils'
export class AffiliateService {
constructor(private prisma: PrismaService) {}

async getAffiliates(): Promise<Affiliate[]> {
return this.prisma.affiliate.findMany()
}

Comment thread
kaladinlight marked this conversation as resolved.
async getAffiliateByWalletAddress(walletAddress: string): Promise<Affiliate | null> {
const affiliate = await this.prisma.affiliate.findUnique({ where: { walletAddress } })
if (!affiliate) return null
Expand Down Expand Up @@ -105,15 +108,15 @@ export class AffiliateService {
}

async getAffiliateSwaps(
partnerCode: string,
partnerCode: string | undefined,
options: { startDate?: Date; endDate?: Date; limit: number; cursor?: string },
): Promise<PaginatedSwaps> {
) {
const { startDate, endDate, limit, cursor } = options

const items = await this.prisma.swap.findMany({
...swapCursorArgs(limit, cursor),
where: {
partnerCode,
partnerCode: partnerCode ?? { not: null },
...(startDate || endDate
? {
createdAt: {
Expand All @@ -125,8 +128,17 @@ export class AffiliateService {
},
})

const swaps = items.map((item) => {
const swap = toSwap(item)
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
return { ...swap, feeUsd, partnerFeeUsd, volumeUsd }
})

return {
swaps: items.map(toSwap),
swaps,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
nextCursor: getNextCursor(items, limit),
}
}
Expand Down
3 changes: 2 additions & 1 deletion apps/swap-service/src/affiliate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export class AffiliateStatsQueryDto {
}

export class AffiliateSwapsQueryDto extends PaginationQueryDto {
@IsOptional()
@Matches(PARTNER_CODE_REGEX, { message: PARTNER_CODE_MESSAGE })
partnerCode: string
partnerCode?: string

@IsOptional()
@Type(() => Date)
Expand Down
9 changes: 0 additions & 9 deletions apps/swap-service/src/swaps/swaps.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,4 @@ export class SwapsController {
) {
return this.swapsService.calculateReferralFees(referralCode, startDate, endDate)
}

@Get('affiliate-fees/:partnerCode')
async getAffiliateFees(
@Param('partnerCode') partnerCode: string,
@Query('startDate', OptionalDatePipe) startDate?: Date,
@Query('endDate', OptionalDatePipe) endDate?: Date,
) {
return this.swapsService.calculateAffiliateFees(partnerCode, startDate, endDate)
}
}
34 changes: 0 additions & 34 deletions apps/swap-service/src/swaps/swaps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
calculateFeeForSwap,
computeSellAmountUsd,
fetchUsdPrices,
getPartnerFeeRate,
toSwap,
toSwapperSwap,
} from './utils'
Expand Down Expand Up @@ -311,39 +310,6 @@ export class SwapsService {
}
}

async calculateAffiliateFees(partnerCode: string, startDate?: Date, endDate?: Date): Promise<Fees> {
logger.log(
`Calculating affiliate fees for ${partnerCode}, period: ${startDate?.toISOString()} - ${endDate?.toISOString()}`,
)

const fees = await this.aggregateFees({
baseWhere: { partnerCode, isAffiliateVerified: true, status: 'SUCCESS', origin: 'api' },
startDate,
endDate,
calcFee: (swap) => {
const fee = calculateFeeForSwap(swap)
if (!fee) return null
const rate = getPartnerFeeRate(fee.verifiedBps, swap.partnerBps)
return { feeUsd: fee.feeUsd * rate, volumeUsd: fee.volumeUsd }
},
})

logger.log(
`Affiliate fees for ${partnerCode}\n` +
` period: ${fees.periodCount} swaps, $${fees.periodVolumeUsd.toFixed(2)} volume, $${fees.periodFeesUsd.toFixed(2)} fee\n` +
` all-time: ${fees.allTimeCount} swaps, $${fees.allTimeFeesUsd.toFixed(2)} fee`,
)

return {
swapCount: fees.periodCount,
periodVolumeUsd: fees.periodVolumeUsd.toFixed(2),
periodFeeUsd: fees.periodFeesUsd.toFixed(2),
allTimeFeeUsd: fees.allTimeFeesUsd.toFixed(2),
periodStart: startDate?.toISOString(),
periodEnd: endDate?.toISOString(),
}
}

private async aggregateFees(params: AggregateFeesParams): Promise<FeeTotals> {
const { baseWhere, startDate, endDate, calcFee } = params

Expand Down
Loading