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
8 changes: 6 additions & 2 deletions apps/web/lib/actions/partners/create-manual-commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,12 @@ export const createManualCommissionAction = authActionClient
createdAt: new Date(saleEventData.timestamp + "Z"), // add the "Z" to the timestamp to make it UTC
user,
context: {
customer: { country: customer.country },
sale: { productId },
customer: {
country: customer.country,
},
sale: {
productId,
},
},
})),
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/api/conversions/track-sale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,10 @@ const _trackSale = async ({
context: {
customer: {
country: customer.country,
source: source!,
source,
},
sale: {
productId: metadata?.productId as string,
productId: metadata?.productId,
amount: saleData.amount,
},
},
Expand Down
52 changes: 34 additions & 18 deletions apps/web/lib/partners/create-partner-commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,36 @@ export const createPartnerCommission = async ({
earnings = amount;
amount = 0;
} else {
if (["lead", "sale"].includes(event) && customerId) {
firstCommission = await prisma.commission.findFirst({
where: {
partnerId,
customerId,
type: event,
},
orderBy: {
createdAt: "asc",
},
select: {
rewardId: true,
status: true,
createdAt: true,
},
});

const subscriptionDurationMonths = firstCommission
? differenceInMonths(new Date(), firstCommission.createdAt)
: 0;

context = {
...context,
customer: {
...context?.customer,
subscriptionDurationMonths,
},
};
}

reward = determinePartnerReward({
event,
programEnrollment,
Expand Down Expand Up @@ -126,22 +156,6 @@ export const createPartnerCommission = async ({
// 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 {
firstCommission = await prisma.commission.findFirst({
where: {
partnerId,
customerId,
type: event,
},
orderBy: {
createdAt: "asc",
},
select: {
rewardId: true,
status: true,
createdAt: true,
},
});

if (firstCommission) {
// if first commission is fraud or canceled, skip commission creation
if (["fraud", "canceled"].includes(firstCommission.status)) {
Expand Down Expand Up @@ -207,6 +221,7 @@ export const createPartnerCommission = async ({
console.log(
`Partner ${partnerId} is only eligible for first-sale commissions, skipping commission creation...`,
);

return {
commission: null,
programEnrollment,
Expand All @@ -216,15 +231,16 @@ export const createPartnerCommission = async ({

// Recurring sale reward (maxDuration > 0)
else {
const monthsDifference = differenceInMonths(
const subscriptionDurationMonths = differenceInMonths(
new Date(),
firstCommission.createdAt,
);

if (monthsDifference >= reward.maxDuration) {
if (subscriptionDurationMonths >= reward.maxDuration) {
console.log(
`Partner ${partnerId} has reached max duration for ${event} event, skipping commission creation...`,
);

return {
commission: null,
programEnrollment,
Expand Down
6 changes: 6 additions & 0 deletions apps/web/lib/zod/schemas/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export const REWARD_CONDITIONS: Record<
},
],
},
{
id: "subscriptionDurationMonths",
label: "Subscription duration",
type: "number",
},
],
},
PARTNER_ENTITY,
Expand Down Expand Up @@ -347,6 +352,7 @@ export const rewardContextSchema = z.object({
.object({
country: z.string().nullish(),
source: z.enum(CUSTOMER_SOURCES).default("tracked").nullish(),
subscriptionDurationMonths: z.number().nullish(),
})
.optional(),

Expand Down
66 changes: 66 additions & 0 deletions apps/web/tests/rewards/reward-conditions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1660,4 +1660,70 @@ describe("evaluateRewardConditions", () => {
expect(result).toBeNull(); // Should not match when country is null
});
});

describe("subscription duration conditions", () => {
describe("customer.subscriptionDurationMonths", () => {
test("should match when subscription duration meets greater_than_or_equal condition", () => {
const conditions = [
{
operator: "AND" as const,
type: "flat" as const,
amountInCents: 5000,
conditions: [
{
entity: "customer" as const,
attribute: "subscriptionDurationMonths" as const,
operator: "greater_than_or_equal" as const,
value: 12,
},
],
},
];

const context: RewardContext = {
customer: {
subscriptionDurationMonths: 12,
},
};

const result = evaluateRewardConditions({
conditions,
context,
});

expect(result).toEqual(conditions[0]);
});

test("should not match when subscription duration is less than condition value", () => {
const conditions = [
{
operator: "AND" as const,
type: "flat" as const,
amountInCents: 5000,
conditions: [
{
entity: "customer" as const,
attribute: "subscriptionDurationMonths" as const,
operator: "greater_than_or_equal" as const,
value: 12,
},
],
},
];

const context: RewardContext = {
customer: {
subscriptionDurationMonths: 6,
},
};

const result = evaluateRewardConditions({
conditions,
context,
});

expect(result).toBe(null);
});
});
});
});
61 changes: 36 additions & 25 deletions apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
currencyFormatter,
pluralize,
} from "@dub/utils";
import { formatDuration } from "date-fns";
import { useRef } from "react";
import * as z from "zod/v4";

Expand Down Expand Up @@ -118,6 +119,8 @@ export function ProgramRewardModifiersTooltipContent({
);
}

// TODO:
// This became a bit of a mess, let's clean it up a bit.
const RewardItem = ({
reward,
conditions,
Expand Down Expand Up @@ -168,8 +171,7 @@ const RewardItem = ({
<span className="shrink-0 text-lg leading-none">&bull;</span>
<span className="min-w-0">
{idx === 0 ? "If" : capitalize(operator.toLowerCase())}{" "}
{condition.entity}{" "}
{(attribute?.label ?? condition.attribute)?.toLowerCase()}{" "}
{condition.entity} {attribute?.label?.toLowerCase()}{" "}
{CONDITION_OPERATOR_LABELS[condition.operator]}{" "}
{condition.value &&
(condition.attribute === "country"
Expand All @@ -180,29 +182,32 @@ const RewardItem = ({
.join(", ")
: COUNTRIES[condition.value?.toString()] ??
condition.value
: // Non-country value(s)
Array.isArray(condition.value)
? // Basic array
(attribute?.options
? (condition.value as string[] | number[]).map(
(v) =>
attribute.options?.find((o) => o.id === v)
?.label ?? v,
)
: condition.value
).join(", ")
: condition.attribute === "productId" && condition.label
? // Product label
condition.label
: attribute?.type === "currency"
? // Currency value
currencyFormatter(Number(condition.value))
: // Everything else
attribute?.options
? attribute.options.find(
(o) => o.id === condition.value,
)?.label ?? condition.value.toString()
: condition.value.toString())}
: condition.attribute === "subscriptionDurationMonths"
? formatSubscriptionDuration(Number(condition.value))
: // Non-country value(s)
Array.isArray(condition.value)
? // Basic array
(attribute?.options
? (condition.value as string[] | number[]).map(
(v) =>
attribute.options?.find((o) => o.id === v)
?.label ?? v,
)
: condition.value
).join(", ")
: condition.attribute === "productId" &&
condition.label
? // Product label
condition.label
: attribute?.type === "currency"
? // Currency value
currencyFormatter(Number(condition.value))
: // Everything else
attribute?.options
? attribute.options.find(
(o) => o.id === condition.value,
)?.label ?? condition.value.toString()
: condition.value.toString())}
</span>
</li>
);
Expand All @@ -212,3 +217,9 @@ const RewardItem = ({
</div>
);
};

function formatSubscriptionDuration(v: number): string {
return formatDuration(
v >= 12 ? { years: Math.floor(v / 12), months: v % 12 } : { months: v },
);
}
4 changes: 4 additions & 0 deletions apps/web/ui/partners/rewards/rewards-logic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,10 @@ function ConditionLogic({
)}
</InlineBadgePopover>

{condition.attribute === "subscriptionDurationMonths" && (
<span> months</span>
)}

{condition.attribute === "productId" && condition.value && (
<button
type="button"
Expand Down