Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
109 changes: 2 additions & 107 deletions apps/web/app/(ee)/api/cron/auto-approve-partner/route.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,4 @@
import { getPartnerApplicationRisks } from "@/lib/api/fraud/get-partner-application-risks";
import { withCron } from "@/lib/cron/with-cron";
import { approvePartnerEnrollment } from "@/lib/partners/approve-partner-enrollment";
import { getPlanCapabilities } from "@/lib/plan-capabilities";
import { prisma } from "@dub/prisma";
import * as z from "zod/v4";
import { logAndRespond } from "../utils";

export const dynamic = "force-dynamic";

const schema = z.object({
programId: z.string(),
partnerId: z.string(),
});

// POST /api/cron/auto-approve-partner
// This route is used to auto-approve a partner enrolled in a program
export const POST = withCron(async ({ rawBody }) => {
const { programId, partnerId } = schema.parse(JSON.parse(rawBody));

const programEnrollment = await prisma.programEnrollment.findUnique({
where: {
partnerId_programId: {
partnerId,
programId,
},
},
include: {
partnerGroup: true,
partner: {
include: {
platforms: true,
},
},
},
});

if (!programEnrollment) {
return logAndRespond(
`Partner ${partnerId} not found in program ${programId}. Skipping auto-approval.`,
);
}

const group = programEnrollment.partnerGroup;

if (!group) {
return logAndRespond(
`Group not found for partner ${partnerId} in program ${programId}. Skipping auto-approval.`,
);
}

if (!group.autoApprovePartnersEnabledAt) {
return logAndRespond(
`Group ${group.id} does not have auto-approval enabled. Skipping auto-approval.`,
);
}

if (programEnrollment.status !== "pending") {
return logAndRespond(
`${partnerId} is in ${programEnrollment.status} status. Skipping auto-approval.`,
);
}

// Check if the workspace plan has fraud event management capabilities
// If enabled, we'll evaluate risk signals before auto-approving
const program = await prisma.program.findUniqueOrThrow({
where: {
id: programId,
},
include: {
workspace: {
include: {
users: {
where: {
role: "owner",
},
take: 1,
},
},
},
},
});

const { canManageFraudEvents } = getPlanCapabilities(program.workspace.plan);

if (canManageFraudEvents) {
const { riskSeverity } = await getPartnerApplicationRisks({
program,
partner: programEnrollment.partner,
});

if (riskSeverity === "high") {
return logAndRespond(
`Partner ${partnerId} has high risk. Skipping auto-approval.`,
);
}
}

await approvePartnerEnrollment({
programId,
partnerId,
userId: program.workspace.users[0].userId,
groupId: programEnrollment.groupId,
});

return logAndRespond(
`Successfully auto-approved partner ${partnerId} in program ${programId}.`,
);
});
// TODO: Remove in 5 mins
export { POST } from "../partners/auto-approve/route";
109 changes: 109 additions & 0 deletions apps/web/app/(ee)/api/cron/partners/auto-approve/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { getPartnerApplicationRisks } from "@/lib/api/fraud/get-partner-application-risks";
import { withCron } from "@/lib/cron/with-cron";
import { approvePartnerEnrollment } from "@/lib/partners/approve-partner-enrollment";
import { getPlanCapabilities } from "@/lib/plan-capabilities";
import { prisma } from "@dub/prisma";
import * as z from "zod/v4";
import { logAndRespond } from "../../utils";

export const dynamic = "force-dynamic";

const schema = z.object({
programId: z.string(),
partnerId: z.string(),
});

