-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Bounties updates #2786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bounties updates #2786
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughUpdates APIs and schemas for bounties and partner profiles, introduces optional workflow skipping for commission creation, adjusts UI components to new response shapes, replaces a hook import by deleting the old file, and adds integration tests plus test harness support. Includes conditional PATCH logic and partial update validation. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as Partner Bounty UI
participant API as Partner Programs API
participant DB as Database
Note over API,UI: Listing bounties (changed: single submission, partner stats)
User->>UI: Open program bounties
UI->>API: GET /partner-profile/programs/:programId/bounties
API->>DB: Query bounties + workflow + submissions + links
DB-->>API: Results
API-->>UI: [{ ..., submission, performanceCondition, partner{..., totalCommissions} }]
Note over API: Conditional PATCH update to submissionRequirements
UI->>API: PATCH /bounties/:id { ...maybe submissionRequirements }
API->>DB: Update bounty (only set submissionRequirements if provided)
DB-->>API: Updated bounty
API-->>UI: Updated bounty
sequenceDiagram
autonumber
participant WF as Award Bounty Workflow
participant CPC as createPartnerCommission
participant WB as Webhooks
participant AUD as Audit Log
Note over WF,CPC: New option: skipWorkflow=true
WF->>CPC: createPartnerCommission({ ..., skipWorkflow: true })
alt isClawback || skipWorkflow
CPC-->>WB: (no workflow trigger)
else normal commission with workflow
CPC->>WB: Trigger workflow
end
CPC->>AUD: Record commission event
CPC-->>WF: Commission created
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (3)
66-71: Validate endsAt against the effective startsAt (existing or updated)With partial updates,
startsAtmay be omitted. CompareendsAttostartsAt ?? bounty.startsAtafter fetching the bounty to avoid accepting invalid ranges.Apply:
- if (startsAt && endsAt && endsAt < startsAt) { - throw new DubApiError({ - message: "endsAt must be on or after startsAt.", - code: "bad_request", - }); - } + // ...fetch bounty first... + const effectiveStartsAt = startsAt ?? bounty.startsAt; + if (endsAt && effectiveStartsAt && endsAt < effectiveStartsAt) { + throw new DubApiError({ + message: "endsAt must be on or after startsAt.", + code: "bad_request", + }); + }
94-115: Make PATCH data fields conditional; don’t pass undefined to PrismaAfter making the schema partial, always include fields only when provided. Also only touch groups when
groupIdsis provided; current logic resets groups to empty on omittedgroupIds.Apply:
- const updatedBounty = await tx.bounty.update({ + const updatedBounty = await tx.bounty.update({ where: { - id: bounty.id, + id: bounty.id, + programId, // defense-in-depth }, data: { - name, - description, - startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4) - endsAt, - rewardAmount, + ...(name !== undefined && { name }), + ...(description !== undefined && { description }), + ...(startsAt !== undefined && { startsAt }), + ...(endsAt !== undefined && { endsAt }), + ...(rewardAmount !== undefined && { rewardAmount }), ...(bounty.type === "submission" && submissionRequirements !== undefined && { submissionRequirements: submissionRequirements ?? Prisma.JsonNull, }), - groups: { - deleteMany: {}, - create: groups.map((group) => ({ - groupId: group.id, - })), - }, + ...(groupIds !== undefined && { + groups: { + deleteMany: {}, + create: groups.map((group) => ({ groupId: group.id })), + }, + }), }, });Notes:
- Remove the
!onstartsAtand gate the assignment instead.- Keep
groupIdsuntouched when omitted.
184-201: Scope DELETE by programId and consider transactional checkFor multi-tenant safety, include
programIdin the deletewhere. Also, the submissions count check happens outside the transaction; a submission could arrive between the check and delete.Apply:
- await prisma.$transaction(async (tx) => { - const bounty = await tx.bounty.delete({ - where: { - id: bountyId, - }, - }); + await prisma.$transaction(async (tx) => { + // Re-check inside the transaction + const current = await tx.bounty.findUniqueOrThrow({ + where: { id: bountyId, programId }, + include: { _count: { select: { submissions: true } } }, + }); + if (current._count.submissions > 0) { + throw new DubApiError({ + message: + "Bounties with submissions cannot be deleted. You can archive them instead.", + code: "bad_request", + }); + } + const bounty = await tx.bounty.delete({ + where: { id: bountyId, programId }, + });apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
329-369: Fix “Add end date” toggle being hidden when hasEndDate is falseThe toggle lives inside a container that’s hidden when
!hasEndDate, making it impossible to enable. Show the toggle always; only hide/animate the date picker.Apply:
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - className={!hasEndDate ? "hidden" : ""} - style={{ display: !hasEndDate ? "none" : "block" }} - > - <div className="flex items-center gap-4"> + <div className="flex items-center gap-4"> <Switch fn={setHasEndDate} checked={hasEndDate} trackDimensions="w-8 h-4" thumbDimensions="w-3 h-3" thumbTranslate="translate-x-4" /> <div className="flex flex-col gap-1"> <h3 className="text-sm font-medium text-neutral-700"> Add end date </h3> </div> - </div> - - {hasEndDate && ( - <div className="mt-6 p-px"> - <Controller - control={control} - name="endsAt" - render={({ field }) => ( - <SmartDateTimePicker - value={field.value} - onChange={(date) => - field.onChange(date ?? undefined) - } - label="End date" - placeholder='E.g. "in 3 months"' - /> - )} - /> - </div> - )} - </AnimatedSizeContainer> + </div> + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + className={hasEndDate ? "" : "hidden"} + style={{ display: hasEndDate ? "block" : "none" }} + > + <div className="mt-6 p-px"> + <Controller + control={control} + name="endsAt" + render={({ field }) => ( + <SmartDateTimePicker + value={field.value} + onChange={(date) => + field.onChange(date ?? undefined) + } + label="End date" + placeholder='E.g. "in 3 months"' + /> + )} + /> + </div> + </AnimatedSizeContainer>
326-326: Remove stray debug text for startsAt error
{errors.startsAt && "test"}leaks to UI. Replace with a proper error message or remove.Apply:
- {errors.startsAt && "test"} + {errors.startsAt && ( + <p className="mt-1 text-xs text-red-600"> + {errors.startsAt.message ?? "Start date is required"} + </p> + )}
382-384: Align client-side validation with schema (min 1)Server schema enforces
rewardAmount >= 1. Client rules allow0, causing avoidable round-trips.Apply:
- rules={{ - required: true, - min: 0, - }} + rules={{ + required: true, + min: 1, + }}apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts (2)
64-69: Fetch only the latest submission (or enforce uniqueness).If uniqueness isn’t guaranteed, explicitly
take: 1with an order to make the selected submission deterministic and reduce payload.Apply:
submissions: { where: { partnerId: partner.id, }, + take: 1, + orderBy: { createdAt: "desc" }, },If
(bountyId, partnerId)is unique, keep this for payload minimization anyway.
20-37: Exclude archived bounties.Add
archivedAt: nullto thewhereclause so partners don’t see archived items.Apply:
const bounties = await prisma.bounty.findMany({ where: { programId: program.id, + archivedAt: null, startsAt: { lte: now, },
🧹 Nitpick comments (11)
apps/web/tests/utils/integration.ts (1)
101-108: Add consistency and optional workspaceId support to delete helper
- Consistency: Other delete helpers mix presence checks; either add the falsy-id guard to all or drop it here for uniformity.
- Multi-tenant: Several UI calls include
?workspaceId=.... If your test env doesn’t always infer workspace from token, consider accepting an optionalworkspaceIdand appending it to the path.apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
117-126: Support clearing performanceCondition (null) and avoid silent no-opCurrently the workflow updates only when a non-null
performanceConditionis provided. If clients sendnullto clear performance logic, consider handling it by deleting/clearingtriggerConditionsor the workflow.Example:
- if (updatedBounty.workflowId && performanceCondition) { + if (updatedBounty.workflowId && performanceCondition !== undefined) { + if (performanceCondition === null) { + await tx.workflow.update({ + where: { id: updatedBounty.workflowId }, + data: { triggerConditions: [] }, + }); + } else { await tx.workflow.update({ where: { id: updatedBounty.workflowId, }, data: { triggerConditions: [performanceCondition], }, }); + } }apps/web/lib/zod/schemas/partner-profile.ts (1)
137-148: Singularsubmissionfield aligns with new API; consider default for consistency.Setting
submissionto.nullable()is fine. To guarantee presence (alwaysnullwhen absent) and mirror how the route populates it, add a default.Apply:
- submission: BountySubmissionSchema.nullable(), + submission: BountySubmissionSchema.nullable().default(null),apps/web/lib/api/workflows/execute-award-bounty-action.ts (1)
80-88: Good call:skipWorkflow: trueprevents recursive/duplicate workflow triggers.This avoids a second
commissionEarnedworkflow when the award action itself creates a commission.Add a short comment to document intent:
const commission = await createPartnerCommission({ event: "custom", partnerId, programId: bounty.programId, amount: bounty.rewardAmount, quantity: 1, description: `Commission for successfully completed "${bounty.name}" bounty.`, + // Prevent re-triggering commissionEarned workflow from this commission skipWorkflow: true, });apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx (1)
21-23: Avoid duplicate click handlers; rely on the outer button.Current setup triggers the modal twice via event bubbling. Remove the inner
onClick.Apply:
- onClick={() => setShowClaimBountyModal(true)} + onClick={() => setShowClaimBountyModal(true)} ... - <div - className="group-hover:ring-border-subtle flex h-7 w-fit items-center rounded-lg bg-black px-2.5 text-sm text-white transition-all group-hover:ring-2" - onClick={() => setShowClaimBountyModal(true)} - > + <div + className="group-hover:ring-border-subtle flex h-7 w-fit items-center rounded-lg bg-black px-2.5 text-sm text-white transition-all group-hover:ring-2" + >Also applies to: 79-83
apps/web/lib/partners/create-partner-commission.ts (2)
39-40:skipWorkflowparam: sane default and signature.Defaulting to
falseminimizes risk for existing callers.Add JSDoc to clarify when to use it:
- export const createPartnerCommission = async ({ + /** + * Create a partner commission and propagate side-effects (webhooks, workflows, audit logs). + * @param skipWorkflow When true, suppresses commissionEarned workflow execution (webhook still fires). + * Use for system-initiated commissions that would otherwise re-trigger the same workflow. + */ + export const createPartnerCommission = async ({Also applies to: 55-56
313-317: Harden error logging.
errormay not be anError. Coerce safely.Apply:
- await log({ - message: `Error creating commission - ${error.message}`, + const msg = error instanceof Error ? error.message : JSON.stringify(error); + await log({ + message: `Error creating commission - ${msg}`, type: "errors", mention: true, });apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts (1)
72-85: Type-safety on parse errors.Zod parse will throw and result in a 500. Consider
.safeParseto return a typed 400 with details if shape mismatches occur (e.g., data drift).I can draft a small helper to standardize API schema validation responses.
apps/web/tests/bounties/index.test.ts (3)
32-55: Assert performance condition persisted.Strengthen the POST (performance) test to verify condition roundtrip.
expect(status).toEqual(200); expect(bounty).toMatchObject({ id: expect.any(String), ...performanceBounty, }); + expect(bounty).toMatchObject({ + performanceCondition: { + attribute: "totalLeads", + operator: "gte", + value: 100, + }, + });If the API returns this on a different property, adapt accordingly.
87-95: Also assert the created bounty appears in the list.Catches list filtering/serialization issues.
expect(status).toEqual(200); expect(bounties.length).toBeGreaterThanOrEqual(1); + expect(bounties.some((b: any) => b.id === bountyId)).toBe(true);
5-22: Reduce flakiness by making names unique per run.In case of uniqueness constraints, suffix names.
+const SUFFIX = Math.random().toString(36).slice(2, 8); const submissionBounty = { - name: "Submission Bounty", + name: `Submission Bounty ${SUFFIX}`, @@ const performanceBounty = { - name: "Performance Bounty", + name: `Performance Bounty ${SUFFIX}`,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts(1 hunks)apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-action-button.tsx(1 hunks)apps/web/lib/api/workflows/execute-award-bounty-action.ts(1 hunks)apps/web/lib/partners/create-partner-commission.ts(3 hunks)apps/web/lib/zod/schemas/bounties.ts(1 hunks)apps/web/lib/zod/schemas/partner-profile.ts(1 hunks)apps/web/tests/bounties/index.test.ts(1 hunks)apps/web/tests/utils/integration.ts(1 hunks)apps/web/ui/shared/use-api-mutation.ts(0 hunks)
💤 Files with no reviewable changes (1)
- apps/web/ui/shared/use-api-mutation.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.
Applied to files:
apps/web/lib/zod/schemas/bounties.tsapps/web/lib/zod/schemas/partner-profile.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.ts
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/bounties.tsapps/web/lib/zod/schemas/partner-profile.ts
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/lib/zod/schemas/bounties.tsapps/web/lib/zod/schemas/partner-profile.tsapps/web/tests/bounties/index.test.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsxapps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts
📚 Learning: 2025-08-25T17:39:38.965Z
Learnt from: devkiran
PR: dubinc/dub#2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx:45-56
Timestamp: 2025-08-25T17:39:38.965Z
Learning: In the bounty system, each partner can only submit to the same bounty once. This means totalSubmissions (pending + approved + rejected) equals the number of unique partners who have submitted, making UI text like "X of Y partners completed" accurate when using totalSubmissions.
Applied to files:
apps/web/lib/zod/schemas/partner-profile.tsapps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
🔇 Additional comments (9)
apps/web/lib/zod/schemas/bounties.ts (1)
46-50: Approve: partial update semantics for PATCHMaking
updateBountySchemaa partial ofcreateBountySchema(while omittingtype) is the right move for PATCH. Ensure route logic conditionally applies fields to avoid sendingundefinedto Prisma (see route.ts comments).Would you like me to scan the API handlers for any remaining unconditional assignments that might send
undefinedto Prisma?apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (2)
105-108: Approve: only scale performance value for currency attributesPrevents unintended scaling for non-currency metrics. Looks correct.
3-3: All imports successfully migratedSearch across
.ts/.tsxfiles found no remaining references to@/ui/shared/use-api-mutation.apps/web/lib/zod/schemas/partner-profile.ts (1)
96-100: Email masking regex: verify it handles non-dot local parts.Pattern
(?<=^.).+(?=.@)assumes a dot before@. Many emails won't match, leaving the address unmasked beyond the first char. If unintentional, consider masking up to@instead.I can propose a hardened regex if desired.
apps/web/lib/api/workflows/execute-award-bounty-action.ts (1)
60-66: No action needed—unique constraint on (bountyId, partnerId) already defined at packages/prisma/schema/bounty.prisma:80.Likely an incorrect or invalid review comment.
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx (2)
10-15: Schema alignment: usingbounty.submissionis correct.Matches the new singular field from the API/schema.
53-55: BountySubmissionSchema date fields: EnsurecreatedAtandreviewedAtinapps/web/lib/zod/schemas/bounties.tsare declared asz.union([z.string(), z.date()])(or via.or(z.date())) to matchformatDate’s acceptedDate | stringinput.apps/web/lib/partners/create-partner-commission.ts (1)
256-258: Workflow gating logic looks correct.
!isClawback && !skipWorkflowprevents clawbacks and opted-out events from triggering workflows.Also applies to: 300-306
apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts (1)
74-82: Singularsubmissionand simplifiedperformanceCondition: LGTM.Matches the updated Zod schema and reduces client-side branching.
| if (!response.ok) { | ||
| const { error } = await response.json(); | ||
| throw new Error(error.message); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden error parsing on failed DELETE
If the server doesn’t return { error: { message } } or JSON at all, this will throw a secondary error. Fall back to statusText/generic copy.
Apply:
- if (!response.ok) {
- const { error } = await response.json();
- throw new Error(error.message);
- }
+ if (!response.ok) {
+ let message = "Failed to delete bounty";
+ try {
+ const payload = await response.json();
+ message = payload?.error?.message ?? message;
+ } catch {
+ message = response.statusText || message;
+ }
+ throw new Error(message);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!response.ok) { | |
| const { error } = await response.json(); | |
| throw new Error(error.message); | |
| } | |
| if (!response.ok) { | |
| let message = "Failed to delete bounty"; | |
| try { | |
| const payload = await response.json(); | |
| message = payload?.error?.message ?? message; | |
| } catch { | |
| message = response.statusText || message; | |
| } | |
| throw new Error(message); | |
| } |
🤖 Prompt for AI Agents
In
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-action-button.tsx
around lines 47 to 50, the error handling assumes response.json() returns {
error: { message } } and will crash if the body isn't JSON or has a different
shape; change it to safely attempt to parse JSON inside a try/catch, extract an
error message with a fallback chain (e.g. parsed?.error?.message ||
parsed?.message), and if parsing fails or no message exists, use
response.statusText or a generic message before throwing new Error with that
fallback string.
| loading: "Deleting bounty...", | ||
| success: "Bounty deleted successfully!", | ||
| error: (err) => err, | ||
| error: (error) => error, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return a string to toast instead of an Error object
toast.promise expects a string/ReactNode. Returning the Error object will render [object Object].
Apply:
- success: "Bounty deleted successfully!",
- error: (error) => error,
+ success: "Bounty deleted successfully!",
+ error: (error: Error) => error.message,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| loading: "Deleting bounty...", | |
| success: "Bounty deleted successfully!", | |
| error: (err) => err, | |
| error: (error) => error, | |
| }, | |
| loading: "Deleting bounty...", | |
| success: "Bounty deleted successfully!", | |
| error: (error: Error) => error.message, | |
| }, |
🤖 Prompt for AI Agents
In
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-action-button.tsx
around lines 55 to 58, the toast.promise error handler currently returns the
Error object which renders as "[object Object]"; change the handler to return a
string by extracting the message (e.g., error?.message) or using String(error)
as a safe fallback so toast receives a string/ReactNode; ensure it handles
null/undefined errors gracefully (e.g., return error?.message || String(error)
|| "An error occurred").
| @@ -0,0 +1,130 @@ | |||
| import { Bounty } from "@dub/prisma/client"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t type HTTP JSON responses as Prisma Bounty.
Prisma Date fields are Date objects, but API returns ISO strings. Using Bounty here is unsound. Drop the generics (or use an API DTO).
-import { Bounty } from "@dub/prisma/client";
...
- const { status, data: bounty } = await http.post<Bounty>({
+ const { status, data: bounty } = await http.post({
...
- const { status, data: bounty } = await http.post<Bounty>({
+ const { status, data: bounty } = await http.post({
...
- const { status, data: bounty } = await http.get<Bounty>({
+ const { status, data: bounty } = await http.get({
...
- const { status, data: bounties } = await http.get<Bounty[]>({
+ const { status, data: bounties } = await http.get({
...
- const { status, data: bounty } = await http.patch<Bounty>({
+ const { status, data: bounty } = await http.patch({Also applies to: 33-34, 58-59, 76-77, 88-89, 104-105
🤖 Prompt for AI Agents
In apps/web/tests/bounties/index.test.ts around lines 1 and also at 33-34,
58-59, 76-77, 88-89, 104-105, the test code types HTTP JSON responses as the
Prisma Bounty type which has Date fields as Date objects while the API returns
ISO date strings; change these response typings to either drop the generic
entirely (use plain any/unknown) or introduce an API DTO/interface (e.g.,
ApiBounty with date fields as string) and use that in the tests, ensure mocked
response objects contain ISO string dates (or serialize Date to toISOString())
and adjust assertions to compare strings or parse to Date before comparing as
needed.
| rewardAmount: 1000, | ||
| }; | ||
|
|
||
| const BOUNTY_GROUP_ID = "grp_1K3TDVB6NS9PRARMTTXZSPQ2D"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Avoid hard-coded group ID; resolve dynamically (or via env).
A fixed grp_* risks flakes across envs. Resolve at runtime (prefer fetching/creating a test group) or allow override via env.
-const BOUNTY_GROUP_ID = "grp_1K3TDVB6NS9PRARMTTXZSPQ2D";
+let GROUP_ID = process.env.DUB_TEST_GROUP_ID ?? "";
...
- beforeAll(async () => {
- ({ http } = await h.init());
- });
+ beforeAll(async () => {
+ ({ http } = await h.init());
+ if (!GROUP_ID) {
+ const { data: groups } = await http.get<Array<{ id: string }>>({
+ path: "/groups",
+ });
+ GROUP_ID = groups?.[0]?.id!;
+ }
+ });
...
- groupIds: [BOUNTY_GROUP_ID],
+ groupIds: [GROUP_ID],
...
- groupIds: [BOUNTY_GROUP_ID],
+ groupIds: [GROUP_ID],If a “create group” helper exists in IntegrationHarness, prefer creating an isolated group for these tests.
Also applies to: 27-37, 36-43, 61-63
🏁 Script executed:
#!/bin/bash
set -e
# Search for IntegrationHarness class definition
rg -n "class IntegrationHarness" -C3 || true
# Search for any createGroup helper
rg -n "createGroup" -C3 || true
# Search for HTTP POST to /groups endpoint
rg -n "http\\.post.*\\/groups" -C3 || true
# Search for any reference to fetching groups
rg -n "\\/groups" -C3 || trueLength of output: 8350
🏁 Script executed:
#!/bin/bash
set -e
# Display first 200 lines of IntegrationHarness to check for group creation helpers
sed -n '1,200p' apps/web/tests/utils/integration.tsLength of output: 2267
🏁 Script executed:
#!/bin/bash
set -e
# Show the test file to inspect BOUNTY_GROUP_ID usage and context
sed -n '1,200p' apps/web/tests/bounties/index.test.tsLength of output: 3324
🏁 Script executed:
#!/bin/bash
set -e
# Inspect integrationTestEnv to see if default group ID is provided
rg -n "integrationTestEnv" -C3 apps/web/tests/utils/env.tsLength of output: 374
🏁 Script executed:
#!/bin/bash
set -e
sed -n '1,200p' 'apps/web/app/(ee)/api/groups/route.ts'Length of output: 3539
Decouple tests from hard-coded group ID
In apps/web/tests/bounties/index.test.ts at line 24, replace the fixed BOUNTY_GROUP_ID with a mutable GROUP_ID sourced from process.env.DUB_TEST_GROUP_ID (added to integrationTestEnv and CI) or fetched at runtime via http.get("/groups") (or created via POST), and update all groupIds: [BOUNTY_GROUP_ID] to use groupIds: [GROUP_ID].
🤖 Prompt for AI Agents
In apps/web/tests/bounties/index.test.ts around line 24, replace the hard-coded
BOUNTY_GROUP_ID constant with a mutable GROUP_ID obtained from
process.env.DUB_TEST_GROUP_ID (ensure integrationTestEnv/CI sets this) or, if
absent, fetch at runtime using an API call to GET /groups (or create a group
with POST and use its id); then update every occurrence of groupIds:
[BOUNTY_GROUP_ID] to use groupIds: [GROUP_ID] so tests are decoupled from a
fixed group id and work in CI and local runs.
| describe.sequential.skip("/bounties/**", async () => { | ||
| const h = new IntegrationHarness(); | ||
| const { http } = await h.init(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove async describe; move async setup to beforeAll and gate by env (not global skip).
Vitest doesn't await async describe callbacks. This will break once unskipped. Use beforeAll for async init and an env toggle to run in CI selectively.
- import { describe, expect, onTestFinished, test } from "vitest";
+ import { describe, expect, onTestFinished, test, beforeAll } from "vitest";
...
-describe.sequential.skip("/bounties/**", async () => {
- const h = new IntegrationHarness();
- const { http } = await h.init();
+const d = process.env.RUN_BOUNTIES_TESTS ? describe : describe.skip;
+d.sequential("/bounties/**", () => {
+ const h = new IntegrationHarness();
+ let http: any;
+ beforeAll(async () => {
+ ({ http } = await h.init());
+ });Also applies to: 2-2
🤖 Prompt for AI Agents
In apps/web/tests/bounties/index.test.ts around lines 26 to 29, the describe
callback is declared async and relies on awaiting setup inside it (and is
globally skipped); Vitest does not await async describe callbacks — remove the
async from describe, move the async initialization (new IntegrationHarness() and
await h.init()) into a beforeAll hook, and gate the suite execution with an
environment check (e.g., skip or run the suite based on
process.env.RUN_INTEGRATION or CI flag) instead of using describe.skip globally
so the setup is awaited correctly when the suite runs.
| test("POST /bounties - submission based", async () => { | ||
| const { status, data: bounty } = await http.post<Bounty>({ | ||
| path: "/bounties", | ||
| body: { | ||
| ...submissionBounty, | ||
| groupIds: [BOUNTY_GROUP_ID], | ||
| }, | ||
| }); | ||
|
|
||
| expect(status).toEqual(200); | ||
| expect(bounty).toMatchObject({ | ||
| id: expect.any(String), | ||
| ...submissionBounty, | ||
| }); | ||
|
|
||
| bountyId = bounty.id; | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Ensure cleanup for submission bounty on failures too.
If a later test fails before DELETE, the record lingers. Add onTestFinished cleanup.
bountyId = bounty.id;
+
+ onTestFinished(async () => {
+ if (bountyId) await h.deleteBounty(bountyId);
+ });Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/tests/bounties/index.test.ts around lines 57 to 74, the test creates
a submission bounty but only deletes it in a later DELETE test, so if subsequent
tests fail the bounty remains; register a cleanup handler (e.g., add the created
bountyId to the test-suite's onTestFinished/afterEach cleanup list or call a
teardown function immediately after creation) that attempts to DELETE the bounty
by id regardless of test outcome; ensure the handler checks for a non-empty
bountyId and calls the same http.delete endpoint used by the suite, and remove
or skip duplicate deletion if the later test already removed it.
| test("PATCH /bounties/{bountyId}", async () => { | ||
| const toUpdate = { | ||
| name: "Submission Bounty Updated", | ||
| endsAt: new Date().toISOString(), | ||
| rewardAmount: 2000, | ||
| submissionRequirements: ["image"], | ||
| }; | ||
|
|
||
| const { status, data: bounty } = await http.patch<Bounty>({ | ||
| path: `/bounties/${bountyId}`, | ||
| body: { | ||
| ...toUpdate, | ||
| type: "performance", // should skip the type update | ||
| }, | ||
| }); | ||
|
|
||
| expect(status).toEqual(200); | ||
| expect(bounty).toMatchObject({ | ||
| id: expect.any(String), | ||
| ...submissionBounty, | ||
| ...toUpdate, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add a PATCH test that omits submissionRequirements to ensure it’s preserved.
This directly guards the regression fixed in this PR (omitting should not nullify for submissions).
test("PATCH /bounties/{bountyId}", async () => {
@@
});
+
+ test("PATCH /bounties/{bountyId} - omit submissionRequirements retains existing", async () => {
+ const { status, data: bounty } = await http.patch({
+ path: `/bounties/${bountyId}`,
+ body: {
+ rewardAmount: 2500, // omit submissionRequirements
+ },
+ });
+ expect(status).toEqual(200);
+ expect(bounty).toMatchObject({
+ id: expect.any(String),
+ submissionRequirements: ["image"], // unchanged from prior update
+ rewardAmount: 2500,
+ type: "submission",
+ });
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test("PATCH /bounties/{bountyId}", async () => { | |
| const toUpdate = { | |
| name: "Submission Bounty Updated", | |
| endsAt: new Date().toISOString(), | |
| rewardAmount: 2000, | |
| submissionRequirements: ["image"], | |
| }; | |
| const { status, data: bounty } = await http.patch<Bounty>({ | |
| path: `/bounties/${bountyId}`, | |
| body: { | |
| ...toUpdate, | |
| type: "performance", // should skip the type update | |
| }, | |
| }); | |
| expect(status).toEqual(200); | |
| expect(bounty).toMatchObject({ | |
| id: expect.any(String), | |
| ...submissionBounty, | |
| ...toUpdate, | |
| }); | |
| }); | |
| test("PATCH /bounties/{bountyId}", async () => { | |
| const toUpdate = { | |
| name: "Submission Bounty Updated", | |
| endsAt: new Date().toISOString(), | |
| rewardAmount: 2000, | |
| submissionRequirements: ["image"], | |
| }; | |
| const { status, data: bounty } = await http.patch<Bounty>({ | |
| path: `/bounties/${bountyId}`, | |
| body: { | |
| ...toUpdate, | |
| type: "performance", // should skip the type update | |
| }, | |
| }); | |
| expect(status).toEqual(200); | |
| expect(bounty).toMatchObject({ | |
| id: expect.any(String), | |
| ...submissionBounty, | |
| ...toUpdate, | |
| }); | |
| }); | |
| test("PATCH /bounties/{bountyId} - omit submissionRequirements retains existing", async () => { | |
| const { status, data: bounty } = await http.patch<Bounty>({ | |
| path: `/bounties/${bountyId}`, | |
| body: { | |
| rewardAmount: 2500, // omit submissionRequirements | |
| }, | |
| }); | |
| expect(status).toEqual(200); | |
| expect(bounty).toMatchObject({ | |
| id: expect.any(String), | |
| submissionRequirements: ["image"], // unchanged from prior update | |
| rewardAmount: 2500, | |
| type: "submission", | |
| }); | |
| }); |
🤖 Prompt for AI Agents
In apps/web/tests/bounties/index.test.ts around lines 96 to 118, add a test that
PATCHes /bounties/{bountyId} without including submissionRequirements to ensure
the field is preserved (guarding the regression where omitting it would nullify
submissionRequirements for submission-type bounties); send a PATCH body
containing only updatable fields (e.g., name, endsAt, rewardAmount, and a type
that should be ignored) but omit submissionRequirements, then assert status is
200 and the returned bounty still contains the original submissionRequirements
from submissionBounty (merge expectations accordingly) and other updated fields.
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores