FEAT: Dynamic bounty start date - #4045
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds partner tag-based bounty eligibility, bounty timing modes, and related API, workflow, cron, UI, and schema updates. The change also propagates partner tag IDs through enrollment queries, submission checks, and bounty responses, while updating webhook samples, tests, and supporting docs. ChangesPartner Tag Bounty Eligibility
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
apps/web/lib/api/tags/throw-if-invalid-partner-tag-ids.ts (1)
24-26: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse a
Setfor invalid-tag detection to avoid quadratic scans.The current
filter + somecheck is O(n*m). ASetmakes this O(n) and keeps behavior identical.♻️ Suggested refactor
- const invalidPartnerTagIds = partnerTagIds?.filter( - (partnerTagId) => !partnerTags?.some((tag) => tag.id === partnerTagId), - ); + const validPartnerTagIdSet = new Set(partnerTags.map((tag) => tag.id)); + const invalidPartnerTagIds = partnerTagIds.filter( + (partnerTagId) => !validPartnerTagIdSet.has(partnerTagId), + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/api/tags/throw-if-invalid-partner-tag-ids.ts` around lines 24 - 26, The current implementation of invalidPartnerTagIds has O(n*m) complexity due to the nested filter and some operations. To optimize this, create a Set from the valid partnerTags IDs first (by mapping tag.id from the partnerTags array into a Set), then use that Set in the filter operation to check if each partnerTagId exists. This changes the complexity to O(n) while maintaining identical behavior. Replace the filter and some check with a simple Set.has() lookup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/app/`(ee)/api/cron/bounties/notify-partners/route.ts:
- Around line 16-18: The TODO comment near the top of the route.ts file is vague
and provides no actionable context about what needs to be fixed. Either replace
the comment with a specific, detailed description of what issue needs to be
addressed (including relevant details like function names, expected behavior, or
known problems), or remove the comment entirely if the issue has already been
resolved or is no longer relevant.
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/bounty-eligibility.tsx:
- Around line 164-170: The useEffect hook that checks the threshold condition
for `groups` (and similarly for `partnerTags` at line 230) includes `useAsync`
in its dependency array, causing a flip-flop effect. When the threshold is met
and `useAsync` is set to true via `setUseAsync`, this change triggers the effect
again, but now the condition evaluates differently and sets it back to false.
Remove `useAsync` from the dependency array in both effects and instead use a
functional updater pattern with `setUseAsync` that checks the previous state
value to prevent this repeating cycle. Apply this fix to both occurrences: the
one with `groups` and `GROUPS_MAX_PAGE_SIZE` around line 166, and the one with
`partnerTags` and `PARTNER_TAGS_MAX_PAGE_SIZE` around line 230.
In `@apps/web/lib/bounty/api/bounty-eligibility.ts`:
- Around line 75-85: The isPartnerEligibleForBounty function can return null
instead of a boolean because when partnerGroupId is null/falsy, the expression
(partnerGroupId && bountyTagIds.includes(partnerGroupId)) evaluates to null
rather than false. Fix this by explicitly converting the inGroup assignment to
always return a boolean value, either by using the double negation operator (!!)
on the inGroup expression or by ensuring the logical AND operation in the return
statement at line 84 always produces a proper boolean result.
In `@apps/web/lib/bounty/api/get-bounty-submission-upload-url.ts`:
- Around line 16-21: The `ProgramEnrollmentWithPartnerTags` type incorrectly
references `BountyPartnerTag` instead of `ProgramPartnerTag` for the
`programPartnerTags` field. Since `ProgramEnrollment` has a relation to
`ProgramPartnerTag` and the actual data returned from
`getProgramEnrollmentOrThrow` provides `ProgramPartnerTag[]`, change the type
definition to use `ProgramPartnerTag` instead of `BountyPartnerTag` in the Pick
statement. Apply the same fix in the other location mentioned in
`apps/web/lib/embed/referrals/auth.ts` at line 32 to maintain type consistency
across the codebase.
In `@apps/web/ui/partners/tags/partner-tags-multi-select.tsx`:
- Around line 55-65: The useEffect hook in partner-tags-multi-select.tsx is
creating an oscillation loop due to the `!useAsync` condition in the Boolean
expression. The effect sets useAsync to true when partnerTags.length exceeds
PARTNER_TAGS_MAX_PAGE_SIZE, but then immediately toggles it back to false on the
next run because useAsync is now true. Remove the `!useAsync` check from the
condition so that the effect only enables useAsync when the threshold is met
without toggling it back off. The condition should simply be
`Boolean(partnerTags && partnerTags.length >= PARTNER_TAGS_MAX_PAGE_SIZE)`.
---
Nitpick comments:
In `@apps/web/lib/api/tags/throw-if-invalid-partner-tag-ids.ts`:
- Around line 24-26: The current implementation of invalidPartnerTagIds has
O(n*m) complexity due to the nested filter and some operations. To optimize
this, create a Set from the valid partnerTags IDs first (by mapping tag.id from
the partnerTags array into a Set), then use that Set in the filter operation to
check if each partnerTagId exists. This changes the complexity to O(n) while
maintaining identical behavior. Replace the filter and some check with a simple
Set.has() lookup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1e1f6c78-ed99-42ae-ab4c-b251134211aa
📒 Files selected for processing (35)
apps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.tsapps/web/app/(ee)/api/cron/bounties/notify-partners/route.tsapps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/social-content-stats/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/social-content-stats/route.tsapps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.tsapps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/add-edit-bounty-sheet.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/bounty-eligibility.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/use-add-edit-bounty-form.tsapps/web/lib/actions/partners/tags/update-program-partner-tags.tsapps/web/lib/actions/partners/upload-bounty-submission-file.tsapps/web/lib/api/tags/throw-if-invalid-partner-tag-ids.tsapps/web/lib/api/workflows/execute-complete-bounty-workflow.tsapps/web/lib/api/workflows/execute-workflows.tsapps/web/lib/bounty/api/bounty-eligibility.tsapps/web/lib/bounty/api/create-bounty-submission.tsapps/web/lib/bounty/api/get-bounties-by-groups.tsapps/web/lib/bounty/api/get-bounties-for-partner.tsapps/web/lib/bounty/api/get-bounty-submission-upload-url.tsapps/web/lib/bounty/api/get-bounty-with-details.tsapps/web/lib/bounty/api/trigger-draft-bounty-submissions.tsapps/web/lib/embed/referrals/auth.tsapps/web/lib/types.tsapps/web/lib/webhook/sample-events/bounty-created.jsonapps/web/lib/webhook/sample-events/bounty-updated.jsonapps/web/lib/zod/schemas/bounties.tsapps/web/lib/zod/schemas/partner-profile.tsapps/web/prisma/schema/bounty.prismaapps/web/prisma/schema/tag.prismaapps/web/ui/partners/tags/partner-tags-multi-select.tsx
💤 Files with no reviewable changes (2)
- apps/web/lib/bounty/api/get-bounties-by-groups.ts
- apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.ts
Treat null eligibility selections as empty so PATCH clears existing restrictions, and refresh the eligibility picker when selections change.
|
@coderabbitai review. Look for breaking changes and security issues. |
|
@coderabbitai full review please |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx (1)
25-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winInclude partner-tag filters in the recipient preview.
partnerTagsis now part of the preview model, but this modal still derives the "selected partners" count fromgroupIdsonly. Tag-limited bounties will therefore show an inflated recipient count and still read like a group-only audience.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx around lines 25 - 37, The recipient preview in ConfirmCreateBountyModal is still counting only group-based targeting, so tag-limited bounties can show the wrong audience size. Update the preview logic in confirm-create-bounty-modal.tsx to include partner-tag filtering alongside groups, using the bounty.partnerTags field in the same path that computes the “selected partners” count. Make sure the displayed copy reflects the combined audience rules instead of implying a group-only selection.apps/web/lib/rewardful/import-partners.ts (1)
194-204: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winSet
groupJoinedAton the approval path too.This only stamps
groupJoinedAton insert. If an existingprogramEnrollmentis approved through theupdatebranch while that field is stillnull, the new relative bounty timing has no join timestamp to anchor to for imported partners.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/rewardful/import-partners.ts` around lines 194 - 204, The approval flow in import-partners only sets groupJoinedAt in the create branch, so existing programEnrollments approved through the update branch can stay null. Update the approval path in the programEnrollment upsert logic to also assign groupJoinedAt when setting status to approved, using the same timestamp approach as the create path, and keep the change localized around the programEnrollment create/update object.apps/web/lib/api/groups/move-partners-to-group.ts (1)
74-92: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winExclude enrollments already in the target group before updating.
Line 85 makes this helper non-idempotent. Because the query never filters out rows whose
groupIdis alreadygroup.id, a retry or no-op move will resetgroupJoinedAt, which can reopen group-based bounty windows and still trigger the downstream notifications/workflows for partners who did not actually move.Suggested fix
- partnerIds = programEnrollments.map(({ partnerId }) => partnerId); + const enrollmentsToMove = programEnrollments.filter( + ({ partnerGroup }) => partnerGroup?.id !== group.id, + ); + + if (enrollmentsToMove.length === 0) { + return 0; + } + + partnerIds = enrollmentsToMove.map(({ partnerId }) => partnerId); const { count } = await prisma.programEnrollment.updateMany({ where: { partnerId: { in: partnerIds, }, programId, },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/api/groups/move-partners-to-group.ts` around lines 74 - 92, Exclude enrollments already assigned to the destination group before calling prisma.programEnrollment.updateMany in movePartnersToGroup. Add a where condition that filters out rows whose groupId already equals group.id so retries and no-op moves do not reset groupJoinedAt or retrigger downstream side effects; keep the update logic otherwise the same and apply the check alongside the existing partnerId and programId filters.apps/web/lib/actions/partners/bulk-invite-partners.ts (1)
127-140: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftDon't stamp
groupJoinedAtfor still-invited enrollments.This row is created with
status: "invited", sogroupJoinedAtbecomes the invite time rather than the actual join/approval time. That will start relative bounty windows before the partner ever accepts the invite. Move this write to the approval/acceptance path instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/actions/partners/bulk-invite-partners.ts` around lines 127 - 140, The bulk invite flow in bulk-invite-partners.ts is setting groupJoinedAt while creating ProgramEnrollment rows with status "invited", which incorrectly treats the invite time as the actual group join time. Remove that assignment from the createMany payload in the bulk invite path, and set groupJoinedAt only in the acceptance/approval flow where the enrollment actually becomes active. Use the ProgramEnrollment creation logic in bulk-invite-partners.ts as the place to find the invite-time write and move the timestamp update to the approval handler that transitions the status out of "invited".apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)
96-102: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRelative bounties still render as if they have no end date.
The new fallback fixes the start side, but this branch still derives the end side from
endsAtonly. For relative bounties,endsAtis null andendsAfterDayscarries the duration, so owners will seeWhen a partner joins → No end dateeven when the bounty is actually time-bounded.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx around lines 96 - 102, The bounty date display in bounty-info.tsx still treats the end date as missing for relative bounties because it only checks bounty.endsAt. Update the end-side branch in the bounty range rendering to also derive a date from bounty.endsAfterDays when bounty.startsAt is absent, using the same relative-bounty logic already applied on the start side. Keep the change localized to the bounty-info.tsx display condition so relative bounties render their actual time-bounded end instead of “No end date.”apps/web/lib/firstpromoter/import-partners.ts (1)
173-195: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftPopulate
groupJoinedAtwhen this upsert approves an existing enrollment.Right now only the
createbranch stamps the new field. IfpartnerId_programIdalready exists,update: { status: "approved" }leavesgroupJoinedAtnull, so relative bounty windows still have no join date on this approval path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/firstpromoter/import-partners.ts` around lines 173 - 195, The `prisma.programEnrollment.upsert` in `import-partners.ts` only sets `groupJoinedAt` in the create path, so existing enrollments approved through the update path remain without a join timestamp. Update the `update` branch for the `programEnrollment` upsert to also set `groupJoinedAt` (and keep the status approval logic) so `partnerId_programId` records get a join date when they are newly approved via this flow.apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)
97-110: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winInclude default-group enrollments when filtering bounty recipients.
For bounties assigned to the program default group, enrollments with
groupId: nullinherit that group but are excluded by this query. Add anORbranch forgroupId: nullwhengroupIdscontainsbounty.program.defaultGroupId.🐛 Proposed fix
...(groupIds.length > 0 && { - groupId: { - in: groupIds, - }, + OR: [ + { + groupId: { + in: groupIds, + }, + }, + ...(bounty.program.defaultGroupId && + groupIds.includes(bounty.program.defaultGroupId) + ? [{ groupId: null }] + : []), + ], }),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/cron/bounties/notify-partners/route.ts around lines 97 - 110, The recipient filter in the bounty partner notification route is missing enrollments that inherit the program default group via a null groupId, so adjust the query building in the bounty recipient selection logic to include an OR branch for groupId null whenever groupIds contains bounty.program.defaultGroupId. Update the existing filter block that handles groupIds and partnerTagIds in the notify-partners route so default-group enrollments are matched alongside explicit group memberships.apps/web/lib/integrations/slack/transform.ts (1)
452-505: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRelative bounties lose their end window in Slack.
For
startMode === "relative", this now always rendersStarts when partner joins, even when the bounty ends after a fixed number of days. A bounty like “starts on join, ends 14 days later” will look open-ended in the notification.Suggested change
const { id, name, description, rewardAmount, rewardDescription, submissionRequirements, type, startMode, startsAt, endsAt, + endsAfterDays, } = data; @@ { type: "mrkdwn", text: `*Duration*\n${ - startMode === "relative" || !startsAt - ? "Starts when partner joins" + startMode === "relative" + ? `Starts when partner joins${endsAfterDays ? ` - ends ${endsAfterDays} day${endsAfterDays === 1 ? "" : "s"} later` : ""}` + : !startsAt + ? "No start date" : `${startsAt.toLocaleDateString()}${endsAt ? ` - ${endsAt.toLocaleDateString()}` : " (No end date)"}` }`, },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/integrations/slack/transform.ts` around lines 452 - 505, The Slack bounty formatter in transform.ts is dropping the end date for relative bounties because the Duration text in the bounty event block treats all startMode === "relative" cases as open-ended. Update the duration logic in the bounty payload formatter to preserve the end window when endsAt is present, while still showing “Starts when partner joins” only for the relative start label; use the existing event block construction around eventMessages and getBountyRewardDescription to locate the affected section.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx (1)
76-97: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReplace
font-regularwithfont-normal.
font-regularisn't a valid Tailwind utility in this codebase, so these changed rows won't get the intended 400 weight. Swap the changed occurrences tofont-normal. Based on learnings, Tailwind’s normal font weight (400) must usefont-normal.Proposed fix
- <div className="text-content-subtle font-regular flex items-center gap-2 text-sm"> + <div className="text-content-subtle font-normal flex items-center gap-2 text-sm"> @@ - className="font-regular" + className="font-normal" @@ - <div className="text-content-subtle font-regular flex items-center gap-2 text-sm"> + <div className="text-content-subtle font-normal flex items-center gap-2 text-sm"> @@ - <div className="text-content-subtle font-regular flex min-w-0 items-center gap-2 text-sm"> + <div className="text-content-subtle font-normal flex min-w-0 items-center gap-2 text-sm"> @@ - <span className="font-regular text-sm text-neutral-700"> + <span className="font-normal text-sm text-neutral-700">Also applies to: 135-153
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx around lines 76 - 97, The updated BountyCard styling uses an invalid Tailwind class, so replace every new `font-regular` usage with `font-normal` in `BountyCard` and the related changed rows mentioned in the review; use the existing `BountyRewardDescription` and surrounding text wrapper elements to locate the affected className values and keep the intended 400 font weight.Source: Learnings
🟠 Major comments (21)
apps/web/prisma/schema/tag.prisma-35-37 (1)
35-37: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winShip the backing Prisma migration with this schema change.
This relation now depends on
BountyPartnerTag, but the supplied PR stack does not include a migration that creates the table/indexes. Deploying this as-is will break any read/write path that touches bounty partner tags.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/prisma/schema/tag.prisma` around lines 35 - 37, The Prisma schema change in Tag depends on the BountyPartnerTag relation, but the PR is missing the migration that creates the backing table and indexes. Add the corresponding Prisma migration alongside the schema update so the database for BountyPartnerTag exists before this relation is deployed; verify the migration matches the Tag/BountyPartnerTag model changes and is included in the stack.apps/web/lib/zod/schemas/partner-profile.ts-164-169 (1)
164-169: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAvoid silently removing fields from the partner bounty payload.
omit({ partnerTags: true, socialMetricsLastSyncedAt: true })changes the response shape for every consumer ofPartnerBountySchema. If this contract change is intentional, it needs endpoint/version coordination or a deprecation path instead of disappearing in-place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/zod/schemas/partner-profile.ts` around lines 164 - 169, The PartnerBountySchema contract is dropping fields in-place via BountySchema.omit, which can silently break existing consumers. Update PartnerBountySchema so it does not remove partnerTags and socialMetricsLastSyncedAt without coordination; either keep the full BountySchema shape here or move the trimmed shape into a new, clearly versioned schema/endpoint and preserve the existing one for backward compatibility.apps/web/lib/bounty/api/get-bounties-for-partner.ts-43-49 (1)
43-49: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't pre-filter expired absolute bounties here.
buildActiveBountyPeriodWhere()removes absolute bounties onceendsAthas passed, so those rows never reach the latergetEffectiveBountyPeriod(...)mapping. The partner-profile list can no longer populate its expired tab for absolute-mode bounties. Query for "started + eligible + not archived" here, then let the returned effective dates drive active/expired classification.Proposed fix
const bounties = await prisma.bounty.findMany({ where: { programId: program.id, - ...buildActiveBountyPeriodWhere(), + archivedAt: null, + OR: [ + { + startMode: "relative", + }, + { + startMode: "absolute", + startsAt: { + lt: new Date(), + }, + }, + ], ...buildBountyEligibilityWhere({ groupId: groupId || program.defaultGroupId, partnerTagIds, }),Also applies to: 83-95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/api/get-bounties-for-partner.ts` around lines 43 - 49, The partner bounty query in getBountiesForPartner is incorrectly pre-filtering out expired absolute bounties by spreading buildActiveBountyPeriodWhere() into prisma.bounty.findMany, which prevents later getEffectiveBountyPeriod() classification from seeing them. Remove that active-period filter from the initial fetch and keep only the “started + eligible + not archived” constraints (via buildBountyEligibilityWhere and the existing base where conditions), then rely on the returned effective dates for active vs expired tab grouping.apps/web/app/(ee)/api/bounties/[bountyId]/route.ts-94-103 (1)
94-103: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winNormalize
startsAtbefore both validation and persistence whenstartModechanges.Line 97 validates against the current row's
startsAt, but Lines 226-247 write a different normalized value. That makesabsolute -> relativefail unless the client also sendsstartsAt: null, whilerelative -> absolutewith nostartsAtleavesstartMode = "absolute"andstartsAt = null. That violates the invariant assumed bygetEffectiveBountyPeriod()for absolute bounties.Suggested fix
const nextStartMode = startMode !== undefined ? startMode : bounty.startMode; + const normalizedStartsAt = + nextStartMode === "relative" + ? null + : startsAt !== undefined + ? startsAt ?? new Date() + : bounty.startMode === "relative" + ? new Date() + : bounty.startsAt; + validateBounty({ type: bounty.type, - startsAt: startsAt !== undefined ? startsAt : bounty.startsAt, + startsAt: normalizedStartsAt, endsAt: endsAt !== undefined ? endsAt : bounty.endsAt, startMode: nextStartMode, endsAfterDays: endsAfterDays !== undefined ? endsAfterDays : bounty.endsAfterDays, @@ - let startsAtUpdate: { startsAt?: Date | null } = {}; - - if (nextStartMode === "relative") { - startsAtUpdate = { startsAt: null }; - } else if (startsAt !== undefined) { - startsAtUpdate = { startsAt: startsAt ?? new Date() }; - } + const startsAtUpdate = + nextStartMode === bounty.startMode && startsAt === undefined + ? {} + : { startsAt: normalizedStartsAt };Also applies to: 226-247
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/bounties/[bountyId]/route.ts around lines 94 - 103, Normalize startsAt consistently in the bounty update flow so validation and persistence use the same value when startMode changes. In the update handler around validateBounty and the persistence logic in the route, derive a single effective startsAt based on the incoming startMode and use it both for validateBounty and for the stored record. Ensure the Bounty update path preserves the invariant expected by getEffectiveBountyPeriod(), especially for absolute bounties, by handling absolute-to-relative and relative-to-absolute transitions without relying on the existing row’s startsAt.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/use-add-edit-bounty-form.ts-77-79 (1)
77-79: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't let relative durations unlock the submission-window flow.
hasEndDatenow flips true forendsAfterDays, but the rest of the submission-window logic in this hook still requires a realendsAt. With "when a partner joins" + "ends in 2 weeks", the UI enables that switch and then immediately rejects the form as invalid.Also applies to: 227-230
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/use-add-edit-bounty-form.ts around lines 77 - 79, The add/edit bounty form is treating endsAfterDays as a submission-window end date, which incorrectly enables the submission-window flow without a real endsAt value. Update useAddEditBountyForm so hasEndDate only reflects an actual date-based end (endsAt) and does not flip true for relative durations; keep the submission-window logic aligned with the existing validation and payload handling in this hook so the UI only unlocks that flow when endsAt is present.apps/web/lib/bounty/api/get-bounty-submission-upload-url.ts-16-33 (1)
16-33: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winResolve the effective group before running upload eligibility.
This path only carries the raw
programEnrollment.groupId, and the new helper call has no access toprogram.defaultGroupId. Partners whose enrollment hasgroupId = nullwill therefore be treated as groupless here, even though they should inherit the program default group. That blocks valid uploads for bounties targeted at the default group.Suggested direction
type ProgramEnrollmentWithPartnerTags = Pick< ProgramEnrollment, | "programId" | "partnerId" | "groupId" | "createdAt" | "groupJoinedAt" | "status" > & { + defaultGroupId: string | null; programPartnerTags: Pick<ProgramPartnerTag, "partnerTagId">[]; };throwIfPartnerCannotSubmitBounty({ - programEnrollment, + programEnrollment: { + ...programEnrollment, + groupId: programEnrollment.groupId ?? programEnrollment.defaultGroupId, + }, bounty, });You'd also need to load
defaultGroupIdfrom the related program/enrollment fetch.Also applies to: 90-101
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/api/get-bounty-submission-upload-url.ts` around lines 16 - 33, The upload eligibility flow in get-bounty-submission-upload-url is using programEnrollment.groupId directly, which can miss the program’s default group for enrollments with no explicit group. Update the programEnrollment data shape and the related fetch so getBountySubmissionUploadUrl (and the helper call around the eligibility check) also has access to program.defaultGroupId, then resolve the effective group before evaluating upload eligibility. Ensure the effective-group logic is used consistently wherever this eligibility path is applied.apps/web/lib/zod/schemas/bounties.ts-105-108 (1)
105-108: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winKeep the create-bounty request schema backward compatible.
startModeandpartnerTagIdsare required at parse time now, so callers still sending the old payload shape will fail validation before the API can apply the previous default behavior. This is easy to hit with stale dashboard tabs during rollout.Suggested fix
- startMode: z.enum(BountyStartMode), + startMode: z.enum(BountyStartMode).nullish(), startsAt: parseDateSchema.nullish(), endsAt: parseDateSchema.nullish(), endsAfterDays: z.number().int().positive().nullish(), @@ - partnerTagIds: z.array(z.string()).nullable(), + partnerTagIds: z.array(z.string()).nullish(),Then normalize them in the route to
"absolute"andnullrespectively before validation/persistence.Also applies to: 130-130
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/zod/schemas/bounties.ts` around lines 105 - 108, Keep the create-bounty schema backward compatible by not requiring `startMode` and `partnerTagIds` at parse time in the bounties Zod schemas; adjust the schema around `startMode` and `partnerTagIds` so old payloads still validate, then normalize missing values in the create-bounty route to `"absolute"` and `null` before persistence. Update the corresponding schema definitions and any route logic that consumes them so stale clients can still submit the old request shape without failing validation.apps/web/app/(ee)/api/cron/bounties/sync-social-metrics/route.ts-2-5 (1)
2-5: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winFilter by effective active period before syncing metrics.
The cron now checks only
endsAt, and only aftergetSocialMetricsUpdates. This can call external metric providers for expired submissions and can submit draft rows before their effectivestartsAt.Suggested fix
import { getEffectiveBountyPeriod, isBountyExpired, + isBountyStarted, } from "`@/lib/bounty/bounty-period`";- const newMetrics = await getSocialMetricsUpdates({ - bounty, - submissions, - }); - const minCount = bountyInfo.socialMetrics?.minCount; if (!minCount) { return logAndRespond( `Bounty ${bountyId} has no minimum social metrics count. Skipping...`, ); } - const submissionById = new Map(submissions.map((s) => [s.id, s])); + const activeSubmissions = submissions.filter((submission) => { + if (!submission.programEnrollment) return false; + + const { startsAt, endsAt } = getEffectiveBountyPeriod({ + programEnrollment: submission.programEnrollment, + bounty, + }); + + return isBountyStarted(startsAt) && !isBountyExpired(endsAt); + }); + + const newMetrics = + activeSubmissions.length > 0 + ? await getSocialMetricsUpdates({ + bounty, + submissions: activeSubmissions, + }) + : []; + + const submissionById = new Map(activeSubmissions.map((s) => [s.id, s])); @@ - if (!submission || !submission.programEnrollment) { + if (!submission) { continue; } - - const { endsAt } = getEffectiveBountyPeriod({ - programEnrollment: submission.programEnrollment, - bounty, - }); - - if (isBountyExpired(endsAt)) { - continue; - }Also applies to: 103-137
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/cron/bounties/sync-social-metrics/route.ts around lines 2 - 5, The sync-social-metrics cron currently only filters on endsAt after calling getSocialMetricsUpdates, which can still query providers for expired items and include drafts before startsAt. Update the flow in the route handler and related metric collection logic to first check the effective active window using getEffectiveBountyPeriod and isBountyExpired before invoking getSocialMetricsUpdates, and make sure only submissions whose current time falls within the effective period are passed through for syncing.apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts-66-70 (1)
66-70: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winReturn
not_foundfor ineligible bounties.
bad_requestconfirms the bounty exists to a partner who cannot access it. Keep hidden eligibility failures indistinguishable from missing bounties.Suggested fix
if (!isEligible) { throw new DubApiError({ - code: "bad_request", - message: "You are not eligible for this bounty.", + code: "not_found", + message: `Bounty ${bountyId} not found.`, }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts around lines 66 - 70, The ineligible-bounty branch in the route handler should not reveal existence via bad_request; update the `if (!isEligible)` path in `route.ts` to throw a `DubApiError` with `not_found` instead, keeping the response indistinguishable from a missing bounty while preserving the existing access-check flow in the same handler.apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts-61-64 (1)
61-64: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winApply the default group fallback before eligibility checks.
isPartnerEligibleForBountychecksprogramEnrollment.groupIddirectly. For partners inheritingprogram.defaultGroupId,groupIdcan benull, so default-group bounties are incorrectly hidden.Suggested fix
- const isEligible = isPartnerEligibleForBounty({ - programEnrollment, + const effectiveProgramEnrollment = { + ...programEnrollment, + groupId: programEnrollment.groupId ?? program.defaultGroupId, + }; + + const isEligible = isPartnerEligibleForBounty({ + programEnrollment: effectiveProgramEnrollment, bounty, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts around lines 61 - 64, Apply the default group fallback before calling isPartnerEligibleForBounty in the bounty route handler. The eligibility check currently uses programEnrollment.groupId directly, so when that value is null and the partner should inherit program.defaultGroupId, default-group bounties get filtered out. Update the programEnrollment object used in the route’s eligibility flow so groupId falls back to program.defaultGroupId before passing it into isPartnerEligibleForBounty.apps/web/scripts/migrations/backfill-group-joined-at.ts-6-12 (1)
6-12: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winThis backfill also stamps non-joined enrollments.
The filter only checks
groupId/groupJoinedAt, so any still-invitedrow getsgroupJoinedAt = createdAtas if the partner had already joined. That will make relative bounty windows start too early for historical invites; the backfill needs to restrict itself to actually joined/approved enrollments.Also applies to: 34-35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/scripts/migrations/backfill-group-joined-at.ts` around lines 6 - 12, The backfill query in the migration script is too broad and is stamping invited enrollments as joined. Update the `programEnrollment.findMany` filter in the backfill logic to include only actually joined/approved enrollments, using the existing enrollment status fields alongside `groupId` and `groupJoinedAt`, so that `groupJoinedAt` is only set for real joins. Also make the same restriction wherever the backfill is applied later in the script to keep the selection consistent.apps/web/prisma/schema/bounty.prisma-32-47 (1)
32-47: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAdd the Prisma migration for these schema changes.
The selected files add new
Bountycolumns plus theBountyPartnerTagtable, but there isn't a corresponding Prisma migration in this PR. Shipping the code without the DDL will break every path that reads or writes these fields.Also applies to: 82-91
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/prisma/schema/bounty.prisma` around lines 32 - 47, Add the missing Prisma migration for the new Bounty schema changes. The schema updates in Bounty and the new BountyPartnerTag model need matching DDL, so create a migration that adds the new columns/tables and any required indexes or constraints. Make sure the migration is generated from the Prisma models in bounty.prisma and includes the Bounty and BountyPartnerTag changes so the database stays in sync with the schema.apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts-60-67 (1)
60-67: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon’t let the pre-start window silently drop all submissions.
Line 63 still allows processing when the bounty starts in under 10 minutes, but
canPartnerSubmitBountynow rejects those same partners as “not started yet.” That path creates no submissions and does not enqueue a retry for the actual start time.🐛 Proposed direction
- if (diffMinutes >= 10) { + if (diffMinutes > 0) { return logAndRespond( `Bounty ${bountyId} not started yet, it will start at ${bounty.startsAt.toISOString()}`, ); }If this route intentionally pre-creates drafts shortly before start, use an eligibility helper that skips only the start-date gate for that pre-start window.
Also applies to: 171-177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/cron/bounties/create-draft-submissions/route.ts around lines 60 - 67, The pre-start handling in create-draft-submissions is inconsistent: the route still processes bounties within 10 minutes of start, but canPartnerSubmitBounty rejects those partners as not started yet, so no drafts are created and no retry is scheduled. Update the eligibility logic in the bounty draft flow (create-draft-submissions/route.ts and the shared canPartnerSubmitBounty path) so the pre-start window uses a helper that bypasses only the start-date gate, while preserving the rest of the partner eligibility checks.apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts-93-110 (1)
93-110: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winInclude inherited default-group partners in the enrollment query.
Filtering only
groupId in bountyGroupIdsexcludes enrollments withgroupId: nullthat inheritbounty.program.defaultGroupId. Those partners never reach the later eligibility check.🐛 Proposed fix
...(bountyGroupIds.length > 0 && { - groupId: { - in: bountyGroupIds, - }, + OR: [ + { + groupId: { + in: bountyGroupIds, + }, + }, + ...(bounty.program.defaultGroupId && + bountyGroupIds.includes(bounty.program.defaultGroupId) + ? [{ groupId: null }] + : []), + ], }),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/cron/bounties/create-draft-submissions/route.ts around lines 93 - 110, The enrollment query in the draft-submissions cron route is filtering only by the explicit bounty group IDs, which drops partners whose enrollment has a null groupId but still inherits the bounty program’s default group. Update the query-building logic in the route’s enrollments lookup to include these inherited default-group partners alongside the existing groupId filter, so they can pass into the later eligibility check. Use the surrounding enrollment query and the bounty program default-group handling in this route to locate the fix.apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts-78-83 (1)
78-83: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winNormalize default-group enrollments before checking bounty eligibility.
canPartnerSubmitBountyonly seesprogramEnrollment.groupId, so partners withgroupId: nulldo not match bounties assigned toprogram.defaultGroupIdeven though line 17 fetches it. Pass an effective group ID into the helper or normalize the enrollment before filtering.🐛 Proposed fix
const eligiblePartnerIds = programEnrollments - .filter((programEnrollment) => - canPartnerSubmitBounty({ - programEnrollment, + .filter((programEnrollment) => + canPartnerSubmitBounty({ + programEnrollment: { + ...programEnrollment, + groupId: programEnrollment.groupId ?? program.defaultGroupId, + }, bounty, }), )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts` around lines 78 - 83, Normalize program enrollments before the eligibility filter in trigger-draft-bounty-submissions.ts so default-group partners can match bounties assigned to program.defaultGroupId. Update the filtering path around eligiblePartnerIds and canPartnerSubmitBounty to use an effective group ID (falling back from programEnrollment.groupId to the program’s default group) instead of relying only on programEnrollment.groupId. Keep the helper’s call site aligned with that normalized value so null groupId enrollments are evaluated correctly.apps/web/lib/bounty/api/bounty-eligibility.ts-125-138 (1)
125-138: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftAccount for partners inheriting the default group.
Group eligibility only compares bounty groups to
programEnrollment.groupId. Enrollments withgroupId: nullinherit the program default group, so default-group bounties are treated as ineligible unless callers manually normalize the group first.🐛 Proposed direction
type PartnerBountyEligibilityParams = { + programDefaultGroupId?: string | null; programEnrollment: Pick< @@ export function isPartnerEligibleForBounty({ + programDefaultGroupId, programEnrollment, bounty, }: PartnerBountyEligibilityParams): boolean { @@ - const partnerGroupId = programEnrollment.groupId; + const partnerGroupId = programEnrollment.groupId ?? programDefaultGroupId;Then pass
program.defaultGroupIdfrom routes/workflows that use this helper.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/api/bounty-eligibility.ts` around lines 125 - 138, The group eligibility check currently only uses programEnrollment.groupId, so enrollments that inherit the program default group are being rejected for default-group bounties. Update the eligibility logic in the bounty eligibility helper so it falls back to the program’s default group when groupId is null, instead of comparing only partnerGroupId. Also update the routes/workflows that call this helper to pass program.defaultGroupId through, so the helper can resolve inherited group membership consistently.apps/web/lib/bounty/api/bounty-eligibility.ts-148-228 (1)
148-228: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRestore enrollment-status gating in the submission eligibility helper.
programEnrollment.statusis part of the helper input but is never checked. Since submission flows now delegate tothrowIfPartnerCannotSubmitBounty, pending/rejected enrollments can pass if group/tag/date checks match.🛡️ Proposed fix
export function canPartnerSubmitBounty({ programEnrollment, bounty, }: PartnerBountyEligibilityParams): boolean { + if (!["approved", "invited"].includes(programEnrollment.status)) { + console.log(`Partner enrollment is not active for bounty ${bounty.id}.`); + return false; + } + const isEligible = isPartnerEligibleForBounty({ programEnrollment, bounty, }); @@ export function throwIfPartnerCannotSubmitBounty({ programEnrollment, bounty, }: PartnerBountyEligibilityParams) { + if (!["approved", "invited"].includes(programEnrollment.status)) { + throw new DubApiError({ + code: "forbidden", + message: "You are not allowed to submit this bounty.", + }); + } + const isEligible = isPartnerEligibleForBounty({ programEnrollment, bounty, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/api/bounty-eligibility.ts` around lines 148 - 228, The submission eligibility helpers currently ignore program enrollment status even though PartnerBountyEligibilityParams includes programEnrollment.status. Update canPartnerSubmitBounty and throwIfPartnerCannotSubmitBounty to gate on the enrollment status before any bounty checks, so only approved/active enrollments can proceed. Use the existing helper inputs and symbols like canPartnerSubmitBounty, throwIfPartnerCannotSubmitBounty, and programEnrollment to add the missing status validation consistently in both paths.apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/invite/page.tsx-74-77 (1)
74-77: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winFetch bounties with the invited partner's tag context.
This page already has a concrete partner enrollment, but
getGroupBountiesevaluates eligibility with an empty tag set. Tag-scoped bounties that this invited partner is eligible for will never show up here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/invite/page.tsx around lines 74 - 77, The invited partner’s bounty lookup is missing their tag context, so tag-scoped eligible bounties are filtered out. Update the `invite/page.tsx` flow where `getGroupBounties` is called to pass the invited partner’s tags from the concrete enrollment/partner context instead of an empty tag set. Use the existing `program`, `group`, and partner enrollment data in this page to build the tag input for `getGroupBounties`, ensuring the invited partner’s eligibility is evaluated correctly.apps/web/lib/api/workflows/execute-move-group-workflow.ts-29-32 (1)
29-32: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRefresh
groupMoveDisabledAtfrom Prisma before applying the move.Line 63 is now checking the stale flag from the queued context, not the fresh enrollment row. If move rules were disabled after the workflow was queued, this handler can still move the partner anyway. Please select
groupMoveDisabledAtin the refresh query and use that value for the skip guard.Proposed fix
- const { programId, partnerId, groupId, groupMoveDisabledAt } = - programEnrollment; + const { programId, partnerId, groupId } = programEnrollment; @@ select: { groupId: true, + groupMoveDisabledAt: true, }, }); @@ - if (groupMoveDisabledAt) { + if (programEnrollmentRefreshed.groupMoveDisabledAt) { console.log( `Partner ${partnerId} has group move rules disabled. Skipping..`, );Also applies to: 42-63
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/api/workflows/execute-move-group-workflow.ts` around lines 29 - 32, Refresh the `groupMoveDisabledAt` value from Prisma before the move guard in `executeMoveGroupWorkflow`; the current check is using the stale value from `context.programEnrollment`. Update the refresh query in this workflow to select `groupMoveDisabledAt` along with the other enrollment fields, then use the freshly loaded enrollment row for the skip condition before moving the partner.apps/web/lib/api/workflows/execute-complete-bounty-workflow.ts-127-130 (1)
127-130: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winUse the effective bounty start for the net-new check.
These lines now skip the
performanceScope === "new"guard wheneverbounty.startsAtis null, but this PR also introduced the “when a partner joins” start mode. In that case, pre-existing customers can slip through as net-new. ComparecustomerFirstSaleAtagainst the effective start from the bounty-period helper, not the rawbounty.startsAt.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/api/workflows/execute-complete-bounty-workflow.ts` around lines 127 - 130, The net-new eligibility check in executeCompleteBountyWorkflow is using bounty.startsAt directly, which breaks the “when a partner joins” start mode. Update the performanceScope === "new" guard to compare customerFirstSaleAt against the effective start returned by the bounty-period helper (the same start used for the bounty period), and keep the existing null/ordering checks around that derived value.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/bounty-eligibility.tsx-97-128 (1)
97-128: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDon't snapshot the toggle state from the initial field value.
Line 97 only reads
defaultEnabledon the first render. In the edit flow, if the form is hydrated or reset with existinggroupIds/partnerTagIdsafter mount, this section can stay collapsed while the form still carries those restrictions, so the UI says “All … eligible” even though IDs are selected.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/bounty-eligibility.tsx around lines 97 - 128, The BountyEligibility component is only initializing local toggle state from defaultEnabled once, so it can drift from the actual form values after hydration or reset. Update BountyEligibility to derive or sync enabled from the current field values / props used in the edit flow (for example the groupIds and partnerTagIds state coming from the parent) instead of snapshotting only on first render, so the switch and collapsed content always reflect the real eligibility restrictions.
🟡 Minor comments (7)
apps/web/lib/api/tags/throw-if-invalid-partner-tag-ids.ts-28-31 (1)
28-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse the same validation error code as
throwIfInvalidGroupIds.Invalid
groupIdsand invalidpartnerTagIdsnow go through equivalent validation helpers, but this one returnsunprocessable_entitywhile the existing group path returnsbad_request. That makes the same bounty POST/PATCH validation failure surface with two different API codes depending on which selector was wrong.Suggested fix
if (invalidPartnerTagIds?.length) { throw new DubApiError({ - code: "unprocessable_entity", + code: "bad_request", message: `Invalid partner tag IDs detected: ${invalidPartnerTagIds.join(", ")}`, }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/api/tags/throw-if-invalid-partner-tag-ids.ts` around lines 28 - 31, The validation helper in throwIfInvalidPartnerTagIds uses a different API error code than throwIfInvalidGroupIds, so align the DubApiError code with the group-id path. Update the error thrown from throwIfInvalidPartnerTagIds to use the same bad_request code while keeping the existing invalid PartnerTagIds message and the same invalidPartnerTagIds check.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx-112-114 (1)
112-114: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDon't render relative end timing as an absolute date.
This fixes the start label for relative bounties, but the preview still appends
bounty.endsAtas a fixed date. In the create flow that value is synthesized fromendsAfterDays, so the modal can show"When a partner joins → Jul …"even though the real rule is "ends N days after joining."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx around lines 112 - 114, The bounty preview in confirm-create-bounty-modal still treats relative end timing as an absolute date. Update the display logic around the bounty.startsAt/bounty.endsAt rendering so that when the bounty is relative (derived from endsAfterDays), the modal shows relative wording instead of formatting bounty.endsAt with formatDate. Use the confirm-create-bounty-modal component and its existing bounty timing fields to branch between absolute and relative display.apps/web/tests/webhooks/index.test.ts-57-68 (1)
57-68: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHandle nullable bounty timestamps in the webhook test schema.
BountySchema.startsAtis now nullable, but this extension still forcesstartsAtthroughz.string(). Relative-start bounty webhooks withstartsAt: nullwill fail test parsing even though the payload is valid.endsAtshould be treated the same way for consistency.Suggested fix
const bountyWebhookEventSchemaExtended = BountySchema.extend({ - startsAt: z.string().transform((str) => new Date(str)), - endsAt: z.string().transform((str) => (str ? new Date(str) : null)), + startsAt: z + .string() + .nullable() + .transform((str) => (str ? new Date(str) : null)), + endsAt: z + .string() + .nullable() + .transform((str) => (str ? new Date(str) : null)), submissionsOpenAt: z .string() .nullable()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/tests/webhooks/index.test.ts` around lines 57 - 68, The webhook test schema is still treating bounty timestamps as required strings, so nullable payloads fail parsing. Update bountyWebhookEventSchemaExtended to match BountySchema by allowing startsAt to be nullable before transforming it, and apply the same nullable handling to endsAt for consistency. Keep the existing transform pattern in the schema used by the webhook tests so relative-start bounty events with null timestamps parse correctly.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx-157-157 (1)
157-157: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReplace
font-regularwithfont-normal.
font-regularis not a valid Tailwind utility in this repo, so these labels won't get the intended 400 weight. Based on learnings, this codebase requiresfont-normalfor normal font weight.Also applies to: 174-174, 206-206
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx at line 157, The label text in bounty-info.tsx is using an invalid Tailwind class for normal weight; update the affected className entries in the bounty info markup to use font-normal instead of font-regular. Make the same replacement in the related label elements referenced in the diff, including the sections around the bounty info rows so the intended 400 font weight is applied consistently.Source: Learnings
apps/web/lib/bounty/PARTNER_PROFILE_BOUNTY_ROUTES.md-293-301 (1)
293-301: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd language identifiers to these fenced blocks.
markdownlint is already flagging these three flow-diagram fences. Adding a language like
textwill keep the new doc lint-clean.Also applies to: 305-317, 321-328
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/PARTNER_PROFILE_BOUNTY_ROUTES.md` around lines 293 - 301, The fenced flow-diagram blocks in PARTNER_PROFILE_BOUNTY_ROUTES.md are missing language identifiers, causing markdownlint failures. Update the affected fenced blocks around the route flow diagrams to include a valid language tag such as text, and make the same change for the other flagged diagram fences in the document so the markdown remains lint-clean.Source: Linters/SAST tools
apps/web/lib/bounty/PARTNER_PROFILE_BOUNTY_ROUTES.md-54-60 (1)
54-60: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDocument that
partnerTagsare stripped too.This route description still says the response only strips
groups, but the partner-facing schema now omitspartnerTagsas well. The doc should match the actual response contract here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/bounty/PARTNER_PROFILE_BOUNTY_ROUTES.md` around lines 54 - 60, Update the bounty route behavior documentation to reflect the actual response contract: in the route description for the partner profile bounty flow, note that both `groups` and `partnerTags` are stripped from the response. Keep the rest of the listed behavior unchanged and make the wording match the partner-facing schema used by this route.apps/web/tests/bounties/index.test.ts-121-130 (1)
121-130: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRegister the cleanup before the assertions.
If the create call succeeds but one of the assertions throws,
onTestFinishednever gets registered and the bounty stays behind for later tests. Move the cleanup right after the POST and guard it onbounty?.id.Suggested change
const { status, data: bounty } = await http.post<Bounty>({ path: "/bounties", body: { ...submissionBounty, groupIds: [E2E_PARTNER_GROUP.id], startMode: "relative", endsAfterDays: 14, endsAt: null, }, }); + + if (bounty?.id) { + onTestFinished(async () => { + await h.deleteBounty(bounty.id); + }); + } expect(status).toEqual(200); expect(bounty).toMatchObject({ id: expect.any(String), startMode: "relative", endsAfterDays: 14, endsAt: null, }); - - onTestFinished(async () => { - await h.deleteBounty(bounty.id); - }); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/tests/bounties/index.test.ts` around lines 121 - 130, Register the cleanup immediately after the bounty is created in the test so it always runs even if later assertions fail. In the relevant test around the POST result, move the onTestFinished cleanup before the expect calls and guard the delete with bounty?.id so the bounty is removed whenever creation succeeds. Use the existing bounty and h.deleteBounty references to locate and update the test flow.
Bounties cleanup
|
Closing this PR in favor of #4187 |
Summary by CodeRabbit
Release Notes