Skip to content

Commit d768598

Browse files
committed
checkPaymentMethodMandate
1 parent 24fcb32 commit d768598

File tree

2 files changed

+46
-0
lines changed

2 files changed

+46
-0
lines changed

apps/web/lib/actions/partners/confirm-payouts.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-progr
77
import { getProgramOrThrow } from "@/lib/api/programs/get-program-or-throw";
88
import {
99
CUTOFF_PERIOD_MAX_PAYOUTS,
10+
DIRECT_DEBIT_PAYMENT_METHOD_TYPES,
1011
INVOICE_MIN_PAYOUT_AMOUNT_CENTS,
1112
PAYMENT_METHOD_TYPES,
1213
STRIPE_PAYMENT_METHOD_NORMALIZATION,
@@ -15,6 +16,7 @@ import { qstash } from "@/lib/cron";
1516
import { exceededLimitError } from "@/lib/exceeded-limit-error";
1617
import { CUTOFF_PERIOD_ENUM } from "@/lib/partners/cutoff-period";
1718
import { stripe } from "@/lib/stripe";
19+
import { checkPaymentMethodMandate } from "@/lib/stripe/check-payment-method-mandate";
1820
import { getWebhooks } from "@/lib/webhook/get-webhooks";
1921
import { prisma } from "@dub/prisma";
2022
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
@@ -161,6 +163,20 @@ export const confirmPayoutsAction = authActionClient
161163
throw new Error("Fast settlement is only supported for ACH payment.");
162164
}
163165

166+
if (DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(paymentMethod.type)) {
167+
// check if mandate is valid
168+
const mandate = await checkPaymentMethodMandate({
169+
paymentMethodId,
170+
});
171+
if (!mandate) {
172+
// remove the payment method
173+
await stripe.paymentMethods.detach(paymentMethodId);
174+
throw new Error(
175+
"No active mandate found for this bank account. Please set up a new bank account for payouts under your billing settings page.",
176+
);
177+
}
178+
}
179+
164180
const invoice = await prisma.$transaction(async (tx) => {
165181
// Generate the next invoice number by counting the number of invoices for the workspace
166182
const totalInvoices = await tx.invoice.count({
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const checkPaymentMethodMandate = async ({
2+
paymentMethodId,
3+
}: {
4+
paymentMethodId: string;
5+
}) => {
6+
// Check mandate via REST API (mandates require Stripe-Version 2025-12-15.preview)
7+
// TODO: Update this to use the Stripe SDK when we upgrade to the new API version
8+
const mandatesResponse = await fetch(
9+
`https://api.stripe.com/v1/mandates?payment_method=${encodeURIComponent(paymentMethodId)}&status=active`,
10+
{
11+
method: "GET",
12+
headers: {
13+
"Stripe-Version": "2025-12-15.preview",
14+
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
15+
},
16+
},
17+
);
18+
19+
if (!mandatesResponse.ok) {
20+
const errText = await mandatesResponse.text();
21+
throw new Error(`Failed to verify mandate: ${errText}`);
22+
}
23+
24+
const { data: mandatesData } = await mandatesResponse.json();
25+
if (mandatesData.length === 0) {
26+
return null;
27+
}
28+
29+
return mandatesData[0];
30+
};

0 commit comments

Comments
 (0)