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
89 changes: 89 additions & 0 deletions apps/web/app/(ee)/api/cron/payouts/payout-paid/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { sendEmail } from "@dub/email";
import PartnerPayoutWithdrawalCompleted from "@dub/email/templates/partner-payout-withdrawal-completed";
import { prisma } from "@dub/prisma";
import { currencyFormatter, log } from "@dub/utils";
import { z } from "zod";
import { logAndRespond } from "../../utils";

const payloadSchema = z.object({
stripeAccount: z.string(),
stripePayout: z.object({
id: z.string(),
traceId: z.string().nullable(),
amount: z.number(),
currency: z.string(),
arrivalDate: z.number(),
}),
});

// POST /api/cron/payouts/payout-paid
export async function POST(req: Request) {
try {
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const { stripeAccount, stripePayout } = payloadSchema.parse(
JSON.parse(rawBody),
);

const partner = await prisma.partner.findUnique({
where: {
stripeConnectId: stripeAccount,
},
select: {
email: true,
},
});

if (!partner) {
return logAndRespond(
`Partner not found with Stripe connect account ${stripeAccount}. Skipping...`,
);
}

const updatedPayouts = await prisma.payout.updateMany({
where: {
status: "sent",
stripePayoutId: stripePayout.id,
stripePayoutTraceId: stripePayout.traceId,
},
data: {
status: "completed",
},
});

if (partner.email) {
const sentEmail = await sendEmail({
variant: "notifications",
subject: `Your ${currencyFormatter(stripePayout.amount, { currency: stripePayout.currency })} auto-withdrawal from Dub has been transferred to your bank`,
to: partner.email,
react: PartnerPayoutWithdrawalCompleted({
email: partner.email,
payout: {
amount: stripePayout.amount,
currency: stripePayout.currency,
arrivalDate: stripePayout.arrivalDate,
traceId: stripePayout.traceId,
},
}),
});

console.log(
`Sent email to partner ${partner.email} (${stripeAccount}): ${JSON.stringify(sentEmail, null, 2)}`,
);
}

return logAndRespond(
`Updated ${updatedPayouts.count} payouts for partner ${partner.email} (${stripeAccount}) to "completed" status.`,
);
} catch (error) {
await log({
message: `Error handling "payout.paid" ${error.message}.`,
type: "errors",
});

return handleAndReturnErrorResponse(error);
}
}
65 changes: 20 additions & 45 deletions apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,36 @@
import { sendEmail } from "@dub/email";
import PartnerPayoutWithdrawalCompleted from "@dub/email/templates/partner-payout-withdrawal-completed";
import { prisma } from "@dub/prisma";
import { currencyFormatter } from "@dub/utils";
import { qstash } from "@/lib/cron";
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
import Stripe from "stripe";

const queue = qstash.queue({
queueName: "payout-paid",
});

export async function payoutPaid(event: Stripe.Event) {
const stripeAccount = event.account;

if (!stripeAccount) {
return "No stripeConnectId found in event. Skipping...";
}

const partner = await prisma.partner.findUnique({
where: {
stripeConnectId: stripeAccount,
},
});

if (!partner) {
return `Partner not found with Stripe connect account ${stripeAccount}. Skipping...`;
}

const stripePayout = event.data.object as Stripe.Payout;

const stripePayoutTraceId = stripePayout.trace_id?.value ?? null;

const updatedPayouts = await prisma.payout.updateMany({
where: {
status: "sent",
stripePayoutId: stripePayout.id,
stripePayoutTraceId,
},
data: {
status: "completed",
const response = await queue.enqueueJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/payouts/payout-paid`,
deduplicationId: event.id,
method: "POST",
body: {
stripeAccount,
stripePayout: {
id: stripePayout.id,
traceId: stripePayoutTraceId,
amount: stripePayout.amount,
currency: stripePayout.currency,
arrivalDate: stripePayout.arrival_date,
},
},
});

if (partner.email) {
const sentEmail = await sendEmail({
variant: "notifications",
subject: `Your ${currencyFormatter(stripePayout.amount, { currency: stripePayout.currency })} auto-withdrawal from Dub has been transferred to your bank`,
to: partner.email,
react: PartnerPayoutWithdrawalCompleted({
email: partner.email,
payout: {
amount: stripePayout.amount,
currency: stripePayout.currency,
arrivalDate: stripePayout.arrival_date,
traceId: stripePayoutTraceId,
},
}),
});

console.log(
`Sent email to partner ${partner.email} (${stripeAccount}): ${JSON.stringify(sentEmail, null, 2)}`,
);
}

return `Updated ${updatedPayouts.count} payouts for partner ${partner.email} (${stripeAccount}) to "completed" status`;
return `Enqueued payout paid for partner ${stripeAccount}: ${response.messageId}`;
}