// POST /api/cron/partners/auto-approve
// This route is used to auto-approve a partner enrolled in a program
export const POST = withCron(async ({ rawBody }) => {
const { programId, partnerId } = schema.parse(JSON.parse(rawBody));

const programEnrollment = await prisma.programEnrollment.findUnique({
where: {
partnerId_programId: {
partnerId,
programId,
},
},
include: {
partnerGroup: true,
partner: {
include: {
platforms: true,
},
},
},
});

if (!programEnrollment) {
return logAndRespond(
`Partner ${partnerId} not found in program ${programId}. Skipping auto-approval.`,
);
}

const group = programEnrollment.partnerGroup;

if (!group) {
return logAndRespond(
`Group not found for partner ${partnerId} in program ${programId}. Skipping auto-approval.`,
);
}

if (!group.autoApprovePartnersEnabledAt) {
return logAndRespond(
`Group ${group.id} does not have auto-approval enabled. Skipping auto-approval.`,
);
}

if (programEnrollment.status !== "pending") {
return logAndRespond(
`${partnerId} is in ${programEnrollment.status} status. Skipping auto-approval.`,
);
}

// Check if the workspace plan has fraud event management capabilities
// If enabled, we'll evaluate risk signals before auto-approving
const program = await prisma.program.findUniqueOrThrow({
where: {
id: programId,
},
include: {
workspace: {
include: {
users: {
where: {
role: "owner",
},
take: 1,
},
},
},
},
});

const { canManageFraudEvents } = getPlanCapabilities(program.workspace.plan);

if (canManageFraudEvents) {
const { riskSeverity } = await getPartnerApplicationRisks({
program,
partner: programEnrollment.partner,
});

if (riskSeverity === "high") {
return logAndRespond(
`Partner ${partnerId} has high risk. Skipping auto-approval.`,
);
}
}

await approvePartnerEnrollment({
programId,
partnerId,
userId: program.workspace.users[0].userId,
groupId: programEnrollment.groupId,
});

return logAndRespond(
`Successfully auto-approved partner ${partnerId} in program ${programId}.`,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import PartnerBanned from "@dub/email/templates/partner-banned";
import { prisma } from "@dub/prisma";
import { FraudRuleType } from "@dub/prisma/client";
import * as z from "zod/v4";
import { logAndRespond } from "../../../utils";
import { logAndRespond } from "../../utils";
import { cancelCommissions } from "./cancel-commissions";

const schema = z.object({
programId: z.string(),
partnerId: z.string(),
});

// POST /api/cron/partners/ban/process - do the post-ban processing
// POST /api/cron/partners/ban - handle all side effects of banning a partner
export const POST = withCron(async ({ rawBody }) => {
const { programId, partnerId } = schema.parse(JSON.parse(rawBody));

Expand Down Expand Up @@ -78,6 +78,9 @@ export const POST = withCron(async ({ rawBody }) => {
},
data: {
status: "rejected",
rejectionReason: "other",
rejectionNote:
"Rejected automatically because the partner was banned.",
},
}),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const schema = z.object({

const CACHE_KEY_PREFIX = "merge-partner-accounts";

// POST /api/cron/merge-partner-accounts
// POST /api/cron/partners/merge-accounts
// This route is used to merge a partner account into another account
export async function POST(req: Request) {
let userId: string | null = null;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/actions/partners/ban-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const banPartner = async ({
}),

queue.enqueueJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/ban/process`,
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/ban`,
deduplicationId: `ban-${programId}-${partnerId}`,
method: "POST",
body: {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/actions/partners/bulk-ban-partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const bulkBanPartnersAction = authActionClient
enqueueBatchJobs(
programEnrollments.map(({ programId, partnerId }) => ({
queueName: "ban-partner",
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/ban/process`,
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/ban`,
deduplicationId: `ban-${programId}-${partnerId}`,
body: {
programId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ async function createApplicationAndEnrollment({
// Auto-approve the partner if the group has auto-approval enabled
group.autoApprovePartnersEnabledAt
? qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/auto-approve-partner`,
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/auto-approve`,
delay: 5 * 60,
body: {
programId: program.id,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/actions/partners/merge-partner-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ const mergeAccounts = async ({ userId }: { userId: string }) => {
const { sourceEmail, targetEmail } = accounts;

await qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/merge-partner-accounts`,
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/merge-accounts`,
body: {
userId,
sourceEmail,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/partners/complete-program-applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export async function completeProgramApplications(userEmail: string) {
// Auto-approve the partner if the group has auto-approval enabled
group?.autoApprovePartnersEnabledAt
? qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/auto-approve-partner`,
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/auto-approve`,
delay: 5 * 60,
body: {
programId: program.id,
Expand Down