+
Skip to content

Conversation

devkiran
Copy link
Collaborator

@devkiran devkiran commented Aug 9, 2025

Summary by CodeRabbit

  • New Features

    • Full Bounties: create/edit/delete, listings, detail pages, partner claim/submit flow, submission review (approve/reject), file uploads, scheduled partner notifications, and workflow-driven award automation.
  • Notifications

    • New bounty-related emails (submitted/pending/approved/rejected/completed/new available) and workspace toggle for "New bounty submitted"; partner notification scheduling at bounty start.
  • Integrations

    • Webhooks for bounty.created/updated, Slack templates, and workflow triggers for leads/sales/commissions.
  • UI / UX

    • Sidebar/nav badges, new bounty cards, lists, forms, modals, and submission management UIs.
  • Tests

    • API and webhook tests covering bounty workflows.
  • Chores

    • Clean script now purges .next directories.

Copy link
Contributor

vercel bot commented Aug 9, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Aug 30, 2025 0:56am

Copy link
Contributor

coderabbitai bot commented Aug 9, 2025

Warning

Rate limit exceeded

@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e979bf7 and bd43161.

📒 Files selected for processing (1)
  • apps/web/tests/bounties/index.test.ts (1 hunks)

Walkthrough

Adds a full "bounties" feature: DB models and enums, APIs (CRUD, submissions, counts, partner-facing), workflow engine and triggers/actions, partner server actions and UI, email templates/webhooks, SWR hooks/types, cron notifier, utilities, tests, and related UI/component changes.

Changes

