From 14f40dc5e86edc84b287769bd03fdcc7d7da83e1 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 30 Nov 2025 18:03:06 -0800 Subject: [PATCH 1/3] Resolve fraud events synchronously in banPartner --- .../api/cron/partners/ban/process/route.ts | 31 ++++++------------- apps/web/lib/actions/partners/ban-partner.ts | 18 ++++++++--- .../lib/actions/partners/bulk-ban-partners.ts | 14 +++++++++ .../web/lib/api/fraud/resolve-fraud-events.ts | 5 ++- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/apps/web/app/(ee)/api/cron/partners/ban/process/route.ts b/apps/web/app/(ee)/api/cron/partners/ban/process/route.ts index c658f008bb7..9c50be835e8 100644 --- a/apps/web/app/(ee)/api/cron/partners/ban/process/route.ts +++ b/apps/web/app/(ee)/api/cron/partners/ban/process/route.ts @@ -1,7 +1,6 @@ import { queueDiscountCodeDeletion } from "@/lib/api/discounts/queue-discount-code-deletion"; import { handleAndReturnErrorResponse } from "@/lib/api/errors"; import { createFraudEvents } from "@/lib/api/fraud/create-fraud-events"; -import { resolveFraudEvents } from "@/lib/api/fraud/resolve-fraud-events"; import { linkCache } from "@/lib/api/links/cache"; import { includeTags } from "@/lib/api/links/include-tags"; import { syncTotalCommissions } from "@/lib/api/partners/sync-total-commissions"; @@ -156,27 +155,15 @@ export async function POST(req: Request) { }, }); - await Promise.all([ - // Automatically resolve all pending fraud events for this partner in the current program - resolveFraudEvents({ - where: { - ...commonWhere, - }, - userId, - resolutionReason: - "Resolved automatically because the partner was banned.", - }), - - // Create partnerCrossProgramBan fraud events for other programs where this partner - // is enrolled and approved, to flag potential cross-program fraud risk - createFraudEvents( - programEnrollments.map(({ programId }) => ({ - programId, - partnerId, - type: "partnerCrossProgramBan", - })), - ), - ]); + // Create partnerCrossProgramBan fraud events for other programs where this partner + // is enrolled and approved, to flag potential cross-program fraud risk + await createFraudEvents( + programEnrollments.map(({ programId }) => ({ + programId, + partnerId, + type: "partnerCrossProgramBan", + })), + ); // Send email if (partner.email) { diff --git a/apps/web/lib/actions/partners/ban-partner.ts b/apps/web/lib/actions/partners/ban-partner.ts index 317069303aa..5658486ef0a 100644 --- a/apps/web/lib/actions/partners/ban-partner.ts +++ b/apps/web/lib/actions/partners/ban-partner.ts @@ -2,6 +2,7 @@ import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log"; import { DubApiError } from "@/lib/api/errors"; +import { resolveFraudEvents } from "@/lib/api/fraud/resolve-fraud-events"; import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw"; import { qstash } from "@/lib/cron"; @@ -60,12 +61,14 @@ export const banPartner = async ({ }); } + const commonWhere = { + partnerId, + programId, + }; + const programEnrollmentUpdated = await prisma.programEnrollment.update({ where: { - partnerId_programId: { - partnerId, - programId, - }, + partnerId_programId: commonWhere, }, data: { status: ProgramEnrollmentStatus.banned, @@ -78,6 +81,13 @@ export const banPartner = async ({ }, }); + // Automatically resolve all pending fraud events for this partner in the current program + await resolveFraudEvents({ + where: commonWhere, + userId: user.id, + resolutionReason: "Resolved automatically because the partner was banned.", + }); + waitUntil( Promise.allSettled([ recordAuditLog({ diff --git a/apps/web/lib/actions/partners/bulk-ban-partners.ts b/apps/web/lib/actions/partners/bulk-ban-partners.ts index 091b7ccf82b..539e8530b85 100644 --- a/apps/web/lib/actions/partners/bulk-ban-partners.ts +++ b/apps/web/lib/actions/partners/bulk-ban-partners.ts @@ -1,6 +1,7 @@ "use server"; import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log"; +import { resolveFraudEvents } from "@/lib/api/fraud/resolve-fraud-events"; import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; import { enqueueBatchJobs } from "@/lib/cron/enqueue-batch-jobs"; import { bulkBanPartnersSchema } from "@/lib/zod/schemas/partners"; @@ -64,6 +65,19 @@ export const bulkBanPartnersAction = authActionClient }, }); + await resolveFraudEvents({ + where: { + programEnrollment: { + id: { + in: programEnrollments.map(({ id }) => id), + }, + }, + }, + userId: user.id, + resolutionReason: + "Resolved automatically because the partner was banned.", + }); + waitUntil( Promise.allSettled([ recordAuditLog( diff --git a/apps/web/lib/api/fraud/resolve-fraud-events.ts b/apps/web/lib/api/fraud/resolve-fraud-events.ts index fd63e48053d..69f94aa8ee3 100644 --- a/apps/web/lib/api/fraud/resolve-fraud-events.ts +++ b/apps/web/lib/api/fraud/resolve-fraud-events.ts @@ -53,7 +53,7 @@ export async function resolveFraudEvents({ groupingKey: nanoid(10), }); - await prisma.fraudEvent.updateMany({ + const { count } = await prisma.fraudEvent.updateMany({ where: { groupKey, status: "pending", @@ -66,6 +66,9 @@ export async function resolveFraudEvents({ groupKey: newGroupKey, }, }); + console.info( + `Resolved ${count} fraud events for partner ${firstEvent.partnerId} in program ${firstEvent.programId}`, + ); } return fraudEvents; From 995d299ca51b7d113e730a36364aaa080e6910b9 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 30 Nov 2025 18:09:45 -0800 Subject: [PATCH 2/3] fix onConfirm mutate --- .../(ee)/program/fraud/fraud-event-groups-table.tsx | 7 ++++++- .../[slug]/(ee)/program/partners/partners-table.tsx | 6 ++++++ apps/web/ui/modals/ban-partner-modal.tsx | 11 +++-------- apps/web/ui/modals/bulk-ban-partners-modal.tsx | 11 +++-------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx index c8071f63892..e770fea5620 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/fraud/fraud-event-groups-table.tsx @@ -1,6 +1,7 @@ "use client"; import { FRAUD_RULES_BY_TYPE } from "@/lib/api/fraud/constants"; +import { mutatePrefix } from "@/lib/swr/mutate"; import { useFraudEventGroups } from "@/lib/swr/use-fraud-event-groups"; import { useFraudEventsCount } from "@/lib/swr/use-fraud-events-count"; import { fraudEventGroupProps } from "@/lib/types"; @@ -93,8 +94,9 @@ export function FraudEventGroupsTable() { const { BulkBanPartnersModal, setShowBulkBanPartnersModal } = useBulkBanPartnersModal({ partners: pendingBanPartners, - onConfirm: () => { + onConfirm: async () => { tableRef.current?.resetRowSelection(); + await mutatePrefix("/api/fraud/events"); }, }); @@ -374,6 +376,9 @@ function RowMenuButton({ row }: { row: Row }) { const { BanPartnerModal, setShowBanPartnerModal } = useBanPartnerModal({ partner: fraudEvent.partner, + onConfirm: async () => { + await mutatePrefix("/api/fraud/events"); + }, }); if (fraudEvent.status !== "pending") { diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx index 46b699fc3cb..88c3f599e16 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx @@ -170,6 +170,9 @@ export function PartnersTable() { const { BulkBanPartnersModal, setShowBulkBanPartnersModal } = useBulkBanPartnersModal({ partners: pendingBanPartners, + onConfirm: async () => { + await mutatePrefix("/api/partners"); + }, }); const { columnVisibility, setColumnVisibility } = useColumnVisibility( @@ -633,6 +636,9 @@ function RowMenuButton({ const { BanPartnerModal, setShowBanPartnerModal } = useBanPartnerModal({ partner: row.original, + onConfirm: async () => { + await mutatePrefix("/api/partners"); + }, }); const { UnbanPartnerModal, setShowUnbanPartnerModal } = useUnbanPartnerModal({ diff --git a/apps/web/ui/modals/ban-partner-modal.tsx b/apps/web/ui/modals/ban-partner-modal.tsx index 64baa26f4ac..cdc38aa8ad0 100644 --- a/apps/web/ui/modals/ban-partner-modal.tsx +++ b/apps/web/ui/modals/ban-partner-modal.tsx @@ -1,5 +1,4 @@ import { banPartnerAction } from "@/lib/actions/partners/ban-partner"; -import { mutatePrefix } from "@/lib/swr/mutate"; import useWorkspace from "@/lib/swr/use-workspace"; import { PartnerProps } from "@/lib/types"; import { @@ -33,7 +32,7 @@ function BanPartnerModal({ showBanPartnerModal: boolean; setShowBanPartnerModal: Dispatch>; partner: Pick; - onConfirm?: () => void; + onConfirm?: () => Promise; }) { const { id: workspaceId } = useWorkspace(); @@ -53,13 +52,9 @@ function BanPartnerModal({ const { executeAsync, isPending } = useAction(banPartnerAction, { onSuccess: async () => { + await onConfirm?.(); toast.success("Partner banned successfully!"); - await Promise.all([ - mutatePrefix("/api/partners"), - mutatePrefix("/api/fraud/events"), - ]); setShowBanPartnerModal(false); - onConfirm?.(); }, onError({ error }) { toast.error(error.serverError); @@ -193,7 +188,7 @@ export function useBanPartnerModal({ onConfirm, }: { partner: Pick; - onConfirm?: () => void; + onConfirm?: () => Promise; }) { const [showBanPartnerModal, setShowBanPartnerModal] = useState(false); diff --git a/apps/web/ui/modals/bulk-ban-partners-modal.tsx b/apps/web/ui/modals/bulk-ban-partners-modal.tsx index 285c761b590..cde0baab6af 100644 --- a/apps/web/ui/modals/bulk-ban-partners-modal.tsx +++ b/apps/web/ui/modals/bulk-ban-partners-modal.tsx @@ -1,5 +1,4 @@ import { bulkBanPartnersAction } from "@/lib/actions/partners/bulk-ban-partners"; -import { mutatePrefix } from "@/lib/swr/mutate"; import useWorkspace from "@/lib/swr/use-workspace"; import { EnrolledPartnerProps } from "@/lib/types"; import { @@ -27,8 +26,8 @@ type BulkBanPartnersFormData = z.infer & { interface BulkBanPartnersProps { showBulkBanPartnersModal: boolean; setShowBulkBanPartnersModal: Dispatch>; - onConfirm?: () => void; partners: Pick[]; + onConfirm?: () => Promise; } function BulkBanPartnersModal({ @@ -55,15 +54,11 @@ function BulkBanPartnersModal({ const { executeAsync, isPending } = useAction(bulkBanPartnersAction, { onSuccess: async () => { + await onConfirm?.(); toast.success( `${partnerWord.charAt(0).toUpperCase() + partnerWord.slice(1)} banned successfully!`, ); - await Promise.all([ - mutatePrefix("/api/partners"), - mutatePrefix("/api/fraud/events"), - ]); setShowBulkBanPartnersModal(false); - onConfirm?.(); }, onError({ error }) { toast.error(error.serverError); @@ -227,7 +222,7 @@ export function useBulkBanPartnersModal({ onConfirm, }: { partners: Pick[]; - onConfirm?: () => void; + onConfirm?: () => Promise; }) { const [showBulkBanPartnersModal, setShowBulkBanPartnersModal] = useState(false); From 26daec0f5f66457738913e96b71035626e4094fe Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 30 Nov 2025 18:22:06 -0800 Subject: [PATCH 3/3] missed ts spot --- apps/web/ui/partners/fraud-risks/fraud-review-sheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/ui/partners/fraud-risks/fraud-review-sheet.tsx b/apps/web/ui/partners/fraud-risks/fraud-review-sheet.tsx index fcd56ddf4cc..aa3ee49e58c 100644 --- a/apps/web/ui/partners/fraud-risks/fraud-review-sheet.tsx +++ b/apps/web/ui/partners/fraud-risks/fraud-review-sheet.tsx @@ -50,7 +50,7 @@ function FraudReviewSheetContent({ const { BanPartnerModal, setShowBanPartnerModal } = useBanPartnerModal({ partner, - onConfirm: () => { + onConfirm: async () => { onNext?.(); }, });