@@ -6,7 +6,9 @@ import { parseRequestBody } from "@/lib/api/utils";
66import { WorkflowCondition } from "@/lib/api/workflows/types" ;
77import { validateWorkflowConditions } from "@/lib/api/workflows/validate-workflow-conditions" ;
88import { withWorkspace } from "@/lib/auth" ;
9+ import { bountyEligibilityIncludes } from "@/lib/bounty/api/bounty-availability" ;
910import { generatePerformanceBountyName } from "@/lib/bounty/api/generate-performance-bounty-name" ;
11+ import { getBountyOrThrow } from "@/lib/bounty/api/get-bounty-or-throw" ;
1012import { getBountyWithDetails } from "@/lib/bounty/api/get-bounty-with-details" ;
1113import { PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES } from "@/lib/bounty/api/performance-bounty-scope-attributes" ;
1214import { shouldUpsertDraftSubmissionsOnReopen } from "@/lib/bounty/api/upsert-draft-bounty-submissions" ;
@@ -21,7 +23,7 @@ import {
2123 updateBountySchema ,
2224} from "@/lib/zod/schemas/bounties" ;
2325import { APP_DOMAIN_WITH_NGROK , arrayEqual , deepEqual } from "@dub/utils" ;
24- import { PartnerGroup , Prisma } from "@prisma/client" ;
26+ import { BountyStartMode , PartnerGroup , Prisma } from "@prisma/client" ;
2527import { waitUntil } from "@vercel/functions" ;
2628import { NextResponse } from "next/server" ;
2729
@@ -57,6 +59,8 @@ export const PATCH = withWorkspace(
5759 description,
5860 startsAt,
5961 endsAt,
62+ startMode,
63+ endsAfterDays,
6064 submissionsOpenAt,
6165 submissionFrequency,
6266 maxSubmissions,
@@ -67,26 +71,57 @@ export const PATCH = withWorkspace(
6771 groupIds,
6872 } = updateBountySchema . parse ( await parseRequestBody ( req ) ) ;
6973
70- const bounty = await prisma . bounty . findUniqueOrThrow ( {
71- where : {
72- id : bountyId ,
73- programId,
74- } ,
74+ const bounty = await getBountyOrThrow ( {
75+ bountyId,
76+ programId,
7577 include : {
76- groups : true ,
7778 workflow : true ,
7879 _count : {
7980 select : {
8081 submissions : true ,
8182 } ,
8283 } ,
84+ ...bountyEligibilityIncludes ,
8385 } ,
8486 } ) ;
8587
88+ const nextStartMode =
89+ startMode !== undefined ? startMode : bounty . startMode ;
90+
91+ // Absolute end dates are cleared when switching to relative (unless the
92+ // client explicitly sends endsAt) or when setting endsAfterDays.
93+ let endsAtUpdate : { endsAt ?: Date | null } = { } ;
94+
95+ if ( endsAt !== undefined ) {
96+ endsAtUpdate = { endsAt } ;
97+ } else if ( endsAfterDays != null ) {
98+ endsAtUpdate = { endsAt : null } ;
99+ } else if (
100+ nextStartMode === BountyStartMode . relative &&
101+ bounty . endsAt != null
102+ ) {
103+ endsAtUpdate = { endsAt : null } ;
104+ }
105+
86106 validateBounty ( {
87107 type : bounty . type ,
88- startsAt,
89- endsAt : endsAt !== undefined ? endsAt : bounty . endsAt ,
108+ // Relative bounties never store startsAt; coerce so mode switches don't
109+ // fail validation against a leftover absolute startsAt.
110+ startsAt :
111+ nextStartMode === BountyStartMode . relative
112+ ? null
113+ : startsAt !== undefined
114+ ? startsAt
115+ : bounty . startsAt ,
116+ endsAt :
117+ endsAtUpdate . endsAt !== undefined ? endsAtUpdate . endsAt : bounty . endsAt ,
118+ startMode : nextStartMode ,
119+ endsAfterDays :
120+ endsAfterDays !== undefined
121+ ? endsAfterDays
122+ : nextStartMode === BountyStartMode . absolute
123+ ? null
124+ : bounty . endsAfterDays ,
90125 submissionsOpenAt,
91126 submissionFrequency :
92127 submissionFrequency !== undefined
@@ -123,17 +158,22 @@ export const PATCH = withWorkspace(
123158
124159 // if groupIds is provided and is different from the current groupIds, update the groups
125160 let updatedPartnerGroups : PartnerGroup [ ] | undefined = undefined ;
126- if (
127- groupIds &&
128- ! arrayEqual (
129- bounty . groups . map ( ( group ) => group . groupId ) ,
130- groupIds ,
131- )
132- ) {
133- updatedPartnerGroups = await throwIfInvalidGroupIds ( {
134- programId,
135- groupIds,
136- } ) ;
161+ let shouldUpdatePartnerGroups = false ;
162+
163+ if ( groupIds !== undefined ) {
164+ const currentGroupIds = bounty . groups . map ( ( group ) => group . groupId ) ;
165+ const newGroupIds = groupIds || [ ] ;
166+
167+ if ( ! arrayEqual ( currentGroupIds , newGroupIds ) ) {
168+ if ( newGroupIds . length > 0 ) {
169+ updatedPartnerGroups = await throwIfInvalidGroupIds ( {
170+ programId,
171+ groupIds : newGroupIds ,
172+ } ) ;
173+ }
174+
175+ shouldUpdatePartnerGroups = true ;
176+ }
137177 }
138178
139179 // Prevent updates if `performanceCondition.attribute` differs from the current value if there are existing submissions
@@ -189,6 +229,19 @@ export const PATCH = withWorkspace(
189229 } ) ;
190230 }
191231
232+ // Relative bounties start when a partner joins, so startsAt is cleared.
233+ // For absolute bounties, only update startsAt when explicitly provided.
234+ let startsAtUpdate : { startsAt ?: Date | null } = { } ;
235+
236+ if ( nextStartMode === BountyStartMode . relative ) {
237+ startsAtUpdate = { startsAt : null } ;
238+ } else if ( startsAt !== undefined ) {
239+ startsAtUpdate = { startsAt : startsAt ?? new Date ( ) } ;
240+ } else if ( bounty . startsAt === null ) {
241+ // Switching relative -> absolute without a startsAt: default to now
242+ startsAtUpdate = { startsAt : new Date ( ) } ;
243+ }
244+
192245 const data = await prisma . $transaction ( async ( tx ) => {
193246 const updatedBounty = await tx . bounty . update ( {
194247 where : {
@@ -197,8 +250,15 @@ export const PATCH = withWorkspace(
197250 data : {
198251 name : bountyName ?? undefined ,
199252 description,
200- startsAt : startsAt ! , // Can remove the ! when we're on a newer TS version (currently 5.4.4)
201- endsAt,
253+ ...startsAtUpdate ,
254+ ...endsAtUpdate ,
255+ ...( startMode !== undefined && { startMode } ) ,
256+ ...( endsAfterDays !== undefined
257+ ? { endsAfterDays }
258+ : nextStartMode === BountyStartMode . absolute &&
259+ bounty . endsAfterDays != null
260+ ? { endsAfterDays : null }
261+ : { } ) ,
202262 submissionsOpenAt :
203263 bounty . type === "submission" ? submissionsOpenAt : null ,
204264 ...( bounty . type === "submission" &&
@@ -214,18 +274,21 @@ export const PATCH = withWorkspace(
214274 submissionRequirements !== undefined && {
215275 submissionRequirements : submissionRequirements ?? Prisma . DbNull ,
216276 } ) ,
217- ...( updatedPartnerGroups && {
277+ ...( shouldUpdatePartnerGroups && {
218278 groups : {
219279 deleteMany : { } ,
220- create : updatedPartnerGroups . map ( ( group ) => ( {
221- groupId : group . id ,
222- } ) ) ,
280+ ...( updatedPartnerGroups &&
281+ updatedPartnerGroups . length > 0 && {
282+ create : updatedPartnerGroups . map ( ( group ) => ( {
283+ groupId : group . id ,
284+ } ) ) ,
285+ } ) ,
223286 } ,
224287 } ) ,
225288 } ,
226289 include : {
227290 workflow : true ,
228- groups : true ,
291+ ... bountyEligibilityIncludes ,
229292 } ,
230293 } ) ;
231294
@@ -290,7 +353,9 @@ export const PATCH = withWorkspace(
290353 body : {
291354 bountyId : bounty . id ,
292355 } ,
293- notBefore : Math . floor ( data . startsAt . getTime ( ) / 1000 ) ,
356+ ...( data . startsAt && {
357+ notBefore : Math . floor ( data . startsAt . getTime ( ) / 1000 ) ,
358+ } ) ,
294359 } ) ,
295360 ] ) ,
296361 ) ;
@@ -309,19 +374,17 @@ export const DELETE = withWorkspace(
309374 const { bountyId } = params ;
310375 const programId = getDefaultProgramIdOrThrow ( workspace ) ;
311376
312- const bounty = await prisma . bounty . findUniqueOrThrow ( {
313- where : {
314- id : bountyId ,
315- programId,
316- } ,
377+ const bounty = await getBountyOrThrow ( {
378+ bountyId,
379+ programId,
317380 include : {
318- groups : true ,
319381 workflow : true ,
320382 _count : {
321383 select : {
322384 submissions : true ,
323385 } ,
324386 } ,
387+ ...bountyEligibilityIncludes ,
325388 } ,
326389 } ) ;
327390
0 commit comments