Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve payouts cron #1803

Merged
merged 8 commits into from
Dec 17, 2024
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
32 changes: 9 additions & 23 deletions apps/web/app/api/cron/process-payouts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,30 @@ export async function GET(req: Request) {
try {
await verifyVercelSignature(req);

const partners = await prisma.programEnrollment.findMany({
const pendingSales = await prisma.sale.groupBy({
by: ["programId", "partnerId"],
where: {
status: "approved",
status: "pending",
},
_count: {
id: true,
},
});

if (!partners.length) {
if (!pendingSales.length) {
return;
}

const currentDate = new Date();

const periodStart = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
1,
);

const periodEnd = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
0,
);

// TODO:
// We need a batter way to handle this recursively
for (const { programId, partnerId } of partners) {
for (const { programId, partnerId } of pendingSales) {
await createSalesPayout({
programId,
partnerId,
periodStart,
periodEnd,
});
}

return NextResponse.json({
message: `Calculated payouts for ${partners.length} partners`,
});
return NextResponse.json(pendingSales);
} catch (error) {
return handleAndReturnErrorResponse(error);
}
Expand Down
24 changes: 15 additions & 9 deletions apps/web/lib/actions/partners/create-manual-payout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ export const createManualPayoutAction = authActionClient
throw new Error("No short link found for this partner in this program.");
}

let payout: Payout | undefined = undefined;
let payout: Payout | null = null;

// Create a payout for sales
if (type === "sales") {
// create the payout
payout = await createSalesPayout({
programId,
partnerId,
periodStart: start!,
periodEnd: end!,
description: description ?? undefined,
});
payout =
(await createSalesPayout({
programId,
partnerId,
periodStart: start,
periodEnd: end,
description: description ?? undefined,
})) ?? null;

if (!payout) {
throw new Error(
"No valid sales found for this period. Payout was not created.",
);
}
}

// Create a payout for clicks, leads, and custom events
Expand Down
125 changes: 73 additions & 52 deletions apps/web/lib/partners/create-sales-payout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const createSalesPayout = async ({
}: {
programId: string;
partnerId: string;
periodStart: Date;
periodEnd: Date;
periodStart?: Date | null;
periodEnd?: Date | null;
description?: string;
}) => {
return await prisma.$transaction(async (tx) => {
Expand All @@ -23,10 +23,6 @@ export const createSalesPayout = async ({
partnerId,
payoutId: null,
status: "pending", // We only want to pay out sales that are pending (not refunded / fraud / duplicate)
createdAt: {
gte: periodStart,
lte: periodEnd,
},
// Referral commissions are held for 30 days before becoming available.
// createdAt: {
// lte: subDays(new Date(), 30),
Expand All @@ -35,75 +31,100 @@ export const createSalesPayout = async ({
select: {
id: true,
earnings: true,
createdAt: true,
},
});

if (!sales.length) {
return;
}

const amount = sales.reduce((total, sale) => total + sale.earnings, 0);
const quantity = sales.length;
let payout: Payout | null = null;

// Check if the partner has another pending payout
payout = await tx.payout.findFirst({
where: {
programId,
partnerId,
status: "pending",
type: "sales",
},
});
const amount = sales.reduce((total, sale) => total + sale.earnings, 0);

// Update the existing payout
if (payout) {
await tx.payout.update({
where: {
id: payout.id,
},
data: {
amount: {
increment: amount,
},
quantity: {
increment: quantity,
},
periodEnd,
},
});
if (!periodStart) {
// get the earliest sale date
periodStart = sales.reduce(
(min, sale) => (sale.createdAt < min ? sale.createdAt : min),
sales[0].createdAt,
);
}

console.info("Payout updated", payout);
if (!periodEnd) {
// get the end of the month of the latest sale
// e.g. if the latest sale is 2024-12-16, the periodEnd should be 2024-12-31
const latestSale = sales.reduce(
(max, sale) => (sale.createdAt > max ? sale.createdAt : max),
sales[0].createdAt,
);
periodEnd = new Date(
latestSale.getFullYear(),
latestSale.getMonth() + 1,
0,
);
}

// Create the payout
else {
payout = await tx.payout.create({
data: {
id: createId({ prefix: "po_" }),
let payout: Payout | null = null;

// only create a payout if the total sale amount is greater than 0
if (amount > 0) {
// Check if the partner has another pending payout
const existingPayout = await tx.payout.findFirst({
where: {
programId,
partnerId,
amount,
periodStart,
periodEnd,
quantity,
description,
status: "pending",
type: "sales",
},
});

console.info("Payout created", payout);
}
// Update the existing payout
if (existingPayout) {
payout = await tx.payout.update({
where: {
id: existingPayout.id,
},
data: {
amount: {
increment: amount,
},
quantity: {
increment: quantity,
},
periodEnd,
description: existingPayout.description ?? "Dub Partners payout",
},
});

if (!payout) {
throw new Error(
`Failed to create payout for ${partnerId} in ${programId}.`,
);
console.info("Payout updated", payout);
}

// Create the payout
else {
payout = await tx.payout.create({
data: {
id: createId({ prefix: "po_" }),
programId,
partnerId,
amount,
periodStart,
periodEnd,
quantity,
description: description ?? "Dub Partners payout",
},
});

console.info("Payout created", payout);
}
}

// Update the sales records
await tx.sale.updateMany({
where: { id: { in: sales.map((sale) => sale.id) } },
data: { payoutId: payout.id, status: "processed" },
data: {
status: "processed",
...(payout ? { payoutId: payout.id } : {}),
},
});

return payout;
Expand Down
Loading