Skip to content

Commit

Permalink
Improve program <> partner flow
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-tey committed Feb 26, 2025
1 parent ece04d1 commit 4bf3052
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 199 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function AddPartnerSheetContent({ setIsOpen }: AddPartnerSheetProps) {
url: program?.url,
trackConversion: true,
programId: program?.id,
folderId: program?.defaultFolderId,
}),
});

Expand Down
109 changes: 104 additions & 5 deletions apps/web/lib/actions/partners/accept-program-invite.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"use server";

import { getEvents } from "@/lib/analytics/get-events";
import { calculateSaleEarnings } from "@/lib/api/sales/calculate-sale-earnings";
import { createId } from "@/lib/api/utils";
import { determinePartnerReward } from "@/lib/partners/determine-partner-reward";
import { SaleEvent } from "@/lib/types";
import { prisma } from "@dub/prisma";
import { EventType, Link } from "@prisma/client";
import { waitUntil } from "@vercel/functions";
import { z } from "zod";
import { authPartnerActionClient } from "../safe-action";
import { backfillLinkData } from "./backfill-link-data";

const acceptProgramInviteSchema = z.object({
programInviteId: z.string(),
Expand All @@ -27,26 +31,121 @@ export const acceptProgramInviteAction = authPartnerActionClient
data: {
id: createId({ prefix: "pge_" }),
programId: programInvite.programId,
linkId: programInvite.linkId,
partnerId: partner.id,
status: "approved",
links: {
connect: {
id: programInvite.linkId,
},
},
},
include: {
links: true,
},
}),
prisma.programInvite.delete({
where: { id: programInvite.id },
}),
]);

const partnerLink = programEnrollment.links[0];

// TODO: send partner.created webhook
waitUntil(
backfillLinkData({
programId: programInvite.programId,
recordSalesAsCommissions({
link: partnerLink,
programId: programEnrollment.programId,
partnerId: partner.id,
linkId: programInvite.linkId,
}),
);

return {
id: programEnrollment.id,
};
});

const recordSalesAsCommissions = async ({
link,
programId,
partnerId,
}: {
link: Pick<Link, "id" | "sales">;
programId: string;
partnerId: string;
}) => {
if (link.sales === 0) {
console.log(`Link ${link.id} has no sales, skipping backfill`);
return;
}

const reward = await determinePartnerReward({
programId,
partnerId,
event: "sale",
});

if (!reward) {
return;
}

const { program, partner } = await prisma.programEnrollment.findUniqueOrThrow(
{
where: {
partnerId_programId: {
partnerId,
programId,
},
},
include: {
program: {
include: {
workspace: true,
},
},
partner: true,
},
},
);

const { workspace } = program;

const saleEvents = await getEvents({
workspaceId: workspace.id,
linkId: link.id,
event: "sales",
interval: "all",
page: 1,
limit: 5000,
sortOrder: "desc",
sortBy: "timestamp",
});

const data = saleEvents.map((e: SaleEvent) => ({
id: createId({ prefix: "cm_" }),
programId: program.id,
partnerId: partner.id,
linkId: link.id,
invoiceId: e.invoice_id || null,
customerId: e.customer.id,
eventId: e.eventId,
amount: e.sale.amount,
type: EventType.sale,
quantity: 1,
currency: "usd",
createdAt: new Date(e.timestamp),
earnings: calculateSaleEarnings({
reward,
sale: {
quantity: 1,
amount: e.sale.amount,
},
}),
}));

if (data.length > 0) {
await prisma.commission.createMany({
data,
skipDuplicates: true,
});
}
};
18 changes: 14 additions & 4 deletions apps/web/lib/actions/partners/approve-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const approvePartnerAction = authActionClient
throw new Error("Link is already associated with another partner.");
}

const [_, updatedLink] = await Promise.all([
const [programEnrollment, updatedLink] = await Promise.all([
prisma.programEnrollment.update({
where: {
partnerId_programId: {
Expand All @@ -48,6 +48,9 @@ export const approvePartnerAction = authActionClient
data: {
status: "approved",
},
include: {
partner: true,
},
}),

// update link to have programId and partnerId
Expand All @@ -58,6 +61,7 @@ export const approvePartnerAction = authActionClient
data: {
programId,
partnerId,
folderId: program.defaultFolderId,
},
include: {
tags: {
Expand All @@ -69,9 +73,15 @@ export const approvePartnerAction = authActionClient
}),
]);

// TODO: [partners] Notify partner of approval?
// TODO: send partner.created webhook
waitUntil(recordLink(updatedLink));
const partner = programEnrollment.partner;

waitUntil(
Promise.allSettled([
recordLink(updatedLink),
// TODO: [partners] Notify partner of approval?
// TODO: send partner.created webhook
]),
);

return {
ok: true,
Expand Down
108 changes: 0 additions & 108 deletions apps/web/lib/actions/partners/backfill-link-data.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/web/lib/actions/partners/reject-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export const rejectPartnerAction = authActionClient
},
data: {
status: "rejected",
linkId: null,
},
});

Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/api/partners/enroll-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const enrollPartner = async ({
programId: program.id,
partnerId: upsertedPartner.id,
folderId: program.defaultFolderId,
trackConversion: true,
},
include: includeTags,
})
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/api/partners/invite-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const invitePartner = async ({
data: {
programId: program.id,
folderId: program.defaultFolderId,
trackConversion: true,
},
})
.then((link) => recordLink(link)),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/zod/schemas/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LinkSchema } from "./links";
import { RewardSchema } from "./rewards";
import { parseDateSchema } from "./utils";

export const HOLDING_PERIOD_DAYS = [0, 30, 60, 90];
export const HOLDING_PERIOD_DAYS = [0, 14, 30, 60, 90];

export const ProgramSchema = z.object({
id: z.string(),
Expand Down
Loading

0 comments on commit 4bf3052

Please sign in to comment.