From 6d8e25091d4ef95880f518ded96b5848fc4443ad Mon Sep 17 00:00:00 2001
From: Steven Tey
Date: Mon, 28 Jul 2025 15:43:37 -0700
Subject: [PATCH 1/5] Improve payout emails
---
.../connect/webhook/balance-available.ts | 7 ++
.../api/stripe/connect/webhook/payout-paid.ts | 19 ++++
.../partner-payout-withdrawal-completed.tsx | 97 +++++++++++++++++++
.../partner-payout-withdrawal-initiated.tsx | 9 +-
4 files changed, 129 insertions(+), 3 deletions(-)
create mode 100644 packages/email/src/templates/partner-payout-withdrawal-completed.tsx
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
index 71cf193f105..b9971aa0344 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
@@ -130,6 +130,13 @@ export async function balanceAvailable(event: Stripe.Event) {
react: PartnerPayoutWithdrawalInitiated({
email: partner.email,
amount: payout.amount,
+ expectedDate: new Date(payout.arrival_date * 1000).toLocaleDateString(
+ "en-US",
+ {
+ month: "long",
+ day: "numeric",
+ },
+ ),
}),
});
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
index dd97cf58538..cd84a17ea2c 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
@@ -1,3 +1,5 @@
+import { sendEmail } from "@dub/email";
+import PartnerPayoutWithdrawalCompleted from "@dub/email/templates/partner-payout-withdrawal-completed";
import { prisma } from "@dub/prisma";
import Stripe from "stripe";
@@ -39,4 +41,21 @@ export async function payoutPaid(event: Stripe.Event) {
console.log(
`Updated ${updatedPayouts.count} payouts for partner ${partner.email} (${stripeAccount}) to "completed" status`,
);
+
+ if (partner.email) {
+ const sentEmail = await sendEmail({
+ variant: "notifications",
+ subject: "Your funds have been transferred to your bank account",
+ email: partner.email,
+ react: PartnerPayoutWithdrawalCompleted({
+ email: partner.email,
+ amount: stripePayout.amount,
+ traceId: stripePayout.id,
+ }),
+ });
+
+ console.log(
+ `Sent email to partner ${partner.email} (${stripeAccount}): ${JSON.stringify(sentEmail, null, 2)}`,
+ );
+ }
}
diff --git a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
new file mode 100644
index 00000000000..7602e2129f5
--- /dev/null
+++ b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
@@ -0,0 +1,97 @@
+import { currencyFormatter, DUB_WORDMARK } from "@dub/utils";
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Link,
+ Preview,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+import { Footer } from "../components/footer";
+
+// Send this email after payout.paid webhook is received
+export default function PartnerPayoutWithdrawalCompleted({
+ email = "panic@thedis.co",
+ amount = 45590,
+ traceId = "DUB PARTN-XYZ",
+}: {
+ email: string;
+ amount: number;
+ traceId?: string;
+}) {
+ const amountInDollars = currencyFormatter(amount / 100, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+
+ const fiveBusinessDaysFromNow = (() => {
+ let date = new Date();
+ let businessDays = 0;
+ while (businessDays < 5) {
+ date.setDate(date.getDate() + 1);
+ // Skip weekends (0 = Sunday, 6 = Saturday)
+ if (date.getDay() !== 0 && date.getDay() !== 6) {
+ businessDays++;
+ }
+ }
+ return date.toLocaleDateString("en-US", {
+ month: "long",
+ day: "numeric",
+ });
+ })();
+
+ return (
+
+
+ Your funds have been transferred to your bank account
+
+
+
+
+
+
+
+
+ Your funds have been transferred to your bank account
+
+
+
+
+ {amountInDollars}
+ {" "}
+ has been transferred from your Stripe Express account to your
+ connected bank account.
+
+
+
+ Banks can take up to 5 business days to process payouts. Wait
+ until{" "}
+
+ {fiveBusinessDaysFromNow}
+ {" "}
+ and then contact your bank using the trace ID{" "}
+ {traceId}.
+
+
+
+ If you still have any questions, contact{" "}
+
+ Stripe support
+
+ .
+
+
+
+
+
+
+ );
+}
diff --git a/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx b/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
index 4054518a0cb..e33332a196b 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
@@ -18,9 +18,11 @@ import { Footer } from "../components/footer";
export default function PartnerPayoutWithdrawalInitiated({
email = "panic@thedis.co",
amount = 45590,
+ expectedDate = "Aug 4",
}: {
email: string;
amount: number;
+ expectedDate: string;
}) {
const amountInDollars = currencyFormatter(amount / 100, {
minimumFractionDigits: 2,
@@ -47,13 +49,14 @@ export default function PartnerPayoutWithdrawalInitiated({
{amountInDollars}
{" "}
- is being transferred to your bank account.
+ is being transferred from your Stripe Express account to your
+ connected bank account.
- Depending on your bank's location, this process can{" "}
+ The funds are expected to arrive in your bank account by{" "}
- can take anywhere between 1-14 business days
+ {expectedDate}
. If there are any delays, please contact{" "}
Date: Mon, 28 Jul 2025 15:46:24 -0700
Subject: [PATCH 2/5] fix trace_id
---
.../api/stripe/connect/webhook/payout-paid.ts | 2 +-
.../partner-payout-withdrawal-completed.tsx | 16 +++++++++++++---
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
index cd84a17ea2c..5b08dcec9c7 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
@@ -50,7 +50,7 @@ export async function payoutPaid(event: Stripe.Event) {
react: PartnerPayoutWithdrawalCompleted({
email: partner.email,
amount: stripePayout.amount,
- traceId: stripePayout.id,
+ traceId: stripePayout.trace_id as string | null,
}),
});
diff --git a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
index 7602e2129f5..9189f214986 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
@@ -22,7 +22,7 @@ export default function PartnerPayoutWithdrawalCompleted({
}: {
email: string;
amount: number;
- traceId?: string;
+ traceId: string | null;
}) {
const amountInDollars = currencyFormatter(amount / 100, {
minimumFractionDigits: 2,
@@ -74,10 +74,20 @@ export default function PartnerPayoutWithdrawalCompleted({
{fiveBusinessDaysFromNow}
{" "}
- and then contact your bank using the trace ID{" "}
- {traceId}.
+ and then contact your bank
+ {traceId
+ ? ` using the following trace ID (reference number):`
+ : "."}
+ {traceId && (
+
+
+ {traceId}
+
+
+ )}
+
If you still have any questions, contact{" "}
Date: Mon, 28 Jul 2025 15:48:03 -0700
Subject: [PATCH 3/5] Update partner-payout-withdrawal-completed.tsx
---
.../src/templates/partner-payout-withdrawal-completed.tsx | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
index 9189f214986..244a22f544b 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
@@ -81,10 +81,8 @@ export default function PartnerPayoutWithdrawalCompleted({
{traceId && (
-
-
- {traceId}
-
+
+ {traceId}
)}
From 6b689d08875fb56ad569486c45aeb222efaa856b Mon Sep 17 00:00:00 2001
From: Steven Tey
Date: Mon, 28 Jul 2025 16:07:54 -0700
Subject: [PATCH 4/5] arrivalDate: number
---
.../api/stripe/connect/webhook/balance-available.ts | 8 +-------
.../app/(ee)/api/stripe/connect/webhook/payout-paid.ts | 1 +
.../templates/partner-payout-withdrawal-completed.tsx | 10 ++++++----
.../templates/partner-payout-withdrawal-initiated.tsx | 9 ++++++---
4 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
index b9971aa0344..7687f0c97ed 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
@@ -130,13 +130,7 @@ export async function balanceAvailable(event: Stripe.Event) {
react: PartnerPayoutWithdrawalInitiated({
email: partner.email,
amount: payout.amount,
- expectedDate: new Date(payout.arrival_date * 1000).toLocaleDateString(
- "en-US",
- {
- month: "long",
- day: "numeric",
- },
- ),
+ arrivalDate: payout.arrival_date,
}),
});
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
index 5b08dcec9c7..f3c56a32da5 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
@@ -50,6 +50,7 @@ export async function payoutPaid(event: Stripe.Event) {
react: PartnerPayoutWithdrawalCompleted({
email: partner.email,
amount: stripePayout.amount,
+ arrivalDate: stripePayout.arrival_date,
traceId: stripePayout.trace_id as string | null,
}),
});
diff --git a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
index 244a22f544b..c2fbd796ba6 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
@@ -18,10 +18,12 @@ import { Footer } from "../components/footer";
export default function PartnerPayoutWithdrawalCompleted({
email = "panic@thedis.co",
amount = 45590,
+ arrivalDate = 1722163200,
traceId = "DUB PARTN-XYZ",
}: {
email: string;
amount: number;
+ arrivalDate: number;
traceId: string | null;
}) {
const amountInDollars = currencyFormatter(amount / 100, {
@@ -29,8 +31,8 @@ export default function PartnerPayoutWithdrawalCompleted({
maximumFractionDigits: 2,
});
- const fiveBusinessDaysFromNow = (() => {
- let date = new Date();
+ const fiveBusinessDaysFromArrivalDate = (() => {
+ let date = new Date(arrivalDate * 1000);
let businessDays = 0;
while (businessDays < 5) {
date.setDate(date.getDate() + 1);
@@ -40,7 +42,7 @@ export default function PartnerPayoutWithdrawalCompleted({
}
}
return date.toLocaleDateString("en-US", {
- month: "long",
+ month: "short",
day: "numeric",
});
})();
@@ -72,7 +74,7 @@ export default function PartnerPayoutWithdrawalCompleted({
Banks can take up to 5 business days to process payouts. Wait
until{" "}
- {fiveBusinessDaysFromNow}
+ {fiveBusinessDaysFromArrivalDate}
{" "}
and then contact your bank
{traceId
diff --git a/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx b/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
index e33332a196b..75c05d60c76 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
@@ -18,11 +18,11 @@ import { Footer } from "../components/footer";
export default function PartnerPayoutWithdrawalInitiated({
email = "panic@thedis.co",
amount = 45590,
- expectedDate = "Aug 4",
+ arrivalDate = 1722163200,
}: {
email: string;
amount: number;
- expectedDate: string;
+ arrivalDate: number;
}) {
const amountInDollars = currencyFormatter(amount / 100, {
minimumFractionDigits: 2,
@@ -56,7 +56,10 @@ export default function PartnerPayoutWithdrawalInitiated({
The funds are expected to arrive in your bank account by{" "}
- {expectedDate}
+ {new Date(arrivalDate * 1000).toLocaleDateString("en-US", {
+ month: "long",
+ day: "numeric",
+ })}
. If there are any delays, please contact{" "}
Date: Mon, 28 Jul 2025 16:14:45 -0700
Subject: [PATCH 5/5] address more partner feedback, specifically fix currency
support
---
.../connect/webhook/balance-available.ts | 15 +++++----
.../api/stripe/connect/webhook/payout-paid.ts | 9 ++++--
.../partner-payout-withdrawal-completed.tsx | 31 ++++++++++++-------
.../partner-payout-withdrawal-initiated.tsx | 28 +++++++++++------
4 files changed, 53 insertions(+), 30 deletions(-)
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
index 7687f0c97ed..6debc7c324c 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts
@@ -80,7 +80,7 @@ export async function balanceAvailable(event: Stripe.Event) {
availableBalance = Math.floor(availableBalance / 100) * 100;
}
- const payout = await stripe.payouts.create(
+ const stripePayout = await stripe.payouts.create(
{
amount: availableBalance,
currency,
@@ -93,7 +93,7 @@ export async function balanceAvailable(event: Stripe.Event) {
);
console.log(
- `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${payout.id} (${currencyFormatter(payout.amount / 100, { maximumFractionDigits: 2, currency })})`,
+ `Stripe payout created for partner ${partner.email} (${stripeAccount}): ${stripePayout.id} (${currencyFormatter(stripePayout.amount / 100, { maximumFractionDigits: 2, currency: stripePayout.currency })})`,
);
const transfers = await stripe.transfers.list({
@@ -114,12 +114,12 @@ export async function balanceAvailable(event: Stripe.Event) {
},
},
data: {
- stripePayoutId: payout.id,
+ stripePayoutId: stripePayout.id,
},
});
console.log(
- `Updated ${updatedPayouts.count} payouts for partner ${partner.email} (${stripeAccount}) to have the stripePayoutId: ${payout.id}`,
+ `Updated ${updatedPayouts.count} payouts for partner ${partner.email} (${stripeAccount}) to have the stripePayoutId: ${stripePayout.id}`,
);
if (partner.email) {
@@ -129,8 +129,11 @@ export async function balanceAvailable(event: Stripe.Event) {
email: partner.email,
react: PartnerPayoutWithdrawalInitiated({
email: partner.email,
- amount: payout.amount,
- arrivalDate: payout.arrival_date,
+ payout: {
+ amount: stripePayout.amount,
+ currency: stripePayout.currency,
+ arrivalDate: stripePayout.arrival_date,
+ },
}),
});
diff --git a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
index f3c56a32da5..96ac31e9cb8 100644
--- a/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
+++ b/apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
@@ -49,9 +49,12 @@ export async function payoutPaid(event: Stripe.Event) {
email: partner.email,
react: PartnerPayoutWithdrawalCompleted({
email: partner.email,
- amount: stripePayout.amount,
- arrivalDate: stripePayout.arrival_date,
- traceId: stripePayout.trace_id as string | null,
+ payout: {
+ amount: stripePayout.amount,
+ currency: stripePayout.currency,
+ arrivalDate: stripePayout.arrival_date,
+ traceId: stripePayout.trace_id as string | null,
+ },
}),
});
diff --git a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
index c2fbd796ba6..4d76446d733 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-completed.tsx
@@ -17,22 +17,29 @@ import { Footer } from "../components/footer";
// Send this email after payout.paid webhook is received
export default function PartnerPayoutWithdrawalCompleted({
email = "panic@thedis.co",
- amount = 45590,
- arrivalDate = 1722163200,
- traceId = "DUB PARTN-XYZ",
+ payout = {
+ amount: 45590,
+ currency: "usd",
+ arrivalDate: 1722163200,
+ traceId: "DUB-PARTN-ABCD-XYZ-123456",
+ },
}: {
email: string;
- amount: number;
- arrivalDate: number;
- traceId: string | null;
+ payout: {
+ amount: number;
+ currency: string;
+ arrivalDate: number;
+ traceId: string | null;
+ };
}) {
- const amountInDollars = currencyFormatter(amount / 100, {
+ const amountInDollars = currencyFormatter(payout.amount / 100, {
+ currency: payout.currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const fiveBusinessDaysFromArrivalDate = (() => {
- let date = new Date(arrivalDate * 1000);
+ let date = new Date(payout.arrivalDate * 1000);
let businessDays = 0;
while (businessDays < 5) {
date.setDate(date.getDate() + 1);
@@ -77,14 +84,14 @@ export default function PartnerPayoutWithdrawalCompleted({
{fiveBusinessDaysFromArrivalDate}
{" "}
and then contact your bank
- {traceId
+ {payout.traceId
? ` using the following trace ID (reference number):`
: "."}
- {traceId && (
-
- {traceId}
+ {payout.traceId && (
+
+ {payout.traceId}
)}
diff --git a/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx b/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
index 75c05d60c76..8794ad7273a 100644
--- a/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
+++ b/packages/email/src/templates/partner-payout-withdrawal-initiated.tsx
@@ -17,14 +17,21 @@ import { Footer } from "../components/footer";
// Send this email after initiating a Stripe payout to the partner
export default function PartnerPayoutWithdrawalInitiated({
email = "panic@thedis.co",
- amount = 45590,
- arrivalDate = 1722163200,
+ payout = {
+ amount: 45590,
+ currency: "usd",
+ arrivalDate: 1722163200,
+ },
}: {
email: string;
- amount: number;
- arrivalDate: number;
+ payout: {
+ amount: number;
+ currency: string;
+ arrivalDate: number;
+ };
}) {
- const amountInDollars = currencyFormatter(amount / 100, {
+ const amountInDollars = currencyFormatter(payout.amount / 100, {
+ currency: payout.currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
@@ -56,10 +63,13 @@ export default function PartnerPayoutWithdrawalInitiated({
The funds are expected to arrive in your bank account by{" "}
- {new Date(arrivalDate * 1000).toLocaleDateString("en-US", {
- month: "long",
- day: "numeric",
- })}
+ {new Date(payout.arrivalDate * 1000).toLocaleDateString(
+ "en-US",
+ {
+ month: "short",
+ day: "numeric",
+ },
+ )}
. If there are any delays, please contact{" "}