Cohort / File(s) Summary
Prisma schema & exports
packages/prisma/schema/.../*.prisma, packages/prisma/client.ts
Add Bounty, BountyGroup, BountySubmission models and enums; Workflow model and enums; wiring of relations to Program/Partner/Commission/User/ProgramEnrollment; re-export new enums/types.
Workspace APIs (bounties)
apps/web/app/(ee)/api/bounties/route.ts, apps/web/app/(ee)/api/bounties/[bountyId]/route.ts, apps/web/app/(ee)/api/bounties/count/submissions/route.ts
Add list/create, get/patch/delete bounty handlers and submissions-count endpoint with validation, transactions, plan gating, audit/webhook/qstash scheduling.
Submissions endpoints & helpers
apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.ts, apps/web/lib/api/bounties/*, apps/web/lib/api/groups/throw-if-invalid-group-ids.ts
Add submissions listing (partner vs submission flows), getPartnersWithBountySubmission raw SQL helper, getBountyWithDetails, and group-id validator.
Partner-profile API & partner hooks
apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts, apps/web/lib/swr/use-partner-program-bounties.ts
Partner-facing GET returning visible bounties with partner metrics and performanceCondition; SWR hook to fetch partner bounties.
Workflow engine & integrations
apps/web/lib/api/workflows/*, apps/web/lib/zod/schemas/workflows.ts, apps/web/lib/api/conversions/*, apps/web/lib/partners/create-partner-commission.ts
New executeWorkflows orchestration, awardBounty action, workflow schemas/util, and triggers invoked on leads/sales/commissions; createPartnerCommission gains skipWorkflow and may call executeWorkflows.
Partner server actions
apps/web/lib/actions/partners/*.ts
Add partner-authenticated actions: create submission, upload file (signed URL + rate limit), approve submission (create commission + email + audit), reject submission (cancel commission + email).
Cron notifier
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
QStash-verified cron endpoint to batch-email eligible partners at bounty start with paginated re-enqueueing.
Admin & partner UI
apps/web/app/**/program/bounties/**, apps/web/app/(ee)/partners.dub.co/**, apps/web/ui/partners/bounties/**
New pages and components: bounty list/detail, header, info, submissions table, add/edit sheet, action button, submission details sheet, claim modal, partner-bounty cards, performance/thumbnail components, filters and badges.
SWR hooks, types & zod schemas
apps/web/lib/swr/*, apps/web/lib/types.ts, apps/web/lib/zod/schemas/bounties.ts, apps/web/lib/zod/schemas/partner-profile.ts
Add useBounty, useBountySubmissionsCount, useBountySubmissionFilters, types for bounties/submissions/workflows, and comprehensive bounty-related Zod schemas.
Emails & webhook support
packages/email/src/templates/*.tsx, apps/web/lib/webhook/*, apps/web/lib/webhook/sample-events/*
New bounty email templates (submitted, pending, approved, rejected, completed, new-bounty-available), thumbnail component, webhook triggers (bounty.created/updated), sample payloads and types.
Audit logs & webhook types
apps/web/lib/api/audit-logs/schemas.ts, apps/web/lib/webhook/types.ts
Add bounty/bounty_submission audit actions and targets; extend webhook payload union to include bounty payload.
Sidebar & navigation
apps/web/ui/layout/sidebar/*, apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx
Add Bounties nav items and badges (pending submission counts / program counts); relocate Resources into Engagement group; extend SidebarNavData props.
Shared UI / components
packages/ui/src/{card-selector,progress-circle}.tsx, packages/ui/src/icons/nucleo/{trophy,heart}.tsx, packages/ui/src/menu-item.tsx, apps/web/ui/shared/*
Add CardSelector, ProgressCircle, trophy/heart icons, MenuItem.disabledTooltip support, AmountInput, AnimatedEmptyState.description typed ReactNode, and minor popover/client adjustments.
Utilities & small changes
apps/web/lib/api/create-id.ts, packages/utils/src/functions/*, apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts, apps/web/app/(ee)/app.dub.co/embed/referrals/utils.ts, package.json
Add allowed id prefixes ("bnty_","bnty_sub_","wf_"), arrayEqual util, currency formatter tweak, expose newBountySubmitted notification preference, aggregate referrals metrics, and extend root clean script.
Tests & harness
apps/web/tests/bounties/index.test.ts, apps/web/tests/webhooks/index.test.ts, apps/web/tests/utils/integration.ts
Add bounty integration tests, webhook schema updates for bounty events, and IntegrationHarness.deleteBounty helper.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Partner
  participant UI as Partner UI
  participant API as Server (partner actions)
  participant DB as Prisma
  participant Store as R2/Storage
  participant Mail as Email Service

  Partner->>UI: Open claim modal & attach image
  UI->>API: uploadBountySubmissionFileAction(programId,bountyId)
  API->>DB: validate enrollment & bounty
  API->>Store: request signed PUT URL
  API-->>UI: signedUrl + destinationUrl
  UI->>Store: PUT file to signedUrl
  UI->>API: createBountySubmissionAction(files, urls, description)
  API->>DB: create BountySubmission (pending)
  API-->>Mail: batch notify program owners (Pending) and notify partner (Submitted)
  API-->>UI: { success: true }
Loading
sequenceDiagram
  autonumber
  actor System
  participant Event as Lead/Sale Event
  participant Server as API
  participant WF as executeWorkflows
  participant DB as Prisma

  Event->>Server: process lead/sale
  Server->>WF: executeWorkflows({ programId, partnerId, trigger })
  WF->>DB: load workflows + enrollment metrics
  alt condition satisfied
    WF->>DB: execute action awardBounty -> create commission & bountySubmission
    DB-->>WF: commission + submission created
    WF-->>Server: action completed
  else
    WF-->>Server: no-op
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Poem

I twitch my whiskers, nose alight,
A shiny bounty hops in sight.
Workflows hum and trophies gleam,
Partners sprint to chase the dream.
Code and carrots, hop—rewards on stream! 🥕🏆

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

@steven-tey steven-tey changed the title Bounty FEAT: Bounties Aug 14, 2025
@steven-tey steven-tey changed the base branch from main to partner-groups August 14, 2025 04:45
Base automatically changed from partner-groups to main August 15, 2025 17:33
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

♻️ Duplicate comments (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1)

94-96: Stop logging partner emails (PII).

Do not print raw email addresses to logs.

-      console.log(
-        `Sending emails to ${programEnrollmentChunk.length} partners: ${programEnrollmentChunk.map(({ partner }) => partner.email).join(", ")}`
-      );
+      console.info(
+        `Sending ${programEnrollmentChunk.length} bounty emails (page ${page}, bounty ${bountyId}).`,
+      );
🧹 Nitpick comments (5)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (5)

20-21: Define a named batch size constant.

Improves readability and keeps batch size consistent.

 const MAX_PAGE_SIZE = 5000;
+
+const BATCH_SIZE = 100;

138-140: Report actual recipients, not enrollments, in completion log.

Aligns logs with real sends.

-    return logAndRespond(
-      `Finished sending emails to ${programEnrollments.length} partners for bounty ${bountyId}.`,
-    );
+    return logAndRespond(
+      `Finished sending emails to ${typeof recipients !== "undefined" ? recipients.length : programEnrollments.filter(({ partner }) => Boolean(partner.email)).length} partners for bounty ${bountyId}.`,
+    );

46-50: Return 404 for missing bounty.

Better semantics and easier to monitor.

-    if (!bounty) {
-      return logAndRespond(`Bounty ${bountyId} not found.`, {
-        logLevel: "error",
-      });
-    }
+    if (!bounty) {
+      return logAndRespond(`Bounty ${bountyId} not found.`, {
+        logLevel: "error",
+        status: 404,
+      });
+    }

141-145: Type-safe error logging in catch.

Avoids potential TS errors when useUnknownInCatchVariables is enabled.

-    await log({
-      message: "New bounties published cron failed. Error: " + error.message,
-      type: "errors",
-    });
+    const msg =
+      error instanceof Error ? error.message : JSON.stringify(error);
+    await log({
+      message: `New bounties published cron failed. Error: ${msg}`,
+      type: "errors",
+    });

97-121: Fail fast if email provider is not configured.

Optional, but avoids silently dropping emails when resend is undefined.

Consider adding a guard before sending:

+    if (!resend) {
+      return logAndRespond(
+        "Email provider not configured; cannot send bounty notifications.",
+        { status: 500, logLevel: "error" },
+      );
+    }

Would you like me to add this guard?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf55c0 and ce575ef.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (5)
apps/web/app/(ee)/api/bounties/route.ts (1)
  • POST (72-209)
apps/web/app/(ee)/api/cron/utils.ts (1)
  • logAndRespond (1-13)
packages/email/src/templates/new-bounty-available.tsx (1)
  • NewBountyAvailable (18-102)
packages/utils/src/constants/main.ts (1)
  • APP_DOMAIN_WITH_NGROK (20-25)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-181)
⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build

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

Caution

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

⚠️ Outside diff range comments (9)
apps/web/ui/partners/partner-application-sheet.tsx (1)

208-240: Harden createLink: normalize inputs and guard error paths

Current logic can mis-handle full URLs (with protocol) and may throw if the error payload isn’t shaped as expected. Also, it proceeds when program config is missing.

Apply this diff to make input handling and error surfacing robust:

-  const createLink = async (search: string) => {
-    if (!search) throw new Error("No link entered")
-
-    const shortKey = search.startsWith(program?.domain + "/")
-      ? search.substring((program?.domain + "/").length)
-      : search
-
-    const response = await fetch(`/api/links?workspaceId=${workspaceId}`, {
+  const createLink = async (search: string) => {
+    const raw = (search || "").trim()
+    if (!raw) throw new Error("No link entered")
+    if (!program?.id || !program?.domain || !program?.url) {
+      throw new Error("Program is not fully configured (id/domain/url).")
+    }
+
+    // Accept either a short key or a full short URL (with/without protocol)
+    let shortKey = raw
+    try {
+      // Handle full URL (https://domain/key or http://domain/key)
+      const u = new URL(raw)
+      if (u.host === program.domain && u.pathname.length > 1) {
+        shortKey = u.pathname.replace(/^\/+/, "")
+      }
+    } catch {
+      // Not a valid URL; try "domain/key" without protocol
+      const noProto = raw.replace(/^(https?:)?\/\//i, "")
+      const prefix = program.domain + "/"
+      if (noProto.startsWith(prefix)) {
+        shortKey = noProto.substring(prefix.length)
+      }
+    }
+
+    const response = await fetch(`/api/links?workspaceId=${workspaceId}`, {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
       },
       body: JSON.stringify({
-        domain: program?.domain,
+        domain: program.domain,
         key: shortKey,
-        url: program?.url,
+        url: program.url,
         trackConversion: true,
-        programId: program?.id,
-        folderId: program?.defaultFolderId,
+        programId: program.id,
+        folderId: program?.defaultFolderId,
       }),
     })
-
-    const result = await response.json()
-
-    if (!response.ok) {
-      const { error } = result
-      throw new Error(error.message)
-    }
-
-    setSelectedLinkId(result.id)
-
-    return result.id
+    let result: any = null
+    try {
+      result = await response.json()
+    } catch {
+      // noop; keep result as null
+    }
+    if (!response.ok) {
+      const message =
+        (result?.error?.message as string) ||
+        (result?.message as string) ||
+        "Failed to create link"
+      throw new Error(message)
+    }
+    setSelectedLinkId(result.id)
+    return result.id
   }
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (1)

313-322: Sorting bug: column id mismatch (“commissions” vs “totalCommissions”).

The defined column id is totalCommissions, so sorting on “Commissions” won’t work.

     sortableColumns: [
       "createdAt",
       "clicks",
       "leads",
       "conversions",
       "sales",
       "saleAmount",
-      "commissions",
+      "totalCommissions",
       "netRevenue",
     ],
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-clawback-sheet.tsx (1)

68-72: Avoid floating-point money errors; round to cents before sending.

Multiplying a JS float by 100 can produce 1–2¢ errors (e.g., 0.29 * 100 → 28.999…). Round to an integer minor unit.

-    await executeAsync({
-      ...data,
-      amount: data.amount * 100,
-      workspaceId,
-    });
+    const amountInCents = Math.round(Number(data.amount) * 100);
+    await executeAsync({
+      ...data,
+      amount: amountInCents,
+      workspaceId,
+    });
packages/email/src/templates/program-application-reminder.tsx (1)

34-36: Fix product-name inconsistency: “Dub Partner” vs “Dub Partners”

Use “Dub Partners” consistently (Preview currently says “Dub Partner”). Also aligns with the URL and product naming elsewhere.

Apply this diff:

-        Your application to {program.name} has been saved, but you still need to
-        create your Dub Partner account to complete your application.
+        Your application to {program.name} has been saved, but you still need to
+        create your Dub Partners account to complete your application.

Also applies to: 49-52

apps/web/lib/actions/partners/mark-commission-duplicate.ts (1)

52-64: Make payout + commission updates atomic; guard against negative amounts

Two writes occur separately and can leave data inconsistent if one fails or under concurrency. Wrap in a single Prisma transaction and clamp the revised payout amount to ≥ 0.

-    if (commission.payout) {
-      const earnings = commission.earnings;
-      const revisedAmount = commission.payout.amount - earnings;
-
-      await prisma.payout.update({
-        where: {
-          id: commission.payout.id,
-        },
-        data: {
-          amount: revisedAmount,
-        },
-      });
-    }
-
-    await prisma.commission.update({
-      where: {
-        id: commission.id,
-      },
-      data: {
-        status: "duplicate",
-        payoutId: null,
-      },
-    });
+    await prisma.$transaction(async (tx) => {
+      if (commission.payout) {
+        const earnings = commission.earnings;
+        const revisedAmount = Math.max(
+          0,
+          commission.payout.amount - earnings,
+        );
+        await tx.payout.update({
+          where: { id: commission.payout.id },
+          data: { amount: revisedAmount },
+        });
+      }
+      await tx.commission.update({
+        where: { id: commission.id },
+        data: {
+          status: "duplicate",
+          payoutId: null,
+        },
+      });
+    });

If these fields are Prisma Decimal, prefer precise arithmetic:

// Example if using Prisma.Decimal
import { Prisma } from "@prisma/client";
const revisedAmount = Prisma.Decimal.max(
  new Prisma.Decimal(0),
  new Prisma.Decimal(commission.payout.amount).minus(commission.earnings),
);

Also applies to: 66-74

apps/web/ui/shared/animated-empty-state.tsx (1)

61-73: Add rel when using target="_blank" to prevent reverse tabnabbing

Include rel="noopener noreferrer" with external targets.

-          <Link
+          <Link
             href={learnMoreHref}
             target={learnMoreTarget}
+            rel={learnMoreTarget === "_blank" ? "noopener noreferrer" : undefined}
             className={cn(
apps/web/lib/api/create-id.ts (1)

2-2: Harden randomness source across runtimes (Node/web).

crypto.getRandomValues isn’t available on the Node “crypto” module import; prefer global webcrypto (or Node’s crypto.webcrypto) with a randomFillSync fallback to avoid runtime failures and ensure strong entropy.

-import crypto from "crypto";
+import { webcrypto as nodeWebCrypto, randomFillSync } from "crypto";
@@
-  // Randomness (80 bits = 10 bytes)
-  crypto.getRandomValues(buf.subarray(6));
+  // Randomness (80 bits = 10 bytes)
+  const wc: Crypto | undefined =
+    typeof globalThis !== "undefined"
+      ? ((globalThis as any).crypto as Crypto | undefined) ?? nodeWebCrypto
+      : nodeWebCrypto;
+  if (wc?.getRandomValues) {
+    wc.getRandomValues(buf.subarray(6));
+  } else {
+    // Node fallback
+    randomFillSync(buf, 6);
+  }

Also applies to: 54-54

apps/web/ui/partners/partner-details-sheet.tsx (1)

152-159: Fix cents-based decimal check for Revenue formatting

saleAmount is stored in cents; use % 100 to decide if there are fractional dollars.

-                    : currencyFormatter(partner.saleAmount / 100, {
-                        minimumFractionDigits:
-                          partner.saleAmount % 1 === 0 ? 0 : 2,
-                        maximumFractionDigits: 2,
-                      }),
+                    : currencyFormatter(partner.saleAmount / 100, {
+                        minimumFractionDigits:
+                          partner.saleAmount % 100 === 0 ? 0 : 2,
+                        maximumFractionDigits: 2,
+                      }),
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)

339-347: Guard getSubscriptionProductId when charge.subscription is null for one-time payments.

charge.subscription as string will pass null at runtime on one-time payments and may throw inside getSubscriptionProductId. Guard and pass undefined or skip.

-    const productId = await getSubscriptionProductId({
-      stripeSubscriptionId: charge.subscription as string,
-      stripeAccountId,
-      livemode: event.livemode,
-    });
+    const productId = charge.subscription
+      ? await getSubscriptionProductId({
+          stripeSubscriptionId: charge.subscription,
+          stripeAccountId,
+          livemode: event.livemode,
+        })
+      : undefined;

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

♻️ Duplicate comments (16)
apps/web/ui/partners/bounties/bounty-logic.tsx (2)

55-62: Reset value when attribute changes to avoid stale/invalid state.

Clears the numeric value when switching between count/currency attributes to prevent mixed units.

-  const { control, watch } = useAddEditBountyForm();
+  const { control, watch, setValue, clearErrors } = useAddEditBountyForm();
-              <InlineBadgePopoverMenu
-                selectedValue={field.value}
-                onSelect={field.onChange}
+              <InlineBadgePopoverMenu
+                selectedValue={field.value}
+                onSelect={(newAttr) => {
+                  field.onChange(newAttr);
+                  setValue("performanceCondition.value", undefined, {
+                    shouldDirty: true,
+                    shouldValidate: true,
+                  });
+                  clearErrors?.("performanceCondition.value");
+                }}
                 items={WORKFLOW_ATTRIBUTES.map((attribute) => ({
                   text: WORKFLOW_ATTRIBUTE_LABELS[attribute],
                   value: attribute,
                 }))}
               />

Also applies to: 24-24


72-79: “0” is valid but currently marked invalid (falsy check).

Don’t treat 0 as invalid.

               text={
                 value
                   ? isCurrencyAttribute(attribute)
                     ? currencyFormatter(value)
-                    : value
+                    : value
                   : "amount"
               }
-              invalid={!value}
+              invalid={value === undefined || value === null}
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (5)

220-221: Round dollars→cents to avoid float precision errors.

Prevent off-by-one cents (e.g., 12.34*100 → 1233.999…).

-    data.rewardAmount = data.rewardAmount * 100;
+    data.rewardAmount = Math.round(data.rewardAmount * 100);
-        value: isCurrency ? condition.value * 100 : condition.value,
+        value: isCurrency ? Math.round(condition.value * 100) : condition.value,

Also applies to: 241-242


245-247: Use submitted data.type instead of potentially stale watched value.

Prevents mis-routing if type changed just before submit.

-    } else if (type === "submission") {
+    } else if (data.type === "submission") {
       data.performanceCondition = null;
     }

332-389: “Add end date” toggle is hidden when unchecked → impossible to enable.

Render the Switch always; only collapse the picker.

-                    <AnimatedSizeContainer
-                      height
-                      transition={{ ease: "easeInOut", duration: 0.2 }}
-                      className={!hasEndDate ? "hidden" : ""}
-                      style={{ display: !hasEndDate ? "none" : "block" }}
-                    >
-                      <div className="flex items-center gap-4">
-                        <Switch
-                          fn={setHasEndDate}
-                          checked={hasEndDate}
-                          trackDimensions="w-8 h-4"
-                          thumbDimensions="w-3 h-3"
-                          thumbTranslate="translate-x-4"
-                        />
-                        <div className="flex flex-col gap-1">
-                          <h3 className="text-sm font-medium text-neutral-700">
-                            Add end date
-                          </h3>
-                        </div>
-                      </div>
-
-                      {hasEndDate && (
+                    <div className="mt-2 flex items-center gap-4">
+                      <Switch
+                        fn={setHasEndDate}
+                        checked={hasEndDate}
+                        trackDimensions="w-8 h-4"
+                        thumbDimensions="w-3 h-3"
+                        thumbTranslate="translate-x-4"
+                      />
+                      <div className="flex flex-col gap-1">
+                        <h3 className="text-sm font-medium text-neutral-700">
+                          Add end date
+                        </h3>
+                      </div>
+                    </div>
+
+                    <AnimatedSizeContainer
+                      height
+                      transition={{ ease: "easeInOut", duration: 0.2 }}
+                    >
+                      {hasEndDate && (
                         <div className="mt-6 p-px">
                           <SmartDateTimePicker
                             value={watch("endsAt")}
                             onChange={(date) => {
                               setValue("endsAt", date, {
                                 shouldDirty: true,
                               });
                             }}
                             label="End date"
                             placeholder='E.g. "in 3 months"'
                           />
                         </div>
                       )}
-                    </AnimatedSizeContainer>
+                    </AnimatedSizeContainer>

347-347: Remove stray “test” text from error slot.

-                      {errors.startsAt && "test"}
+                      {/* Optional: render a proper message if rules/resolver provide one */}
+                      {errors.startsAt && null}

