From ac45809bf425caaf2fae893aa1a7f4c715b7d1da Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 11 Sep 2025 17:04:30 -0700 Subject: [PATCH 1/3] Deduplicate lead commission creation --- .../lib/partners/create-partner-commission.ts | 130 ++++++++++-------- 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/apps/web/lib/partners/create-partner-commission.ts b/apps/web/lib/partners/create-partner-commission.ts index 6b6dcbdfd53..18aa4340f3f 100644 --- a/apps/web/lib/partners/create-partner-commission.ts +++ b/apps/web/lib/partners/create-partner-commission.ts @@ -86,19 +86,20 @@ export const createPartnerCommission = async ({ return; } - // for click/lead events, it's super simple – just multiply the reward amount by the quantity - if (event === "click" || event === "lead") { + // for click events, it's super simple – just multiply the reward amount by the quantity + if (event === "click") { earnings = reward.amount * quantity; - // for sale events, we need to check: + // for lead and sale events, we need to check the first time this partner-customer combination was recorded (for deduplication) + // for sale rewards specifically, we also need to check: // 1. if the partner has reached the max duration for the reward (if applicable) // 2. if the previous commission were marked as fraud or canceled - } else if (event === "sale") { + } else { const firstCommission = await prisma.commission.findFirst({ where: { partnerId, customerId, - type: "sale", + type: event, }, orderBy: { createdAt: "asc", @@ -111,73 +112,88 @@ export const createPartnerCommission = async ({ }); if (firstCommission) { - // if partner's reward was updated and different from the first commission's reward - // we need to make sure it wasn't changed from one-time to recurring so we don't create a new commission - if ( - firstCommission.rewardId && - firstCommission.rewardId !== reward.id - ) { - const originalReward = await prisma.reward.findUnique({ - where: { - id: firstCommission.rewardId, - }, - select: { - id: true, - maxDuration: true, - }, - }); + // for lead events, we need to check if the partner has already been issued a lead reward for this customer + if (event === "lead") { + console.log( + `Partner ${partnerId} has already been issued a lead reward for this customer ${customerId}, skipping commission creation...`, + ); + return; + // for sale rewards, we need to check if partner's reward was updated and different from the first commission's reward + // we need to make sure it wasn't changed from one-time to recurring so we don't create a new commission + } else { if ( - typeof originalReward?.maxDuration === "number" && - originalReward.maxDuration === 0 + firstCommission.rewardId && + firstCommission.rewardId !== reward.id ) { - console.log( - `Partner ${partnerId} is only eligible for first-sale commissions based on the original reward ${originalReward.id}, skipping commission creation...`, - ); - return; - } - } + const originalReward = await prisma.reward.findUnique({ + where: { + id: firstCommission.rewardId, + }, + select: { + id: true, + maxDuration: true, + }, + }); - // for reward types with a max duration, we need to check if the first commission is within the max duration - // if it's beyond the max duration, we should not create a new commission - if (typeof reward?.maxDuration === "number") { - // One-time sale reward (maxDuration === 0) - if (reward.maxDuration === 0) { - console.log( - `Partner ${partnerId} is only eligible for first-sale commissions, skipping commission creation...`, - ); - return; + if ( + typeof originalReward?.maxDuration === "number" && + originalReward.maxDuration === 0 + ) { + console.log( + `Partner ${partnerId} is only eligible for first-sale commissions based on the original reward ${originalReward.id}, skipping commission creation...`, + ); + return; + } } - // Recurring sale reward - else { - const monthsDifference = differenceInMonths( - new Date(), - firstCommission.createdAt, - ); - - if (monthsDifference >= reward.maxDuration) { + // for sale rewards with a max duration, we need to check if the first commission is within the max duration + // if it's beyond the max duration, we should not create a new commission + if (typeof reward?.maxDuration === "number") { + // One-time sale reward (maxDuration === 0) + if (reward.maxDuration === 0) { console.log( - `Partner ${partnerId} has reached max duration for ${event} event, skipping commission creation...`, + `Partner ${partnerId} is only eligible for first-sale commissions, skipping commission creation...`, ); return; } + + // Recurring sale reward (maxDuration > 0) + else { + const monthsDifference = differenceInMonths( + new Date(), + firstCommission.createdAt, + ); + + if (monthsDifference >= reward.maxDuration) { + console.log( + `Partner ${partnerId} has reached max duration for ${event} event, skipping commission creation...`, + ); + return; + } + } } - } - // if first commission is fraud or canceled, the commission will be set to fraud or canceled as well - if ( - firstCommission.status === "fraud" || - firstCommission.status === "canceled" - ) { - status = firstCommission.status; + // if first commission is fraud or canceled, the commission will be set to fraud or canceled as well + if ( + firstCommission.status === "fraud" || + firstCommission.status === "canceled" + ) { + status = firstCommission.status; + } } } - earnings = calculateSaleEarnings({ - reward, - sale: { quantity, amount }, - }); + // for lead events, we just multiply the reward amount by the quantity + if (event === "lead") { + earnings = reward.amount * quantity; + // for sale events, we need to calculate the earnings based on the sale amount + } else { + earnings = calculateSaleEarnings({ + reward, + sale: { quantity, amount }, + }); + } } // handle rewards with max reward amount limit From 4ada1982d4263834a094d1e3334a24c56ce96a8d Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 11 Sep 2025 17:11:38 -0700 Subject: [PATCH 2/3] remove maxAmount --- .../lib/partners/create-partner-commission.ts | 33 +------------------ apps/web/lib/zod/schemas/rewards.ts | 1 - packages/prisma/schema/reward.prisma | 1 - 3 files changed, 1 insertion(+), 34 deletions(-) diff --git a/apps/web/lib/partners/create-partner-commission.ts b/apps/web/lib/partners/create-partner-commission.ts index 18aa4340f3f..c1f04909edc 100644 --- a/apps/web/lib/partners/create-partner-commission.ts +++ b/apps/web/lib/partners/create-partner-commission.ts @@ -90,7 +90,7 @@ export const createPartnerCommission = async ({ if (event === "click") { earnings = reward.amount * quantity; - // for lead and sale events, we need to check the first time this partner-customer combination was recorded (for deduplication) + // for lead and sale events, we need to check if this partner-customer combination was recorded already (for deduplication) // for sale rewards specifically, we also need to check: // 1. if the partner has reached the max duration for the reward (if applicable) // 2. if the previous commission were marked as fraud or canceled @@ -195,37 +195,6 @@ export const createPartnerCommission = async ({ }); } } - - // handle rewards with max reward amount limit - if (reward.maxAmount) { - const totalRewards = await prisma.commission.aggregate({ - where: { - earnings: { - gt: 0, - }, - programId, - partnerId, - status: { - in: ["pending", "processed", "paid"], - }, - type: event, - }, - _sum: { - earnings: true, - }, - }); - - const totalEarnings = totalRewards._sum.earnings || 0; - if (totalEarnings >= reward.maxAmount) { - console.log( - `Partner ${partnerId} has reached max reward amount for ${event} event, skipping commission creation...`, - ); - return; - } - - const remainingRewardAmount = reward.maxAmount - totalEarnings; - earnings = Math.max(0, Math.min(earnings, remainingRewardAmount)); - } } try { diff --git a/apps/web/lib/zod/schemas/rewards.ts b/apps/web/lib/zod/schemas/rewards.ts index f7376ec4f07..7bae3af13af 100644 --- a/apps/web/lib/zod/schemas/rewards.ts +++ b/apps/web/lib/zod/schemas/rewards.ts @@ -139,7 +139,6 @@ export const RewardSchema = z.object({ type: z.nativeEnum(RewardStructure), amount: z.number(), maxDuration: z.number().nullish(), - maxAmount: z.number().nullish(), modifiers: z.any().nullish(), // TODO: Fix this }); diff --git a/packages/prisma/schema/reward.prisma b/packages/prisma/schema/reward.prisma index d17d62778f9..b38c3b6ca12 100644 --- a/packages/prisma/schema/reward.prisma +++ b/packages/prisma/schema/reward.prisma @@ -17,7 +17,6 @@ model Reward { type RewardStructure @default(percentage) amount Int @default(0) maxDuration Int? // in months (0 -> not recurring, null -> infinite) - maxAmount Int? // how much a partner can receive payouts (in cents) modifiers Json? @db.Json createdAt DateTime @default(now()) updatedAt DateTime @updatedAt From 2fb3e75555d04b61d3822ce9ad15d3ae50c6b274 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 11 Sep 2025 17:21:55 -0700 Subject: [PATCH 3/3] fix create button height on program pages --- .../[slug]/(ee)/program/bounties/create-bounty-button.tsx | 6 ++++-- .../program/commissions/commission-popover-buttons.tsx | 2 +- .../(ee)/program/commissions/create-commission-button.tsx | 6 ++++-- .../(ee)/program/commissions/create-commission-sheet.tsx | 4 ++-- .../[slug]/(ee)/program/groups/create-group-button.tsx | 6 ++++-- .../[slug]/(ee)/program/partners/import-export-buttons.tsx | 2 +- .../[slug]/(ee)/program/partners/invite-partner-button.tsx | 6 ++++-- .../[slug]/(ee)/program/partners/page-client.tsx | 7 ------- .../(dashboard)/[slug]/(ee)/program/partners/page.tsx | 4 ++-- .../{create-commission.ts => create-manual-commission.ts} | 2 +- 10 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/page-client.tsx rename apps/web/lib/actions/partners/{create-commission.ts => create-manual-commission.ts} (99%) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx index cffd7382c80..69dd77cea05 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/create-bounty-button.tsx @@ -1,9 +1,10 @@ "use client"; -import { Button, useKeyboardShortcut } from "@dub/ui"; +import { Button, useKeyboardShortcut, useMediaQuery } from "@dub/ui"; import { useBountySheet } from "./add-edit-bounty-sheet"; export function CreateBountyButton() { + const { isMobile } = useMediaQuery(); const { BountySheet, setShowCreateBountySheet } = useBountySheet({ nested: false, }); @@ -16,8 +17,9 @@ export function CreateBountyButton() {