diff --git a/apps/web/app/api/cron/process-payouts/route.ts b/apps/web/app/api/cron/process-payouts/route.ts index e830e49242..73a729f448 100644 --- a/apps/web/app/api/cron/process-payouts/route.ts +++ b/apps/web/app/api/cron/process-payouts/route.ts @@ -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); } diff --git a/apps/web/lib/actions/partners/create-manual-payout.ts b/apps/web/lib/actions/partners/create-manual-payout.ts index 777ae77421..44bb73f231 100644 --- a/apps/web/lib/actions/partners/create-manual-payout.ts +++ b/apps/web/lib/actions/partners/create-manual-payout.ts @@ -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 diff --git a/apps/web/lib/partners/create-sales-payout.ts b/apps/web/lib/partners/create-sales-payout.ts index 9149205c1d..7355c54b40 100644 --- a/apps/web/lib/partners/create-sales-payout.ts +++ b/apps/web/lib/partners/create-sales-payout.ts @@ -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) => { @@ -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), @@ -35,6 +31,7 @@ export const createSalesPayout = async ({ select: { id: true, earnings: true, + createdAt: true, }, }); @@ -42,68 +39,92 @@ export const createSalesPayout = async ({ 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;