Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,58 +90,30 @@ 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<string, unknown> => {
const [[args]] = findMany.mock.calls as Array<[{ where: Record<string, unknown> }]>
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 () => {
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' })
expect(where).not.toHaveProperty('affiliate')
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 })
Expand Down
8 changes: 2 additions & 6 deletions apps/swap-service/src/affiliate/affiliate.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.getAffiliateSwaps(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.getAffiliateStats(query.partnerCode, query)
}

@Get(':address')
Expand Down
79 changes: 3 additions & 76 deletions apps/swap-service/src/affiliate/affiliate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -64,50 +58,7 @@ export class AffiliateService {
return this.prisma.affiliate.update({ where: { walletAddress }, data: updateData })
}

async getAffiliateStatsByAddress(address: string, options: AffiliateStatsQueryDto): Promise<AffiliateStatsResult> {
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(
async getAffiliateStats(
partnerCode: string,
options: { startDate?: Date; endDate?: Date },
): Promise<AffiliateStatsResult> {
Expand Down Expand Up @@ -153,31 +104,7 @@ export class AffiliateService {
}
}

async getAffiliateSwapsByAddress(address: string, options: AffiliateSwapsQueryDto): Promise<PaginatedSwaps> {
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(
async getAffiliateSwaps(
partnerCode: string,
options: { startDate?: Date; endDate?: Date; limit: number; cursor?: string },
): Promise<PaginatedSwaps> {
Expand Down
14 changes: 2 additions & 12 deletions apps/swap-service/src/affiliate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion apps/swap-service/src/swaps/swaps.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion apps/swap-service/src/swaps/swaps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ export class SwapsService {
}
}

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