Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -3,6 +3,7 @@ import { getStripeOutboundPayment } from "@/lib/stripe/get-stripe-outbound-payme
import { OUTBOUND_PAYMENT_FAILURE_REASONS } from "@/lib/stripe/stripe-v2-schemas";
import { pluralize } from "@dub/utils";
import Stripe from "stripe";
import { notifyPartnerStablecoinPayoutFailed } from "./utils/notify-partner-stablecoin-payout-failed";

export async function outboundPaymentFailed(event: Stripe.ThinEvent) {
const { related_object: relatedObject } = event;
Expand Down Expand Up @@ -30,8 +31,7 @@ export async function outboundPaymentFailed(event: Stripe.ThinEvent) {
},
});

// TODO:
// Send email notification
await notifyPartnerStablecoinPayoutFailed(outboundPaymentId);

return `Updated ${updatedPayouts.count} ${pluralize("payout", updatedPayouts.count)} to failed status.`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getStripeOutboundPayment } from "@/lib/stripe/get-stripe-outbound-payme
import { OUTBOUND_PAYMENT_RETURNED_REASONS } from "@/lib/stripe/stripe-v2-schemas";
import { pluralize } from "@dub/utils";
import Stripe from "stripe";
import { notifyPartnerStablecoinPayoutFailed } from "./utils/notify-partner-stablecoin-payout-failed";

export async function outboundPaymentReturned(event: Stripe.ThinEvent) {
const { related_object: relatedObject } = event;
Expand Down Expand Up @@ -30,8 +31,7 @@ export async function outboundPaymentReturned(event: Stripe.ThinEvent) {
},
});

// TODO:
// Send email notification
await notifyPartnerStablecoinPayoutFailed(outboundPaymentId);

return `Updated ${updatedPayouts.count} ${pluralize("payout", updatedPayouts.count)} to failed status.`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { prisma } from "@/lib/prisma";
import { sendEmail } from "@dub/email";
import PartnerStablecoinPayoutFailed from "@dub/email/templates/partner-stablecoin-payout-failed";
import { currencyFormatter, prettyPrint } from "@dub/utils";
import { PayoutStatus } from "@prisma/client";

function maskCryptoWalletAddress(address: string) {
return address.length > 10
? `${address.slice(0, 6)}••••${address.slice(-4)}`
: address;
}

export async function notifyPartnerStablecoinPayoutFailed(
outboundPaymentId: string,
) {
const payouts = await prisma.payout.findMany({
where: {
stripePayoutId: outboundPaymentId,
status: PayoutStatus.failed,
},
include: {
partner: {
select: {
email: true,
cryptoWalletAddress: true,
},
},
program: {
select: {
name: true,
},
},
},
});

if (payouts.length === 0) {
console.log(
`No payouts found for outbound payment ${outboundPaymentId}. Skipping email send...`,
);
return;
}

const partner = payouts[0].partner;

if (!partner.email) {
console.log(
`Partner email not found for outbound payment ${outboundPaymentId}. Skipping email send...`,
);
return;
}

const totalAmount = payouts.reduce((acc, payout) => acc + payout.amount, 0);
const failureReason = payouts[0].failureReason ?? undefined;

const programs = [
...new Map(
payouts.map((payout) => [payout.program.name, payout.program]),
).values(),
];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const maskedAddress = partner.cryptoWalletAddress
? maskCryptoWalletAddress(partner.cryptoWalletAddress)
: undefined;

const emailResponse = await sendEmail({
variant: "notifications",
subject: `Your recent partner payout of ${currencyFormatter(totalAmount)} failed`,
to: partner.email,
react: PartnerStablecoinPayoutFailed({
email: partner.email,
programs,
payout: {
amount: totalAmount,
failureReason,
},
wallet: maskedAddress
? {
maskedAddress,
}
: undefined,
}),
});
Comment thread
devkiran marked this conversation as resolved.

console.log(
`Stablecoin payout failed email sent to partner ${partner.email}:`,
prettyPrint(emailResponse),
);
Comment thread
devkiran marked this conversation as resolved.
}
135 changes: 135 additions & 0 deletions packages/email/src/templates/partner-stablecoin-payout-failed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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 to the partner when a stablecoin payout fails or is returned
export default function PartnerStablecoinPayoutFailed({
programs = [{ name: "Acme" }],
payout = {
amount: 530000,
failureReason: "The destination wallet address is invalid.",
},
wallet = {
maskedAddress: "0x1234••••5678",
network: "ethereum",
},
email = "panic@thedis.co",
}: {
programs: {
name: string;
}[];
payout: {
amount: number; // in cents
failureReason?: string;
};
wallet?: {
maskedAddress?: string;
network?: string;
};
email: string;
}) {
const amountFormatted = currencyFormatter(payout.amount);
const programNamesFormatted = programs.map((p) => p.name).join(", ");

return (
<Html>
<Head />
<Preview>Action Required - Your recent stablecoin payout failed</Preview>
<Tailwind>
<Body className="mx-auto my-auto bg-white font-sans">
<Container className="mx-auto my-8 max-w-[600px] px-8 py-8">
<Section className="mt-8">
<Img src={DUB_WORDMARK} height="32" alt="Dub" />
</Section>

<Heading className="mx-0 my-8 p-0 text-lg font-medium text-black">
Your recent payout
{programs.length === 1
? ` from ${programNamesFormatted}`
: ""}{" "}
failed
</Heading>

<Text>
We attempted to send your recent payout of{" "}
<span className="font-semibold text-purple-600">
{amountFormatted}
</span>
{programs.length > 1 ? (
<>
{" "}
from{" "}
<span className="font-semibold text-purple-600">
{programNamesFormatted}
</span>
</>
) : null}{" "}
to your stablecoin wallet
{wallet?.maskedAddress ? (
<>
{" "}
(
<span className="font-semibold text-purple-600">
{wallet.maskedAddress}
{wallet.network ? ` · ${wallet.network}` : ""}
</span>
)
</>
) : null}
, but the transaction failed.
</Text>

{payout.failureReason && (
<Text className="text-sm leading-6 text-neutral-600">
Reason:{" "}
<span className="font-semibold italic text-neutral-800">
{payout.failureReason}
</span>
</Text>
)}

<Text>
Payout failures are usually due to incorrect wallet configuration.
Please reconnect your stablecoin wallet at your earliest
convenience and retry the payout from your{" "}
<Link
href="https://partners.dub.co/payouts?settings=true"
className="font-medium text-black underline"
>
Payout settings
</Link>
.
</Text>

<Section className="my-8">
<Link
className="rounded-lg bg-neutral-900 px-6 py-3 text-[13px] font-medium text-white no-underline"
href="https://partners.dub.co/payouts?settings=true"
>
Reconnect wallet
</Link>
</Section>

<Text className="text-sm leading-6 text-neutral-600">
If you have any questions, just reply to this email.
</Text>

<Footer email={email} />
</Container>
</Body>
</Tailwind>
</Html>
);
}
Loading