Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9eae7e4
Limit bounty eligibility to selected partner tags
devkiran Jun 16, 2026
b1c9f8f
Merge branch 'main' into limit-bounties-partner-tags
devkiran Jun 18, 2026
f1954d3
Merge branch 'main' into limit-bounties-partner-tags
devkiran Jun 19, 2026
c0564f1
Merge branch 'main' into limit-bounties-partner-tags
devkiran Jun 22, 2026
d1a47dd
Filter partner bounties by partner tag eligibility
devkiran Jun 22, 2026
2609375
Consolidate bounty eligibility logic into shared module
devkiran Jun 22, 2026
f027ea8
Enforce partner tag eligibility on partner bounty endpoints
devkiran Jun 22, 2026
10e25c6
Enforce bounty eligibility on submission and upload flows
devkiran Jun 22, 2026
15bdd7e
Enforce partner tag eligibility in workflows and fix eligibility logic
devkiran Jun 22, 2026
038f88c
Centralize bounty eligibility includes and standardize bounty lookups
devkiran Jun 22, 2026
7e0c44f
Apply partner tag eligibility to draft submissions and bounty APIs
devkiran Jun 22, 2026
c3a8552
Trigger draft bounty submissions when partner tags are added
devkiran Jun 22, 2026
f68639d
Add bounty eligibility UI for groups and partner tags
devkiran Jun 22, 2026
abc40dc
Update route.ts
devkiran Jun 22, 2026
f435665
Fix bounty group and partner tag removal on update
devkiran Jun 22, 2026
07835fa
address CR comments
devkiran Jun 23, 2026
d07b97a
Update route.ts
devkiran Jun 23, 2026
556ed95
Update execute-complete-bounty-workflow.ts
devkiran Jun 23, 2026
90e572b
Merge branch 'main' into limit-bounties-partner-tags
devkiran Jun 23, 2026
bb85bfa
Merge branch 'main' into limit-bounties-partner-tags
devkiran Jun 24, 2026
f02f422
code cleanup
devkiran Jun 24, 2026
fc8c125
add throwIfPartnerNotEligibleForBounty
devkiran Jun 24, 2026
6e93fb9
Update trigger-draft-bounty-submissions.ts
devkiran Jun 24, 2026
6c080e5
Show partner tags on bounty card and detail page
devkiran Jun 24, 2026
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
106 changes: 77 additions & 29 deletions apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
import { DubApiError } from "@/lib/api/errors";
import { throwIfInvalidGroupIds } from "@/lib/api/groups/throw-if-invalid-group-ids";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { throwIfInvalidPartnerTagIds } from "@/lib/api/tags/throw-if-invalid-partner-tag-ids";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import { bountyEligibilityIncludes } from "@/lib/bounty/api/bounty-eligibility";
import { generatePerformanceBountyName } from "@/lib/bounty/api/generate-performance-bounty-name";
import { getBountyOrThrow } from "@/lib/bounty/api/get-bounty-or-throw";
import { getBountyWithDetails } from "@/lib/bounty/api/get-bounty-with-details";
import { PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES } from "@/lib/bounty/api/performance-bounty-scope-attributes";
import { validateBounty } from "@/lib/bounty/api/validate-bounty";
Expand All @@ -18,7 +21,7 @@ import {
updateBountySchema,
} from "@/lib/zod/schemas/bounties";
import { arrayEqual, deepEqual } from "@dub/utils";
import { PartnerGroup, Prisma } from "@prisma/client";
import { PartnerGroup, PartnerTag, Prisma } from "@prisma/client";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";

Expand Down Expand Up @@ -69,21 +72,20 @@ export const PATCH = withWorkspace(
submissionRequirements,
performanceCondition,
groupIds,
partnerTagIds,
} = updateBountySchema.parse(await parseRequestBody(req));

