diff --git a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts index 26ac7a1..04c4d3b 100644 --- a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts +++ b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts @@ -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 = {}) => ({ + 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) + }) +}) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index f6b1bcf..3144d07 100644 --- a/apps/swap-service/src/affiliate/affiliate.controller.ts +++ b/apps/swap-service/src/affiliate/affiliate.controller.ts @@ -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) diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index c3cb4b5..9a20877 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -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' @@ -14,6 +13,10 @@ import { isReservedPartnerCode } from './utils' export class AffiliateService { constructor(private prisma: PrismaService) {} + async getAffiliates(): Promise { + return this.prisma.affiliate.findMany() + } + async getAffiliateByWalletAddress(walletAddress: string): Promise { const affiliate = await this.prisma.affiliate.findUnique({ where: { walletAddress } }) if (!affiliate) return null @@ -105,15 +108,15 @@ export class AffiliateService { } async getAffiliateSwaps( - partnerCode: string, + partnerCode: string | undefined, options: { startDate?: Date; endDate?: Date; limit: number; cursor?: string }, - ): Promise { + ) { 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: { @@ -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, nextCursor: getNextCursor(items, limit), } } diff --git a/apps/swap-service/src/affiliate/types.ts b/apps/swap-service/src/affiliate/types.ts index ef8a945..ef60b17 100644 --- a/apps/swap-service/src/affiliate/types.ts +++ b/apps/swap-service/src/affiliate/types.ts @@ -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) diff --git a/apps/swap-service/src/swaps/swaps.controller.ts b/apps/swap-service/src/swaps/swaps.controller.ts index cab0cfd..49054ad 100644 --- a/apps/swap-service/src/swaps/swaps.controller.ts +++ b/apps/swap-service/src/swaps/swaps.controller.ts @@ -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) - } } diff --git a/apps/swap-service/src/swaps/swaps.service.ts b/apps/swap-service/src/swaps/swaps.service.ts index 9d98521..79358fc 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -35,7 +35,6 @@ import { calculateFeeForSwap, computeSellAmountUsd, fetchUsdPrices, - getPartnerFeeRate, toSwap, toSwapperSwap, } from './utils' @@ -311,39 +310,6 @@ export class SwapsService { } } - async calculateAffiliateFees(partnerCode: string, startDate?: Date, endDate?: Date): Promise { - 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 { const { baseWhere, startDate, endDate, calcFee } = params