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
Show all changes
72 commits
Select commit Hold shift + click to select a range
6301979
Refactor payout settings modal to use a sheet component
devkiran Oct 30, 2025
df10cc7
reuse the webhook validation logic
devkiran Oct 30, 2025
c4a8358
store the payout mode
devkiran Oct 30, 2025
ee44a7f
display the payout methods in the settings sheet
devkiran Oct 30, 2025
056d5ae
Update program-payout-methods.tsx
devkiran Oct 30, 2025
5d04f9f
Add ProgramPayoutRouting component for managing payout modes in settings
devkiran Oct 30, 2025
e39e190
add empty state
devkiran Oct 30, 2025
e13b17e
Update partner-row-item.tsx
devkiran Oct 30, 2025
b2186e4
Merge branch 'main' into external-payouts
devkiran Nov 3, 2025
2dfb621
Enhance payout calculations and UI for external payouts
devkiran Nov 3, 2025
2a3fa3d
Refactor payout logic and improve webhook validation messages
devkiran Nov 3, 2025
928c148
Implement external payout processing and webhook integration
devkiran Nov 3, 2025
a07cd5e
Update validate-webhook.ts
devkiran Nov 3, 2025
d5c525e
Update is-external-payout.ts
devkiran Nov 3, 2025
c69bc68
Update confirm-payouts-sheet.tsx
devkiran Nov 3, 2025
baa075f
Refactor payout processing and enhance partner data handling
devkiran Nov 3, 2025
5fec019
Update process-payouts.ts
devkiran Nov 3, 2025
030dafe
Update payout-table.tsx
devkiran Nov 3, 2025
ac6b743
Merge branch 'main' into external-payouts
devkiran Nov 4, 2025
fa34a9e
Merge branch 'main' into external-payouts
devkiran Nov 4, 2025
6eef27f
Refactor partner payout settings to include external payout enrollmen…
devkiran Nov 4, 2025
33b1445
Add partner payout details sheet component and update imports
devkiran Nov 4, 2025
90ff124
Implement payout eligibility filter and add webhook handling for exte…
devkiran Nov 4, 2025
446418f
Update process-payouts.ts
devkiran Nov 4, 2025
bfe21fa
Update index.test.ts
devkiran Nov 4, 2025
1843837
fix the icons
devkiran Nov 4, 2025
dccd7c8
Update route.ts
devkiran Nov 4, 2025
cd58f0d
Update process-payouts.ts
devkiran Nov 4, 2025
2ab3743
add slack template for the new trigger
devkiran Nov 4, 2025
75110c3
Add maxDuration to webhook route, refactor payout variable names, and…
devkiran Nov 4, 2025
3ae5e89
Update confirm-payouts.ts
devkiran Nov 4, 2025
637358c
Merge branch 'main' into external-payouts
devkiran Nov 4, 2025
13a9553
Add payoutMode to program and update email template
marcusljf Nov 4, 2025
1d6c54b
Enhance external payout handling by integrating webhook checks and up…
devkiran Nov 4, 2025
6287804
Removed hybrid
marcusljf Nov 4, 2025
4d45bd0
Merge branch 'external-payouts' of https://github.com/dubinc/dub into…
devkiran Nov 4, 2025
a24530a
Refactor payout processing to utilize invoice payoutMode
devkiran Nov 4, 2025
8d5ae29
Refactor payout externality check method
devkiran Nov 4, 2025
532e2fd
Merge branch 'main' into external-payouts
devkiran Nov 5, 2025
b9d3ebb
Refactor payout processing to use 'mode' instead of 'payoutMode' for …
devkiran Nov 5, 2025
471ccfd
improve payout mode logic across various components.
devkiran Nov 5, 2025
4687fcb
fix email sending for external payouts
devkiran Nov 5, 2025
057cf81
Refactor payout externality checks to use getEffectivePayoutMode for …
devkiran Nov 5, 2025
6ad5626
Refactor payout transformation logic to consistently use 'mode' prope…
devkiran Nov 5, 2025
ba80d55
Enhance invoice visibility logic by restricting invoice links to inte…
devkiran Nov 5, 2025
7a76a9f
Update partner-payout-settings-sheet.tsx
devkiran Nov 5, 2025
de04463
Update program-payout-methods.tsx
devkiran Nov 5, 2025
1689c2e
Merge branch 'main' into external-payouts
devkiran Nov 5, 2025
971d53d
Create update-payout-mode-to-internal.ts
devkiran Nov 5, 2025
18adda1
Refactor payout eligibility logic by introducing getEligiblePayouts f…
devkiran Nov 5, 2025
4f53dec
Update invoice.prisma
devkiran Nov 5, 2025
c611e30
Update program-payout-methods.tsx
devkiran Nov 5, 2025
038432f
Update confirm-payouts-sheet.tsx
devkiran Nov 5, 2025
e6ade74
Update confirm-payouts.ts
devkiran Nov 5, 2025
13de9a3
Update get-webhooks.ts
devkiran Nov 5, 2025
e01e537
Refactor payout mode references to use 'payoutMode' instead of 'mode'
devkiran Nov 5, 2025
b2aca15
Merge branch 'main' into external-payouts
steven-tey Nov 5, 2025
45bf31f
Update program-payout-settings-sheet.tsx
steven-tey Nov 5, 2025
b84aa1a
Merge branch 'main' into external-payouts
steven-tey Nov 5, 2025
af6910e
update a bunch of tooltip/UI copywriting
steven-tey Nov 6, 2025
b61851f
fix tests
steven-tey Nov 6, 2025
7f74e59
simplify: remove externalPayoutsEnabledAt, rely on payoutMode
steven-tey Nov 6, 2025
b6b44ac
Update program-payout-mode-section.tsx
steven-tey Nov 6, 2025
8f63f9d
Improve processExternalPayouts (#3065)
steven-tey Nov 6, 2025
349d2cc
refactor Payout method display
devkiran Nov 6, 2025
b047881
Add payoutMode checks in queueExternalPayouts and queueStripePayouts …
devkiran Nov 6, 2025
81100ba
Update partner-payout-settings-sheet.tsx
devkiran Nov 6, 2025
0540ed2
Update queue-external-payouts.ts
devkiran Nov 6, 2025
1e352a5
Update process-payouts.ts
devkiran Nov 6, 2025
7db7816
address coderabbit feedback
steven-tey Nov 6, 2025
de0e16a
simplify prisma call
steven-tey Nov 6, 2025
b601d74
address coderabbit feedback, finalize copy
steven-tey Nov 6, 2025
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
@@ -0,0 +1,148 @@
import { queueBatchEmail } from "@/lib/email/queue-batch-email";
import { sendWorkspaceWebhook } from "@/lib/webhook/publish";
import { payoutWebhookEventSchema } from "@/lib/zod/schemas/payouts";
import type PartnerPayoutConfirmed from "@dub/email/templates/partner-payout-confirmed";
import { prisma } from "@dub/prisma";
import { Invoice } from "@dub/prisma/client";

export async function queueExternalPayouts(
invoice: Pick<
Invoice,
"id" | "paymentMethod" | "programId" | "workspaceId" | "payoutMode"
>,
) {
// All payouts are processed internally, hence no need to queue external payouts
if (invoice.payoutMode === "internal") {
console.log(`Invoice ${invoice.id} is paid internally. Skipping...`);
return;
}

// should never happen, but just in case
if (!invoice.programId) {
console.log(`Invoice ${invoice.id} has no program ID. Skipping...`);
return;
}

const program = await prisma.program.findUnique({
where: {
id: invoice.programId,
},
select: {
id: true,
name: true,
logo: true,
supportEmail: true,
},
});

// should never happen, but just in case
if (!program) {
console.log(`Program not found for invoice ${invoice.id}. Skipping...`);
return;
}

const externalPayouts = await prisma.payout.findMany({
where: {
invoiceId: invoice.id,
status: "processing",
mode: "external",
},
include: {
partner: {
include: {
programs: {
where: {
programId: program.id,
},
select: {
tenantId: true,
status: true,
},
},
},
},
},
});

if (externalPayouts.length === 0) {
console.log("No external payouts found for invoice", invoice.id);
return;
}

const webhooks = await prisma.webhook.findMany({
where: {
projectId: invoice.workspaceId,
disabledAt: null,
triggers: {
array_contains: ["payout.confirmed"],
},
},
select: {
id: true,
url: true,
secret: true,
},
});

if (webhooks.length === 0) {
console.log(
`No webhooks found for workspace ${invoice.workspaceId} for invoice ${invoice.id}. Skipping...`,
);
return;
}

for (const payout of externalPayouts) {
try {
const data = payoutWebhookEventSchema.parse({
...payout,
partner: {
...payout.partner,
...payout.partner.programs[0],
},
});

await sendWorkspaceWebhook({
workspace: {
id: invoice.workspaceId,
webhookEnabled: true,
},
webhooks,
data,
trigger: "payout.confirmed",
});
} catch (error) {
console.error(error.message);
}
}

await queueBatchEmail<typeof PartnerPayoutConfirmed>(
externalPayouts
.filter((payout) => payout.partner.email)
.map((payout) => ({
to: payout.partner.email!,
subject: "You've got money coming your 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}`,
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ const stripeChargeMetadataSchema = z.object({
});

export async function queueStripePayouts(
invoice: Pick<Invoice, "id" | "paymentMethod" | "stripeChargeMetadata">,
invoice: Pick<
Invoice,
"id" | "paymentMethod" | "stripeChargeMetadata" | "payoutMode"
>,
) {
// All payouts are processed externally, hence no need to queue Stripe payouts
if (invoice.payoutMode === "external") {
return;
}

const { id: invoiceId, paymentMethod, stripeChargeMetadata } = invoice;

// Find the id of the charge that was used to fund the transfer
Expand All @@ -36,6 +44,7 @@ export async function queueStripePayouts(
where: {
invoiceId,
status: "processing",
mode: "internal",
},
});

Expand All @@ -58,6 +67,7 @@ export async function queueStripePayouts(
...(paymentMethod === "card" && { chargeId }),
},
});

console.log(
`Enqueued Stripe payout for invoice ${invoiceId} and partner ${partnerId}: ${response.messageId}`,
);
Expand Down
10 changes: 7 additions & 3 deletions apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { prisma } from "@dub/prisma";
import { log } from "@dub/utils";
import { z } from "zod";
import { logAndRespond } from "../../utils";
import { queueExternalPayouts } from "./queue-external-payouts";
import { queueStripePayouts } from "./queue-stripe-payouts";
import { sendPaypalPayouts } from "./send-paypal-payouts";

Expand All @@ -13,6 +14,7 @@ export const maxDuration = 600; // This function can run for a maximum of 10 min
const payloadSchema = z.object({
invoiceId: z.string(),
});

// POST /api/cron/payouts/charge-succeeded
// This route is used to process the charge-succeeded event from Stripe.
// We're intentionally offloading this to a cron job so we can return a 200 to Stripe immediately.
Expand Down Expand Up @@ -51,10 +53,12 @@ export async function POST(req: Request) {
}

await Promise.allSettled([
// Queue Stripe payouts
queueStripePayouts(invoice),
sendPaypalPayouts({
invoiceId,
}),
// Send PayPal payouts
sendPaypalPayouts(invoice),
// Queue external payouts
queueExternalPayouts(invoice),
]);

return logAndRespond(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ 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";

export async function sendPaypalPayouts({ invoiceId }: { invoiceId: string }) {
export async function sendPaypalPayouts(invoice: Pick<Invoice, "id">) {
const payouts = await prisma.payout.findMany({
where: {
invoiceId,
invoiceId: invoice.id,
status: "processing",
mode: "internal",
partner: {
payoutsEnabledAt: {
not: null,
Expand Down Expand Up @@ -40,7 +42,7 @@ export async function sendPaypalPayouts({ invoiceId }: { invoiceId: string }) {

const batchPayout = await createPayPalBatchPayout({
payouts,
invoiceId,
invoiceId: invoice.id,
});

console.log("PayPal batch payout created", batchPayout);
Expand Down
Loading