From d85ad68654610e572aa1e1e43b5bec1beea8935f Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 7 Jul 2025 11:15:29 +0530 Subject: [PATCH 01/32] remove minPayoutAmount --- .../cron/payouts/confirm/confirm-payouts.ts | 5 +- .../api/cron/payouts/confirm/split-payouts.ts | 5 +- .../payouts/reminders/program-owners/route.ts | 48 -------------- .../[programId]/payouts/count/route.ts | 5 +- .../[programId]/payouts/eligible/route.ts | 6 +- .../api/programs/[programId]/payouts/route.ts | 5 +- .../(dashboard)/payouts/payout-table.tsx | 55 ++-------------- .../(enrolled)/earnings/earnings-table.tsx | 2 - .../(ee)/program/payouts/payout-table.tsx | 30 +-------- .../[slug]/(ee)/program/rewards/settings.tsx | 66 +------------------ .../lib/actions/partners/update-program.ts | 6 -- apps/web/lib/api/audit-logs/schemas.ts | 1 - apps/web/lib/partners/constants.ts | 1 - apps/web/lib/rewardful/import-campaign.ts | 7 -- apps/web/lib/zod/schemas/payouts.ts | 1 - apps/web/lib/zod/schemas/programs.ts | 23 +------ .../partners/check-pending-paypal-payouts.ts | 16 ++--- .../ui/partners/commission-status-badges.tsx | 3 +- .../partners/payout-status-badge-partner.tsx | 14 +--- packages/prisma/schema/program.prisma | 22 +++---- 20 files changed, 36 insertions(+), 285 deletions(-) diff --git a/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts b/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts index 8c86f24e4df..ffd48be5474 100644 --- a/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts +++ b/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts @@ -43,7 +43,7 @@ export async function confirmPayouts({ | "payoutsLimit" | "payoutFee" >; - program: Pick; + program: Pick; userId: string; paymentMethodId: string; cutoffPeriod?: CUTOFF_PERIOD_TYPES; @@ -57,9 +57,6 @@ export async function confirmPayouts({ programId: program.id, status: "pending", invoiceId: null, - amount: { - gte: program.minPayoutAmount, - }, partner: { payoutsEnabledAt: { not: null, diff --git a/apps/web/app/(ee)/api/cron/payouts/confirm/split-payouts.ts b/apps/web/app/(ee)/api/cron/payouts/confirm/split-payouts.ts index 540ffa8e3b3..c7112313d5b 100644 --- a/apps/web/app/(ee)/api/cron/payouts/confirm/split-payouts.ts +++ b/apps/web/app/(ee)/api/cron/payouts/confirm/split-payouts.ts @@ -11,7 +11,7 @@ export async function splitPayouts({ program, cutoffPeriod, }: { - program: Pick; + program: Pick; cutoffPeriod: CUTOFF_PERIOD_TYPES; }) { const payouts = await prisma.payout.findMany({ @@ -19,9 +19,6 @@ export async function splitPayouts({ programId: program.id, status: "pending", invoiceId: null, - amount: { - gte: program.minPayoutAmount, - }, partner: { payoutsEnabledAt: { not: null, diff --git a/apps/web/app/(ee)/api/cron/payouts/reminders/program-owners/route.ts b/apps/web/app/(ee)/api/cron/payouts/reminders/program-owners/route.ts index 3c228f3cf19..0bdf5c88f18 100644 --- a/apps/web/app/(ee)/api/cron/payouts/reminders/program-owners/route.ts +++ b/apps/web/app/(ee)/api/cron/payouts/reminders/program-owners/route.ts @@ -1,7 +1,6 @@ import { handleAndReturnErrorResponse } from "@/lib/api/errors"; import { limiter } from "@/lib/cron/limiter"; import { verifyVercelSignature } from "@/lib/cron/verify-vercel"; -import { DUB_MIN_PAYOUT_AMOUNT_CENTS } from "@/lib/partners/constants"; import { sendEmail } from "@dub/email"; import ProgramPayoutReminder from "@dub/email/templates/program-payout-reminder"; import { prisma } from "@dub/prisma"; @@ -28,24 +27,10 @@ export async function GET(req: Request) { ); } - const programsWithCustomMinPayouts = await prisma.program.findMany({ - where: { - minPayoutAmount: { - gt: DUB_MIN_PAYOUT_AMOUNT_CENTS, - }, - }, - }); - const pendingPayouts = await prisma.payout.groupBy({ by: ["programId"], where: { status: "pending", - amount: { - gte: DUB_MIN_PAYOUT_AMOUNT_CENTS, - }, - programId: { - notIn: programsWithCustomMinPayouts.map((p) => p.id), - }, partner: { payoutsEnabledAt: { not: null, @@ -60,39 +45,6 @@ export async function GET(req: Request) { }, }); - for (const program of programsWithCustomMinPayouts) { - console.log( - `Manually calculating pending payout for program ${program.id} which has a custom min payout amount of ${program.minPayoutAmount}`, - ); - - const pendingPayout = await prisma.payout.aggregate({ - where: { - programId: program.id, - status: "pending", - amount: { - gte: program.minPayoutAmount, - }, - partner: { - payoutsEnabledAt: { - not: null, - }, - }, - }, - _sum: { - amount: true, - }, - _count: { - _all: true, - }, - }); - - pendingPayouts.push({ - programId: program.id, - _sum: pendingPayout._sum, - _count: pendingPayout._count, - }); - } - if (!pendingPayouts.length) { return NextResponse.json("No pending payouts found. Skipping..."); } diff --git a/apps/web/app/(ee)/api/programs/[programId]/payouts/count/route.ts b/apps/web/app/(ee)/api/programs/[programId]/payouts/count/route.ts index abb8dc4d765..70582b3dd1e 100644 --- a/apps/web/app/(ee)/api/programs/[programId]/payouts/count/route.ts +++ b/apps/web/app/(ee)/api/programs/[programId]/payouts/count/route.ts @@ -13,7 +13,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { const { partnerId, groupBy, eligibility, status, invoiceId } = payoutsCountQuerySchema.parse(searchParams); - const { minPayoutAmount } = await getProgramOrThrow({ + await getProgramOrThrow({ workspaceId: workspace.id, programId, }); @@ -22,9 +22,6 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { programId, ...(partnerId && { partnerId }), ...(eligibility === "eligible" && { - amount: { - gte: minPayoutAmount, - }, partner: { payoutsEnabledAt: { not: null, diff --git a/apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts b/apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts index 27457a814ca..31125645da1 100644 --- a/apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts +++ b/apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts @@ -30,7 +30,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { const { cutoffPeriod } = confirmPayoutsQuerySchema.parse(searchParams); - const { minPayoutAmount } = await getProgramOrThrow({ + await getProgramOrThrow({ workspaceId: workspace.id, programId, }); @@ -43,9 +43,6 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { where: { programId, status: "pending", - amount: { - gte: minPayoutAmount, - }, partner: { payoutsEnabledAt: { not: null, @@ -86,7 +83,6 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { amount: newPayoutAmount, }; }) - .filter((payout) => payout.amount >= minPayoutAmount); } return NextResponse.json(z.array(PayoutResponseSchema).parse(payouts)); diff --git a/apps/web/app/(ee)/api/programs/[programId]/payouts/route.ts b/apps/web/app/(ee)/api/programs/[programId]/payouts/route.ts index d4f337067b6..009f6602605 100644 --- a/apps/web/app/(ee)/api/programs/[programId]/payouts/route.ts +++ b/apps/web/app/(ee)/api/programs/[programId]/payouts/route.ts @@ -14,7 +14,7 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { const programId = getDefaultProgramIdOrThrow(workspace); const parsed = payoutsQuerySchema.parse(searchParams); - const { minPayoutAmount } = await getProgramOrThrow({ + await getProgramOrThrow({ workspaceId: workspace.id, programId, }); @@ -36,9 +36,6 @@ export const GET = withWorkspace(async ({ workspace, searchParams }) => { ...(status && { status }), ...(partnerId && { partnerId }), ...(eligibility === "eligible" && { - amount: { - gte: minPayoutAmount, - }, partner: { payoutsEnabledAt: { not: null, diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx index b6fd13ac839..85e0a065522 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx @@ -6,11 +6,9 @@ import { PartnerPayoutResponse } from "@/lib/types"; import { PayoutRowMenu } from "@/ui/partners/payout-row-menu"; import { PayoutStatusBadgePartner } from "@/ui/partners/payout-status-badge-partner"; import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state"; -import { PayoutStatus } from "@dub/prisma/client"; import { AnimatedSizeContainer, Filter, - SimpleTooltipContent, Table, Tooltip, usePagination, @@ -85,12 +83,7 @@ export function PayoutTable() { }, { header: "Status", - cell: ({ row }) => ( - - ), + cell: ({ row }) => , }, { id: "paidAt", @@ -107,11 +100,10 @@ export function PayoutTable() { header: "Amount", cell: ({ row }) => (
- + {currencyFormatter(row.original.amount / 100, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} {["completed", "processing"].includes(row.original.status) && ( @@ -222,40 +214,3 @@ export function PayoutTable() { ); } - -function AmountRowItem({ - amount, - status, - minPayoutAmount, -}: { - amount: number; - status: PayoutStatus; - minPayoutAmount: number; -}) { - const display = currencyFormatter(amount / 100, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); - - if (status === PayoutStatus.pending && amount < minPayoutAmount) { - return ( - - } - > - - {display} - - - ); - } - - return display; -} diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx index 4fe5b4bef1c..0d93474e030 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx @@ -215,8 +215,6 @@ export function EarningsTablePartner({ limit }: { limit?: number }) { tooltip={badge.tooltip({ holdingPeriodDays: programEnrollment?.program.holdingPeriodDays ?? 0, - minPayoutAmount: - programEnrollment?.program.minPayoutAmount ?? 10000, supportEmail: programEnrollment?.program.supportEmail ?? "support@dub.co", })} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx index a30920d2151..e1c763adc08 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx @@ -1,8 +1,6 @@ "use client"; -import { DUB_MIN_PAYOUT_AMOUNT_CENTS } from "@/lib/partners/constants"; import usePayoutsCount from "@/lib/swr/use-payouts-count"; -import useProgram from "@/lib/swr/use-program"; import useWorkspace from "@/lib/swr/use-workspace"; import { PayoutResponse } from "@/lib/types"; import { PartnerRowItem } from "@/ui/partners/partner-row-item"; @@ -16,7 +14,6 @@ import { StatusBadge, Table, Tooltip, - TooltipContent, usePagination, useRouterStuff, useTable, @@ -31,7 +28,6 @@ import { import { formatPeriod } from "@dub/utils/src/functions/datetime"; import { fetcher } from "@dub/utils/src/functions/fetcher"; import { PayoutDetailsSheet } from "app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet"; -import { useParams } from "next/navigation"; import { memo, useEffect, useState } from "react"; import useSWR from "swr"; import { usePayoutFilters } from "./use-payout-filters"; @@ -308,37 +304,13 @@ function AmountRowItem({ status: PayoutStatus; payoutsEnabled: boolean; }) { - const { slug } = useParams(); - const { program } = useProgram(); const display = currencyFormatter(amount / 100, { minimumFractionDigits: 2, maximumFractionDigits: 2, }); - const minPayoutAmount = - program?.minPayoutAmount || DUB_MIN_PAYOUT_AMOUNT_CENTS; - if (status === PayoutStatus.pending) { - if (amount < minPayoutAmount) { - return ( - - } - > - - {display} - - - ); - } else if (!payoutsEnabled) { + if (!payoutsEnabled) { return ( diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx index dc0f8f1a7d6..bfbd7ab20d4 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx @@ -2,20 +2,18 @@ import { parseActionError } from "@/lib/actions/parse-action-errors"; import { updateProgramAction } from "@/lib/actions/partners/update-program"; -import { handleMoneyInputChange, handleMoneyKeyDown } from "@/lib/form-utils"; import useProgram from "@/lib/swr/use-program"; import useWorkspace from "@/lib/swr/use-workspace"; import { ProgramProps } from "@/lib/types"; import { HOLDING_PERIOD_DAYS } from "@/lib/zod/schemas/programs"; -import { Button, DynamicTooltipWrapper, TooltipContent } from "@dub/ui"; -import { cn } from "@dub/utils"; +import { Button } from "@dub/ui"; import { useAction } from "next-safe-action/hooks"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { mutate } from "swr"; import { SettingsRow } from "../program-settings-row"; -type FormData = Pick; +type FormData = Pick; export function RewardSettings() { const { id: workspaceId, plan } = useWorkspace(); @@ -29,9 +27,6 @@ export function RewardSettings() { mode: "onBlur", defaultValues: { holdingPeriodDays: program?.holdingPeriodDays, - minPayoutAmount: program?.minPayoutAmount - ? program?.minPayoutAmount / 100 - : undefined, }, }); @@ -87,63 +82,6 @@ export function RewardSettings() {
- - -
-
-
- - $ - - - - ), - } - : undefined - } - > -
- -
-
- - - USD - -
-
-
-
+ + ); +} + +export function useProgramPayoutSettingsModal() { + const [showProgramPayoutSettingsModal, setShowProgramPayoutSettingsModal] = + useState(false); + + const ProgramPayoutSettingsModalCallback = useCallback(() => { + return ( + + ); + }, [showProgramPayoutSettingsModal, setShowProgramPayoutSettingsModal]); + + return useMemo( + () => ({ + setShowProgramPayoutSettingsModal, + ProgramPayoutSettingsModal: ProgramPayoutSettingsModalCallback, + }), + [setShowProgramPayoutSettingsModal, ProgramPayoutSettingsModalCallback], + ); +} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/page.tsx index e119837f120..e9c97b1f9ec 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/page.tsx @@ -1,7 +1,6 @@ import { PageContent } from "@/ui/layout/page-content"; import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper"; import { Rewards } from "./rewards"; -import { RewardSettings } from "./settings"; export default async function ProgramSettingsRewardsPage() { return ( @@ -14,9 +13,8 @@ export default async function ProgramSettingsRewardsPage() { }} > -
+
-
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx deleted file mode 100644 index f2c02865f50..00000000000 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/settings.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { parseActionError } from "@/lib/actions/parse-action-errors"; -import { updateProgramAction } from "@/lib/actions/partners/update-program"; -import { handleMoneyInputChange, handleMoneyKeyDown } from "@/lib/form-utils"; -import useProgram from "@/lib/swr/use-program"; -import useWorkspace from "@/lib/swr/use-workspace"; -import { ProgramProps } from "@/lib/types"; -import { HOLDING_PERIOD_DAYS } from "@/lib/zod/schemas/programs"; -import { Button } from "@dub/ui"; -import { cn } from "@dub/utils"; -import { useAction } from "next-safe-action/hooks"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { mutate } from "swr"; -import { SettingsRow } from "../program-settings-row"; - -type FormData = Pick; - -export function RewardSettings() { - const { id: workspaceId } = useWorkspace(); - const { program } = useProgram(); - - const { - register, - handleSubmit, - setValue, - formState: { isDirty, isValid, isSubmitting, errors }, - } = useForm({ - mode: "onBlur", - }); - - useEffect(() => { - if (program) { - setValue("holdingPeriodDays", program.holdingPeriodDays); - setValue("minPayoutAmount", program.minPayoutAmount / 100); - } - }, [program, setValue]); - - const { executeAsync } = useAction(updateProgramAction, { - onSuccess: async () => { - toast.success("Reward settings updated successfully."); - await mutate(`/api/programs/${program?.id}?workspaceId=${workspaceId}`); - }, - onError: ({ error }) => { - toast.error(parseActionError(error, "Failed to update reward settings.")); - }, - }); - - const onSubmit = async (data: FormData) => { - if (!workspaceId) { - return; - } - - await executeAsync({ - workspaceId, - ...data, - }); - }; - - return ( -
-
-
-

- Reward settings -

-

- These are applied to all reward types -

-
-
- -
-
- -
-
-
- - -
-
-
- - $ - - - - - - USD - -
-
-
-
-
-
-
-
-
- ); -} diff --git a/apps/web/lib/actions/partners/update-program.ts b/apps/web/lib/actions/partners/update-program.ts index f4422663b4e..33118e3a9d2 100644 --- a/apps/web/lib/actions/partners/update-program.ts +++ b/apps/web/lib/actions/partners/update-program.ts @@ -45,6 +45,8 @@ export const updateProgramAction = authActionClient defaultFolderId, } = parsedInput; + console.log({ minPayoutAmount }); + const programId = getDefaultProgramIdOrThrow(workspace); const program = await getProgramOrThrow({ workspaceId: workspace.id, From 1b6b5f2bd405188eea23eb8dbcd9a0d2fc7f47d1 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Mon, 7 Jul 2025 15:27:46 -0700 Subject: [PATCH 28/32] small changes --- .../[programSlug]/(enrolled)/earnings/earnings-table.tsx | 2 +- .../program/payouts/program-payout-settings-modal.tsx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx index 4fe5b4bef1c..f70fc7bbd67 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx @@ -216,7 +216,7 @@ export function EarningsTablePartner({ limit }: { limit?: number }) { holdingPeriodDays: programEnrollment?.program.holdingPeriodDays ?? 0, minPayoutAmount: - programEnrollment?.program.minPayoutAmount ?? 10000, + programEnrollment?.program.minPayoutAmount ?? 0, supportEmail: programEnrollment?.program.supportEmail ?? "support@dub.co", })} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/program-payout-settings-modal.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/program-payout-settings-modal.tsx index 788cd469b68..9be934b3fc6 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/program-payout-settings-modal.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/program-payout-settings-modal.tsx @@ -97,11 +97,8 @@ function ProgramPayoutSettingsModalInner({
-

- Set the holding period before payouts are released -