Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Aug 29, 2025

Summary by CodeRabbit

  • New Features

    • Partner bounty cards now display the latest submission when available.
    • Partner data in programs includes total commissions for clearer earnings insight.
  • Bug Fixes

    • Updating bounties no longer clears submission requirements when not provided.
    • Corrected scaling for performance-based conditions on currency attributes.
    • Delete failures now surface precise server error messages.
  • Tests

    • Added integration tests covering bounty creation, retrieval, update, and deletion.
  • Chores

    • Streamlined bounty update validation to support partial updates.
    • Internal data shape aligned to expose a single latest submission.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Updates 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

Cohort / File(s) Summary
Bounty API updates
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
PATCH now updates submissionRequirements only when bounty.type is "submission" and a value is provided; avoids resetting to JSON NULL when omitted.
Partner programs bounty listing API
apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts
GET response now returns a single submission (first or null), simplifies trigger condition extraction, and expands partner payload with aggregated stats and totalCommissions.
Partner UI card alignment
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
Consumes bounty.submission instead of bounty.submissions?.[0]; updates dependent rendering and modal hook usage.
Bounty add/edit sheet + hook import
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx, apps/web/ui/shared/use-api-mutation.ts
Switches useApiMutation import to @/lib/swr/use-api-mutation; removes old hook file. Adjusts performanceCondition value scaling only for currency attributes; removes onError toast.
Workflow: award bounty action
apps/web/lib/api/workflows/execute-award-bounty-action.ts, apps/web/lib/partners/create-partner-commission.ts
Adds skipWorkflow?: boolean (default false) to createPartnerCommission; call site passes skipWorkflow: true during award action; workflow triggering now gated by !isClawback && !skipWorkflow.
Zod schemas
apps/web/lib/zod/schemas/bounties.ts, apps/web/lib/zod/schemas/partner-profile.ts
updateBountySchema made .partial() (all updatable fields optional). PartnerBountySchema switches from submissions array to single submission (nullable).
Integration tests and harness
apps/web/tests/bounties/index.test.ts, apps/web/tests/utils/integration.ts
Adds skipped end-to-end bounty CRUD tests and harness method deleteBounty(id) to support DELETE endpoint.

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
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Manual commissions #2517 — Also changes create-partner-commission signature and workflow handling, likely overlapping with the new skipWorkflow logic.

Suggested reviewers

  • steven-tey

Poem

I nudge the burrow’s build with care,
One hop skips workflows here and there.
A single carrot—submission—shown,
Conditions trimmed, now clearly known.
Tests asleep, but soon they’ll run—
Bounties bloom beneath the sun.
Thump-thump: shipped—another one! 🥕✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bounties-updates

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link
Contributor

vercel bot commented Aug 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Aug 29, 2025 3:51pm

@devkiran
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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, startsAt may be omitted. Compare endsAt to startsAt ?? bounty.startsAt after 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 Prisma

After making the schema partial, always include fields only when provided. Also only touch groups when groupIds is provided; current logic resets groups to empty on omitted groupIds.

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 ! on startsAt and gate the assignment instead.
  • Keep groupIds untouched when omitted.

184-201: Scope DELETE by programId and consider transactional check

For multi-tenant safety, include programId in the delete where. 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 false

The 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 allow 0, 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: 1 with 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: null to the where clause 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 optional workspaceId and 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-op

Currently the workflow updates only when a non-null performanceCondition is provided. If clients send null to clear performance logic, consider handling it by deleting/clearing triggerConditions or 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: Singular submission field aligns with new API; consider default for consistency.

Setting submission to .nullable() is fine. To guarantee presence (always null when 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: true prevents recursive/duplicate workflow triggers.

This avoids a second commissionEarned workflow 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: skipWorkflow param: sane default and signature.

Defaulting to false minimizes 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.

error may not be an Error. 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 .safeParse to 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.

📥 Commits

Reviewing files that changed from the base of the PR and between feaaeb0 and 1f11afd.

📒 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.ts
  • apps/web/lib/zod/schemas/partner-profile.ts
  • apps/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.ts
  • apps/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.ts
  • apps/web/lib/zod/schemas/partner-profile.ts
  • apps/web/tests/bounties/index.test.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/bounties/partner-bounty-card.tsx
  • apps/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.ts
  • apps/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 PATCH

Making updateBountySchema a partial of createBountySchema (while omitting type) is the right move for PATCH. Ensure route logic conditionally applies fields to avoid sending undefined to Prisma (see route.ts comments).

Would you like me to scan the API handlers for any remaining unconditional assignments that might send undefined to 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 attributes

Prevents unintended scaling for non-currency metrics. Looks correct.


3-3: All imports successfully migrated

Search across .ts/.tsx files 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: using bounty.submission is correct.

Matches the new singular field from the API/schema.


53-55: BountySubmissionSchema date fields: Ensure createdAt and reviewedAt in apps/web/lib/zod/schemas/bounties.ts are declared as z.union([z.string(), z.date()]) (or via .or(z.date())) to match formatDate’s accepted Date | string input.

apps/web/lib/partners/create-partner-commission.ts (1)

256-258: Workflow gating logic looks correct.

!isClawback && !skipWorkflow prevents 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: Singular submission and simplified performanceCondition: LGTM.

Matches the updated Zod schema and reduces client-side branching.

Comment on lines +47 to +50
if (!response.ok) {
const { error } = await response.json();
throw new Error(error.message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 55 to 58
loading: "Deleting bounty...",
success: "Bounty deleted successfully!",
error: (err) => err,
error: (error) => error,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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";
Copy link
Contributor

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 || true

Length 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.ts

Length 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.ts

Length 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.ts

Length 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.

Comment on lines 26 to 29
describe.sequential.skip("/bounties/**", async () => {
const h = new IntegrationHarness();
const { http } = await h.init();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Comment on lines +57 to +74
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;
});

Copy link
Contributor

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.

Comment on lines +96 to +118
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,
});
});
Copy link
Contributor

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants