Skip to content

Commit e1a0dd9

Browse files
authored
Merge branch 'main' into earnings-calculator
2 parents 643aae5 + f8e5b5d commit e1a0dd9

File tree

13 files changed

+705
-118
lines changed

13 files changed

+705
-118
lines changed

apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getPayoutEligibilityFilter } from "@/lib/api/payouts/payout-eligibility-filter";
22
import { FAST_ACH_FEE_CENTS, FOREX_MARKUP_RATE } from "@/lib/constants/payouts";
33
import { qstash } from "@/lib/cron";
4+
import { calculatePayoutFeeWithWaiver } from "@/lib/partners/calculate-payout-fee-with-waiver";
45
import {
56
CUTOFF_PERIOD,
67
CUTOFF_PERIOD_TYPES,
@@ -41,6 +42,8 @@ interface ProcessPayoutsProps {
4142
| "payoutsUsage"
4243
| "payoutsLimit"
4344
| "payoutFee"
45+
| "payoutFeeWaiverLimit"
46+
| "payoutFeeWaiverUsage"
4447
| "webhookEnabled"
4548
>;
4649
program: Pick<
@@ -155,9 +158,19 @@ export async function processPayouts({
155158
`Using payout fee of ${payoutFee} for payment method ${paymentMethod.type}`,
156159
);
157160

158-
const fastAchFee =
159-
invoice.paymentMethod === "ach_fast" ? FAST_ACH_FEE_CENTS : 0;
160-
const invoiceFee = Math.round(totalPayoutAmount * payoutFee) + fastAchFee;
161+
const {
162+
feeFreeAmount,
163+
feeChargedAmount,
164+
feeWaiverRemaining,
165+
fee: invoiceFee,
166+
} = calculatePayoutFeeWithWaiver({
167+
payoutAmount: totalPayoutAmount,
168+
payoutFee,
169+
payoutFeeWaiverLimit: workspace.payoutFeeWaiverLimit,
170+
payoutFeeWaiverUsage: workspace.payoutFeeWaiverUsage,
171+
fastAchFee: invoice.paymentMethod === "ach_fast" ? FAST_ACH_FEE_CENTS : 0,
172+
});
173+
161174
const invoiceTotal = totalPayoutAmount + invoiceFee;
162175

163176
console.log({
@@ -166,6 +179,9 @@ export async function processPayouts({
166179
totalPayoutAmount,
167180
invoiceFee,
168181
invoiceTotal,
182+
feeFreeAmount,
183+
feeChargedAmount,
184+
feeWaiverRemaining,
169185
});
170186

171187
await prisma.invoice.update({
@@ -244,6 +260,9 @@ export async function processPayouts({
244260
payoutsUsage: {
245261
increment: totalPayoutAmount,
246262
},
263+
payoutFeeWaiverUsage: {
264+
increment: feeFreeAmount,
265+
},
247266
},
248267
include: {
249268
users: {

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
} from "@dub/utils";
3939
import NumberFlow from "@number-flow/react";
4040
import Link from "next/link";
41-
import { CSSProperties, useMemo } from "react";
41+
import { CSSProperties, ReactNode, useMemo } from "react";
4242
import { UsageChart } from "./usage-chart";
4343

4444
export default function PlanUsage() {
@@ -55,6 +55,8 @@ export default function PlanUsage() {
5555
payoutsUsage,
5656
payoutsLimit,
5757
payoutFee,
58+
payoutFeeWaiverLimit,
59+
payoutFeeWaiverUsage,
5860
domains,
5961
domainsLimit,
6062
foldersUsage,
@@ -122,6 +124,33 @@ export default function PlanUsage() {
122124
return tabs;
123125
}, [usage, usageLimit, linksUsage, linksLimit, totalLinks]);
124126

127+
// Display the payout fee in a readable format
128+
const payoutFeeDisplay = useMemo((): ReactNode => {
129+
if (!plan || payoutFee === undefined) return undefined;
130+
131+
const hasTieredPricing = payoutFeeWaiverLimit && payoutFeeWaiverLimit > 0;
132+
const hasWaiverRemaining =
133+
payoutFeeWaiverUsage !== undefined &&
134+
payoutFeeWaiverUsage < payoutFeeWaiverLimit!;
135+
136+
if (hasTieredPricing && hasWaiverRemaining) {
137+
const waiverLimitFormatted = nFormatter(payoutFeeWaiverLimit / 100, {
138+
full: payoutFeeWaiverLimit < 100000,
139+
});
140+
141+
return (
142+
<>
143+
<span className="text-neutral-400 line-through">
144+
{payoutFee * 100}%
145+
</span>{" "}
146+
0% for the first ${waiverLimitFormatted}
147+
</>
148+
);
149+
}
150+
151+
return `${payoutFee * 100}%`;
152+
}, [plan, payoutFee, payoutFeeWaiverLimit, payoutFeeWaiverUsage]);
153+
125154
return (
126155
<div className="rounded-xl border border-neutral-200 bg-white">
127156
<div className="flex flex-col items-start justify-between gap-y-4 p-6 md:px-8 lg:flex-row">
@@ -231,11 +260,7 @@ export default function PlanUsage() {
231260
<UsageCategory
232261
title="Payout fees"
233262
icon={CirclePercentage}
234-
usage={
235-
plan && payoutFee !== undefined
236-
? `${payoutFee * 100}%`
237-
: undefined
238-
}
263+
usage={payoutFeeDisplay}
239264
href="https://dub.co/help/article/partner-payouts#payout-fees-and-timing"
240265
/>
241266
</div>
@@ -439,7 +464,7 @@ function UsageTabCard({
439464
function UsageCategory(data: {
440465
title: string;
441466
icon: Icon;
442-
usage?: number | string;
467+
usage?: number | string | ReactNode;
443468
usageLimit?: number;
444469
href?: string;
445470
unit?: string;
@@ -465,9 +490,7 @@ function UsageCategory(data: {
465490
{usage || usage === 0 ? (
466491
<p>
467492
{typeof usage === "number"
468-
? `${unit ?? ""}${nFormatter(usage / (unit === "$" ? 100 : 1), {
469-
full: true,
470-
})}`
493+
? `${unit ?? ""}${nFormatter(usage / (unit === "$" ? 100 : 1), { full: true })}`
471494
: usage}
472495
</p>
473496
) : (

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const confirmPayoutsAction = authActionClient
168168
const mandate = await checkPaymentMethodMandate({
169169
paymentMethodId,
170170
});
171+
171172
if (!mandate) {
172173
// if mandate is not valid, remove the payment method
173174
await stripe.paymentMethods.detach(paymentMethodId);

0 commit comments

Comments
 (0)