From e5cd5d2cb8cbd40a4cdd9faae1b7cc9ab1f6ff66 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:06:05 -0600 Subject: [PATCH 1/2] feat(swap-service): affiliate stats/swaps reads are partnerCode-only Contract step after #44: now that public-api queries by partnerCode, drop the legacy address path from /v1/affiliate/{stats,swaps}. - DTOs require partnerCode, drop the optional address field - controller routes straight to the partnerCode methods (no address branch) - remove the now-dead getAffiliate{Stats,Swaps}ByAddress service methods and their tests partnerAddress remains on swaps (payout snapshot) and the write-path reverse-lookup is untouched. Co-Authored-By: Claude Opus 4.8 --- .../__tests__/affiliate.service.test.ts | 28 ------- .../src/affiliate/affiliate.controller.ts | 8 +- .../src/affiliate/affiliate.service.ts | 75 +------------------ apps/swap-service/src/affiliate/types.ts | 14 +--- 4 files changed, 5 insertions(+), 120 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 3785ea7..ec977f6 100644 --- a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts +++ b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts @@ -90,8 +90,6 @@ describe('AffiliateService.createAffiliate', () => { }) describe('AffiliateService attribution reads', () => { - const address = '0x1111111111111111111111111111111111111111' - // Pull the `where` from the first prisma.swap.findMany call as a typed object so the // assertions below don't trip the no-unsafe-any lint rules on jest's `any`-typed calls. const whereOfFirstCall = (findMany: jest.Mock): Record => { @@ -99,32 +97,6 @@ describe('AffiliateService attribution reads', () => { return args.where } - it('getAffiliateStatsByAddress filters through the affiliate relation, never partnerAddress', async () => { - const findMany = jest.fn().mockResolvedValue([]) - const service = new AffiliateService(makePrismaMock(undefined, findMany)) - - await service.getAffiliateStatsByAddress(address, {}) - - const where = whereOfFirstCall(findMany) - expect(where).toMatchObject({ - affiliate: { OR: [{ walletAddress: address }, { receiveAddress: address }] }, - }) - expect(where).not.toHaveProperty('partnerAddress') - }) - - it('getAffiliateSwapsByAddress filters through the affiliate relation, never partnerAddress', async () => { - const findMany = jest.fn().mockResolvedValue([]) - const service = new AffiliateService(makePrismaMock(undefined, findMany)) - - await service.getAffiliateSwapsByAddress(address, { limit: 50 }) - - const where = whereOfFirstCall(findMany) - expect(where).toMatchObject({ - affiliate: { OR: [{ walletAddress: address }, { receiveAddress: address }] }, - }) - expect(where).not.toHaveProperty('partnerAddress') - }) - it('getAffiliateSwapsByPartnerCode filters directly on partnerCode, with no join or address', async () => { const findMany = jest.fn().mockResolvedValue([]) const service = new AffiliateService(makePrismaMock(undefined, findMany)) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index cc4922f..c89e75f 100644 --- a/apps/swap-service/src/affiliate/affiliate.controller.ts +++ b/apps/swap-service/src/affiliate/affiliate.controller.ts @@ -32,16 +32,12 @@ export class AffiliateController { @Get('swaps') async getSwaps(@Query() query: AffiliateSwapsQueryDto) { - if (query.partnerCode) return this.affiliateService.getAffiliateSwapsByPartnerCode(query.partnerCode, query) - if (query.address) return this.affiliateService.getAffiliateSwapsByAddress(query.address, query) - throw new BadRequestException('partnerCode or address is required') + return this.affiliateService.getAffiliateSwapsByPartnerCode(query.partnerCode, query) } @Get('stats') async getStats(@Query() query: AffiliateStatsQueryDto) { - if (query.partnerCode) return this.affiliateService.getAffiliateStatsByPartnerCode(query.partnerCode, query) - if (query.address) return this.affiliateService.getAffiliateStatsByAddress(query.address, query) - throw new BadRequestException('partnerCode or address is required') + return this.affiliateService.getAffiliateStatsByPartnerCode(query.partnerCode, query) } @Get(':address') diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index 722e708..a45068e 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -7,13 +7,7 @@ import { PaginatedSwaps } from '../swaps/types' import { calculateFeeForSwap, getPartnerFeeRate, toSwap } from '../swaps/utils' import { getNextCursor, swapCursorArgs } from '../utils/pagination' -import type { - AffiliateStatsQueryDto, - AffiliateStatsResult, - AffiliateSwapsQueryDto, - CreateAffiliateDto, - UpdateAffiliateDto, -} from './types' +import type { AffiliateStatsResult, CreateAffiliateDto, UpdateAffiliateDto } from './types' import { isReservedPartnerCode } from './utils' @Injectable() @@ -64,49 +58,6 @@ export class AffiliateService { return this.prisma.affiliate.update({ where: { walletAddress }, data: updateData }) } - async getAffiliateStatsByAddress(address: string, options: AffiliateStatsQueryDto): Promise { - const { startDate, endDate } = options - - const items = await this.prisma.swap.findMany({ - where: { - affiliate: { OR: [{ walletAddress: address }, { receiveAddress: address }] }, - status: 'SUCCESS', - isAffiliateVerified: true, - ...(startDate || endDate - ? { - createdAt: { - ...(startDate && { gte: startDate }), - ...(endDate && { lte: endDate }), - }, - } - : {}), - }, - }) - - let totalSwaps = 0 - let totalVolumeUsd = 0 - let totalFeesEarnedUsd = 0 - - for (const item of items) { - const swap = toSwap(item) - - const fee = calculateFeeForSwap(swap) - if (!fee) continue - - const rate = getPartnerFeeRate(fee.verifiedBps, swap.partnerBps) - - totalSwaps++ - totalVolumeUsd += fee.volumeUsd - totalFeesEarnedUsd += fee.feeUsd * rate - } - - return { - totalSwaps, - totalVolumeUsd: totalVolumeUsd.toFixed(2), - totalFeesEarnedUsd: totalFeesEarnedUsd.toFixed(2), - } - } - async getAffiliateStatsByPartnerCode( partnerCode: string, options: { startDate?: Date; endDate?: Date }, @@ -153,30 +104,6 @@ export class AffiliateService { } } - async getAffiliateSwapsByAddress(address: string, options: AffiliateSwapsQueryDto): Promise { - const { startDate, endDate, limit, cursor } = options - - const items = await this.prisma.swap.findMany({ - ...swapCursorArgs(limit, cursor), - where: { - affiliate: { OR: [{ walletAddress: address }, { receiveAddress: address }] }, - ...(startDate || endDate - ? { - createdAt: { - ...(startDate && { gte: startDate }), - ...(endDate && { lte: endDate }), - }, - } - : {}), - }, - }) - - return { - swaps: items.map(toSwap), - nextCursor: getNextCursor(items, limit), - } - } - async getAffiliateSwapsByPartnerCode( partnerCode: string, options: { startDate?: Date; endDate?: Date; limit: number; cursor?: string }, diff --git a/apps/swap-service/src/affiliate/types.ts b/apps/swap-service/src/affiliate/types.ts index e517d15..ef8a945 100644 --- a/apps/swap-service/src/affiliate/types.ts +++ b/apps/swap-service/src/affiliate/types.ts @@ -14,13 +14,8 @@ export interface AffiliateStatsResult { } export class AffiliateStatsQueryDto { - @IsOptional() - @IsEthereumAddress() - address?: string - - @IsOptional() @Matches(PARTNER_CODE_REGEX, { message: PARTNER_CODE_MESSAGE }) - partnerCode?: string + partnerCode: string @IsOptional() @Type(() => Date) @@ -34,13 +29,8 @@ export class AffiliateStatsQueryDto { } export class AffiliateSwapsQueryDto extends PaginationQueryDto { - @IsOptional() - @IsEthereumAddress() - address?: string - - @IsOptional() @Matches(PARTNER_CODE_REGEX, { message: PARTNER_CODE_MESSAGE }) - partnerCode?: string + partnerCode: string @IsOptional() @Type(() => Date) From 8c489238066538667ed7ff785be2a9cb1782b674 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:25:34 -0600 Subject: [PATCH 2/2] refactor(swap-service): drop redundant ByPartnerCode suffix Each method now has a single variant (the by-address ones are gone), so the suffix is noise: - getAffiliateStatsByPartnerCode -> getAffiliateStats - getAffiliateSwapsByPartnerCode -> getAffiliateSwaps - calculateAffiliateFeesByPartnerCode -> calculateAffiliateFees Co-Authored-By: Claude Opus 4.8 --- .../src/affiliate/__tests__/affiliate.service.test.ts | 8 ++++---- apps/swap-service/src/affiliate/affiliate.controller.ts | 4 ++-- apps/swap-service/src/affiliate/affiliate.service.ts | 4 ++-- apps/swap-service/src/swaps/swaps.controller.ts | 2 +- apps/swap-service/src/swaps/swaps.service.ts | 2 +- 5 files changed, 10 insertions(+), 10 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 ec977f6..26ac7a1 100644 --- a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts +++ b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts @@ -97,11 +97,11 @@ describe('AffiliateService attribution reads', () => { return args.where } - it('getAffiliateSwapsByPartnerCode filters directly on partnerCode, with no join or address', async () => { + it('getAffiliateSwaps filters directly on partnerCode, with no join or address', async () => { const findMany = jest.fn().mockResolvedValue([]) const service = new AffiliateService(makePrismaMock(undefined, findMany)) - await service.getAffiliateSwapsByPartnerCode('goodcode', { limit: 50 }) + await service.getAffiliateSwaps('goodcode', { limit: 50 }) const where = whereOfFirstCall(findMany) expect(where).toMatchObject({ partnerCode: 'goodcode' }) @@ -109,11 +109,11 @@ describe('AffiliateService attribution reads', () => { expect(where).not.toHaveProperty('partnerAddress') }) - it('getAffiliateStatsByPartnerCode filters directly on partnerCode, with no join or address', async () => { + it('getAffiliateStats filters directly on partnerCode, with no join or address', async () => { const findMany = jest.fn().mockResolvedValue([]) const service = new AffiliateService(makePrismaMock(undefined, findMany)) - const result = await service.getAffiliateStatsByPartnerCode('goodcode', {}) + const result = await service.getAffiliateStats('goodcode', {}) const where = whereOfFirstCall(findMany) expect(where).toMatchObject({ partnerCode: 'goodcode', status: 'SUCCESS', isAffiliateVerified: true }) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index c89e75f..f6b1bcf 100644 --- a/apps/swap-service/src/affiliate/affiliate.controller.ts +++ b/apps/swap-service/src/affiliate/affiliate.controller.ts @@ -32,12 +32,12 @@ export class AffiliateController { @Get('swaps') async getSwaps(@Query() query: AffiliateSwapsQueryDto) { - return this.affiliateService.getAffiliateSwapsByPartnerCode(query.partnerCode, query) + return this.affiliateService.getAffiliateSwaps(query.partnerCode, query) } @Get('stats') async getStats(@Query() query: AffiliateStatsQueryDto) { - return this.affiliateService.getAffiliateStatsByPartnerCode(query.partnerCode, query) + return this.affiliateService.getAffiliateStats(query.partnerCode, query) } @Get(':address') diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index a45068e..c3cb4b5 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -58,7 +58,7 @@ export class AffiliateService { return this.prisma.affiliate.update({ where: { walletAddress }, data: updateData }) } - async getAffiliateStatsByPartnerCode( + async getAffiliateStats( partnerCode: string, options: { startDate?: Date; endDate?: Date }, ): Promise { @@ -104,7 +104,7 @@ export class AffiliateService { } } - async getAffiliateSwapsByPartnerCode( + async getAffiliateSwaps( partnerCode: string, options: { startDate?: Date; endDate?: Date; limit: number; cursor?: string }, ): Promise { diff --git a/apps/swap-service/src/swaps/swaps.controller.ts b/apps/swap-service/src/swaps/swaps.controller.ts index adc9902..cab0cfd 100644 --- a/apps/swap-service/src/swaps/swaps.controller.ts +++ b/apps/swap-service/src/swaps/swaps.controller.ts @@ -48,6 +48,6 @@ export class SwapsController { @Query('startDate', OptionalDatePipe) startDate?: Date, @Query('endDate', OptionalDatePipe) endDate?: Date, ) { - return this.swapsService.calculateAffiliateFeesByPartnerCode(partnerCode, startDate, endDate) + 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 85b3ab5..9d98521 100644 --- a/apps/swap-service/src/swaps/swaps.service.ts +++ b/apps/swap-service/src/swaps/swaps.service.ts @@ -311,7 +311,7 @@ export class SwapsService { } } - async calculateAffiliateFeesByPartnerCode(partnerCode: string, startDate?: Date, endDate?: Date): Promise { + async calculateAffiliateFees(partnerCode: string, startDate?: Date, endDate?: Date): Promise { logger.log( `Calculating affiliate fees for ${partnerCode}, period: ${startDate?.toISOString()} - ${endDate?.toISOString()}`, )