Skip to content

Commit 42237b5

Browse files
steven-teydevkiran
andauthored
Improve application review flow (dubinc#3664)
Co-authored-by: Kiran K <kiran@dub.co>
1 parent 83c8801 commit 42237b5

23 files changed

Lines changed: 1152 additions & 242 deletions

File tree

apps/web/app/(ee)/api/admin/partners/platforms/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ const postSchema = z.object({
1818
// POST /api/admin/partners/platforms
1919
export const POST = withAdmin(
2020
async ({ req }) => {
21-
const { partnerId, platform, identifier: rawIdentifier, postUrl } = postSchema
22-
.parse(await req.json());
21+
const {
22+
partnerId,
23+
platform,
24+
identifier: rawIdentifier,
25+
postUrl,
26+
} = postSchema.parse(await req.json());
2327

2428
const partner = await prisma.partner.findUnique({
2529
where: { id: partnerId },

apps/web/app/(ee)/api/cron/partners/auto-reject/route.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { evaluateApplicationRequirements } from "@/lib/partners/evaluate-applica
44
import { sendEmail } from "@dub/email";
55
import PartnerApplicationRejected from "@dub/email/templates/partner-application-rejected";
66
import { prisma } from "@dub/prisma";
7-
import { ProgramEnrollmentStatus } from "@dub/prisma/client";
7+
import {
8+
ProgramApplicationRejectionReason,
9+
ProgramEnrollmentStatus,
10+
} from "@dub/prisma/client";
811
import * as z from "zod/v4";
912
import { logAndRespond } from "../../utils";
1013

@@ -74,21 +77,47 @@ export const POST = withCron(async ({ rawBody }) => {
7477
);
7578
}
7679

77-
const { count } = await prisma.programEnrollment.updateMany({
78-
where: {
79-
id: programEnrollment.id,
80-
status: ProgramEnrollmentStatus.pending,
81-
},
82-
data: {
83-
status: ProgramEnrollmentStatus.rejected,
84-
clickRewardId: null,
85-
leadRewardId: null,
86-
saleRewardId: null,
87-
discountId: null,
88-
},
80+
const { skipped } = await prisma.$transaction(async (tx) => {
81+
const { count } = await tx.programEnrollment.updateMany({
82+
where: {
83+
id: programEnrollment.id,
84+
status: ProgramEnrollmentStatus.pending,
85+
},
86+
data: {
87+
status: ProgramEnrollmentStatus.rejected,
88+
clickRewardId: null,
89+
leadRewardId: null,
90+
saleRewardId: null,
91+
discountId: null,
92+
},
93+
});
94+
95+
if (count === 0) {
96+
return {
97+
skipped: true,
98+
};
99+
}
100+
101+
if (programEnrollment.applicationId) {
102+
await tx.programApplication.update({
103+
where: {
104+
id: programEnrollment.applicationId,
105+
},
106+
data: {
107+
reviewedAt: new Date(),
108+
rejectionReason:
109+
ProgramApplicationRejectionReason.doesNotMeetRequirements,
110+
rejectionNote: null,
111+
},
112+
});
113+
}
114+
115+
return {
116+
skipped: false,
117+
};
89118
});
90119

91-
if (count === 0) {
120+
if (skipped) {
92121
return logAndRespond(
93122
`Partner ${partnerId} is no longer pending in program ${programId}. Skipping auto-reject.`,
94123
);

apps/web/app/(ee)/api/partner-profile/programs/[programId]/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
1818
saleReward: true,
1919
discount: true,
2020
partnerGroup: true,
21+
application: {
22+
select: {
23+
rejectionReason: true,
24+
rejectionNote: true,
25+
reviewedAt: true,
26+
},
27+
},
2128
},
2229
});
2330

@@ -32,6 +39,13 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
3239
...programEnrollment,
3340
rewards,
3441
group: programEnrollment.partnerGroup,
42+
application: programEnrollment.application
43+
? {
44+
rejectionReason: programEnrollment.application.rejectionReason,
45+
rejectionNote: programEnrollment.application.rejectionNote,
46+
reviewedAt: programEnrollment.application.reviewedAt,
47+
}
48+
: null,
3549
}),
3650
);
3751
});

apps/web/app/(ee)/api/partner-profile/programs/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
3535
},
3636
},
3737
},
38+
application: {
39+
select: {
40+
rejectionReason: true,
41+
rejectionNote: true,
42+
reviewedAt: true,
43+
},
44+
},
3845
...(includeRewardsDiscounts && {
3946
clickReward: true,
4047
leadReward: true,
@@ -62,6 +69,13 @@ export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
6269
enrollment.saleReward,
6370
].filter((r): r is Reward => r !== null)
6471
: [],
72+
application: enrollment.application
73+
? {
74+
rejectionReason: enrollment.application.rejectionReason,
75+
rejectionNote: enrollment.application.rejectionNote,
76+
reviewedAt: enrollment.application.reviewedAt,
77+
}
78+
: null,
6579
};
6680
});
6781

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { DubApiError } from "@/lib/api/errors";
2+
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
3+
import { withWorkspace } from "@/lib/auth";
4+
import { prisma } from "@dub/prisma";
5+
import { NextResponse } from "next/server";
6+
import * as z from "zod/v4";
7+
8+
const querySchema = z.object({
9+
partnerId: z.string().min(1),
10+
});
11+
12+
// GET /api/programs/[programId]/applications/history?partnerId= — applications for this program + partner email
13+
export const GET = withWorkspace(async ({ workspace, searchParams }) => {
14+
const programId = getDefaultProgramIdOrThrow(workspace);
15+
const { partnerId } = querySchema.parse(searchParams);
16+
17+
const programEnrollment = await prisma.programEnrollment.findUnique({
18+
where: {
19+
partnerId_programId: {
20+
partnerId,
21+
programId,
22+
},
23+
},
24+
select: {
25+
applicationId: true,
26+
partner: {
27+
select: {
28+
email: true,
29+
},
30+
},
31+
},
32+
});
33+
34+
if (!programEnrollment?.partner?.email) {
35+
throw new DubApiError({
36+
code: "not_found",
37+
message: "Partner not found in this program.",
38+
});
39+
}
40+
41+
const applicationsByEmail = await prisma.programApplication.findMany({
42+
where: {
43+
programId,
44+
email: programEnrollment.partner.email,
45+
},
46+
orderBy: {
47+
createdAt: "desc",
48+
},
49+
select: {
50+
id: true,
51+
createdAt: true,
52+
reviewedAt: true,
53+
},
54+
});
55+
56+
let applications = applicationsByEmail;
57+
58+
if (
59+
programEnrollment?.applicationId &&
60+
!applications.some((a) => a.id === programEnrollment.applicationId)
61+
) {
62+
const fromEnrollment = await prisma.programApplication.findFirst({
63+
where: {
64+
id: programEnrollment.applicationId,
65+
programId,
66+
},
67+
select: {
68+
id: true,
69+
createdAt: true,
70+
reviewedAt: true,
71+
},
72+
});
73+
74+
if (fromEnrollment) {
75+
applications = [...applications, fromEnrollment].sort(
76+
(a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
77+
);
78+
}
79+
}
80+
81+
return NextResponse.json({ applications });
82+
});

apps/web/app/(ee)/app.dub.co/embed/referrals/bounties/detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export function EmbedBountyDetail({
133133
aria-label="Back to bounties"
134134
title="Back to bounties"
135135
onClick={onBack}
136-
className="bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 hover:bg-bg-emphasis active:scale-95"
136+
className="bg-bg-subtle hover:bg-bg-emphasis flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 active:scale-95"
137137
>
138138
<Trophy className="size-4" />
139139
</button>

apps/web/app/(ee)/app.dub.co/embed/referrals/bounties/submission-detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function EmbedBountySubmissionDetail({
4141
aria-label="Back to bounties"
4242
title="Back to bounties"
4343
onClick={onBackToRoot}
44-
className="bg-bg-subtle flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 hover:bg-bg-emphasis active:scale-95"
44+
className="bg-bg-subtle hover:bg-bg-emphasis flex size-8 shrink-0 items-center justify-center rounded-lg transition-[transform,background-color] duration-150 active:scale-95"
4545
>
4646
<Trophy className="size-4" />
4747
</button>

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/applications/page-client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import usePartner from "@/lib/swr/use-partner";
77
import usePartnersCount from "@/lib/swr/use-partners-count";
88
import useWorkspace from "@/lib/swr/use-workspace";
99
import { EnrolledPartnerProps, PartnerPlatformProps } from "@/lib/types";
10+
import { useApprovePartnerApplicationModal } from "@/ui/modals/approve-partner-application-modal";
1011
import { useBulkApprovePartnersModal } from "@/ui/modals/bulk-approve-partners-modal";
1112
import { useBulkRejectPartnersModal } from "@/ui/modals/bulk-reject-partners-modal";
12-
import { useApprovePartnerApplicationModal } from "@/ui/modals/approve-partner-application-modal";
1313
import { useRejectPartnerApplicationModal } from "@/ui/modals/reject-partner-application-modal";
1414
import { GroupColorCircle } from "@/ui/partners/groups/group-color-circle";
1515
import { PartnerApplicationSheet } from "@/ui/partners/partner-application-sheet";

apps/web/lib/actions/partners/bulk-approve-partners.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,45 @@ export const bulkApprovePartnersAction = authActionClient
4646
groupId: groupId ?? program.defaultGroupId,
4747
});
4848

49-
// Approve the enrollments
50-
await prisma.programEnrollment.updateMany({
51-
where: {
52-
id: {
53-
in: programEnrollments.map(({ id }) => id),
49+
const now = new Date();
50+
51+
await prisma.$transaction(async (tx) => {
52+
await tx.programEnrollment.updateMany({
53+
where: {
54+
id: {
55+
in: programEnrollments.map(({ id }) => id),
56+
},
57+
},
58+
data: {
59+
status: "approved",
60+
createdAt: now,
61+
groupId: group.id,
62+
clickRewardId: group.clickRewardId,
63+
leadRewardId: group.leadRewardId,
64+
saleRewardId: group.saleRewardId,
65+
discountId: group.discountId,
5466
},
55-
},
56-
data: {
57-
status: "approved",
58-
createdAt: new Date(),
59-
groupId: group.id,
60-
clickRewardId: group.clickRewardId,
61-
leadRewardId: group.leadRewardId,
62-
saleRewardId: group.saleRewardId,
63-
discountId: group.discountId,
64-
},
67+
});
68+
69+
const applicationIds = programEnrollments
70+
.map(({ applicationId }) => applicationId)
71+
.filter((id): id is string => Boolean(id));
72+
73+
if (applicationIds.length > 0) {
74+
await tx.programApplication.updateMany({
75+
where: {
76+
id: {
77+
in: applicationIds,
78+
},
79+
},
80+
data: {
81+
reviewedAt: now,
82+
rejectionReason: null,
83+
rejectionNote: null,
84+
userId: user.id,
85+
},
86+
});
87+
}
6588
});
6689

6790
waitUntil(

0 commit comments

Comments
 (0)