402-406: Align client-side rules with Zod schema (min 1).

Server requires rewardAmount >= 1.

-                          rules={{
-                            required: true,
-                            min: 0,
-                          }}
+                          rules={{
+                            required: "Reward amount is required",
+                            min: { value: 1, message: "Reward amount must be greater than 1" },
+                          }}
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx (2)

143-151: Show $0.00 instead of “-” for zero earnings.

Truthiness check hides valid zero values. Use null/undefined check.

-                  value: commission?.earnings
+                  value: commission?.earnings != null
                     ? currencyFormatter(commission.earnings / 100, {
                       minimumFractionDigits: 2,
                       maximumFractionDigits: 2,
                     })
                     : "-",

203-221: Add missing key prop to mapped URL items.

Fix Biome’s useJsxKeyInIterable and React reconciliation.

-                      {submission.urls?.map((url) => (
-                        <div className="relative">
+                      {submission.urls?.map((url, idx) => (
+                        <div className="relative" key={`${idx}-${url}`}>
apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.ts (1)

105-113: Don’t spread ProgramEnrollment onto partner; preserve partner.id and fill required metrics.

Spreading risks id clobbering and missing metrics (leads, conversions, saleAmount). Build partner explicitly and default metrics to 0 to satisfy BountySubmissionExtendedSchema.

-  return submissions.map((submission) => {
-    return {
-      partner: {
-        ...submission.programEnrollment?.partner,
-        ...submission.programEnrollment,
-        // here's we're making sure the programEnrollment ID doesn't override the actual partner ID
-        // TODO: this is a bit messy, we should refactor this
-        id: submission.programEnrollment?.partnerId,
-      },
-      submission,
-      commission: submission.commission,
-      user: submission.user,
-    };
-  });
+  return submissions.map((s) => {
+    const pe = s.programEnrollment!;
+    return {
+      partner: {
+        // core partner fields
+        id: pe.partnerId,
+        name: pe.partner?.name ?? null,
+        email: pe.partner?.email ?? null,
+        image: pe.partner?.image ?? null,
+        country: pe.partner?.country ?? null,
+        payoutsEnabledAt: pe.partner?.payoutsEnabledAt ?? null,
+        bannedAt: pe.partner?.bannedAt ?? null,
+        bannedReason: pe.partner?.bannedReason ?? null,
+        // enrollment
+        groupId: pe.groupId ?? null,
+        status: pe.status ?? null,
+        totalCommissions: pe.totalCommissions ?? 0,
+        // metrics not joined in this path
+        leads: 0,
+        conversions: 0,
+        saleAmount: 0,
+      },
+      submission: s,
+      commission: s.commission,
+      user: s.user,
+    };
+  });
apps/web/lib/actions/partners/approve-bounty-submission.ts (4)

23-27: Enforce role-based authorization before approval.

Restrict to owner/admin (or your chosen roles) to prevent unauthorized payouts.

   .action(async ({ parsedInput, ctx }) => {
     const { workspace, user } = ctx;
+    if (!["owner", "admin"].includes(workspace.role)) {
+      throw new Error("Forbidden: insufficient permissions to approve bounties.");
+    }

44-46: Idempotency: also guard on existing commissionId.

Prevents duplicate approvals via alternative code paths.

-    if (bountySubmission.status === "approved") {
+    if (bountySubmission.status === "approved" || bountySubmission.commissionId) {
       throw new Error("Bounty submission already approved.");
     }

48-57: Validate reward amount > 0.

Avoid zero/invalid payouts.

     if (bounty.type === "performance") {
       throw new Error("Performance based bounties cannot be approved.");
     }
 
+    if (!bounty.rewardAmount || bounty.rewardAmount <= 0) {
+      throw new Error("Bounty reward amount is missing or invalid.");
+    }

52-78: Race-safe, idempotent approval to prevent duplicate commissions.

Use a conditional status flip before commission creation; roll back on failure. Optionally pass eventId=submissionId for dedupe.

-    const commission = await createPartnerCommission({
+    // Optimistic guard to avoid double-approval
+    const updated = await prisma.bountySubmission.updateMany({
+      where: { id: submissionId, status: { not: "approved" }, commissionId: null },
+      data: {
+        status: "approved",
+        reviewedAt: new Date(),
+        userId: user.id,
+        rejectionNote: null,
+        rejectionReason: null,
+      },
+    });
+    if (updated.count === 0) {
+      throw new Error("Bounty submission already approved.");
+    }
+
+    const commission = await createPartnerCommission({
       event: "custom",
       partnerId: bountySubmission.partnerId,
       programId: bountySubmission.programId,
       amount: bounty.rewardAmount,
       quantity: 1,
       user,
-      description: `Commission for successfully completed "${bounty.name}" bounty.`,
+      eventId: submissionId,
+      description: `Commission for successfully completed "${bounty.name || "Untitled Bounty"}" bounty.`,
     });
 
     if (!commission) {
-      throw new Error("Failed to create commission for the bounty submission.");
+      // rollback status flip
+      await prisma.bountySubmission.update({
+        where: { id: submissionId },
+        data: { status: "pending", reviewedAt: null, userId: null },
+      });
+      throw new Error("Failed to create commission for the bounty submission.");
     }
 
-    await prisma.bountySubmission.update({
-      where: {
-        id: submissionId,
-      },
-      data: {
-        status: "approved",
-        reviewedAt: new Date(),
-        userId: user.id,
-        rejectionNote: null,
-        rejectionReason: null,
-        commissionId: commission.id,
-      },
-    });
+    await prisma.bountySubmission.update({
+      where: { id: submissionId },
+      data: { commissionId: commission.id },
+    });
packages/prisma/schema/bounty.prisma (2)

38-39: Avoid cascading delete of Bounty when Workflow is removed.

Prefer onDelete: SetNull to prevent accidental bounty loss.

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

75-76: Avoid deleting submissions when a Commission is removed.

Use SetNull so submission history remains.

-  commission        Commission?        @relation(fields: [commissionId], references: [id], onDelete: Cascade)
+  commission        Commission?        @relation(fields: [commissionId], references: [id], onDelete: SetNull)
🧹 Nitpick comments (8)
apps/web/lib/api/bounties/generate-bounty-name.ts (4)

6-12: Add explicit return type and document money units.

Make intent and units unambiguous for callers and reviewers.

-export const generateBountyName = ({
+/**
+ * rewardAmount is in cents. If condition.attribute is currency-based,
+ * condition.value is also expected in cents.
+ */
+export const generateBountyName = ({
   rewardAmount,
   condition,
 }: {
   rewardAmount: number;
   condition?: WorkflowCondition | null;
-}) => {
+}): string => {

3-3: Support non-USD workspaces and avoid silently rounding cents.

Today we always format in USD and with 0 decimals (per currencyFormatter defaults), which can misrepresent values (e.g., $12.50 → $13) and breaks for non-USD workspaces. Accept an optional currency and format up to 2 fraction digits.

-import { currencyFormatter, nFormatter } from "@dub/utils";
+import { currencyFormatter, nFormatter } from "@dub/utils";
@@
-export const generateBountyName = ({
-  rewardAmount,
-  condition,
-}: {
-  rewardAmount: number;
-  condition?: WorkflowCondition | null;
-}) => {
+export const generateBountyName = ({
+  rewardAmount,
+  condition,
+  currency,
+}: {
+  rewardAmount: number;
+  condition?: WorkflowCondition | null;
+  currency?: string; // ISO 4217, e.g. "USD"
+}): string => {
+  const cf = (v: number) =>
+    currencyFormatter(v, { maximumFractionDigits: 2 }, currency);
@@
-  if (!condition) {
-    return `Earn ${currencyFormatter(rewardAmount / 100)}`;
-  }
+  if (!condition) {
+    return `Earn ${cf(rewardAmount / 100)}`;
+  }
@@
-  const rewardAmountFormatted = currencyFormatter(rewardAmount / 100);
+  const rewardAmountFormatted = cf(rewardAmount / 100);
@@
-  const valueFormatted = isCurrency
-    ? `${currencyFormatter(condition.value / 100)} in`
+  const valueFormatted = isCurrency
+    ? `${cf(condition.value / 100)} in`
     : `${nFormatter(condition.value, { full: true })}`;

Also applies to: 6-12, 14-14, 18-18, 21-21


19-24: Fix grammar for singular metrics and add a safe fallback label.

“1 Conversions” reads awkwardly. Singularize non-currency labels when value === 1 and guard against unmapped attributes to avoid “undefined”.

-  const attributeLabel = WORKFLOW_ATTRIBUTE_LABELS[condition.attribute];
-  const valueFormatted = isCurrency
+  const attributeLabelRaw =
+    WORKFLOW_ATTRIBUTE_LABELS[condition.attribute] ?? "Metric";
+  const attributeLabel =
+    !isCurrency &&
+    condition.value === 1 &&
+    attributeLabelRaw.endsWith("s")
+      ? attributeLabelRaw.slice(0, -1)
+      : attributeLabelRaw;
+  const valueFormatted = isCurrency
     ? `${currencyFormatter(condition.value / 100)} in`
     : `${nFormatter(condition.value, { full: true })}`;
 
-  return `Earn ${rewardAmountFormatted} after generating ${valueFormatted} ${attributeLabel}`;
+  return `Earn ${rewardAmountFormatted} after generating ${valueFormatted} ${attributeLabel}`;

6-25: Add unit tests for name generation edge cases.

Cover: (a) no condition, (b) currency attrs (Revenue/Commissions) with non-integer dollars, (c) non-currency attrs with 1 vs >1, (d) unknown attribute fallback.

I can scaffold tests for these cases—want me to open a follow-up PR?

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

72-76: Improve readability of non-currency amounts (thousands separators).

Format counts for easier scanning.

-                    : value
+                    : Intl.NumberFormat("en-US").format(value as number)

103-113: Use number input with appropriate step for better UX/validation.

Enables numeric keypad on mobile and native constraints.

       <input
+        type="number"
+        inputMode={isCurrency ? "decimal" : "numeric"}
         className={cn(
           "block w-full rounded-md border-neutral-300 px-1.5 py-1 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:w-32 sm:text-sm",
           isCurrency ? "pl-4 pr-12" : "pr-7",
         )}
         {...register("performanceCondition.value", {
           required: true,
           setValueAs: (value: string) => (value === "" ? undefined : +value),
-          min: 0,
+          min: 0,
           onChange: handleMoneyInputChange,
         })}
+        step={isCurrency ? "0.01" : "1"}
       />
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx (2)

177-191: Improve file item accessibility and key stability.

Use a stable key and avoid redundant/meaningless img alt.

-                      {submission.files!.map((file, idx) => (
+                      {submission.files!.map((file, idx) => (
                         <a
-                          key={idx}
+                          key={file.url ?? idx}
                           className="border-border-subtle hover:border-border-default group relative flex size-14 items-center justify-center rounded-md border bg-white"
                           target="_blank"
                           href={file.url}
                           rel="noopener noreferrer"
                         >
                           <div className="relative size-full overflow-hidden rounded-md">
-                            <img src={file.url} alt="object-cover" />
+                            <img src={file.url} alt="" />
                           </div>

264-270: Use type="button" (no form).

This isn’t inside a form; avoid accidental submits if nesting changes.

-              <Button
-                type="submit"
+              <Button
+                type="button"
                 variant="primary"
                 text="Approve"
                 loading={isApprovingBountySubmission}
                 onClick={() => setShowApproveBountySubmissionModal(true)}
               />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ce575ef and ba9c55b.

📒 Files selected for processing (16)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1 hunks)
  • apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.ts (1 hunks)
  • apps/web/app/(ee)/api/bounties/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/page.tsx (1 hunks)
  • apps/web/lib/actions/partners/approve-bounty-submission.ts (1 hunks)
  • apps/web/lib/actions/partners/create-bounty-submission.ts (1 hunks)
  • apps/web/lib/actions/partners/reject-bounty-submission.ts (1 hunks)
  • apps/web/lib/api/audit-logs/schemas.ts (3 hunks)
  • apps/web/lib/api/bounties/generate-bounty-name.ts (1 hunks)
  • apps/web/lib/api/workflows/execute-award-bounty-action.ts (1 hunks)
  • apps/web/lib/zod/schemas/partner-profile.ts (3 hunks)
  • apps/web/ui/partners/bounties/bounty-logic.tsx (1 hunks)
  • packages/prisma/schema/bounty.prisma (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/page.tsx
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/lib/api/workflows/execute-award-bounty-action.ts
  • apps/web/lib/actions/partners/reject-bounty-submission.ts
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
  • apps/web/lib/actions/partners/create-bounty-submission.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/lib/zod/schemas/partner-profile.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.

Applied to files:

  • apps/web/lib/actions/partners/approve-bounty-submission.ts
  • apps/web/lib/api/audit-logs/schemas.ts
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.

Applied to files:

  • apps/web/lib/actions/partners/approve-bounty-submission.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
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/ui/partners/bounties/bounty-logic.tsx
📚 Learning: 2025-08-26T15:03:05.381Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/ui/partners/bounties/bounty-logic.tsx:88-96
Timestamp: 2025-08-26T15:03:05.381Z
Learning: In bounty forms, currency values are stored in cents in the backend but converted to dollars when loaded into forms, and converted back to cents when saved. The form logic works entirely with dollar amounts. Functions like generateBountyName that run during save logic receive cent values and need to divide by 100, but display logic within the form should format dollar values directly.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/ui/partners/bounties/bounty-logic.tsx
🧬 Code graph analysis (7)
apps/web/lib/api/bounties/generate-bounty-name.ts (4)
apps/web/lib/types.ts (1)
  • WorkflowCondition (542-542)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (1-13)
apps/web/lib/api/workflows/utils.ts (1)
  • isCurrencyAttribute (3-4)
apps/web/lib/zod/schemas/workflows.ts (1)
  • WORKFLOW_ATTRIBUTE_LABELS (16-24)
apps/web/lib/actions/partners/approve-bounty-submission.ts (4)
apps/web/lib/partners/create-partner-commission.ts (1)
  • createPartnerCommission (24-321)
apps/web/lib/api/audit-logs/record-audit-log.ts (1)
  • recordAuditLog (43-63)
apps/web/lib/zod/schemas/bounties.ts (1)
  • BountySubmissionSchema (81-92)
packages/email/src/templates/bounty-approved.tsx (1)
  • BountyApproved (18-94)
apps/web/app/(ee)/api/bounties/[bountyId]/submissions/route.ts (5)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
  • GET (22-45)
apps/web/app/(ee)/api/bounties/route.ts (1)
  • GET (28-70)
apps/web/lib/zod/schemas/bounties.ts (2)
  • getBountySubmissionsQuerySchema (140-149)
  • BountySubmissionExtendedSchema (94-123)
apps/web/lib/api/bounties/get-partners-with-bounty-submission.ts (1)
  • getPartnersWithBountySubmission (13-107)
apps/web/lib/types.ts (1)
  • BountySubmissionsQueryFilters (562-564)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (10)
apps/web/lib/types.ts (3)
  • BountyExtendedProps (531-531)
  • BountyProps (529-529)
  • BountySubmissionRequirement (539-540)
apps/web/lib/zod/schemas/bounties.ts (1)
  • createBountySchema (26-44)
packages/ui/src/card-selector.tsx (2)
  • CardSelectorOption (8-13)
  • CardSelector (26-108)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/lib/swr/use-api-mutation.ts (1)
  • useApiMutation (36-123)
apps/web/lib/api/workflows/utils.ts (1)
  • isCurrencyAttribute (3-4)
apps/web/lib/zod/schemas/workflows.ts (1)
  • workflowConditionSchema (59-63)
apps/web/ui/shared/amount-input.tsx (1)
  • AmountInput (19-87)
apps/web/ui/partners/bounties/bounty-logic.tsx (1)
  • BountyLogic (23-87)
apps/web/ui/partners/groups/groups-multi-select.tsx (1)
  • GroupsMultiSelect (31-246)
apps/web/ui/partners/bounties/bounty-logic.tsx (6)
apps/web/lib/zod/schemas/workflows.ts (1)
  • WORKFLOW_ATTRIBUTES (9-14)
apps/web/lib/types.ts (1)
  • WorkflowConditionAttribute (544-544)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (1)
  • useAddEditBountyForm (52-52)
apps/web/ui/shared/inline-badge-popover.tsx (3)
  • InlineBadgePopover (33-76)
  • InlineBadgePopoverMenu (85-174)
  • InlineBadgePopoverContext (25-31)
apps/web/lib/api/workflows/utils.ts (1)
  • isCurrencyAttribute (3-4)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (1-13)
apps/web/lib/api/audit-logs/schemas.ts (1)
apps/web/lib/zod/schemas/bounties.ts (2)
  • BountySchema (59-70)
  • BountySubmissionSchema (81-92)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx (7)
apps/web/lib/types.ts (1)
  • BountySubmissionProps (535-537)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/ui/partners/reject-bounty-submission-modal.tsx (1)
  • useRejectBountySubmissionModal (169-192)
apps/web/lib/actions/partners/approve-bounty-submission.ts (1)
  • approveBountySubmissionAction (20-118)
apps/web/ui/partners/partner-info-section.tsx (1)
  • PartnerInfoSection (13-119)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-status-badges.ts (1)
  • BOUNTY_SUBMISSION_STATUS_BADGES (3-22)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (1-13)
🪛 Biome (2.1.2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx

[error] 204-204: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (5)
apps/web/lib/api/bounties/generate-bounty-name.ts (2)

24-25: Overall: clean, readable, and side-effect free.

Logic is straightforward, early-return is clear, and the output string reads well for the main cases.


1-4: Imports and schema contracts verified
nFormatter and currencyFormatter are re-exported from @dub/utils root; workflowConditionSchema defines value as a number; TypeScript enforces that WORKFLOW_ATTRIBUTE_LABELS covers every WorkflowConditionAttribute.

apps/web/lib/api/audit-logs/schemas.ts (2)

81-87: LGTM: added bounty actions to audit action enum.

Action surface looks consistent with existing events.


171-181: LGTM: target shapes for bounty and bounty_submission.

Using full schemas provides rich metadata; aligns with recorders.

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

23-25: Schema consistency: is bounty.name optional?

Prisma has name String (required) but Zod BountySchema allows name: string | null. Please reconcile (make Prisma optional or tighten Zod).

Would you like a patch aligning both sides (and a migration) to your intended constraint?

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

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载