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

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Sep 22, 2025

Summary by CodeRabbit

  • New Features

    • Configurable submission window for bounties with UI toggles, optional start date, numeric window control, and submissionsOpenAt tracking.
  • Behavior

    • Submissions blocked until configured open time; submit actions show tooltip with remaining wait time and UTC open date.
  • Validation

    • Centralized server-side validation enforcing start/end/submission-window rules, reward requirements, and performance scope with descriptive errors.
  • API

    • Bounty create/update accept and persist submissionsOpenAt; detail endpoints return it.
  • Database

    • Bounty records include submissionsOpenAt plus related indexes/relations.
  • Tests

    • Updated tests to cover submissionsOpenAt in create/update flows.
  • Documentation

    • Webhook sample events for bounty created/updated include submissionsOpenAt.

@vercel
Copy link
Contributor

vercel bot commented Sep 22, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 22, 2025 5:50pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

Walkthrough

Adds a nullable submissionsOpenAt timestamp across DB, schemas, APIs, UI, webhook samples, and tests; centralizes bounty validation into validateBounty; enforces submissionsOpenAt gating for partner submissions and wires the field through create/update/fetch flows.

Changes

Cohort / File(s) Summary
Validation & API routes
apps/web/lib/api/bounties/validate-bounty.ts, apps/web/app/(ee)/api/bounties/route.ts, apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
New exported validateBounty; replaces inline date/reward checks with centralized validation; startsAt defaulting behavior; submissionsOpenAt parsed, validated, and included in create/update payloads.
Database schema & relations
packages/prisma/schema/bounty.prisma
Add Bounty.submissionsOpenAt DateTime?; add/adjust relations and indexes on Bounty and BountySubmission, plus composite unique constraint and indexes.
Zod schemas & data fetch
apps/web/lib/zod/schemas/bounties.ts, apps/web/lib/api/bounties/get-bounty-with-details.ts
Make startsAt nullish in create schema; add submissionsOpenAt to create schema and BountySchema; select/return submissionsOpenAt in fetch.
Create/Edit UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
Add submission-window UI state and controls (hasStartDate, hasEndDate, submissionWindow, submissionsOpenAt), auto-calc submissionsOpenAt, expanded validation/error logic, NumberStepper and formatDate usage, and form wiring for submissionsOpenAt.
Claim modal / partner UI
apps/web/ui/partners/bounties/claim-bounty-modal.tsx
Compute hasSubmissionsOpen from submissionsOpenAt; disable Submit with tooltip showing open date when not open; wire server error rendering.
Submission action
apps/web/lib/actions/partners/create-bounty-submission.ts
New guard: block submissions before submissionsOpenAt, compute human-readable wait time via formatDistanceToNow, and throw descriptive error if not open.
Webhook samples
apps/web/lib/webhook/sample-events/bounty-created.json, apps/web/lib/webhook/sample-events/bounty-updated.json
Add submissionsOpenAt: null to sample event payloads.
Tests
apps/web/tests/bounties/index.test.ts
Add submissionsOpenAt to fixtures and update POST/PATCH tests to include/assert startsAt, endsAt, and submissionsOpenAt.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant UI
  participant API as "API Route (Create/PATCH)"
  participant Validator as validateBounty
  participant DB

  Client->>UI: Fill bounty form (type, startsAt, endsAt, submissionsOpenAt, rewards)
  UI->>API: Submit create/update payload
  API->>Validator: validateBounty(payload)
  alt Validation passes
    Validator-->>API: ok
    API->>DB: Create/Update Bounty (includes submissionsOpenAt)
    DB-->>API: success
    API-->>UI: 200 OK + bounty
  else Validation fails
    Validator-->>API: error (bad_request)
    API-->>UI: 400 error
  end
Loading
sequenceDiagram
  autonumber
  actor Partner
  participant UI as "Claim Modal"
  participant Action as create-bounty-submission
  participant DB

  Partner->>UI: Attempt to submit
  UI->>Action: POST submission
  Action->>DB: Load bounty (includes submissionsOpenAt)
  alt submissionsOpenAt in future
    Action-->>UI: Error "Submissions not open yet" (+ wait time)
    UI-->>Partner: Disabled + tooltip with open time
  else Submissions open
    Action->>DB: Validate + create submission
    DB-->>Action: success
    Action-->>UI: success
    UI-->>Partner: Confirmation
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • TWilson023

Poem

🥕 I hop with data in my paw,
I watch the window, keep the law.
If submissions sleep till their date,
I'll guard the gate and kindly wait.
Hooray — the bounties open straight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Bounty submission timing" is concise and directly reflects the primary change in this PR—introducing and enforcing a submissionsOpenAt-based submission timing across API, UI, validation, and database schema—so a reviewer scanning the history will understand the main intent.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bounty-submission-timing

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

620-640: Disable Submit until submissions open; fallback error string.

  • The button isn’t disabled when submissions aren’t open, so disabledTooltip never shows. Add !hasSubmissionsOpen to disabled.
  • Provide a fallback when result.serverError is undefined to avoid “Error: undefined”.
-                    <Button
+                    <Button
                       variant="primary"
                       text="Submit"
                       className="h-9 rounded-lg px-3"
                       type="submit"
                       name="submit" // for submitter.name detection above
                       loading={isDraft === false}
-                      disabled={fileUploading || isDraft === true}
+                      disabled={fileUploading || isDraft === true || !hasSubmissionsOpen}
                       disabledTooltip={
                         !hasSubmissionsOpen
                           ? `Submissions are not open yet. They will open in ${formatDistanceToNow(bounty.submissionsOpenAt!, { addSuffix: true })} (on ${formatDate(
                               bounty.submissionsOpenAt!,
                               {
                                 month: "short",
                                 day: "numeric",
                                 year: "numeric",
                                 timeZone: "UTC",
                               },
                             )}).`
                           : undefined
                       }
                     />
