Thanks to visit codestin.com
Credit goes to github.com

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
@@ -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,
});
Expand All @@ -16,8 +17,9 @@ export function CreateBountyButton() {
<Button
type="button"
onClick={() => setShowCreateBountySheet(true)}
text="Create bounty"
text={`Create${isMobile ? "" : " bounty"}`}
shortcut="C"
className="h-8 px-3 sm:h-9"
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function CommissionPopoverButtons() {
<Button
onClick={() => setOpenPopover(!openPopover)}
variant="secondary"
className="w-auto px-1.5"
className="h-8 w-auto px-1.5 sm:h-9"
icon={<ThreeDots className="h-5 w-5 text-neutral-500" />}
/>
</Popover>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { Button, useKeyboardShortcut } from "@dub/ui";
import { Button, useKeyboardShortcut, useMediaQuery } from "@dub/ui";
import { useCreateCommissionSheet } from "./create-commission-sheet";

export function CreateCommissionButton() {
const { isMobile } = useMediaQuery();
const { createCommissionSheet, setIsOpen: setShowCreateCommissionSheet } =
useCreateCommissionSheet({
nested: false,
Expand All @@ -18,8 +19,9 @@ export function CreateCommissionButton() {
<Button
type="button"
onClick={() => setShowCreateCommissionSheet(true)}
text="Create commission"
text={`Create${isMobile ? "" : " commission"}`}
shortcut="C"
className="h-8 px-3 sm:h-9"
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createCommissionAction } from "@/lib/actions/partners/create-commission";
import { createManualCommissionAction } from "@/lib/actions/partners/create-manual-commission";
import { handleMoneyKeyDown } from "@/lib/form-utils";
import { mutatePrefix } from "@/lib/swr/mutate";
import useRewards from "@/lib/swr/use-rewards";
Expand Down Expand Up @@ -113,7 +113,7 @@ function CreateCommissionSheetContent(props: CreateCommissionSheetProps) {
}
}, [commissionType]);

const { executeAsync, isPending } = useAction(createCommissionAction, {
const { executeAsync, isPending } = useAction(createManualCommissionAction, {
onSuccess: async () => {
toast.success("A commission has been created for the partner!");
setIsOpen(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import useGroupsCount from "@/lib/swr/use-groups-count";
import useWorkspace from "@/lib/swr/use-workspace";
import { usePartnersUpgradeModal } from "@/ui/partners/partners-upgrade-modal";
import { Button, useKeyboardShortcut } from "@dub/ui";
import { Button, useKeyboardShortcut, useMediaQuery } from "@dub/ui";
import { useCreateGroupModal } from "./create-group-modal";

export function CreateGroupButton() {
const { isMobile } = useMediaQuery();
const { groupsLimit, nextPlan } = useWorkspace();
const { groupsCount } = useGroupsCount();

Expand Down Expand Up @@ -37,7 +38,8 @@ export function CreateGroupButton() {
<Button
type="button"
onClick={handleCreateGroup}
text="Create group"
text={`Create${isMobile ? "" : " group"}`}
className="h-8 px-3 sm:h-9"
shortcut={!disabled ? "C" : undefined}
disabled={disabled}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function ImportExportButtons() {
<Button
onClick={() => setOpenPopover(!openPopover)}
variant="secondary"
className="w-auto px-1.5"
className="h-8 w-auto px-1.5 sm:h-9"
icon={<ThreeDots className="h-5 w-5 text-neutral-500" />}
/>
</Popover>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { Button, useKeyboardShortcut } from "@dub/ui";
import { Button, useKeyboardShortcut, useMediaQuery } from "@dub/ui";
import { useInvitePartnerSheet } from "./invite-partner-sheet";

export function InvitePartnerButton() {
const { isMobile } = useMediaQuery();
const { invitePartnerSheet, setIsOpen: setShowInvitePartnerSheet } =
useInvitePartnerSheet();

Expand All @@ -15,8 +16,9 @@ export function InvitePartnerButton() {
<Button
type="button"
onClick={() => setShowInvitePartnerSheet(true)}
text="Invite partner"
text={`Invite${isMobile ? "" : " partner"}`}
shortcut="P"
className="h-8 px-3 sm:h-9"
/>
</>
);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PageContent } from "@/ui/layout/page-content";
import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper";
import { ImportExportButtons } from "./import-export-buttons";
import { InvitePartnerButton } from "./invite-partner-button";
import { ProgramPartnersPageClient } from "./page-client";
import { PartnersTable } from "./partners-table";

export default function ProgramPartners() {
return (
Expand All @@ -21,7 +21,7 @@ export default function ProgramPartners() {
}
>
<PageWidthWrapper>
<ProgramPartnersPageClient />
<PartnersTable />
</PageWidthWrapper>
</PageContent>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { WorkflowTrigger } from "@prisma/client";
import { waitUntil } from "@vercel/functions";
import { authActionClient } from "../safe-action";

export const createCommissionAction = authActionClient
export const createManualCommissionAction = authActionClient
.schema(createCommissionSchema)
.action(async ({ parsedInput, ctx }) => {
const { workspace, user } = ctx;
Expand Down
159 changes: 72 additions & 87 deletions apps/web/lib/partners/create-partner-commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
} else if (event === "sale") {
} else {
const firstCommission = await prisma.commission.findFirst({
where: {
partnerId,
customerId,
type: "sale",
type: event,
},
orderBy: {
createdAt: "asc",
Expand All @@ -111,104 +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 },
});
}

// 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;
// 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 },
});
}

const remainingRewardAmount = reward.maxAmount - totalEarnings;
earnings = Math.max(0, Math.min(earnings, remainingRewardAmount));
}
}

Expand Down
1 change: 0 additions & 1 deletion apps/web/lib/zod/schemas/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand Down
1 change: 0 additions & 1 deletion packages/prisma/schema/reward.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down