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
Original file line number Diff line number Diff line change
Expand Up @@ -117,31 +117,29 @@ export async function queueExternalPayouts(
}

await queueBatchEmail<typeof PartnerPayoutConfirmed>(
externalPayouts
.filter((payout) => payout.partner.email)
.map((payout) => ({
to: payout.partner.email!,
subject: `Your ${currencyFormatter(payout.amount)} payout for ${program.name} is on the way`,
variant: "notifications",
replyTo: program.supportEmail || "noreply",
templateName: "PartnerPayoutConfirmed",
templateProps: {
email: payout.partner.email!,
program: {
id: program.id,
name: program.name,
logo: program.logo,
},
payout: {
id: payout.id,
amount: payout.amount,
startDate: payout.periodStart,
endDate: payout.periodEnd,
mode: "external",
paymentMethod: invoice.paymentMethod ?? "ach",
},
externalPayouts.map((payout) => ({
to: payout.partner.email!,
subject: `Your ${currencyFormatter(payout.amount)} payout for ${program.name} is on the way`,
variant: "notifications",
replyTo: program.supportEmail || "noreply",
templateName: "PartnerPayoutConfirmed",
templateProps: {
email: payout.partner.email!,
program: {
id: program.id,
name: program.name,
logo: program.logo,
},
})),
payout: {
id: payout.id,
amount: payout.amount,
startDate: payout.periodStart,
endDate: payout.periodEnd,
mode: "external",
paymentMethod: invoice.paymentMethod ?? "ach",
},
},
})),
{
idempotencyKey: `payout-confirmed-external/${invoice.id}`,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { qstash } from "@/lib/cron";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN_WITH_NGROK, log } from "@dub/utils";
import { APP_DOMAIN_WITH_NGROK, chunk, log } from "@dub/utils";
import { Invoice } from "@prisma/client";
import { z } from "zod";

Expand Down Expand Up @@ -52,24 +52,30 @@ export async function queueStripePayouts(
queueName: "send-stripe-payout",
});

for (const { partnerId } of partnersInCurrentInvoice) {
const response = await queue.enqueueJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/payouts/send-stripe-payout`,
deduplicationId: `${invoiceId}-${partnerId}`,
method: "POST",
body: {
invoiceId,
partnerId,
// only pass chargeId if payment method is card
// this is because we're passing chargeId as source_transaction for card payouts since card payouts can take a short time to settle fully
// we omit chargeId/source_transaction for other payment methods (ACH, SEPA, etc.) since those settle via charge.succeeded webhook after ~4 days
// x-slack-ref: https://dub.slack.com/archives/C074P7LMV9C/p1758776038825219?thread_ts=1758769780.982089&cid=C074P7LMV9C
...(paymentMethod === "card" && { chargeId }),
},
});
const chunkedPartners = chunk(partnersInCurrentInvoice, 100);

for (let i = 0; i < chunkedPartners.length; i++) {
const partnersInChunk = chunkedPartners[i];
await Promise.allSettled(
partnersInChunk.map(({ partnerId }) => {
return queue.enqueueJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/payouts/send-stripe-payout`,
deduplicationId: `${invoiceId}-${partnerId}`,
method: "POST",
body: {
invoiceId,
partnerId,
// only pass chargeId if payment method is card
// this is because we're passing chargeId as source_transaction for card payouts since card payouts can take a short time to settle fully
// we omit chargeId/source_transaction for other payment methods (ACH, SEPA, etc.) since those settle via charge.succeeded webhook after ~4 days
// x-slack-ref: https://dub.slack.com/archives/C074P7LMV9C/p1758776038825219?thread_ts=1758769780.982089&cid=C074P7LMV9C
...(paymentMethod === "card" && { chargeId }),
},
});
}),
);
console.log(
`Enqueued Stripe payout for invoice ${invoiceId} and partner ${partnerId}: ${response.messageId}`,
`Enqueued Stripe payout for ${partnersInChunk.length} partners in chunk ${i + 1} of ${chunkedPartners.length}`,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { queueBatchEmail } from "@/lib/email/queue-batch-email";
import { createPayPalBatchPayout } from "@/lib/paypal/create-batch-payout";
import { sendBatchEmail } from "@dub/email";
import PartnerPayoutProcessed from "@dub/email/templates/partner-payout-processed";
import { prisma } from "@dub/prisma";
import { Invoice } from "@dub/prisma/client";
Expand Down Expand Up @@ -61,19 +61,18 @@ export async function sendPaypalPayouts(invoice: Pick<Invoice, "id">) {

console.log(`Updated ${updatedPayouts.count} payouts to "sent" status`);

const batchEmails = await sendBatchEmail(
await queueBatchEmail<typeof PartnerPayoutProcessed>(
payouts.map((payout) => ({
variant: "notifications",
to: payout.partner.email!,
subject: `You've received a ${currencyFormatter(payout.amount)} payout from ${payout.program.name}`,
react: PartnerPayoutProcessed({
templateName: "PartnerPayoutProcessed",
templateProps: {
email: payout.partner.email!,
program: payout.program,
payout,
variant: "paypal",
}),
},
})),
);

console.log("Resend batch emails sent", JSON.stringify(batchEmails, null, 2));
}
3 changes: 3 additions & 0 deletions apps/web/lib/email/queue-batch-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export async function queueBatchEmail<TTemplate extends (props: any) => any>(
idempotencyKey?: string; // Used for both QStash deduplication AND Resend idempotency
},
): Promise<string[]> {
// filter out emails without a `to` address
emails = emails.filter((email) => Boolean(email.to));

if (emails.length === 0) {
console.log("No emails to queue. Skipping...");
return [];
Expand Down
36 changes: 17 additions & 19 deletions apps/web/scripts/perplexity/ban-partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,26 +175,24 @@ async function main() {
console.log("commissionsRes", commissionsRes);

const qstashRes = await queueBatchEmail<typeof PartnerBanned>(
programEnrollments
.filter((p) => p.partner.email)
.map((p) => ({
to: p.partner.email!,
subject: `You've been banned from the ${program.name} Partner Program`,
variant: "notifications",
replyTo: program.supportEmail || "noreply",
templateName: "PartnerBanned",
templateProps: {
partner: {
name: p.partner.name,
email: p.partner.email!,
},
program: {
name: program.name,
slug: program.slug,
},
bannedReason: BAN_PARTNER_REASONS[bannedReason],
programEnrollments.map((p) => ({
to: p.partner.email!,
subject: `You've been banned from the ${program.name} Partner Program`,
variant: "notifications",
replyTo: program.supportEmail || "noreply",
templateName: "PartnerBanned",
templateProps: {
partner: {
name: p.partner.name,
email: p.partner.email!,
},
})),
program: {
name: program.name,
slug: program.slug,
},
bannedReason: BAN_PARTNER_REASONS[bannedReason],
},
})),
);
console.log("qstashRes", qstashRes);
}
Expand Down
36 changes: 17 additions & 19 deletions apps/web/scripts/perplexity/deactivate-partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,24 @@ async function main() {
console.log("redisRes", redisRes);

const qstashRes = await queueBatchEmail<typeof PartnerDeactivated>(
partners
.filter((p) => p.partner.email)
.map((p) => ({
variant: "notifications",
subject: "Your partnership with Perplexity has been deactivated",
to: p.partner.email!,
replyTo: program.supportEmail || "noreply",
templateName: "PartnerDeactivated",
templateProps: {
partner: {
name: p.partner.name,
email: p.partner.email!,
},
program: {
name: program.name,
slug: program.slug,
},
deactivatedReason: "because...",
partners.map((p) => ({
variant: "notifications",
subject: "Your partnership with Perplexity has been deactivated",
to: p.partner.email!,
replyTo: program.supportEmail || "noreply",
templateName: "PartnerDeactivated",
templateProps: {
partner: {
name: p.partner.name,
email: p.partner.email!,
},
})),
program: {
name: program.name,
slug: program.slug,
},
deactivatedReason: "because...",
},
})),
);
console.log("qstashRes", qstashRes);
}
Expand Down
36 changes: 17 additions & 19 deletions apps/web/scripts/perplexity/review-bounties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,24 @@ async function main() {
}

const qstashRes = await queueBatchEmail<typeof BountyApproved>(
bountySubmissions
.filter((s) => s.partner.email)
.map((s) => ({
subject: "Bounty approved!",
to: s.partner.email!,
variant: "notifications",
replyTo: s.program.supportEmail || "noreply",
templateName: "BountyApproved",
templateProps: {
email: s.partner.email!,
program: {
name: s.program.name,
slug: s.program.slug,
},
bounty: {
name: bounty.name,
type: bounty.type,
},
bountySubmissions.map((s) => ({
subject: "Bounty approved!",
to: s.partner.email!,
variant: "notifications",
replyTo: s.program.supportEmail || "noreply",
templateName: "BountyApproved",
templateProps: {
email: s.partner.email!,
program: {
name: s.program.name,
slug: s.program.slug,
},
})),
bounty: {
name: bounty.name,
type: bounty.type,
},
},
})),
);
console.log("qstashRes", qstashRes);
}
Expand Down