const bounty = await prisma.bounty.findUniqueOrThrow({
where: {
id: bountyId,
programId,
},
const bounty = await getBountyOrThrow({
bountyId,
programId,
include: {
groups: true,
workflow: true,
_count: {
select: {
submissions: true,
},
},
...bountyEligibilityIncludes,
},
});

Expand Down Expand Up @@ -120,17 +122,44 @@ export const PATCH = withWorkspace(

// if groupIds is provided and is different from the current groupIds, update the groups
let updatedPartnerGroups: PartnerGroup[] | undefined = undefined;
if (
groupIds &&
!arrayEqual(
bounty.groups.map((group) => group.groupId),
groupIds,
)
) {
updatedPartnerGroups = await throwIfInvalidGroupIds({
programId,
groupIds,
});
let shouldUpdatePartnerGroups = false;

if (groupIds !== undefined) {
const currentGroupIds = bounty.groups.map((group) => group.groupId);
const newGroupIds = groupIds || [];

if (!arrayEqual(currentGroupIds, newGroupIds)) {
if (newGroupIds.length > 0) {
updatedPartnerGroups = await throwIfInvalidGroupIds({
programId,
groupIds: newGroupIds,
});
}

shouldUpdatePartnerGroups = true;
}
}

// if partnerTagIds is provided and is different from the current partnerTagIds, update the partner tags
let updatedPartnerTags: PartnerTag[] | undefined = undefined;
let shouldUpdatePartnerTags = false;

if (partnerTagIds !== undefined) {
const currentPartnerTagIds = bounty.partnerTags.map(
(tag) => tag.partnerTagId,
);
const newPartnerTagIds = partnerTagIds || [];

if (!arrayEqual(currentPartnerTagIds, newPartnerTagIds)) {
if (newPartnerTagIds.length > 0) {
updatedPartnerTags = await throwIfInvalidPartnerTagIds({
programId,
partnerTagIds: newPartnerTagIds,
});
}

shouldUpdatePartnerTags = true;
}
}

// Prevent updates if `performanceCondition.attribute` differs from the current value if there are existing submissions
Expand Down Expand Up @@ -211,18 +240,33 @@ export const PATCH = withWorkspace(
submissionRequirements !== undefined && {
submissionRequirements: submissionRequirements ?? Prisma.DbNull,
}),
...(updatedPartnerGroups && {
...(shouldUpdatePartnerGroups && {
groups: {
deleteMany: {},
create: updatedPartnerGroups.map((group) => ({
groupId: group.id,
})),
...(updatedPartnerGroups &&
updatedPartnerGroups.length > 0 && {
create: updatedPartnerGroups.map((group) => ({
groupId: group.id,
})),
}),
},
}),
...(shouldUpdatePartnerTags && {
partnerTags: {
deleteMany: {},
...(updatedPartnerTags &&
updatedPartnerTags.length > 0 && {
create: updatedPartnerTags.map((tag) => ({
partnerTagId: tag.id,
})),
}),
},
}),
},
include: {
workflow: true,
groups: true,
partnerTags: true,
workflow: true,
},
});

Expand All @@ -246,6 +290,9 @@ export const PATCH = withWorkspace(
const updatedBounty = BountySchema.parse({
...data,
groups: data.groups.map(({ groupId }) => ({ id: groupId })),
partnerTags: data.partnerTags.map(({ partnerTagId }) => ({
id: partnerTagId,
})),
performanceCondition: data.workflow?.triggerConditions?.[0],
});

Expand Down Expand Up @@ -295,19 +342,17 @@ export const DELETE = withWorkspace(
const { bountyId } = params;
const programId = getDefaultProgramIdOrThrow(workspace);

const bounty = await prisma.bounty.findUniqueOrThrow({
where: {
id: bountyId,
programId,
},
const bounty = await getBountyOrThrow({
bountyId,
programId,
include: {
groups: true,
workflow: true,
_count: {
select: {
submissions: true,
},
},
...bountyEligibilityIncludes,
},
});

Expand Down Expand Up @@ -338,6 +383,9 @@ export const DELETE = withWorkspace(
const deletedBounty = BountySchema.parse({
...bounty,
groups: bounty.groups.map(({ groupId }) => ({ id: groupId })),
partnerTags: bounty.partnerTags.map(({ partnerTagId }) => ({
id: partnerTagId,
})),
performanceCondition: bounty.workflow?.triggerConditions?.[0],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ export const GET = withWorkspace(
await getBountyOrThrow({
bountyId,
programId,
include: {
groups: true,
},
});

const {
Expand Down
78 changes: 51 additions & 27 deletions apps/web/app/(ee)/api/bounties/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { DubApiError } from "@/lib/api/errors";
import { throwIfInvalidGroupIds } from "@/lib/api/groups/throw-if-invalid-group-ids";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { throwIfInvalidPartnerTagIds } from "@/lib/api/tags/throw-if-invalid-partner-tag-ids";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import {
bountyEligibilityIncludes,
buildBountyEligibilityWhere,
} from "@/lib/bounty/api/bounty-eligibility";
import { generatePerformanceBountyName } from "@/lib/bounty/api/generate-performance-bounty-name";
import { validateBounty } from "@/lib/bounty/api/validate-bounty";
import { qstash } from "@/lib/cron";
Expand Down Expand Up @@ -42,10 +47,22 @@ export const GET = withWorkspace(
programId,
include: {
program: true,
programPartnerTags: {
select: {
partnerTagId: true,
},
},
},
})
: null;

const partnerGroupId =
programEnrollment?.groupId || programEnrollment?.program.defaultGroupId;
const partnerTagIds =
programEnrollment?.programPartnerTags.map(
({ partnerTagId }) => partnerTagId,
) || [];

const [bounties, allBountiesSubmissionsCount] = await Promise.all([
prisma.bounty.findMany({
where: {
Expand All @@ -57,36 +74,20 @@ export const GET = withWorkspace(
{
OR: [{ endsAt: null }, { endsAt: { gt: new Date() } }],
},
// Filter by partner's group eligibility
{
OR: [
{
groups: {
none: {},
},
},
{
groups: {
some: {
groupId:
programEnrollment.groupId ||
programEnrollment.program.defaultGroupId,
},
},
},
],
...buildBountyEligibilityWhere({
groupId: partnerGroupId,
partnerTagIds,
}),
},
],
}),
},
include: {
groups: {
select: {
groupId: true,
},
},
...bountyEligibilityIncludes,
},
}),

includeSubmissionsCount
? prisma.bountySubmission.groupBy({
by: ["bountyId", "status"],
Expand Down Expand Up @@ -131,6 +132,9 @@ export const GET = withWorkspace(
return BountyListSchema.parse({
...bounty,
groups: bounty.groups.map(({ groupId }) => ({ id: groupId })),
partnerTags: bounty.partnerTags.map(({ partnerTagId }) => ({
id: partnerTagId,
})),
...(allBountiesSubmissionsCount && {
submissionsCountData: aggregateSubmissionsCountForBounty(bounty.id),
}),
Expand Down Expand Up @@ -171,6 +175,7 @@ export const POST = withWorkspace(
maxSubmissions,
submissionRequirements,
groupIds,
partnerTagIds,
performanceCondition,
performanceScope,
sendNotificationEmails,
Expand All @@ -191,10 +196,17 @@ export const POST = withWorkspace(
});
}

const partnerGroups = await throwIfInvalidGroupIds({
programId,
groupIds,
});
const [partnerGroups, partnerTags] = await Promise.all([
throwIfInvalidGroupIds({
programId,
groupIds,
}),

throwIfInvalidPartnerTagIds({
programId,
partnerTagIds,
}),
]);

// Bounty name
let bountyName = name;
Expand Down Expand Up @@ -268,17 +280,29 @@ export const POST = withWorkspace(
},
},
}),
...(partnerTags.length && {
partnerTags: {
createMany: {
data: partnerTags.map(({ id }) => ({
partnerTagId: id,
})),
},
},
}),
},
include: {
workflow: true,
groups: true,
...bountyEligibilityIncludes,
},
});
});

const createdBounty = BountySchema.parse({
...bounty,
groups: bounty.groups.map(({ groupId }) => ({ id: groupId })),
partnerTags: bounty.partnerTags.map(({ partnerTagId }) => ({
id: partnerTagId,
})),
performanceCondition: bounty.workflow?.triggerConditions?.[0],
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createId } from "@/lib/api/create-id";
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { evaluateWorkflowConditions } from "@/lib/api/workflows/evaluate-workflow-conditions";
import { bountyEligibilityIncludes } from "@/lib/bounty/api/bounty-eligibility";
import { qstash } from "@/lib/cron";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { aggregatePartnerLinksStats } from "@/lib/partners/aggregate-partner-links-stats";
Expand Down Expand Up @@ -41,9 +42,9 @@ export async function POST(req: Request) {
id: bountyId,
},
include: {
groups: true,
program: true,
workflow: true,
...bountyEligibilityIncludes,
},
});

Expand Down Expand Up @@ -75,8 +76,10 @@ export async function POST(req: Request) {
return logAndRespond(`Bounty ${bountyId} has no workflow.`);
}

// Find groupIds
const groupIds = bounty.groups.map(({ groupId }) => groupId);
const partnerTagIds = bounty.partnerTags.map(
({ partnerTagId }) => partnerTagId,
);

// Find program enrollments
const programEnrollments = await prisma.programEnrollment.findMany({
Expand All @@ -92,6 +95,15 @@ export async function POST(req: Request) {
in: partnerIds,
},
}),
...(partnerTagIds.length > 0 && {
programPartnerTags: {
some: {
partnerTagId: {
in: partnerTagIds,
},
},
},
}),
status: {
in: ["approved", "invited"],
},
Expand Down
Loading
Loading