-      if (!result?.data?.success) {
-        throw new Error(result?.serverError);
+      if (!result?.data?.success) {
+        throw new Error(result?.serverError || "Failed to create submission.");
       }
🧹 Nitpick comments (12)
apps/web/lib/webhook/sample-events/bounty-created.json (1)

8-8: Webhook sample: clarify semantics for null submissionsOpenAt.

Looks good to include. Consider a short comment in docs/samples noting that null means “submissions are open immediately and drafts can be submitted any time,” to avoid integrator confusion.

packages/prisma/schema/bounty.prisma (2)

35-35: Consider indexing submissionsOpenAt for query performance.

If you’ll filter bounties by “submissions currently open” (e.g., submissionsOpenAt <= now()), add an index to support that.

 model Bounty {
@@
   submissionsOpenAt      DateTime?
@@
-  @@index(programId)
+  @@index(programId)
+  @@index([submissionsOpenAt])
 }

30-30: onDelete: Cascade on workflow may be risky.

workflowId String? @unique with workflow @relation(..., onDelete: Cascade) means deleting a workflow deletes the bounty. If workflows are auxiliary, consider onDelete: SetNull to avoid accidental bounty deletion.

-  workflow    Workflow?          @relation(fields: [workflowId], references: [id], onDelete: Cascade)
+  workflow    Workflow?          @relation(fields: [workflowId], references: [id], onDelete: SetNull)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (5)

272-275: End date check: base comparison on explicit start when present.

Keep the current fallback to “now” when no start date is set, but use startsAt when provided to avoid surprises.

-    if (endsAt && endsAt <= effectiveStartDate) {
-      return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`;
+    if (endsAt && startsAt && endsAt <= startsAt) {
+      return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`;
+    }
+    if (endsAt && !startsAt && endsAt <= new Date()) {
+      return "Please choose an end date that is in the future.";
     }

212-217: Ensure submissionWindow has a sensible default when toggled on.

When enabling the submission window, submissionWindow stays null, yet the helper text renders null. Initialize to the min (2) on toggle.

-  useEffect(() => {
-    if (!hasSubmissionWindow) {
+  useEffect(() => {
+    if (!hasSubmissionWindow) {
       setValue("submissionsOpenAt", null);
       setSubmissionWindow(null);
-    }
+    } else if (submissionWindow == null) {
+      setSubmissionWindow(2);
+    }
   }, [hasSubmissionWindow, setValue]);

565-579: Render-friendly copy when submissionWindow is null.

The descriptive text shows “null days” initially. Use a fallback value.

-                              <p className="mt-2 text-xs text-neutral-500">
-                                Submissions open {submissionWindow} days before
+                              <p className="mt-2 text-xs text-neutral-500">
+                                Submissions open {(submissionWindow ?? 2)} days before
                                 the end date. Drafts can be saved until then.
                               </p>

228-233: Remove debug logs.

console.log noise will leak to users and logs.

-      console.log({ startsAt, endsAt, submissionWindow });
+      // no-op
-  console.log({ submissionWindow, submissionsOpenAt });
+  // no-op

Also applies to: 393-394


493-495: Leftover test string.

{errors.startsAt && "test"} should be removed or replaced with a real error message.

-                          {errors.startsAt && "test"}
+                          {!!errors.startsAt && (
+                            <p className="mt-1 text-xs text-red-600">
+                              {errors.startsAt.message ?? "Invalid start date."}
+                            </p>
+                          )}
apps/web/lib/actions/partners/create-bounty-submission.ts (1)

117-125: Pre-open submission guard: good — clarify UX and confirm draft policy.

  • Message UX: consider appending an absolute timestamp to reduce timezone confusion.
  • Product: do we intend to block draft saves before submissionsOpenAt as well? Current guard blocks both drafts and finals.

Suggested tweak (adds absolute time while keeping relative):

-import { formatDistanceToNow } from "date-fns";
+import { format, formatDistanceToNow } from "date-fns";
@@
-  const waitTime = formatDistanceToNow(bounty.submissionsOpenAt, {
+  const waitTime = formatDistanceToNow(bounty.submissionsOpenAt, {
     addSuffix: true,
   });
+  const opensAtLocal = format(bounty.submissionsOpenAt, "PPpp");
@@
-  throw new Error(
-    `Submissions are not open yet. You can submit ${waitTime}.`,
-  );
+  throw new Error(
+    `Submissions are not open yet. You can submit ${waitTime} (opens ${opensAtLocal}).`,
+  );
apps/web/tests/bounties/index.test.ts (2)

62-82: Redundant coverage vs first performance test — intentional?

This test differs by omitting performanceCondition. If that’s the goal (ensure scope can be set without a workflow), keep it. Otherwise, consider consolidating to reduce test time.


126-155: Happy-path create with submissionsOpenAt (LGTM) — add boundary tests.

Recommend adding negative tests to assert validation:

  • submissionsOpenAt before startsAt → 400
  • submissionsOpenAt after endsAt → 400
  • PATCH path updates submissionsOpenAt within bounds

I can draft these if useful.

apps/web/lib/api/bounties/validate-bounty.ts (1)

16-16: Consider making the default behavior more explicit.

The assignment startsAt = startsAt || new Date() mutates the input parameter, which could be unexpected. Consider using a local variable or making this behavior more explicit in the function documentation.

Apply this diff to use a local variable:

-  startsAt = startsAt || new Date();
+  const effectiveStartsAt = startsAt || new Date();

Then update the subsequent references to use effectiveStartsAt instead of startsAt.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d7f0ba and 934bb7c.

📒 Files selected for processing (12)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (4 hunks)
  • apps/web/app/(ee)/api/bounties/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (14 hunks)
  • apps/web/lib/actions/partners/create-bounty-submission.ts (2 hunks)
  • apps/web/lib/api/bounties/get-bounty-with-details.ts (2 hunks)
  • apps/web/lib/api/bounties/validate-bounty.ts (1 hunks)
  • apps/web/lib/webhook/sample-events/bounty-created.json (1 hunks)
  • apps/web/lib/webhook/sample-events/bounty-updated.json (1 hunks)
  • apps/web/lib/zod/schemas/bounties.ts (2 hunks)
  • apps/web/tests/bounties/index.test.ts (4 hunks)
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx (4 hunks)
  • packages/prisma/schema/bounty.prisma (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 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/webhook/sample-events/bounty-updated.json
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx
  • apps/web/lib/webhook/sample-events/bounty-created.json
  • apps/web/lib/api/bounties/get-bounty-with-details.ts
  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/api/bounties/validate-bounty.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/tests/bounties/index.test.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx
  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🧬 Code graph analysis (6)
apps/web/lib/zod/schemas/bounties.ts (1)
apps/web/lib/zod/schemas/utils.ts (1)
  • parseDateSchema (37-40)
apps/web/lib/api/bounties/validate-bounty.ts (2)
apps/web/lib/zod/schemas/bounties.ts (1)
  • createBountySchema (43-72)
apps/web/lib/api/errors.ts (1)
  • DubApiError (75-92)
apps/web/app/(ee)/api/bounties/route.ts (2)
apps/web/lib/zod/schemas/bounties.ts (1)
  • createBountySchema (43-72)
apps/web/lib/api/bounties/validate-bounty.ts (1)
  • validateBounty (7-72)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
apps/web/lib/api/bounties/validate-bounty.ts (1)
  • validateBounty (7-72)
apps/web/tests/bounties/index.test.ts (1)
apps/web/tests/utils/resource.ts (1)
  • E2E_PARTNER_GROUP (90-93)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
  • useApiMutation (36-123)
apps/web/lib/types.ts (1)
  • BountyProps (560-560)
packages/ui/src/number-stepper.tsx (1)
  • NumberStepper (19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (22)
apps/web/lib/webhook/sample-events/bounty-updated.json (1)

8-8: Consistent field in update sample.

Addition is consistent with create sample. Same suggestion to document that null ⇒ always open.

Would you like me to add a brief README entry for webhook consumers?

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (1)

371-379: Submit flow: ensure endsAt present when submission window is set.

You already block in the validator, good. Consider server-side revalidation too (validateBounty) to avoid coupling to UI.

Do we enforce “submissionWindow ⇒ endsAt required” on the server?

apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

143-146: hasSubmissionsOpen logic is correct.

Treating null as open and future dates as closed matches the rest of the PR.

apps/web/lib/zod/schemas/bounties.ts (1)

55-57: Default startsAt on create required — validateBounty not found

createBountySchema allows startsAt to be nullish while BountySchema.startsAt is non-null (z.date()). Ensure the create path (validateBounty or the create handler) sets startsAt = new Date() when omitted to avoid DB insert failures. I could not find validateBounty in the repo — confirm its location or add defaulting in the create flow. submissionsOpenAt alignment is correct (create: parseDateSchema.nullish() vs BountySchema: z.date().nullable()).

apps/web/lib/api/bounties/get-bounty-with-details.ts (1)

19-20: Include submissionsOpenAt end-to-end (LGTM) — verify schema parity.

Selection and return wiring look correct. Please confirm BountySchemaExtended includes submissionsOpenAt to avoid parse errors at GET time.

Also applies to: 98-99

apps/web/lib/actions/partners/create-bounty-submission.ts (1)

18-18: New date-fns import is appropriate.

apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (3)

4-4: Centralized validation import (LGTM).


62-63: PATCH payload now accepts submissionsOpenAt (LGTM).


150-153: Persist submissionsOpenAt (LGTM).

Note: passing undefined to Prisma omits the field update; this is fine here.

apps/web/app/(ee)/api/bounties/route.ts (4)

3-3: Centralized validation import (LGTM).


109-123: Create payload includes submissionsOpenAt (LGTM).


124-126: Defaulting startsAt on create (LGTM).


195-196: Persist submissionsOpenAt on create (LGTM).

apps/web/tests/bounties/index.test.ts (2)

2-2: Helper date utilities import (LGTM).


26-26: Fixture parity: submissionsOpenAt null baseline (LGTM).

apps/web/lib/api/bounties/validate-bounty.ts (7)

7-15: LGTM! Well-structured validation function signature.

The function signature clearly defines the bounty validation parameters and uses proper TypeScript typing with the CreateBountyInput type.


18-24: LGTM! Proper date validation with clear error message.

The validation correctly ensures that endsAt is on or after startsAt, and the error message is descriptive and user-friendly.


26-42: LGTM! Comprehensive submissions timing validation.

The validation logic correctly enforces the date relationship constraints:

  • submissionsOpenAt must be on or after startsAt
  • submissionsOpenAt must be on or before endsAt (when endsAt is provided)

The error messages are clear and informative.


44-57: LGTM! Proper reward validation logic.

The validation correctly handles the different reward requirements:

  • Performance bounties require a rewardAmount
  • Submission bounties require either rewardAmount or rewardDescription

This aligns with the learning from previous reviews about bounty submission requirements.


59-64: LGTM! Good defensive validation.

The negative reward amount check is appropriate and provides a clear error message.


66-71: LGTM! Validates performance scope requirement.

The validation correctly ensures that performance bounties have the required performanceScope field set.


1-72: Verify integration with bounty creation/update endpoints — path missing

The prior verification failed: apps/web/app/.*api.*bounties was not found, so I could not confirm that route handlers now call validateBounty or that inline date/reward checks were removed. Run these from the repo root and paste results:

rg -n --hidden -S 'validateBounty' || true
rg -n --hidden -S 'startsAt.*endsAt|endsAt.*startsAt|submissionsOpenAt.*startsAt|startsAt.*submissionsOpenAt|rewardAmount.*rewardDescription|rewardDescription.*rewardAmount|performanceScope' || true

@devkiran
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 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: 2

🧹 Nitpick comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (4)

178-199: Effect dependency should include endsAt.

When editing an existing bounty, recompute submissionWindow if bounty.endsAt changes.

-  }, [bounty?.submissionsOpenAt]);
+  }, [bounty?.submissionsOpenAt, bounty?.endsAt]);

491-492: Remove stray debug text next to startsAt error.

-                          {errors.startsAt && "test"}
+                          {/* intentionally left blank; errors handled via tooltip/field styles */}

391-392: Remove console.log before merge.

-  console.log({ submissionWindow, submissionsOpenAt });
+  // noop

629-661: Optional: tighten AmountInput numeric parsing/NaN guard.

Defensively coerce to number and handle NaN in one place.

-                                onChange={(e) => {
-                                  const val = e.target.value;
-
-                                  field.onChange(
-                                    val === "" ? null : parseFloat(val),
-                                  );
-                                }}
+                                onChange={(e) => {
+                                  const val = e.target.value;
+                                  const num = Number(val);
+                                  field.onChange(val === "" || Number.isNaN(num) ? null : num);
+                                }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 783356f and 7b88c26.

📒 Files selected for processing (1)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (14 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
  • useApiMutation (36-123)
apps/web/lib/types.ts (1)
  • BountyProps (560-560)
packages/ui/src/number-stepper.tsx (1)
  • NumberStepper (19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)

838-841: Double‑check disabled state interplay with loading.

Button is disabled by validationError even while loading; ensure this doesn’t mask a state where loading is true and form becomes valid mid‑request. Likely fine, but worth confirming.

If needed, I can scan other sheets for consistent disabled/loading patterns.


265-275: Don’t compare end date to “now” when start date is unset.

Only validate end-after-start if a start date is explicitly set.

-    const effectiveStartDate = startsAt || new Date();
-
-    if (endsAt && endsAt <= effectiveStartDate) {
-      return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`;
-    }
+    if (startsAt && endsAt && endsAt <= startsAt) {
+      return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`;
+    }

287-289: Enforce reward > 0 in UI to align with server min(1).

-      if (rewardType === "flat" && isEmpty(rewardAmount)) {
+      if (rewardType === "flat" && (rewardAmount == null || Number(rewardAmount) <= 0)) {
         return "Reward amount is required for flat rate rewards.";
       }
@@
-      if (isEmpty(rewardAmount)) {
+      if (rewardAmount == null || Number(rewardAmount) <= 0) {
         return "Reward amount is required.";
       }

Also align RHF rules:

                             rules={{
-                              required: true,
-                              min: 0,
+                              required: true,
+                              min: 1,
                             }}

Also applies to: 305-307, 636-638

@devkiran
Copy link
Collaborator Author

@coderabbitai final review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

@devkiran I'll conduct a final comprehensive review of all the bounty submission timing changes in this PR.

✅ 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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

620-639: Prevent submitting before open; add disabled gating and reuse parsed date in tooltip

Button remains enabled when submissions aren’t open, causing avoidable server errors. Disable it and use the parsed openAt for tooltip rendering.

Apply this diff:

-                    <Button
+                    <Button
                       variant="primary"
                       text="Submit"
                       className="h-9 rounded-lg px-3"
                       type="submit"
                       name="submit" // for submitter.name detection above
                       loading={isDraft === false}
-                      disabled={fileUploading || isDraft === true}
+                      disabled={fileUploading || isDraft === true || !hasSubmissionsOpen}
                       disabledTooltip={
-                        !hasSubmissionsOpen
-                          ? `Submissions are not open yet. They will open in ${formatDistanceToNow(bounty.submissionsOpenAt!, { addSuffix: true })} (on ${formatDate(
-                              bounty.submissionsOpenAt!,
+                        !hasSubmissionsOpen
+                          ? `Submissions are not open yet. They will open in ${formatDistanceToNow(openAt!, { addSuffix: true })} (on ${formatDate(
+                              openAt!,
                               {
                                 month: "short",
                                 day: "numeric",
                                 year: "numeric",
                                 timeZone: "UTC",
                               },
                             )}).`
                           : undefined
                       }
                     />
🧹 Nitpick comments (10)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

205-205: Fallback error message to avoid “undefined” toast

serverError can be undefined; provide a default.

Apply this diff:

-        throw new Error(result?.serverError);
+        throw new Error(result?.serverError ?? "Failed to create submission.");
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (2)

150-153: Don’t assert non-null; omit when unchanged and null out for performance bounties.

Keep Prisma payloads clean and enforce type semantics for submissionsOpenAt.

-          startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4)
+          startsAt: startsAt ?? undefined,
           endsAt,
-          submissionsOpenAt,
+          submissionsOpenAt:
+            bounty.type === "submission" ? submissionsOpenAt ?? undefined : null,

136-140: Generate performance bounty name from the effective reward.

If rewardAmount isn’t provided on PATCH, fall back to the existing bounty amount to avoid “$0” names.

-      bountyName = generatePerformanceBountyName({
-        rewardAmount: rewardAmount ?? 0, // this shouldn't happen since we return early if rewardAmount is null
+      bountyName = generatePerformanceBountyName({
+        rewardAmount: rewardAmount ?? bounty.rewardAmount ?? 0,
         condition: performanceCondition,
       });
apps/web/app/(ee)/api/bounties/route.ts (2)

124-126: Avoid double-defaulting startsAt in both caller and validator.

Pick one place. Since validateBounty already defaults, remove the local default here.

-    // Use current date as default if startsAt is not provided
-    startsAt = startsAt || new Date();

193-199: Normalize submissionsOpenAt by bounty type.

Ensure performance bounties always persist submissionsOpenAt = null.

           startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4)
           endsAt,
-          submissionsOpenAt,
+          submissionsOpenAt: type === "submission" ? submissionsOpenAt ?? undefined : null,
apps/web/lib/zod/schemas/bounties.ts (1)

55-58: Add schema-level guard: submissionsOpenAt implies endsAt.

Mirror server validation by refining create/update schemas.

 export const createBountySchema = z.object({
@@
-  submissionsOpenAt: parseDateSchema.nullish(),
+  submissionsOpenAt: parseDateSchema.nullish(),
}).superRefine((val, ctx) => {
+  if (val.submissionsOpenAt && !val.endsAt) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      message: "An end date (endsAt) is required when using a submission window.",
+      path: ["endsAt"],
+    });
+  }
});

Also applies to: 96-97

apps/web/tests/bounties/index.test.ts (1)

62-82: This test duplicates the prior performance case.

Consider folding into the first test via additional assertions or deleting the redundancy.

apps/web/lib/api/bounties/validate-bounty.ts (1)

16-17: Consider removing the internal default for startsAt.

Let callers pass the effective startsAt (create: now; update: existing) to avoid context loss.

-  startsAt = startsAt || new Date();
+  // Expect callers to supply effective startsAt (e.g., existing value on PATCH, now() on POST)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (2)

491-492: Remove debug placeholder.

"test" leaks in UI.

-                          {errors.startsAt && "test"}
+                          {!!errors.startsAt && null}

391-392: Remove console.log before merge.

-  console.log({ submissionWindow, submissionsOpenAt });
+  // console.log removed
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d7f0ba and 7b88c26.

📒 Files selected for processing (12)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (4 hunks)
  • apps/web/app/(ee)/api/bounties/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (14 hunks)
  • apps/web/lib/actions/partners/create-bounty-submission.ts (2 hunks)
  • apps/web/lib/api/bounties/get-bounty-with-details.ts (2 hunks)
  • apps/web/lib/api/bounties/validate-bounty.ts (1 hunks)
  • apps/web/lib/webhook/sample-events/bounty-created.json (1 hunks)
  • apps/web/lib/webhook/sample-events/bounty-updated.json (1 hunks)
  • apps/web/lib/zod/schemas/bounties.ts (2 hunks)
  • apps/web/tests/bounties/index.test.ts (5 hunks)
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx (4 hunks)
  • packages/prisma/schema/bounty.prisma (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx
  • apps/web/lib/api/bounties/validate-bounty.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/api/bounties/get-bounty-with-details.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/lib/actions/partners/create-bounty-submission.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/ui/partners/bounties/claim-bounty-modal.tsx
  • apps/web/lib/webhook/sample-events/bounty-updated.json
  • apps/web/lib/api/bounties/validate-bounty.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/api/bounties/get-bounty-with-details.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/lib/webhook/sample-events/bounty-created.json
  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/tests/bounties/index.test.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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🪛 GitHub Actions: Public API Tests
apps/web/tests/bounties/index.test.ts

[error] 51-51: AssertionError: expected 500 to deeply equal 200. Test command: 'pnpm prisma:generate && vitest -no-file-parallelism --bail=1'.

🔇 Additional comments (15)
apps/web/lib/webhook/sample-events/bounty-updated.json (1)

8-8: Confirm downstream tolerance for null vs. missing submissionsOpenAt

Good to include in the sample. Please verify webhook consumers and docs treat this field as optional and nullable (null or absent), and that type expectations are clearly documented.

apps/web/lib/webhook/sample-events/bounty-created.json (1)

8-8: Align webhook docs and consumers with new submissionsOpenAt

Same note as the update event: ensure consumers accept null and that docs specify ISO 8601 string when present.

apps/web/lib/api/bounties/get-bounty-with-details.ts (1)

19-19: LGTM: submissionsOpenAt is selected and returned

Field is plumbed through correctly.

Please confirm the returned type (Date vs ISO string) matches PartnerBountyProps expectations to avoid runtime parsing issues in the UI.

Also applies to: 98-99

apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)

86-94: No await needed here.

validateBounty is synchronous in this PR; the prior “missing await” feedback is obsolete.

apps/web/app/(ee)/api/bounties/route.ts (1)

127-135: No await required.

validateBounty is synchronous in this PR; earlier “missing await” comments don’t apply.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (8)

265-275: Validate endsAt only when a start date is explicitly set.

Avoid false failures due to effectiveStartDate fallback.

-    const effectiveStartDate = startsAt || new Date();
-
-    if (endsAt && endsAt <= effectiveStartDate) {
-      return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`;
-    }
+    if (startsAt && endsAt && endsAt <= startsAt) {
+      return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`;
+    }

287-293: Don’t allow zero rewards in UI.

0 passes isEmpty; enforce > 0.

-      if (rewardType === "flat" && isEmpty(rewardAmount)) {
+      if (rewardType === "flat" && (rewardAmount == null || rewardAmount <= 0)) {
         return "Reward amount is required for flat rate rewards.";
       }

305-307: Performance reward must be > 0.

Mirror server constraint.

-      if (isEmpty(rewardAmount)) {
+      if (rewardAmount == null || rewardAmount <= 0) {
         return "Reward amount is required.";
       }

212-218: Submission window toggle should set a sensible default when enabled.

Initialize to 2 days (or your chosen default) on enable.

   useEffect(() => {
     if (!hasSubmissionWindow) {
       setValue("submissionsOpenAt", null);
       setSubmissionWindow(null);
-    }
+    } else if (submissionWindow == null) {
+      setSubmissionWindow(2);
+    }
-  }, [hasSubmissionWindow, setValue]);
+  }, [hasSubmissionWindow, setValue, submissionWindow]);

219-233: Guard submissionsOpenAt computation behind the toggle.

Compute only when enabled and endsAt present.

-  // Calculate the submissionsOpenAt based on the submissionWindow & endsAt
+  // Calculate submissionsOpenAt based on submissionWindow & endsAt (only when enabled)
   useEffect(() => {
-    if (!submissionWindow || !endsAt) {
+    if (!hasSubmissionWindow || !submissionWindow || !endsAt) {
       return;
     }
@@
-  }, [endsAt, submissionWindow]);
+  }, [endsAt, submissionWindow, hasSubmissionWindow]);

452-459: Keep the toggle visible; only collapse the input area.

The height gate hides the switch when off.

-                    <AnimatedSizeContainer
-                      height
-                      transition={{ ease: "easeInOut", duration: 0.2 }}
-                      style={{
-                        height: hasStartDate ? "auto" : "0px",
-                        overflow: "hidden",
-                      }}
-                    >
+                    <AnimatedSizeContainer
+                      height
+                      transition={{ ease: "easeInOut", duration: 0.2 }}
+                    >

496-503: Same: don’t hide the End date switch.

-                      style={{
-                        height: hasEndDate ? "auto" : "0px",
-                        overflow: "hidden",
-                      }}
+                      // keep the switch visible; conditionally render the picker below

540-580: Same: don’t hide the Submission window switch; show default in copy.

-                          style={{
-                            height: hasSubmissionWindow ? "auto" : "0px",
-                            overflow: "hidden",
-                          }}
+                          // keep the switch visible; conditionally render the stepper below
@@
-                              <p className="mt-2 text-xs text-neutral-500">
-                                Submissions open {submissionWindow} days before
+                              <p className="mt-2 text-xs text-neutral-500">
+                                Submissions open {submissionWindow ?? 2} days before
                                 the end date. Drafts can be saved until then.
                               </p>
packages/prisma/schema/bounty.prisma (1)

26-50: Schema changes look correct — run duplicate checks & plan zero‑downtime migration

The provided script only echoed SQL; execute the queries below on prod/stage and confirm they return zero rows before applying the migration.

-- 1) Duplicate submissions per bounty/partner
SELECT bountyId, partnerId, COUNT(*) c
FROM BountySubmission
GROUP BY bountyId, partnerId
HAVING c > 1;

-- 2) workflowId uniqueness check
SELECT workflowId, COUNT(*) c
FROM Bounty
WHERE workflowId IS NOT NULL
GROUP BY workflowId
HAVING c > 1;
  • If any rows are returned: dedupe/backfill those records before adding the unique constraint.
  • For new indexes/constraints: use a DB-specific online/zero-downtime strategy (e.g., Postgres: CREATE UNIQUE INDEX CONCURRENTLY then attach constraint or other safe workflow) or schedule a maintenance window.
apps/web/tests/bounties/index.test.ts (1)

126-155: Add negative test for submissionsOpenAt without endsAt (expect 400)

Good coverage for the submissionsOpenAt happy path; add a negative test in apps/web/tests/bounties/index.test.ts (around lines 126–155) that posts submissionsOpenAt without endsAt and asserts a 400 response.

Verification script failed in the sandbox: vitest not found (/bin/bash: line 4: vitest: command not found). Run tests locally or in CI to confirm behavior.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

112-121: Don’t set Content-Length in browser PUT; relax JSON assumption on errors

Browsers control Content-Length; setting it can break uploads. Error bodies from object storage are often non‑JSON.

Apply this diff:

-    const uploadResponse = await fetch(signedUrl, {
+    const uploadResponse = await fetch(signedUrl, {
       method: "PUT",
       body: file,
       headers: {
         "Content-Type": file.type,
-        "Content-Length": file.size.toString(),
       },
     });
@@
-    if (!uploadResponse.ok) {
-      const result = await uploadResponse.json();
-      toast.error(result.error.message || "Failed to upload screenshot.");
+    if (!uploadResponse.ok) {
+      const text = await uploadResponse.text().catch(() => "");
+      toast.error(text || "Failed to upload screenshot.");
       return;
     }
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)

148-153: Avoid non-null assertion on optional fields; conditionally include updates

startsAt: startsAt! risks passing undefined at runtime. Only include fields when provided.

Apply this diff:

-          startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4)
-          endsAt,
-          submissionsOpenAt,
+          ...(startsAt !== undefined && { startsAt }),
+          ...(endsAt !== undefined && { endsAt }),
+          ...(submissionsOpenAt !== undefined && { submissionsOpenAt }),
packages/prisma/schema/bounty.prisma (1)

64-94: Use onDelete: SetNull on optional FKs to avoid delete blockers.

File: packages/prisma/schema/bounty.prisma Lines: 64-94 — commissionId and userId are optional; deleting the referenced Commission/User will be blocked by FK constraints without onDelete: SetNull.

-  commission        Commission?        @relation(fields: [commissionId], references: [id])
+  commission        Commission?        @relation(fields: [commissionId], references: [id], onDelete: SetNull)
@@
-  user              User?              @relation(fields: [userId], references: [id])
+  user              User?              @relation(fields: [userId], references: [id], onDelete: SetNull)

Also add an index on (bountyId, status) for reviewer queues.

🧹 Nitpick comments (10)
apps/web/lib/webhook/sample-events/bounty-updated.json (1)

8-8: Sample should include a realistic non-null example somewhere

Null is fine, but consider adding a second event sample (or documenting) with an actual ISO timestamp to clarify expected format/timezone for consumers.

apps/web/lib/webhook/sample-events/bounty-created.json (1)

8-8: Mirror a non-null example for clarity

As above, consider one sample showing submissionsOpenAt as an ISO string to reduce ambiguity for webhook integrators.

apps/web/ui/partners/bounties/claim-bounty-modal.tsx (2)

627-639: Disable “Submit” when submissions aren’t open and reuse parsed date

Currently only the tooltip changes; prevent the action by actually disabling the button and use the parsed openAt for formatting.

Apply this diff:

-                      loading={isDraft === false}
-                      disabled={fileUploading || isDraft === true}
-                      disabledTooltip={
-                        !hasSubmissionsOpen
-                          ? `Submissions are not open yet. They will open in ${formatDistanceToNow(bounty.submissionsOpenAt!, { addSuffix: true })} (on ${formatDate(
-                              bounty.submissionsOpenAt!,
-                              {
-                                month: "short",
-                                day: "numeric",
-                                year: "numeric",
-                                timeZone: "UTC",
-                              },
-                            )}).`
-                          : undefined
-                      }
+                      loading={isDraft === false}
+                      disabled={fileUploading || isDraft === true || !hasSubmissionsOpen}
+                      disabledTooltip={
+                        !hasSubmissionsOpen && openAt
+                          ? `Submissions are not open yet. They will open in ${formatDistanceToNow(openAt, { addSuffix: true })} (on ${formatDate(openAt, {
+                              month: "short",
+                              day: "numeric",
+                              year: "numeric",
+                              timeZone: "UTC",
+                            })}).`
+                          : undefined
+                      }

205-206: Provide a fallback error message

Avoid “Error: undefined” toasts when serverError isn’t set.

Apply this diff:

-        throw new Error(result?.serverError);
+        throw new Error(
+          result?.serverError ?? "Failed to create submission. Please try again."
+        );
apps/web/tests/bounties/index.test.ts (3)

62-82: Drop the redundant performance test block.

This repeats the first test (same payload and expectations) and just increases flake time. Remove or fold into the first test.

-  test("POST /bounties - performance based with performanceScope set to new", async () => {
-    const { status, data: bounty } = await http.post<Bounty>({
-      path: "/bounties",
-      body: {
-        ...performanceBounty,
-        groupIds: [E2E_PARTNER_GROUP.id],
-        performanceScope: "new",
-      },
-    });
-
-    expect(status).toEqual(200);
-    expect(bounty).toMatchObject({
-      id: expect.any(String),
-      ...performanceBounty,
-      performanceScope: "new",
-    });
-
-    onTestFinished(async () => {
-      await h.deleteBounty(bounty.id);
-    });
-  });
+  // Covered by the first performance test (performanceScope: "new")

126-155: Stabilize date handling and assertion clarity for submissionsOpenAt test.

  • Send ISO strings consistently and assert explicitly to avoid spread/null shadowing from submissionBounty.
-    const now = new Date();
-    const startsAt = addDays(now, 1);
-    const endsAt = addDays(startsAt, 30);
-    const submissionsOpenAt = subDays(endsAt, 2);
+    const now = new Date();
+    const startsAt = addDays(now, 1).toISOString();
+    const endsAt = addDays(new Date(startsAt), 30).toISOString();
+    const submissionsOpenAt = subDays(new Date(endsAt), 2).toISOString();
@@
-      body: {
-        ...submissionBounty,
-        startsAt,
-        endsAt,
-        submissionsOpenAt,
-        groupIds: [E2E_PARTNER_GROUP.id],
-      },
+      body: {
+        ...submissionBounty,
+        startsAt,
+        endsAt,
+        submissionsOpenAt,
+        groupIds: [E2E_PARTNER_GROUP.id],
+      },
@@
-    expect(bounty).toMatchObject({
-      id: expect.any(String),
-      ...submissionBounty,
-      startsAt: startsAt.toISOString(),
-      endsAt: endsAt.toISOString(),
-      submissionsOpenAt: submissionsOpenAt.toISOString(),
-    });
+    expect(bounty).toMatchObject({
+      id: expect.any(String),
+      name: submissionBounty.name,
+      description: submissionBounty.description,
+      type: "submission",
+      rewardAmount: submissionBounty.rewardAmount,
+      submissionRequirements: submissionBounty.submissionRequirements,
+      startsAt,
+      endsAt,
+      submissionsOpenAt,
+    });

197-205: Use Date objects consistently or ISO consistently.

You convert endsAt to ISO only in PATCH while POST mixes raw Date/ISO elsewhere. Pick one encoding (ISO strings are fine) throughout to reduce serialization edge cases.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)

566-576: Helper text should mirror the defaulted value.

Prevents “undefined days” copy when submissionWindow is null.

-                                Submissions open {submissionWindow} days before
+                                Submissions open {submissionWindow ?? 2} days before

491-492: Remove leftover debug string in error render.

{errors.startsAt && "test"} leaks a literal “test” into the UI.

-                          {errors.startsAt && "test"}
+                          {errors.startsAt?.message && (
+                            <p className="mt-1 text-xs text-red-600">
+                              {String(errors.startsAt.message)}
+                            </p>
+                          )}

391-392: Drop console.log before merge.

Avoid noisy logs in production UI.

-  console.log({ submissionWindow, submissionsOpenAt });
+  // debug logging removed
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d7f0ba and 7b88c26.

📒 Files selected for processing (12)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (4 hunks)
  • apps/web/app/(ee)/api/bounties/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (14 hunks)
  • apps/web/lib/actions/partners/create-bounty-submission.ts (2 hunks)
  • apps/web/lib/api/bounties/get-bounty-with-details.ts (2 hunks)
  • apps/web/lib/api/bounties/validate-bounty.ts (1 hunks)
  • apps/web/lib/webhook/sample-events/bounty-created.json (1 hunks)
  • apps/web/lib/webhook/sample-events/bounty-updated.json (1 hunks)
  • apps/web/lib/zod/schemas/bounties.ts (2 hunks)
  • apps/web/tests/bounties/index.test.ts (5 hunks)
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx (4 hunks)
  • packages/prisma/schema/bounty.prisma (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/api/bounties/get-bounty-with-details.ts
  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/lib/api/bounties/validate-bounty.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/ui/partners/bounties/claim-bounty-modal.tsx
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/tests/bounties/index.test.ts
  • apps/web/lib/api/bounties/get-bounty-with-details.ts
  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/lib/webhook/sample-events/bounty-updated.json
  • apps/web/lib/webhook/sample-events/bounty-created.json
  • apps/web/lib/api/bounties/validate-bounty.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🪛 GitHub Actions: Public API Tests
apps/web/tests/bounties/index.test.ts

[error] 51-51: POST /bounties - performance based: expected 200 but received 500 (AssertionError)

🔇 Additional comments (16)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

143-146: Make “open” check inclusive and robust to string Dates (repeat)

At the exact open instant, isBefore excludes opening; also guard if the value is an ISO string.

Apply this diff:

-import { formatDistanceToNow, isBefore } from "date-fns";
+import { formatDistanceToNow } from "date-fns";
@@
-  const hasSubmissionsOpen = bounty.submissionsOpenAt
-    ? isBefore(bounty.submissionsOpenAt, new Date())
-    : true;
+  const openAt = bounty.submissionsOpenAt
+    ? new Date(bounty.submissionsOpenAt as any)
+    : null;
+  const hasSubmissionsOpen = !openAt || Date.now() >= openAt.getTime();
apps/web/lib/api/bounties/get-bounty-with-details.ts (1)

19-20: LGTM — field is plumbed through SELECT and response

submissionsOpenAt is selected and returned consistently with other date fields.

Also applies to: 98-99

apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (2)

86-94: Validate against effective dates to prevent invalid windows on PATCH (repeat)

Use DB values when a field is omitted to avoid defaulting to “now”.

Apply this diff:

-    validateBounty({
-      type: bounty.type,
-      startsAt,
-      endsAt,
-      submissionsOpenAt,
-      rewardAmount,
-      rewardDescription,
-      performanceScope: bounty.performanceScope,
-    });
+    const effectiveStartsAt = startsAt ?? bounty.startsAt;
+    const effectiveEndsAt = endsAt ?? bounty.endsAt;
+    const effectiveOpenAt = submissionsOpenAt ?? bounty.submissionsOpenAt;
+    await validateBounty({
+      type: bounty.type,
+      startsAt: effectiveStartsAt,
+      endsAt: effectiveEndsAt,
+      submissionsOpenAt: effectiveOpenAt,
+      rewardAmount,
+      rewardDescription,
+      performanceScope: bounty.performanceScope,
+    });

86-95: Await validation (repeat)

Same as POST: ensure validation runs before update.

Apply this diff if async:

-    validateBounty({
+    await validateBounty({
       type: bounty.type,
       startsAt,
       endsAt,
       submissionsOpenAt,
       rewardAmount,
       rewardDescription,
       performanceScope: bounty.performanceScope,
     });
apps/web/lib/zod/schemas/bounties.ts (1)

55-58: LGTM — schema updates align with API surface

startsAt made nullish and new submissionsOpenAt added on both create and payload schemas.

Also applies to: 96-97

apps/web/lib/actions/partners/create-bounty-submission.ts (1)

117-125: Allow saving drafts before the open time (repeat)

Gate only final submissions on submissionsOpenAt, not drafts.

Apply this diff:

-    if (bounty.submissionsOpenAt && bounty.submissionsOpenAt > now) {
+    if (!isDraft && bounty.submissionsOpenAt && bounty.submissionsOpenAt > now) {
       const waitTime = formatDistanceToNow(bounty.submissionsOpenAt, {
         addSuffix: true,
       });
 
       throw new Error(
         `Submissions are not open yet. You can submit ${waitTime}.`,
       );
     }
apps/web/app/(ee)/api/bounties/route.ts (1)

124-135: Await validation to avoid creating invalid bounties (repeat)

If validateBounty is async, this currently doesn’t block bad payloads.

Run to confirm whether validateBounty is async:

#!/bin/bash
rg -n -C2 'export (const|function) validateBounty' apps/web/lib/api/bounties/validate-bounty.ts || true
rg -n 'async function validateBounty|export const validateBounty\s*=\s*async' apps/web/lib/api/bounties/validate-bounty.ts || true

Apply this diff if async:

-    validateBounty({
+    await validateBounty({
       type,
       startsAt,
       endsAt,
       submissionsOpenAt,
       rewardAmount,
       rewardDescription,
       performanceScope,
     });
apps/web/tests/bounties/index.test.ts (1)

37-55: Investigate 500 on performance bounty creation; capture server error for faster triage.

Augment the assertion to log response body when status ≠ 200 so CI output shows the backend error cause.

-    expect(status).toEqual(200);
+    if (status !== 200) {
+      // Surface backend error in CI
+      // eslint-disable-next-line no-console
+      console.error("POST /bounties (performance) failed:", bounty);
+    }
+    expect(status).toEqual(200);
apps/web/lib/api/bounties/validate-bounty.ts (3)

44-64: Harden reward validation (undefined, non-finite, and non-positive).

Enforce > 0 for performance; for submission, allow either > 0 amount or non-empty description. Treat undefined/null/NaN/≤0 correctly.

-  if (rewardAmount === null || rewardAmount === 0) {
-    if (type === "performance") {
-      throw new DubApiError({
-        code: "bad_request",
-        message: "Reward amount is required for performance bounties.",
-      });
-    } else if (!rewardDescription) {
-      throw new DubApiError({
-        code: "bad_request",
-        message:
-          "For submission bounties, either reward amount or reward description is required.",
-      });
-    }
-  }
-
-  if (rewardAmount && rewardAmount < 0) {
+  const hasAmount = rewardAmount != null;
+  const amountValid =
+    hasAmount && Number.isFinite(rewardAmount) && rewardAmount > 0;
+
+  if (type === "performance") {
+    if (!amountValid) {
+      throw new DubApiError({
+        code: "bad_request",
+        message:
+          "Reward amount is required and must be > 0 for performance bounties.",
+      });
+    }
+  } else {
+    const hasDescription =
+      typeof rewardDescription === "string" &&
+      rewardDescription.trim().length > 0;
+    if (!amountValid && !hasDescription) {
+      throw new DubApiError({
+        code: "bad_request",
+        message:
+          "For submission bounties, either reward amount (> 0) or reward description is required.",
+      });
+    }
+    if (hasAmount && !amountValid) {
+      throw new DubApiError({
+        code: "bad_request",
+        message: "Reward amount must be > 0 when provided.",
+      });
+    }
+  }
+
+  if (hasAmount && Number.isFinite(rewardAmount) && rewardAmount < 0) {
     throw new DubApiError({
       code: "bad_request",
       message: "Reward amount cannot be negative.",
     });
   }

66-71: performanceScope check looks good.

Validation aligns with schema semantics; no change needed.


26-42: Require endsAt when submissionsOpenAt is provided; avoid comparing against undefined.

Also compare against startsAt only when startsAt exists.

-  if (submissionsOpenAt) {
-    if (submissionsOpenAt < startsAt) {
+  if (submissionsOpenAt) {
+    if (!endsAt) {
+      throw new DubApiError({
+        message:
+          "An end date (endsAt) is required when using submissionsOpenAt.",
+        code: "bad_request",
+      });
+    }
+    if (startsAt && submissionsOpenAt < startsAt) {
       throw new DubApiError({
         message:
           "Bounty submissions open date (submissionsOpenAt) must be on or after start date (startsAt).",
         code: "bad_request",
       });
     }
 
-    if (endsAt && submissionsOpenAt > endsAt) {
+    if (submissionsOpenAt > endsAt) {
       throw new DubApiError({
         message:
           "Bounty submissions open date (submissionsOpenAt) must be on or before end date (endsAt).",
         code: "bad_request",
       });
     }
   }
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (4)

219-233: Guard submissionsOpenAt computation by toggle and seed a default window.

Without this, enabling the toggle shows “2” but the form value stays null; also, we compute even when disabled.

-  // Calculate the submissionsOpenAt based on the submissionWindow & endsAt
+  // Calculate submissionsOpenAt based on submissionWindow & endsAt (only when enabled)
   useEffect(() => {
-    if (!submissionWindow || !endsAt) {
+    if (!hasSubmissionWindow || !submissionWindow || !endsAt) {
       return;
     }
@@
-  }, [endsAt, submissionWindow]);
+  }, [endsAt, submissionWindow, hasSubmissionWindow]);

Also set an initial default when enabling:

   useEffect(() => {
     if (!hasSubmissionWindow) {
       setValue("submissionsOpenAt", null);
       setSubmissionWindow(null);
     } else if (submissionWindow == null) {
       setSubmissionWindow(2);
     }
-  }, [hasSubmissionWindow, setValue]);
+  }, [hasSubmissionWindow, setValue, submissionWindow]);

452-459: Keep the toggle row visible; avoid height-gating the entire section.

Current AnimatedSizeContainer height style hides the Switch itself when off, making it impossible to enable.

-                    <AnimatedSizeContainer
-                      height
-                      transition={{ ease: "easeInOut", duration: 0.2 }}
-                      style={{
-                        height: hasStartDate ? "auto" : "0px",
-                        overflow: "hidden",
-                      }}
-                    >
+                    <AnimatedSizeContainer
+                      height
+                      transition={{ ease: "easeInOut", duration: 0.2 }}
+                    >

Apply similarly around lines 496-503 and 540-547.


265-275: Avoid “effectiveStartDate = now” fallback; only compare when startsAt exists.

Otherwise, users without a start date set are forced to choose endsAt > now due to millisecond drift.

-  const validationError = useMemo(() => {
-    if (startsAt && startsAt < new Date()) {
-      return "Please choose a start date that is in the future.";
-    }
-
-    const effectiveStartDate = startsAt || new Date();
-
-    if (endsAt && endsAt <= effectiveStartDate) {
-      return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`;
-    }
+  const validationError = useMemo(() => {
+    if (startsAt && startsAt < new Date()) {
+      return "Please choose a start date that is in the future.";
+    }
+    if (startsAt && endsAt && endsAt <= startsAt) {
+      return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`;
+    }

287-307: Enforce > 0 reward amounts in UI; don’t rely on isEmpty(0).

Match server rules: performance/flat must be strictly positive; custom requires description.

-      if (rewardType === "flat" && isEmpty(rewardAmount)) {
+      if (rewardType === "flat" && (rewardAmount == null || rewardAmount <= 0)) {
         return "Reward amount is required for flat rate rewards.";
       }
@@
-      if (isEmpty(rewardAmount)) {
+      if (rewardAmount == null || rewardAmount <= 0) {
         return "Reward amount is required.";
       }

And update the controller rule:

-                            rules={{
-                              required: true,
-                              min: 0,
-                            }}
+                            rules={{
+                              required: true,
+                              min: 1,
+                            }}
packages/prisma/schema/bounty.prisma (1)

27-50: Reconsider onDelete behavior for the Workflow relation: use SetNull instead of Cascade to preserve Bounties if a Workflow is removed.

-  workflow    Workflow?          @relation(fields: [workflowId], references: [id], onDelete: Cascade)
+  workflow    Workflow?          @relation(fields: [workflowId], references: [id], onDelete: SetNull)

Should deleting a Workflow ever delete its Bounty?

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: 0

🧹 Nitpick comments (4)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)

150-154: Avoid non-null assertion and undefined writes; include fields only when provided.

Prisma ignores undefined, but being explicit avoids surprises and better conveys intent. This also supports partial PATCH cleanly.

Apply:

-          startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4)
-          endsAt,
-          submissionsOpenAt:
-            bounty.type === "submission" ? submissionsOpenAt : null,
+          ...(startsAt !== undefined && { startsAt }),
+          ...(endsAt !== undefined && { endsAt }),
+          ...(bounty.type === "submission"
+            ? submissionsOpenAt !== undefined
+              ? { submissionsOpenAt }
+              : {}
+            : { submissionsOpenAt: null }),
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)

221-235: Guard computation by the toggle to avoid unintended writes.

Only compute submissionsOpenAt when the window is enabled.

Apply:

-  // Calculate the submissionsOpenAt based on the submissionWindow & endsAt
+  // Calculate submissionsOpenAt based on the submissionWindow & endsAt (only when enabled)
   useEffect(() => {
-    if (!submissionWindow || !endsAt) {
+    if (!hasSubmissionWindow || !submissionWindow || !endsAt) {
       return;
     }
@@
   }, [endsAt, submissionWindow]);
+  // also include hasSubmissionWindow in deps

And update deps:

-  }, [endsAt, submissionWindow]);
+  }, [endsAt, submissionWindow, hasSubmissionWindow]);

565-581: Copy should reflect defaulted value when null.

If submissionWindow is null but toggle is on (defaulted to 2), the helper text should still show “2”.

Apply:

-                              <NumberStepper
+                              <NumberStepper
                                 value={submissionWindow ?? 2}
                                 onChange={(v) => setSubmissionWindow(v)}
@@
-                              <p className="mt-2 text-xs text-neutral-500">
-                                Submissions open {submissionWindow} days before
+                              <p className="mt-2 text-xs text-neutral-500">
+                                Submissions open {(submissionWindow ?? 2)} days before
                                 the end date. Drafts can be saved until then.
                               </p>

393-394: Drop console.log noise.

Avoid leaking debug logs in production UI.

Apply:

-  console.log({ submissionWindow, submissionsOpenAt });
+  // console.log removed
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b88c26 and e325d43.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (4 hunks)
  • apps/web/app/(ee)/api/bounties/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (14 hunks)
  • apps/web/lib/actions/partners/create-bounty-submission.ts (2 hunks)
  • apps/web/lib/api/bounties/validate-bounty.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/api/bounties/validate-bounty.ts
🧰 Additional context used
🧠 Learnings (3)
📚 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/actions/partners/create-bounty-submission.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🧬 Code graph analysis (3)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
apps/web/lib/api/bounties/validate-bounty.ts (1)
  • validateBounty (7-80)
apps/web/app/(ee)/api/bounties/route.ts (2)
apps/web/lib/zod/schemas/bounties.ts (1)
  • createBountySchema (43-72)
apps/web/lib/api/bounties/validate-bounty.ts (1)
  • validateBounty (7-80)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
  • useApiMutation (36-123)
apps/web/lib/types.ts (1)
  • BountyProps (560-560)
packages/ui/src/number-stepper.tsx (1)
  • NumberStepper (19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (9)
apps/web/lib/actions/partners/create-bounty-submission.ts (2)

117-129: Good: gate submissionsOpenAt only for final submissions.

This preserves draft-saving while enforcing the opening window for real submissions. Nice UX.


131-135: Don’t mask data integrity issues: remove fallback to [].

Per repo convention, non‑performance bounties must have submissionRequirements; falling back to [] hides broken data and weakens validation.

Apply:

-    const submissionRequirements = submissionRequirementsSchema.parse(
-      bounty.submissionRequirements || [],
-    );
+    const submissionRequirements = submissionRequirementsSchema.parse(
+      bounty.submissionRequirements,
+    );
apps/web/app/(ee)/api/bounties/route.ts (1)

127-135: Validation centralization looks good.

validateBounty is synchronous, so no await is needed. Inputs (including submissionsOpenAt) are correctly validated before create.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (5)

454-461: Toggle becomes unreachable when off — remove height gate from the container.

Collapsing the entire block hides the Switch itself, so users can’t enable Start date.

Apply:

-                    <AnimatedSizeContainer
-                      height
-                      transition={{ ease: "easeInOut", duration: 0.2 }}
-                      style={{
-                        height: hasStartDate ? "auto" : "0px",
-                        overflow: "hidden",
-                      }}
-                    >
+                    <AnimatedSizeContainer
+                      height
+                      transition={{ ease: "easeInOut", duration: 0.2 }}
+                    >

498-505: Same issue for End date — keep the toggle row visible.

Remove the height style to prevent hiding the Switch when hasEndDate is false.

Apply:

-                    <AnimatedSizeContainer
-                      height
-                      transition={{ ease: "easeInOut", duration: 0.2 }}
-                      style={{
-                        height: hasEndDate ? "auto" : "0px",
-                        overflow: "hidden",
-                      }}
-                    >
+                    <AnimatedSizeContainer
+                      height
+                      transition={{ ease: "easeInOut", duration: 0.2 }}
+                    >

543-549: Same issue for Submission window — don’t hide the Switch.

Apply:

-                        <AnimatedSizeContainer
-                          height
-                          transition={{ ease: "easeInOut", duration: 0.2 }}
-                          style={{
-                            height: hasSubmissionWindow ? "auto" : "0px",
-                            overflow: "hidden",
-                          }}
-                        >
+                        <AnimatedSizeContainer
+                          height
+                          transition={{ ease: "easeInOut", duration: 0.2 }}
+                        >

289-291: Enforce > 0 for flat rewards to match server min(1).

isEmpty allows 0; the server rejects 0 via Zod min(1). Align UI to prevent avoidable errors.

Apply:

-      if (rewardType === "flat" && isEmpty(rewardAmount)) {
+      if (
+        rewardType === "flat" &&
+        (rewardAmount == null || Number(rewardAmount) <= 0)
+      ) {
         return "Reward amount is required for flat rate rewards.";
       }

307-309: Enforce > 0 for performance rewards as well.

Apply:

-      if (isEmpty(rewardAmount)) {
+      if (rewardAmount == null || Number(rewardAmount) <= 0) {
         return "Reward amount is required.";
       }
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)

86-94: Use effective DB dates for PATCH validation to prevent invalid ranges.

When startsAt/endsAt/submissionsOpenAt are omitted, validateBounty defaults startsAt to now, which can let endsAt slip before the persisted startsAt or wrongly reject valid updates. Pass effective values falling back to the current bounty’s dates.

Apply:

-    validateBounty({
-      type: bounty.type,
-      startsAt,
-      endsAt,
-      submissionsOpenAt,
-      rewardAmount,
-      rewardDescription,
-      performanceScope: bounty.performanceScope,
-    });
+    const effectiveStartsAt = startsAt ?? bounty.startsAt;
+    const effectiveEndsAt = endsAt ?? bounty.endsAt;
+    const effectiveSubmissionsOpenAt =
+      submissionsOpenAt ?? bounty.submissionsOpenAt ?? undefined;
+
+    validateBounty({
+      type: bounty.type,
+      startsAt: effectiveStartsAt,
+      endsAt: effectiveEndsAt,
+      submissionsOpenAt: effectiveSubmissionsOpenAt,
+      rewardAmount,
+      rewardDescription,
+      performanceScope: bounty.performanceScope,
+    });

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

620-640: Actually disable “Submit” when submissions aren’t open; keep tooltip

Right now the button stays enabled and only shows a tooltip, allowing a confusing submit attempt that the backend will reject. Disable it when not open and reference the parsed openAt for the tooltip.

-                      disabled={fileUploading || isDraft === true}
+                      disabled={!hasSubmissionsOpen || fileUploading || isDraft === true}
                       disabledTooltip={
-                        !hasSubmissionsOpen
-                          ? `Submissions are not open yet. They will open on ${formatDate(
-                              bounty.submissionsOpenAt!,
-                              {
-                                month: "short",
-                                day: "numeric",
-                                year: "numeric",
-                                timeZone: "UTC",
-                              },
-                            )}. In the meantime, you can save your progress as a draft.`
-                          : undefined
+                        !hasSubmissionsOpen
+                          ? `Submissions are not open yet. They will open on ${formatDate(openAt!, {
+                              month: "short",
+                              day: "numeric",
+                              year: "numeric",
+                              timeZone: "UTC",
+                            })}. In the meantime, you can save your progress as a draft.`
+                          : undefined
                       }
🧹 Nitpick comments (4)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)

205-205: Avoid throwing an undefined error message

result?.serverError can be undefined; provide a fallback message so the toast shows something useful.

-        throw new Error(result?.serverError);
+        throw new Error(result?.serverError || "Failed to create submission. Please try again.");
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)

90-93: The isEmpty helper misses edge cases for numeric validation

Using value === "" doesn't handle the case where a numeric input could be 0 which is falsy but valid for some numeric fields. For reward amounts, this could allow 0 to pass as "not empty" when the server requires minimum 1.

-const isEmpty = (value: any) =>
-  value === undefined || value === null || value === "";
+const isEmpty = (value: any) =>
+  value === undefined || value === null || value === "";
+
+const isEmptyOrZero = (value: any) =>
+  isEmpty(value) || value === 0;

332-351: Reward validation still allows 0 amounts despite server requirements

The validation uses isEmpty(rewardAmount) and then checks rewardAmount <= 0, but this creates inconsistent behavior. The isEmpty helper doesn't catch 0, so a user could enter 0 and it would pass the first check but fail the second. However, looking at the server-side requirements, amounts should be minimum 1.

-      if (rewardType === "flat") {
-        if (isEmpty(rewardAmount)) {
-          return "Reward amount is required for flat rate rewards.";
-        }
-        if (rewardAmount !== null && rewardAmount <= 0) {
-          return "Reward amount must be greater than 0.";
-        }
+      if (rewardType === "flat") {
+        if (rewardAmount == null || rewardAmount <= 0) {
+          return "Reward amount is required and must be greater than 0.";
+        }

373-384: Same reward validation issue for performance bounties

The same inconsistent validation pattern exists for performance bounties where 0 could slip through the isEmpty check.

-      if (isEmpty(rewardAmount)) {
-        return "Reward amount is required for performance bounties.";
-      }
-
-      if (rewardAmount !== null && rewardAmount <= 0) {
-        return "Reward amount must be greater than 0.";
-      }
+      if (rewardAmount == null || rewardAmount <= 0) {
+        return "Reward amount is required and must be greater than 0.";
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e325d43 and 409dfce.

📒 Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (13 hunks)
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/ui/partners/bounties/claim-bounty-modal.tsx
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
  • useApiMutation (36-123)
apps/web/lib/types.ts (2)
  • BountyProps (560-560)
  • BountySubmissionRequirement (570-571)
packages/ui/src/number-stepper.tsx (1)
  • NumberStepper (19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (13)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (2)

28-28: Remove unused/isBefore import and prep for inclusive check

We shouldn't rely on isBefore here; we'll switch to an inclusive timestamp comparison and drop this import.

-import { isBefore } from "date-fns";

143-146: Make “submissions open” check inclusive and safe for Date | string

At the exact open instant the current check still reports “closed”, and passing a raw ISO/string to date-fns is brittle. Parse to Date and use an inclusive comparison.

-  const hasSubmissionsOpen = bounty.submissionsOpenAt
-    ? isBefore(bounty.submissionsOpenAt, new Date())
-    : true;
+  const openAt =
+    bounty.submissionsOpenAt instanceof Date
+      ? bounty.submissionsOpenAt
+      : bounty.submissionsOpenAt
+        ? new Date(bounty.submissionsOpenAt as string)
+        : null;
+  const hasSubmissionsOpen = !openAt || Date.now() >= openAt.getTime();
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (11)

29-36: LGTM on UI imports

The new imports for NumberStepper and formatDate support the submission window functionality correctly.


102-113: Submission window calculation logic is sound

The calculation using Math.ceil() and converting milliseconds to days correctly handles the submission window duration between submissionsOpenAt and endsAt.


133-133: Form schema extension for submissionsOpenAt

The form now properly includes the submissionsOpenAt field in the default values, aligning with the broader PR's extension of the data model.


185-200: Proper form state management in toggle handlers

The toggle handlers correctly manage form state with React Hook Form's setValue using appropriate flags (shouldDirty, shouldValidate). When setValue cause state update, such as dirty and touched, these flags ensure the form's dirty state is properly maintained.


202-233: Submission window synchronization works correctly

The logic properly synchronizes submissionsOpenAt with changes to endsAt and submissionWindow by calculating the date offset. The date manipulation using setDate(date.getDate() - submissionWindow) correctly subtracts days from the end date.


235-258: Helper function for submission requirements is well-structured

The updateSubmissionRequirements function correctly handles the array manipulation for image/URL requirements and maintains form state synchronization.


272-320: Comprehensive validation with proper date comparisons

The validation logic addresses the previous timing issues by:

  1. Only validating startsAt against current time when it has changed from the existing bounty
  2. Using formatDate(effectiveStartDate) in error messages for clarity
  3. Properly calculating minimum time gaps and submission window constraints

The logic handles edge cases well, including the submission window not extending before the start date.


441-441: Correct nullification of submissionsOpenAt for performance bounties

Setting submissionsOpenAt to null for performance bounties is correct since submission windows only apply to submission-type bounties.


533-661: Animated submission window UI implementation is well-designed

The submission window section properly:

  1. Uses AnimatedSizeContainer for smooth transitions
  2. Integrates NumberStepper with appropriate min/max constraints (1-30 days)
  3. Shows helpful explanatory text with dynamic values
  4. Disables the toggle when no end date is set
  5. Uses the correct event handlers for state synchronization

The UX is intuitive and prevents invalid states.


921-925: Form submission state properly managed

The submit button correctly uses validationError for disabling and shows appropriate tooltips. The dirty state checking for updates (bounty && !isDirty) prevents unnecessary API calls.


647-658: Accessibility Verified – The NumberStepper already applies role="spinbutton" with aria-valuenow/valuemin/valuemax, keyboard handlers, and the increment/decrement buttons include aria-labels. No changes needed.

@steven-tey steven-tey merged commit f1f11a2 into main Sep 22, 2025
8 checks passed
@steven-tey steven-tey deleted the bounty-submission-timing branch September 22, 2025 18:03
@coderabbitai coderabbitai bot mentioned this pull request Sep 29, 2025
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