Skip to content

Commit 9b487f5

Browse files
Flag partner through the rejected application modal (dubinc#3866)
Co-authored-by: Steven Tey <stevensteel97@gmail.com>
1 parent e14eb63 commit 9b487f5

5 files changed

Lines changed: 230 additions & 51 deletions

File tree

apps/web/app/(ee)/api/partners/applications/reject/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,23 @@ import { NextResponse } from "next/server";
77
// POST /api/partners/applications/reject – Reject a pending partner application
88
export const POST = withWorkspace(
99
async ({ workspace, req, session }) => {
10-
const { partnerId, rejectionReason, rejectionNote, allowImmediateReapply } =
11-
rejectPartnerSchema.parse(await parseRequestBody(req));
10+
const {
11+
partnerId,
12+
rejectionReason,
13+
rejectionNote,
14+
allowImmediateReapply,
15+
flagForFraud,
16+
flagForFraudReason,
17+
} = rejectPartnerSchema.parse(await parseRequestBody(req));
1218

1319
await rejectPartner({
1420
workspace,
1521
partnerId,
1622
rejectionReason,
1723
rejectionNote,
1824
allowImmediateReapply,
25+
flagForFraud,
26+
flagForFraudReason,
1927
userId: session.user.id,
2028
});
2129

apps/web/lib/actions/partners/reject-partner-application.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ export const rejectPartnerApplicationAction = authActionClient
1515
.inputSchema(inputSchema)
1616
.action(async ({ parsedInput, ctx }) => {
1717
const { workspace, user } = ctx;
18-
const { partnerId, rejectionReason, rejectionNote, allowImmediateReapply } =
19-
parsedInput;
18+
const {
19+
partnerId,
20+
rejectionReason,
21+
rejectionNote,
22+
allowImmediateReapply,
23+
flagForFraud,
24+
flagForFraudReason,
25+
} = parsedInput;
2026

2127
throwIfNoPermission({
2228
role: workspace.role,
@@ -29,6 +35,8 @@ export const rejectPartnerApplicationAction = authActionClient
2935
rejectionReason,
3036
rejectionNote,
3137
allowImmediateReapply,
38+
flagForFraud,
39+
flagForFraudReason,
3240
userId: user.id,
3341
});
3442
});

apps/web/lib/api/partners/applications/reject-partner.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,27 @@ export async function rejectPartner({
2424
rejectionReason,
2525
rejectionNote,
2626
allowImmediateReapply,
27+
flagForFraud,
28+
flagForFraudReason,
2729
userId,
2830
}: RejectPartnerInput) {
2931
const programId = getDefaultProgramIdOrThrow(workspace);
3032

33+
if (flagForFraud && allowImmediateReapply) {
34+
throw new DubApiError({
35+
code: "bad_request",
36+
message:
37+
"Cannot flag for fraud when allowing the partner to reapply immediately.",
38+
});
39+
}
40+
41+
if (flagForFraud && (!flagForFraudReason || !flagForFraudReason.trim())) {
42+
throw new DubApiError({
43+
code: "bad_request",
44+
message: "Fraud reason is required when flagging for fraud.",
45+
});
46+
}
47+
3148
const programEnrollment = await prisma.programEnrollment.findUnique({
3249
where: {
3350
partnerId_programId: {
@@ -111,6 +128,16 @@ export async function rejectPartner({
111128
discountId: null,
112129
},
113130
});
131+
132+
if (flagForFraud && flagForFraudReason) {
133+
await tx.fraudAlert.create({
134+
data: {
135+
partnerId,
136+
programId,
137+
reason: flagForFraudReason,
138+
},
139+
});
140+
}
114141
});
115142

116143
const { partner, program } = programEnrollment;

apps/web/lib/zod/schemas/partners.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -904,33 +904,71 @@ export const PROGRAM_APPLICATION_REJECTION_NOTE_MAX_LENGTH = 500;
904904
// Max length for optional `flagForFraudReason` on `FraudAlert`
905905
export const MAX_FRAUD_REASON_LENGTH = 2000;
906906

907-
export const rejectPartnerSchema = z.object({
908-
partnerId: z.string().describe("The ID of the partner to reject."),
909-
rejectionReason: z
910-
.enum(ProgramApplicationRejectionReason)
911-
.optional()
912-
.describe(
913-
"The reason for rejecting the partner application. This will be shared with the partner via email.",
914-
),
915-
rejectionNote: z
916-
.string()
917-
.max(PROGRAM_APPLICATION_REJECTION_NOTE_MAX_LENGTH)
918-
.optional()
919-
.transform((s) => {
920-
const t = s?.trim();
921-
return t === "" ? undefined : t;
922-
})
923-
.describe(
924-
"Additional details about the rejection. This will be shared with the partner via email.",
925-
),
926-
allowImmediateReapply: z
927-
.boolean()
928-
.optional()
929-
.default(false)
930-
.describe(
931-
"When true, pending enrollment is removed so the partner can submit a new application immediately.",
932-
),
933-
});
907+
export const rejectPartnerSchema = z
908+
.object({
909+
partnerId: z.string().describe("The ID of the partner to reject."),
910+
rejectionReason: z
911+
.enum(ProgramApplicationRejectionReason)
912+
.optional()
913+
.describe(
914+
"The reason for rejecting the partner application. This will be shared with the partner via email.",
915+
),
916+
rejectionNote: z
917+
.string()
918+
.max(PROGRAM_APPLICATION_REJECTION_NOTE_MAX_LENGTH)
919+
.optional()
920+
.transform((s) => {
921+
const t = s?.trim();
922+
return t === "" ? undefined : t;
923+
})
924+
.describe(
925+
"Additional details about the rejection. This will be shared with the partner via email.",
926+
),
927+
allowImmediateReapply: z
928+
.boolean()
929+
.optional()
930+
.default(false)
931+
.describe(
932+
"When true, pending enrollment is removed so the partner can submit a new application immediately.",
933+
),
934+
flagForFraud: z
935+
.boolean()
936+
.optional()
937+
.describe(
938+
"Whether to flag the partner for fraud review by the Dub team. Cannot be combined with allowImmediateReapply.",
939+
),
940+
flagForFraudReason: z
941+
.string()
942+
.max(MAX_FRAUD_REASON_LENGTH)
943+
.optional()
944+
.transform((s) => {
945+
const t = s?.trim();
946+
return t === "" ? undefined : t;
947+
})
948+
.describe(
949+
"The reason for flagging the partner for fraud. Required when flagForFraud is true.",
950+
),
951+
})
952+
.superRefine((data, ctx) => {
953+
if (data.allowImmediateReapply && data.flagForFraud) {
954+
ctx.addIssue({
955+
code: "custom",
956+
message:
957+
"Cannot flag for fraud when allowing the partner to reapply immediately.",
958+
path: ["flagForFraud"],
959+
});
960+
}
961+
if (
962+
data.flagForFraud &&
963+
(!data.flagForFraudReason || !data.flagForFraudReason.trim())
964+
) {
965+
ctx.addIssue({
966+
code: "custom",
967+
message: "Fraud reason is required when flagging for fraud.",
968+
path: ["flagForFraudReason"],
969+
});
970+
}
971+
});
934972

935973
export const bulkRejectPartnersSchema = z.object({
936974
workspaceId: z.string(),

0 commit comments

Comments
 (0)