From 53779c4dfb4a3755e48a8e7ed67509cd33bb404e Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:03:31 -0600 Subject: [PATCH 1/6] feat(affiliate): add GET /v1/affiliate registry listing Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/list-affiliates.test.ts | 22 +++++++++++++++++++ .../src/affiliate/affiliate.controller.ts | 5 +++++ .../src/affiliate/affiliate.service.ts | 5 +++++ 3 files changed, 32 insertions(+) create mode 100644 apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts diff --git a/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts b/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts new file mode 100644 index 0000000..c2c5542 --- /dev/null +++ b/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts @@ -0,0 +1,22 @@ +import type { PrismaService } from '../../prisma/prisma.service' +import { AffiliateService } from '../affiliate.service' + +const prismaWith = (findMany: jest.Mock): PrismaService => + ({ affiliate: { findMany }, swap: { findMany: jest.fn() } } as unknown as PrismaService) + +describe('AffiliateService.listAffiliates', () => { + it('maps affiliates to { partnerCode, bps, isActive }', async () => { + const findMany = jest.fn().mockResolvedValue([ + { partnerCode: 'alpha', bps: 60, isActive: true, walletAddress: '0xabc', receiveAddress: null }, + { partnerCode: 'beta', bps: 30, isActive: false, walletAddress: '0xdef', receiveAddress: null }, + ]) + const service = new AffiliateService(prismaWith(findMany)) + + const result = await service.listAffiliates() + + expect(result).toEqual([ + { partnerCode: 'alpha', bps: 60, isActive: true }, + { partnerCode: 'beta', bps: 30, isActive: false }, + ]) + }) +}) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index f6b1bcf..1991099 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 list() { + return this.affiliateService.listAffiliates() + } + @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..e1fdd1a 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -131,6 +131,11 @@ export class AffiliateService { } } + async listAffiliates(): Promise<{ partnerCode: string; bps: number; isActive: boolean }[]> { + const rows = await this.prisma.affiliate.findMany() + return rows.map(({ partnerCode, bps, isActive }) => ({ partnerCode, bps, isActive })) + } + async resolvePartnerCode(partnerCode: string) { const affiliate = await this.getAffiliateByPartnerCode(partnerCode) if (!affiliate) return null From 866eaa4b6f4f496cffa3a956b00928071b8a2808 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:10:11 -0600 Subject: [PATCH 2/6] feat(affiliate): make affiliate/swaps partnerCode optional + enrich rows with fee split Omitting partnerCode now returns all partner swaps (partnerCode not null) so revenue-api can fetch and settle across partners in one call. Each returned row is enriched with affiliateBps/feeUsd/partnerFeeUsd/volumeUsd computed via the existing calculateFeeForSwap/getPartnerFeeRate helpers. Single-partner behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/affiliate-swaps.test.ts | 67 +++++++++++++++++++ .../src/affiliate/affiliate.service.ts | 19 ++++-- apps/swap-service/src/affiliate/types.ts | 3 +- 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts diff --git a/apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts b/apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts new file mode 100644 index 0000000..dde566d --- /dev/null +++ b/apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts @@ -0,0 +1,67 @@ +import type { PrismaService } from '../../prisma/prisma.service' +import { AffiliateService } from '../affiliate.service' + +const swapRow = (over: Record = {}) => ({ + swapId: 's1', + partnerCode: 'alpha', + swapperName: 'THORChain', + sellTxHash: '0xAAA', + buyTxHash: null, + partnerBps: 50, + shapeshiftBps: 10, + affiliateBps: 0, + 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, +}) + +type FindManyArgs = { where: Record } +type FindManyMock = jest.Mock, [FindManyArgs]> + +const prismaWith = (findMany: FindManyMock): PrismaService => + ({ affiliate: { findUnique: jest.fn() }, swap: { findMany } }) as unknown as PrismaService + +describe('AffiliateService.getAffiliateSwaps', () => { + it('omitting partnerCode queries all partner swaps (partnerCode not null)', async () => { + const findMany = jest.fn, [FindManyArgs]>().mockResolvedValue([]) + const service = new AffiliateService(prismaWith(findMany)) + + await service.getAffiliateSwaps(undefined, { limit: 50 }) + + expect(findMany.mock.calls[0][0].where).toMatchObject({ partnerCode: { not: null } }) + }) + + it('providing partnerCode filters to that partner', async () => { + const findMany = jest.fn, [FindManyArgs]>().mockResolvedValue([]) + const service = new AffiliateService(prismaWith(findMany)) + + await service.getAffiliateSwaps('alpha', { limit: 50 }) + + expect(findMany.mock.calls[0][0].where).toMatchObject({ partnerCode: 'alpha' }) + }) + + it('enriches rows with affiliateBps, feeUsd, partnerFeeUsd, volumeUsd', async () => { + const findMany = jest.fn, [FindManyArgs]>().mockResolvedValue([swapRow()]) + const service = new AffiliateService(prismaWith(findMany)) + + const { swaps } = await service.getAffiliateSwaps(undefined, { limit: 50 }) + + // verifiedBps 60, sell 1.0 unit @ $10 => feeUsd = 10 * 60/10000 = 0.06 + // partner rate = 50/60 => partnerFeeUsd = 0.06 * (50/60) = 0.05 + expect(swaps[0].affiliateBps).toBe(60) + expect(swaps[0].feeUsd).toBeCloseTo(0.06, 6) + expect(swaps[0].partnerFeeUsd).toBeCloseTo(0.05, 6) + expect(swaps[0].volumeUsd).toBeCloseTo(10, 6) + }) +}) diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index e1fdd1a..4ccb372 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' @@ -105,15 +104,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 +124,18 @@ export class AffiliateService { }, }) + const swaps = items.map((item) => { + const swap = toSwap(item) + const fee = calculateFeeForSwap(swap) + const affiliateBps = fee?.verifiedBps ?? null + const feeUsd = fee?.feeUsd ?? null + const volumeUsd = fee?.volumeUsd ?? null + const partnerFeeUsd = fee ? fee.feeUsd * getPartnerFeeRate(fee.verifiedBps, swap.partnerBps) : null + return { ...swap, affiliateBps, 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) From 9ead8ade252adafd73fde6e3b5a92389bb3e4619 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:12:17 -0600 Subject: [PATCH 3/6] style(affiliate): fix prettier formatting in list-affiliates test Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/affiliate/__tests__/list-affiliates.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts b/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts index c2c5542..f3bd636 100644 --- a/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts +++ b/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts @@ -2,7 +2,7 @@ import type { PrismaService } from '../../prisma/prisma.service' import { AffiliateService } from '../affiliate.service' const prismaWith = (findMany: jest.Mock): PrismaService => - ({ affiliate: { findMany }, swap: { findMany: jest.fn() } } as unknown as PrismaService) + ({ affiliate: { findMany }, swap: { findMany: jest.fn() } }) as unknown as PrismaService describe('AffiliateService.listAffiliates', () => { it('maps affiliates to { partnerCode, bps, isActive }', async () => { From 507015150b25d9790a20d90cb8a6d45f1036b5fb Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:47:41 -0600 Subject: [PATCH 4/6] refactor(affiliate): return full affiliate rows; consolidate swaps tests - getAffiliates: rename from listAffiliates and return full Affiliate[] with an honest return type (endpoint is behind the shared service api key; callers may want wallet/receive addresses) - fold the one useful affiliate-swaps test (fee-split enrichment: happy path + unpriceable/no-verified-fee edge) into affiliate.service.test.ts as its own section; drop the standalone file and the redundant/tautological cases Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/affiliate-swaps.test.ts | 67 ------------------- .../__tests__/affiliate.service.test.ts | 54 +++++++++++++++ .../__tests__/list-affiliates.test.ts | 22 ------ .../src/affiliate/affiliate.controller.ts | 4 +- .../src/affiliate/affiliate.service.ts | 9 ++- 5 files changed, 60 insertions(+), 96 deletions(-) delete mode 100644 apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts delete mode 100644 apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts diff --git a/apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts b/apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts deleted file mode 100644 index dde566d..0000000 --- a/apps/swap-service/src/affiliate/__tests__/affiliate-swaps.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { PrismaService } from '../../prisma/prisma.service' -import { AffiliateService } from '../affiliate.service' - -const swapRow = (over: Record = {}) => ({ - swapId: 's1', - partnerCode: 'alpha', - swapperName: 'THORChain', - sellTxHash: '0xAAA', - buyTxHash: null, - partnerBps: 50, - shapeshiftBps: 10, - affiliateBps: 0, - 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, -}) - -type FindManyArgs = { where: Record } -type FindManyMock = jest.Mock, [FindManyArgs]> - -const prismaWith = (findMany: FindManyMock): PrismaService => - ({ affiliate: { findUnique: jest.fn() }, swap: { findMany } }) as unknown as PrismaService - -describe('AffiliateService.getAffiliateSwaps', () => { - it('omitting partnerCode queries all partner swaps (partnerCode not null)', async () => { - const findMany = jest.fn, [FindManyArgs]>().mockResolvedValue([]) - const service = new AffiliateService(prismaWith(findMany)) - - await service.getAffiliateSwaps(undefined, { limit: 50 }) - - expect(findMany.mock.calls[0][0].where).toMatchObject({ partnerCode: { not: null } }) - }) - - it('providing partnerCode filters to that partner', async () => { - const findMany = jest.fn, [FindManyArgs]>().mockResolvedValue([]) - const service = new AffiliateService(prismaWith(findMany)) - - await service.getAffiliateSwaps('alpha', { limit: 50 }) - - expect(findMany.mock.calls[0][0].where).toMatchObject({ partnerCode: 'alpha' }) - }) - - it('enriches rows with affiliateBps, feeUsd, partnerFeeUsd, volumeUsd', async () => { - const findMany = jest.fn, [FindManyArgs]>().mockResolvedValue([swapRow()]) - const service = new AffiliateService(prismaWith(findMany)) - - const { swaps } = await service.getAffiliateSwaps(undefined, { limit: 50 }) - - // verifiedBps 60, sell 1.0 unit @ $10 => feeUsd = 10 * 60/10000 = 0.06 - // partner rate = 50/60 => partnerFeeUsd = 0.06 * (50/60) = 0.05 - expect(swaps[0].affiliateBps).toBe(60) - expect(swaps[0].feeUsd).toBeCloseTo(0.06, 6) - expect(swaps[0].partnerFeeUsd).toBeCloseTo(0.05, 6) - expect(swaps[0].volumeUsd).toBeCloseTo(10, 6) - }) -}) 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..1c8dee5 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,57 @@ 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: 0, + 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 affiliateBps/feeUsd/partnerFeeUsd/volumeUsd from the verified fee', 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 + // partner rate = partnerBps/verifiedBps = 50/60 => partnerFeeUsd = 0.06 * (50/60) = 0.05 + expect(swaps[0].affiliateBps).toBe(60) + expect(swaps[0].feeUsd).toBeCloseTo(0.06, 6) + expect(swaps[0].partnerFeeUsd).toBeCloseTo(0.05, 6) + expect(swaps[0].volumeUsd).toBeCloseTo(10, 6) + }) + + 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({ affiliateBps: null, feeUsd: null, partnerFeeUsd: null, volumeUsd: null }) + }) +}) diff --git a/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts b/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts deleted file mode 100644 index f3bd636..0000000 --- a/apps/swap-service/src/affiliate/__tests__/list-affiliates.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { PrismaService } from '../../prisma/prisma.service' -import { AffiliateService } from '../affiliate.service' - -const prismaWith = (findMany: jest.Mock): PrismaService => - ({ affiliate: { findMany }, swap: { findMany: jest.fn() } }) as unknown as PrismaService - -describe('AffiliateService.listAffiliates', () => { - it('maps affiliates to { partnerCode, bps, isActive }', async () => { - const findMany = jest.fn().mockResolvedValue([ - { partnerCode: 'alpha', bps: 60, isActive: true, walletAddress: '0xabc', receiveAddress: null }, - { partnerCode: 'beta', bps: 30, isActive: false, walletAddress: '0xdef', receiveAddress: null }, - ]) - const service = new AffiliateService(prismaWith(findMany)) - - const result = await service.listAffiliates() - - expect(result).toEqual([ - { partnerCode: 'alpha', bps: 60, isActive: true }, - { partnerCode: 'beta', bps: 30, isActive: false }, - ]) - }) -}) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index 1991099..3144d07 100644 --- a/apps/swap-service/src/affiliate/affiliate.controller.ts +++ b/apps/swap-service/src/affiliate/affiliate.controller.ts @@ -41,8 +41,8 @@ export class AffiliateController { } @Get() - async list() { - return this.affiliateService.listAffiliates() + async getAffiliates() { + return this.affiliateService.getAffiliates() } @Get(':address') diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index 4ccb372..85f5f80 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -13,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 @@ -140,11 +144,6 @@ export class AffiliateService { } } - async listAffiliates(): Promise<{ partnerCode: string; bps: number; isActive: boolean }[]> { - const rows = await this.prisma.affiliate.findMany() - return rows.map(({ partnerCode, bps, isActive }) => ({ partnerCode, bps, isActive })) - } - async resolvePartnerCode(partnerCode: string) { const affiliate = await this.getAffiliateByPartnerCode(partnerCode) if (!affiliate) return null From b0cc9eb573afeb247b6fdd679c4bf9a4c475bb93 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:02:12 -0600 Subject: [PATCH 5/6] fix(affiliate): preserve stored affiliateBps; emit swap fee USD as full-precision strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getAffiliateSwaps no longer overwrites the stored affiliateBps with verifiedBps - feeUsd/volumeUsd/partnerFeeUsd are full-precision USD strings (no server-side rounding — clients format as needed), null when the swap isn't priceable - test asserts the stored affiliateBps passes through untouched Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/affiliate.service.test.ts | 21 +++++++++++-------- .../src/affiliate/affiliate.service.ts | 9 ++++---- 2 files changed, 16 insertions(+), 14 deletions(-) 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 1c8dee5..04c4d3b 100644 --- a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts +++ b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts @@ -132,7 +132,7 @@ describe('AffiliateService.getAffiliateSwaps fee-split enrichment', () => { buyTxHash: null, partnerBps: 50, shapeshiftBps: 10, - affiliateBps: 0, + affiliateBps: 55, status: 'SUCCESS', isAffiliateVerified: true, sellAsset: { precision: 8 }, @@ -153,18 +153,19 @@ describe('AffiliateService.getAffiliateSwaps fee-split enrichment', () => { ...over, }) - it('derives affiliateBps/feeUsd/partnerFeeUsd/volumeUsd from the verified fee', async () => { + 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 - // partner rate = partnerBps/verifiedBps = 50/60 => partnerFeeUsd = 0.06 * (50/60) = 0.05 - expect(swaps[0].affiliateBps).toBe(60) - expect(swaps[0].feeUsd).toBeCloseTo(0.06, 6) - expect(swaps[0].partnerFeeUsd).toBeCloseTo(0.05, 6) - expect(swaps[0].volumeUsd).toBeCloseTo(10, 6) + // 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 () => { @@ -173,6 +174,8 @@ describe('AffiliateService.getAffiliateSwaps fee-split enrichment', () => { const { swaps } = await service.getAffiliateSwaps(undefined, { limit: 50 }) - expect(swaps[0]).toMatchObject({ affiliateBps: null, feeUsd: null, partnerFeeUsd: null, volumeUsd: null }) + 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.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index 85f5f80..9a20877 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -131,11 +131,10 @@ export class AffiliateService { const swaps = items.map((item) => { const swap = toSwap(item) const fee = calculateFeeForSwap(swap) - const affiliateBps = fee?.verifiedBps ?? null - const feeUsd = fee?.feeUsd ?? null - const volumeUsd = fee?.volumeUsd ?? null - const partnerFeeUsd = fee ? fee.feeUsd * getPartnerFeeRate(fee.verifiedBps, swap.partnerBps) : null - return { ...swap, affiliateBps, feeUsd, partnerFeeUsd, volumeUsd } + 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 { From 0cfddb957ff22eaeadbf327d24c6e7b0d69bfea4 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:02:12 -0600 Subject: [PATCH 6/6] chore(swap-service): remove unused calculateAffiliateFees endpoint GET /swaps/affiliate-fees/:partnerCode and calculateAffiliateFees had no consumers (superseded by AffiliateService.getAffiliateStats and the affiliate-payouts script). Drops the now-unused getPartnerFeeRate import; aggregateFees/Fees remain in use by the live calculateReferralFees. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/swaps/swaps.controller.ts | 9 ----- apps/swap-service/src/swaps/swaps.service.ts | 34 ------------------- 2 files changed, 43 deletions(-) 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