Skip to content

Allow targeting campaigns and bounties by partner tags - #4221

Open
devkiran wants to merge 43 commits into
dynamic-bounty-start-datefrom
limt-bounties-campaigns-by-tag
Open

Allow targeting campaigns and bounties by partner tags#4221
devkiran wants to merge 43 commits into
dynamic-bounty-start-datefrom
limt-bounties-campaigns-by-tag

Conversation

@devkiran

@devkiran devkiran commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added partner-tag eligibility for bounties and campaigns, including selection, filtering, duplication, and partner-facing visibility.
    • Added support for multiple workflow conditions with new validation and “less than or equal to” comparisons.
    • Improved eligibility summaries and recipient selectors in the dashboard.
  • Bug Fixes

    • Preserved relative bounty end dates during unrelated edits.
    • Improved partner counts, submission eligibility, notifications, and draft submissions when tags are applied.
  • Improvements

    • Standardized campaign and bounty response data.
    • Streamlined campaign scheduling and workflow event handling.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Jul 31, 2026 5:12am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

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

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc561e76-a500-4655-b745-27188c98430d

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds partner-tag eligibility to bounties and campaigns, supports multiple workflow trigger conditions, introduces event-based workflow execution, centralizes response transformation and campaign scheduling, and updates related APIs, UI components, persistence models, and tests.

Changes

Eligibility and workflow modernization

Layer / File(s) Summary
Eligibility and workflow contracts
apps/web/prisma/schema/*, apps/web/lib/zod/schemas/*, apps/web/lib/api/workflows/*, apps/web/lib/bounty/api/transform-bounty.ts, packages/utils/src/functions/*
Partner-tag relations, plural workflow conditions, workflow metadata, validation helpers, transformers, and the pluck utility are added or updated.
Bounty partner-tag eligibility
apps/web/app/(ee)/api/bounties/*, apps/web/lib/bounty/api/*, apps/web/ui/partners/bounties/*, apps/web/ui/partners/*
Bounty creation, updates, retrieval, submission checks, cron processing, and UI eligibility selectors now support partner tags alongside groups.
Campaign audience and scheduling
apps/web/app/(ee)/api/campaigns/*, apps/web/lib/api/campaigns/*, apps/web/app/app.dub.co/.../program/campaigns/*
Campaign APIs and editor flows persist partner tags, manage arrays of trigger conditions, validate conditions, duplicate audiences, and delegate schedule management.
Event-driven workflow execution
apps/web/lib/api/workflows/*, apps/web/app/(ee)/api/stripe/*, apps/web/lib/api/conversions/*, apps/web/lib/integrations/shopify/*
Workflow selection now uses event-derived attributes, enrollment tag data, and updated lead, sale, commission, and enrollment event payloads.
Regression coverage and integrations
apps/web/tests/*, apps/web/lib/webhook/sample-events/*, apps/web/scripts/*, packages/email/src/index.ts
Tests, fixtures, seed scripts, email responses, and partner-tag update side effects are updated for the new data and workflow contracts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • dubinc/dub#4187: Both changes modify bounty PATCH timing behavior around start and end dates.
  • dubinc/dub#4212: Both changes modify bounty PATCH performance-condition handling.
  • dubinc/dub#4241: Both changes modify bounty PATCH behavior connected to draft-submission scheduling.

Suggested reviewers: pepeladeira, steven-tey

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding partner-tag targeting for campaigns and bounties.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch limt-bounties-campaigns-by-tag

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (1)
apps/web/scripts/programs/backfill-reuse-commission.ts (1)

320-336: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

event: "commissionRecorded" doesn't match the metrics passed here.

EVENT_ATTRIBUTES.commissionRecorded resolves to totalCommissions/partnerGroup (apps/web/lib/api/workflows/execute-workflows.ts), yet this call supplies leads, saleAmount, and conversions and no commission total — so lead/sale workflows won't be selected and commission workflows get no value. Given commissionType, the event should likely be derived (leadRecorded vs saleRecorded).

🐛 Suggested fix
       executeWorkflows({
-        event: "commissionRecorded",
+        event: commissionType === "lead" ? "leadRecorded" : "saleRecorded",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/scripts/programs/backfill-reuse-commission.ts` around lines 320 -
336, Update the workflow event in the commissionType branch to use the event
matching the commission type: leadRecorded for leads and saleRecorded for sales.
Keep the existing leads, saleAmount, and conversions metrics aligned with those
event definitions, and do not use commissionRecorded for this path.
🧹 Nitpick comments (10)
apps/web/lib/api/workflows/send-campaign/execute.ts (2)

361-386: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the stray await inside Promise.all.

The await in the ternary makes the branch resolve before Promise.all is even constructed, which is misleading (and inconsistent with the non-commission branch returning a promise).

♻️ Proposed cleanup
     commissions
-      ? await prisma.commission.aggregate({
+      ? prisma.commission.aggregate({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/send-campaign/execute.ts` around lines 361 - 386,
Remove the stray await before prisma.commission.aggregate in the commissions
ternary within the Promise.all call, returning the aggregate promise directly
while preserving the existing Promise.resolve fallback for the non-commission
branch.

439-453: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

take: 1000 silently drops enrollments beyond the cap.

Programs with a large enrollment burst in a 12h window will never receive the campaign for the overflow, with no signal. Consider cursor pagination, or at minimum log when the result count hits the limit so it is observable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/send-campaign/execute.ts` around lines 439 - 453,
Update the programEnrollment query in the campaign execution flow to avoid
silently excluding enrollments beyond take: 1000, preferably by fetching all
matching records through cursor pagination. If retaining the cap, detect when
the result reaches the limit and emit an explicit diagnostic log indicating
possible overflow.
apps/web/lib/api/campaigns/transform-campaign.ts (1)

16-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the payload type from campaignEligibilityIncludes to avoid drift.

The include shape is written twice (value + type literal); adding a field to one will silently diverge from the other.

♻️ Suggested refactor
 export type TransformCampaignInput = Prisma.CampaignGetPayload<{
-  include: {
-    groups: {
-      select: {
-        groupId: true;
-      };
-    };
-    partnerTags: {
-      select: {
-        partnerTagId: true;
-      };
-    };
-    workflow: {
-      select: {
-        triggerConditions: true;
-      };
-    };
-  };
+  include: typeof campaignEligibilityIncludes & {
+    workflow: {
+      select: {
+        triggerConditions: true;
+      };
+    };
+  };
 }>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/campaigns/transform-campaign.ts` around lines 16 - 34,
Update TransformCampaignInput to derive its Prisma payload type from the
existing campaignEligibilityIncludes symbol instead of duplicating the include
literal, ensuring type and runtime include shapes remain synchronized.
apps/web/tests/workflows/send-campaign-workflow.test.ts (1)

858-871: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Negative assertion can pass vacuously.

The positive case polls up to 90s via verifyCampaignSent, yet here emails are checked immediately after triggering. If delivery is async, this assertion passes regardless of whether an email is eventually sent. Add a short wait (or re-check after a delay) before asserting emptiness, and mirror the notification-email cleanup used by the AND-match test in onTestFinished.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/tests/workflows/send-campaign-workflow.test.ts` around lines 858 -
871, Update the negative campaign notification test around the workflow trigger
and emailsSent assertion to wait briefly or re-check after a delay before
asserting no emails were sent, preventing a vacuous immediate pass. Also add the
same notification-email cleanup used by the AND-match test to this test’s
onTestFinished handler.
apps/web/lib/api/workflows/utils.ts (1)

49-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the result object from WORKFLOW_DATA_REQUIREMENTS to avoid drift.

Hardcoding the two keys means adding a third requirement silently returns an incomplete record (the Record<WorkflowDataRequirement, boolean> annotation would catch it, but only if the constant is also updated in lockstep).

return Object.fromEntries(
  WORKFLOW_DATA_REQUIREMENTS.map((r) => [r, requirements.has(r)]),
) as Record<WorkflowDataRequirement, boolean>;

(requires exporting WORKFLOW_DATA_REQUIREMENTS from attribute-definitions.ts)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/utils.ts` around lines 49 - 52, Update the result
construction in the workflow requirements helper to derive every key from
WORKFLOW_DATA_REQUIREMENTS, mapping each requirement to whether requirements
contains it, instead of hardcoding commissions and partnerLinkStats. Export
WORKFLOW_DATA_REQUIREMENTS from attribute-definitions.ts and retain the
Record<WorkflowDataRequirement, boolean> typing for the generated object.
apps/web/lib/api/workflows/check-workflow-conditions.ts (1)

28-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Per-condition validation is now duplicated with validateWorkflowConditions.

Lines 28–86 replicate the server-side loop in apps/web/lib/api/workflows/validate-workflow-conditions.ts (lines 77–142) almost verbatim; the two will drift (the server also enforces sendCampaign duplicate-attribute/exclusivity rules that this checker doesn't). Consider extracting a single collectConditionErrors() helper and having validateWorkflowConditions map the returned messages to DubApiError.

Also, Line 72's condition.value === undefined is redundant — == null already covers undefined.

♻️ Minor cleanup for the redundant guard
-    if (condition.value == null || condition.value === undefined) {
+    if (condition.value == null) {
       errors.push(`Condition ${conditionIndex + 1}: Please enter a value.`);
       continue;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/check-workflow-conditions.ts` around lines 28 -
86, Remove the redundant condition.value === undefined check in the
per-condition validation loop, since condition.value == null already covers both
null and undefined. To eliminate duplicated validation, extract the shared logic
into a collectConditionErrors helper, including sendCampaign
duplicate-attribute/exclusivity rules, then update validateWorkflowConditions to
reuse it and map returned messages to DubApiError.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx (1)

54-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Invalid Tailwind class font-regular used at both new BountyEligibilitySummary call sites. font-regular is not a valid Tailwind utility in this codebase; weight 400 must use font-normal.

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx#L54-L54: change className="font-regular" to className="font-normal".
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx#L136-L136: change className="font-regular" to className="font-normal".

Based on learnings: "Tailwind's normal font weight (400) must use font-normal. Do not use font-regular (it is not a valid Tailwind utility here)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
at line 54, Replace the invalid font-regular className with font-normal at the
BountyEligibilitySummary call sites in
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
(lines 54-54) and
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx
(lines 136-136).

Source: Learnings

apps/web/ui/partners/groups/partner-groups-select.tsx (1)

45-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Near-verbatim duplicate of PartnerTagsSelect. The search/sort/latch state machine is identical apart from entity names; extracting a shared hook (e.g. useAudienceLimitItems) would keep the two selectors from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/groups/partner-groups-select.tsx` around lines 45 - 86,
The duplicated search/sort/latch state machine around sortGroups and the related
useEffect calls should be extracted into a shared hook, such as
useAudienceLimitItems, and reused by both PartnerGroupsSelect and
PartnerTagsSelect. Preserve the existing selected-item ordering, search
behavior, readiness checks, and groups/items update latch while removing the
duplicated implementation.
apps/web/tests/bounties/index.test.ts (1)

222-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't actually exercise clearing tags. The bounty is created with partnerTagIds: null, so the PATCH null transitions from empty → empty. Create with real tag IDs (e.g. an E2E partner tag fixture) so the clear path is verified.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/tests/bounties/index.test.ts` around lines 222 - 254, Update the
test “PATCH /bounties/{bountyId} - clear partner tags” to create the bounty with
one or more real E2E partner tag IDs instead of null, using the available
partner-tag fixture. Keep the PATCH request setting partnerTagIds to null and
retain the assertion that bounty.partnerTags is empty, so the test verifies
clearing existing tags.
apps/web/ui/partners/tags-multi-select.tsx (1)

154-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

This list rendering duplicates the new AudienceLimitSelectShell. Both were added in this PR with the same cmdk list, checkbox markup, and empty/loading branches; only the header (ToggleGroup vs Switch) differs. Consider driving this component through the shell with a pluggable header.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/tags-multi-select.tsx` around lines 154 - 241, Refactor
the tag list rendering around AudienceLimitSelectShell instead of duplicating
its cmdk list, checkbox, empty, and loading branches. Provide the tag-specific
header through the shell’s pluggable header mechanism, preserving the existing
search, selection, async loading, and no-match behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/`(ee)/api/campaigns/[campaignId]/route.ts:
- Around line 89-94: Update the triggerConditions handling in the campaign
update flow and its persistence spread to distinguish an omitted value from an
explicit null or empty array: guard validation with a defined-value check,
normalize null to the workflow’s empty-conditions representation, and persist
the normalized value so explicit null or [] clears existing conditions. Keep the
update behavior unchanged when triggerConditions is undefined.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/audience-eligibility-panel.tsx:
- Around line 16-21: Normalize the value passed from the audience eligibility
form’s `groupIds` field in the `PartnerGroupsSelect` render callback, matching
the existing `partnerTagIds` handling. Ensure an undefined value is converted to
the expected empty selection before reaching `PartnerGroupsSelect` and its
`AudienceLimitSelectShell`, while preserving the field’s change handler.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-recipients-selector.tsx:
- Around line 25-35: Update the selectedGroups derivation and the corresponding
tag selection memo to resolve selected entities by ID from the full available
dataset, following the existing PartnerTagsSelect/PartnerGroupsSelect approach,
rather than filtering only the paginated default list. Ensure selections outside
the first page still contribute to rendered chips and plusCount; if full
entities cannot be fetched, preserve their count with a count-only chip.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/duplicate-logic-warning.tsx:
- Around line 23-25: Update the parsedTriggerConditions initialization to
require triggerConditions to contain at least one item before calling
sendCampaignConditionsSchema.safeParse. Treat an empty array like an absent
value so duplicate campaign lookups are skipped rather than issuing an
unfiltered request.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx:
- Around line 182-187: Update the label expression in the field-rendering logic
to safely handle attributes missing from SEND_CAMPAIGN_ATTRIBUTES, mirroring the
optional operator lookup behavior. Preserve the existing "activity" fallback for
unset or unknown values so rendering does not throw for legacy or renamed keys.
- Around line 66-67: Update canAddCondition in the transactional campaign
condition controls to depend only on availableAttributesToAdd.length > 0,
removing the fields.length > 0 requirement so users can add the first condition
while preserving behavior for populated lists.

In `@apps/web/lib/api/campaigns/schedule-campaigns.ts`:
- Around line 34-50: Update deleteCampaignSchedule so failures from both
qstash.messages.cancel and qstash.schedules.delete are caught and handled as
non-fatal warnings, matching the existing behavior in scheduleMarketingCampaign.
Keep the campaign deletion/cancellation flow successful when QStash reports a
stale or missing message or schedule ID.

In `@apps/web/lib/api/workflows/award-bounty/execute.ts`:
- Around line 39-42: Update the award-bounty workflow handler around
parseWorkflowConfig to validate that conditions contains exactly one entry;
reject the workflow during validation and defensively return before evaluating
action or awarding the bounty when the count differs. Keep using the single
condition only after this guard passes.

In `@apps/web/lib/api/workflows/send-campaign/execute.ts`:
- Around line 347-359: Replace the tag-targeted programEnrollment.findUnique
call with findFirst, keeping the partnerId_programId constraint,
campaignAudienceWhere filters, and programEnrollmentInclude unchanged so nested
relation filters such as programPartnerTags are supported.

In `@apps/web/lib/api/workflows/utils.ts`:
- Around line 20-28: Update isScheduledWorkflow to read triggerConditions
defensively without calling the throwing parseWorkflowConfig path; treat empty,
missing, or invalid workflow conditions as unscheduled and preserve the existing
partnerEnrolledDays detection for valid conditions.

In `@apps/web/lib/bounty/api/transform-bounty.ts`:
- Around line 16-25: Update the transformation around performanceCondition in
transformBounty so an explicitly supplied performanceCondition from the PATCH
result is preserved instead of being overwritten by the pre-update workflow
triggerConditions value. Use the returned bounty condition when present, and
retain the existing triggerConditions fallback only when no explicit condition
was supplied.

In `@apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts`:
- Line 3: Update the draft-submission job flow around getBountiesByGroups and
isEligiblePerformanceBounty to enforce programPartnerTags eligibility before
publishing jobs. Ensure each scheduled job is published only when the actual
partner enrollment matches the bounty’s partner tags, or propagate the matching
partner tag IDs into the scheduled-job creation data so create-draft-submissions
applies the intended filter.

In `@apps/web/tests/workflows/comparison-operators.test.ts`:
- Around line 84-104: Update WORKFLOW_OPERATORS.lte.validate to accept scalar
non-negative numbers, including zero, while rejecting non-numbers, NaN, and
negative values with the existing validation message. Remove the requirement for
a { min, max } object and keep the lte comparator contract consistent with these
tests.

In `@apps/web/ui/partners/audience-limit-select-shell.tsx`:
- Around line 105-113: The onSelect handler in the audience limit select shell
should revert selectedIds to null when deselecting the final item, matching
TagsMultiSelect’s “All tags” behavior; preserve array updates when items remain
selected or a new item is added, and verify the downstream bounty/campaign APIs
receive the intended representation.

In `@apps/web/ui/partners/groups/partner-groups-select.tsx`:
- Around line 37-43: The useAsync effects oscillate because they repeatedly set
state from a Boolean expression that becomes false after async mode is enabled.
Update apps/web/ui/partners/groups/partner-groups-select.tsx lines 37-43,
apps/web/ui/partners/partner-tags-select.tsx lines 34-44, and
apps/web/ui/partners/tags-multi-select.tsx lines 57-67 so each effect only calls
setUseAsync(true) when useAsync is false and the corresponding collection length
meets its maximum page-size threshold, preserving the one-way latch behavior.

---

Outside diff comments:
In `@apps/web/scripts/programs/backfill-reuse-commission.ts`:
- Around line 320-336: Update the workflow event in the commissionType branch to
use the event matching the commission type: leadRecorded for leads and
saleRecorded for sales. Keep the existing leads, saleAmount, and conversions
metrics aligned with those event definitions, and do not use commissionRecorded
for this path.

---

Nitpick comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx:
- Line 54: Replace the invalid font-regular className with font-normal at the
BountyEligibilitySummary call sites in
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
(lines 54-54) and
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx
(lines 136-136).

In `@apps/web/lib/api/campaigns/transform-campaign.ts`:
- Around line 16-34: Update TransformCampaignInput to derive its Prisma payload
type from the existing campaignEligibilityIncludes symbol instead of duplicating
the include literal, ensuring type and runtime include shapes remain
synchronized.

In `@apps/web/lib/api/workflows/check-workflow-conditions.ts`:
- Around line 28-86: Remove the redundant condition.value === undefined check in
the per-condition validation loop, since condition.value == null already covers
both null and undefined. To eliminate duplicated validation, extract the shared
logic into a collectConditionErrors helper, including sendCampaign
duplicate-attribute/exclusivity rules, then update validateWorkflowConditions to
reuse it and map returned messages to DubApiError.

In `@apps/web/lib/api/workflows/send-campaign/execute.ts`:
- Around line 361-386: Remove the stray await before prisma.commission.aggregate
in the commissions ternary within the Promise.all call, returning the aggregate
promise directly while preserving the existing Promise.resolve fallback for the
non-commission branch.
- Around line 439-453: Update the programEnrollment query in the campaign
execution flow to avoid silently excluding enrollments beyond take: 1000,
preferably by fetching all matching records through cursor pagination. If
retaining the cap, detect when the result reaches the limit and emit an explicit
diagnostic log indicating possible overflow.

In `@apps/web/lib/api/workflows/utils.ts`:
- Around line 49-52: Update the result construction in the workflow requirements
helper to derive every key from WORKFLOW_DATA_REQUIREMENTS, mapping each
requirement to whether requirements contains it, instead of hardcoding
commissions and partnerLinkStats. Export WORKFLOW_DATA_REQUIREMENTS from
attribute-definitions.ts and retain the Record<WorkflowDataRequirement, boolean>
typing for the generated object.

In `@apps/web/tests/bounties/index.test.ts`:
- Around line 222-254: Update the test “PATCH /bounties/{bountyId} - clear
partner tags” to create the bounty with one or more real E2E partner tag IDs
instead of null, using the available partner-tag fixture. Keep the PATCH request
setting partnerTagIds to null and retain the assertion that bounty.partnerTags
is empty, so the test verifies clearing existing tags.

In `@apps/web/tests/workflows/send-campaign-workflow.test.ts`:
- Around line 858-871: Update the negative campaign notification test around the
workflow trigger and emailsSent assertion to wait briefly or re-check after a
delay before asserting no emails were sent, preventing a vacuous immediate pass.
Also add the same notification-email cleanup used by the AND-match test to this
test’s onTestFinished handler.

In `@apps/web/ui/partners/groups/partner-groups-select.tsx`:
- Around line 45-86: The duplicated search/sort/latch state machine around
sortGroups and the related useEffect calls should be extracted into a shared
hook, such as useAudienceLimitItems, and reused by both PartnerGroupsSelect and
PartnerTagsSelect. Preserve the existing selected-item ordering, search
behavior, readiness checks, and groups/items update latch while removing the
duplicated implementation.

In `@apps/web/ui/partners/tags-multi-select.tsx`:
- Around line 154-241: Refactor the tag list rendering around
AudienceLimitSelectShell instead of duplicating its cmdk list, checkbox, empty,
and loading branches. Provide the tag-specific header through the shell’s
pluggable header mechanism, preserving the existing search, selection, async
loading, and no-match behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2182a490-8976-48a3-b776-ebc902ca8983

📥 Commits

Reviewing files that changed from the base of the PR and between c3ff876 and 42f8e19.

📒 Files selected for processing (99)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/app/(ee)/api/campaigns/[campaignId]/duplicate/route.ts
  • apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts
  • apps/web/app/(ee)/api/campaigns/route.ts
  • apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
  • apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
  • apps/web/app/(ee)/api/e2e/workflows/route.ts
  • apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/social-content-stats/route.ts
  • apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/social-content-stats/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/sync-customer.ts
  • apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/add-edit-bounty-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/audience-eligibility-panel.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/use-add-edit-bounty-form.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-controls.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-groups-selector.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-recipients-selector.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/duplicate-logic-warning.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/use-campaign-confirmation-modals.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/utils.ts
  • apps/web/lib/actions/partners/accept-program-invite.ts
  • apps/web/lib/actions/partners/upload-bounty-submission-file.ts
  • apps/web/lib/api/campaigns/get-campaign-or-throw.ts
  • apps/web/lib/api/campaigns/schedule-campaigns.ts
  • apps/web/lib/api/campaigns/transform-campaign.ts
  • apps/web/lib/api/campaigns/validate-campaign.ts
  • apps/web/lib/api/commissions/create-manual-commissions.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/fraud/release-hold-commissions.ts
  • apps/web/lib/api/groups/upsert-group-move-rules.ts
  • apps/web/lib/api/partner-tags/throw-if-invalid-partner-tag-ids.ts
  • apps/web/lib/api/workflows/attribute-definitions.ts
  • apps/web/lib/api/workflows/award-bounty/execute.ts
  • apps/web/lib/api/workflows/check-workflow-conditions.ts
  • apps/web/lib/api/workflows/evaluate-workflow-conditions.ts
  • apps/web/lib/api/workflows/execute-workflows.ts
  • apps/web/lib/api/workflows/move-group/execute.ts
  • apps/web/lib/api/workflows/operator-definitions.ts
  • apps/web/lib/api/workflows/parse-workflow-config.ts
  • apps/web/lib/api/workflows/send-campaign/execute.ts
  • apps/web/lib/api/workflows/send-campaign/schema.ts
  • apps/web/lib/api/workflows/types.ts
  • apps/web/lib/api/workflows/utils.ts
  • apps/web/lib/api/workflows/validate-workflow-conditions.ts
  • apps/web/lib/api/workflows/workflow-type-attributes.ts
  • apps/web/lib/bounty/api/bounty-availability.ts
  • apps/web/lib/bounty/api/create-bounty-submission.ts
  • apps/web/lib/bounty/api/get-bounties-by-groups.ts
  • apps/web/lib/bounty/api/get-bounties-for-partner.ts
  • apps/web/lib/bounty/api/get-bounty-submission-upload-url.ts
  • apps/web/lib/bounty/api/get-bounty-with-details.ts
  • apps/web/lib/bounty/api/transform-bounty.ts
  • apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/integrations/shopify/process-order.ts
  • apps/web/lib/swr/use-partners-count-by-groupids.ts
  • apps/web/lib/webhook/sample-events/bounty-created.json
  • apps/web/lib/webhook/sample-events/bounty-updated.json
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/zod/schemas/campaigns.ts
  • apps/web/lib/zod/schemas/partner-profile.ts
  • apps/web/lib/zod/schemas/workflows.ts
  • apps/web/prisma/schema/bounty.prisma
  • apps/web/prisma/schema/campaign.prisma
  • apps/web/prisma/schema/tag.prisma
  • apps/web/prisma/schema/workflow.prisma
  • apps/web/scripts/dev/seed-partner-enrollment.ts
  • apps/web/scripts/dev/seed.ts
  • apps/web/scripts/programs/backfill-reuse-commission.ts
  • apps/web/tests/bounties/index.test.ts
  • apps/web/tests/campaigns/index.test.ts
  • apps/web/tests/workflows/comparison-operators.test.ts
  • apps/web/tests/workflows/find-groups-with-matching-rules.test.ts
  • apps/web/tests/workflows/move-group-workflow.test.ts
  • apps/web/tests/workflows/send-campaign-workflow.test.ts
  • apps/web/ui/partners/audience-limit-select-shell.tsx
  • apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx
  • apps/web/ui/partners/groups/partner-groups-select.tsx
  • apps/web/ui/partners/partner-tags-select.tsx
  • apps/web/ui/partners/tags-multi-select.tsx
  • packages/email/src/index.ts
  • packages/utils/src/functions/index.ts
  • packages/utils/src/functions/pluck.ts
💤 Files with no reviewable changes (5)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/utils.ts
  • apps/web/app/(ee)/api/e2e/workflows/route.ts
  • apps/web/tests/workflows/move-group-workflow.test.ts
  • apps/web/lib/api/workflows/parse-workflow-config.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-groups-selector.tsx

Comment thread apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts Outdated
Comment thread apps/web/lib/bounty/api/transform-bounty.ts
Comment thread apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts Outdated
Comment thread apps/web/tests/workflows/comparison-operators.test.ts
Comment thread apps/web/ui/partners/audience-limit-select-shell.tsx
Comment on lines +37 to +43
useEffect(
() =>
setUseAsync(
Boolean(groups && !useAsync && groups.length >= GROUPS_MAX_PAGE_SIZE),
),
[groups, useAsync],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

useAsync latch oscillates in all three new selectors. Each effect computes setUseAsync(Boolean(items && !useAsync && items.length >= MAX)) while listing useAsync as a dependency, so enabling async search immediately re-runs the effect and disables it again, thrashing the SWR key.

  • apps/web/ui/partners/groups/partner-groups-select.tsx#L37-L43: change to a one-way latch that only calls setUseAsync(true) when !useAsync && groups.length >= GROUPS_MAX_PAGE_SIZE.
  • apps/web/ui/partners/partner-tags-select.tsx#L34-L44: apply the same one-way latch using PARTNER_TAGS_MAX_PAGE_SIZE.
  • apps/web/ui/partners/tags-multi-select.tsx#L57-L67: apply the same one-way latch (or reuse the shared hook if extracted).
📍 Affects 3 files
  • apps/web/ui/partners/groups/partner-groups-select.tsx#L37-L43 (this comment)
  • apps/web/ui/partners/partner-tags-select.tsx#L34-L44
  • apps/web/ui/partners/tags-multi-select.tsx#L57-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/groups/partner-groups-select.tsx` around lines 37 - 43,
The useAsync effects oscillate because they repeatedly set state from a Boolean
expression that becomes false after async mode is enabled. Update
apps/web/ui/partners/groups/partner-groups-select.tsx lines 37-43,
apps/web/ui/partners/partner-tags-select.tsx lines 34-44, and
apps/web/ui/partners/tags-multi-select.tsx lines 57-67 so each effect only calls
setUseAsync(true) when useAsync is false and the corresponding collection length
meets its maximum page-size threshold, preserving the one-way latch behavior.

@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@devkiran: I’ll perform a full review of all changes in this PR.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (8)
apps/web/lib/api/workflows/check-workflow-conditions.ts (1)

55-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor consistency cleanups. Line 62's message lacks the Condition N: prefix used by every other error (and shows the raw attribute key instead of a label), and line 72's === undefined is already covered by == null. Also conditionIndex (line 30) is just an alias for i.

♻️ Suggested tidy-up
-      const operatorLabel = operatorDefinition.label ?? condition.operator;
-      errors.push(
-        `Operator "${operatorLabel}" is not valid for the activity "${condition.attribute}".`,
-      );
+      const operatorLabel = operatorDefinition.label ?? condition.operator;
+      errors.push(
+        `Condition ${i + 1}: Operator "${operatorLabel}" is not valid for the selected activity.`,
+      );
       continue;
     }
 
     // Attributes with inputType "none" (e.g. partnerJoined) don't require a value.
     if (attributeDefinition.inputType === "none") {
       continue;
     }
 
-    if (condition.value == null || condition.value === undefined) {
+    if (condition.value == null) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/check-workflow-conditions.ts` around lines 55 -
75, Update the validation loop to prefix the invalid-operator error with the
condition number and use the resolved attribute label instead of
condition.attribute. Simplify the missing-value check to a single nullish
comparison, and remove the redundant conditionIndex alias by using the loop
index directly in the error message.
apps/web/lib/api/workflows/send-campaign/execute.ts (1)

249-279: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

partnerLinkStats requirement is computed but never used. Link stats columns are always selected in programEnrollmentInclude, so getWorkflowDataRequirements(...).partnerLinkStats is dead information here. Either drop the destructure comment expectation or make the select conditional to keep the requirements contract meaningful.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/send-campaign/execute.ts` around lines 249 - 279,
Update the program enrollment include flow around programEnrollmentInclude and
getWorkflowDataRequirements so partnerLinkStats controls whether the links stats
fields are selected. Make the links selection conditional on the computed
requirement while preserving the existing link ordering and required link
identifiers, ensuring the requirements contract is meaningful.
apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx (1)

85-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate tooltip/label pattern between GroupsLabel and TagsLabel.

Both functions repeat the same "single item inline / multiple items in a scrollable tooltip" structure. Consider extracting a shared EligibilityLabel({ items, renderIcon, renderRow }) helper to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx` around lines 85
- 174, Extract the duplicated single-item versus multi-item rendering flow from
GroupsLabel and TagsLabel into a shared EligibilityLabel helper, accepting items
plus renderIcon and renderRow callbacks. Update both components to supply their
group/tag-specific content while preserving the existing empty handling, inline
label text, count suffix, and scrollable tooltip behavior.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx (1)

105-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Widen the form value type instead of casting null through number. null as unknown as number appears in append, setValue, ConditionRow.onUpdate, and ValueInput.onChange — the empty state is legitimately null, so the triggerConditions[].value type should model number | null and let checkWorkflowConditions reject nulls at save time. The casts currently hide that from the compiler at four sites.

Also applies to: 151-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx
around lines 105 - 116, Update the triggerConditions[].value form type and
related condition types to allow number | null, then remove the
null-through-unknown casts in append, setValue, ConditionRow.onUpdate, and
ValueInput.onChange. Preserve null as the legitimate empty state and keep
checkWorkflowConditions responsible for rejecting null values during save
validation.
apps/web/lib/bounty/api/bounty-availability.ts (1)

42-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant truthiness check on an always-defined array.

groupIds is the result of .filter(...), so groupIds && (Line 62) is dead; same shape reads cleaner as a plain length check.

♻️ Nit
-          ...(groupIds && groupIds.length > 0
+          ...(groupIds.length > 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/bounty/api/bounty-availability.ts` around lines 42 - 51, In
buildBountyEligibilityWhere, remove the redundant groupIds truthiness check and
use a direct groupIds.length condition wherever the filtered array is tested,
preserving the existing behavior for empty and non-empty group ID lists.
apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts (1)

92-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename bountyWithoutGroups now that partnerTags is also stripped.

♻️ Nit
-  const { groups, partnerTags, ...bountyWithoutGroups } = bounty;
+  const { groups, partnerTags, ...bountyWithoutEligibility } = bounty;

(and update the spread below accordingly)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/app/`(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts
at line 92, Rename the destructured remainder variable in the bounty response
handling from bountyWithoutGroups to a name reflecting that both groups and
partnerTags were removed, and update the corresponding spread usage below to use
the new name.
apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts (1)

129-139: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider chunking partnerIds per QStash message.

partnerIds comes straight from the caller (e.g. bulk partner-tag updates in apps/web/lib/actions/partners/tags/update-program-partner-tags.ts), so a single body can grow with the selection size and hit QStash payload limits. Chunking into fixed batches (as the notify-partners cron does for emails) keeps message size bounded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts` around lines 129
- 139, Update the QStash publishing flow in the Promise.allSettled mapping to
split each bounty’s partnerIds into fixed-size batches before calling
qstash.publishJSON. Publish one message per chunk with the same bountyId and
each chunk as partnerIds, reusing the existing batching pattern or shared helper
used by the notify-partners cron to keep payload sizes bounded.
apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts (1)

22-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hydrate programPartnerTags in the referrals embed auth layer instead of per route. Both embed routes issue the same extra programPartnerTag.findMany because the enrollment supplied by withReferralsEmbedToken lacks the new tags relation; including it once removes the duplicated query and keeps eligibility inputs consistent for future routes.

  • apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts#L22-L30: drop the local lookup and pass programEnrollment straight through once the middleware includes tags.
  • apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/social-content-stats/route.ts#L53-L61: same — remove the local partnerTags query and the spread merge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts
around lines 22 - 30, Update withReferralsEmbedToken to hydrate
programPartnerTags on the enrollment, then remove the local tag lookup and merge
in
apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts#L22-L30
and
apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/social-content-stats/route.ts#L53-L61;
both routes should pass programEnrollment directly through the existing
authorization flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/api/workflows/operator-definitions.ts`:
- Around line 39-56: Update the label in the lte operator definition from
“under” to an inclusive phrase such as “at most” or “no more than,” while
preserving the existing validate and evaluate behavior using the <= comparison.

In `@apps/web/lib/api/workflows/validate-workflow-conditions.ts`:
- Around line 31-38: Move the awardBounty condition-count validation before the
early return in the workflow validation flow, ensuring an empty conditions array
is rejected alongside any count other than one. Update the relevant validation
logic near executeAwardBountyWorkflow so exactly one condition is required
before accessing conditions[0].

In `@apps/web/lib/zod/schemas/campaigns.ts`:
- Around line 79-87: Update the triggerConditions preprocessing in
sendCampaignConditionsSchema to catch JSON.parse failures and return an invalid
value that Zod can convert into its normal structured validation error. Preserve
successful JSON parsing and subsequent sendCampaignConditionsSchema validation.

In `@apps/web/tests/bounties/index.test.ts`:
- Around line 201-205: Remove the duplicate partnerTags property declarations
from the response type literals in the POST /bounties tests, including the
declarations near the tests starting at “with partnerTagIds null returns empty
partnerTags” and the corresponding later case. Keep one partnerTags member per
type literal with its existing type.
- Around line 229-245: Update the tag-removal test around the bounty creation
and PATCH flow to seed the bounty with a valid partner tag instead of
partnerTagIds: null. Assert the created or fetched bounty includes that tag,
then PATCH partnerTagIds: null and assert the response contains no partner tags,
ensuring nested tag deletion is actually exercised.

In `@apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx`:
- Around line 25-49: Update the useGroups and usePartnerTags calls in the bounty
eligibility summary to pass the selected IDs from bountyGroups and
bountyPartnerTags, matching the ID-scoped selector fetch behavior. Keep
eligibleGroups and eligibleTags derived from those scoped results, and preserve
the existing loading and empty-state handling.

In `@apps/web/ui/partners/tags-multi-select.tsx`:
- Around line 57-67: Update the effect that derives useAsync from partnerTags so
it only enables the flag when useAsync is currently false and the page reaches
PARTNER_TAGS_MAX_PAGE_SIZE, without subsequently assigning false after it
becomes true. Preserve the existing dependency handling and monotonic true-only
behavior.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts:
- Around line 22-30: Update withReferralsEmbedToken to hydrate
programPartnerTags on the enrollment, then remove the local tag lookup and merge
in
apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts#L22-L30
and
apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/social-content-stats/route.ts#L53-L61;
both routes should pass programEnrollment directly through the existing
authorization flow.

In
`@apps/web/app/`(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts:
- Line 92: Rename the destructured remainder variable in the bounty response
handling from bountyWithoutGroups to a name reflecting that both groups and
partnerTags were removed, and update the corresponding spread usage below to use
the new name.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx:
- Around line 105-116: Update the triggerConditions[].value form type and
related condition types to allow number | null, then remove the
null-through-unknown casts in append, setValue, ConditionRow.onUpdate, and
ValueInput.onChange. Preserve null as the legitimate empty state and keep
checkWorkflowConditions responsible for rejecting null values during save
validation.

In `@apps/web/lib/api/workflows/check-workflow-conditions.ts`:
- Around line 55-75: Update the validation loop to prefix the invalid-operator
error with the condition number and use the resolved attribute label instead of
condition.attribute. Simplify the missing-value check to a single nullish
comparison, and remove the redundant conditionIndex alias by using the loop
index directly in the error message.

In `@apps/web/lib/api/workflows/send-campaign/execute.ts`:
- Around line 249-279: Update the program enrollment include flow around
programEnrollmentInclude and getWorkflowDataRequirements so partnerLinkStats
controls whether the links stats fields are selected. Make the links selection
conditional on the computed requirement while preserving the existing link
ordering and required link identifiers, ensuring the requirements contract is
meaningful.

In `@apps/web/lib/bounty/api/bounty-availability.ts`:
- Around line 42-51: In buildBountyEligibilityWhere, remove the redundant
groupIds truthiness check and use a direct groupIds.length condition wherever
the filtered array is tested, preserving the existing behavior for empty and
non-empty group ID lists.

In `@apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts`:
- Around line 129-139: Update the QStash publishing flow in the
Promise.allSettled mapping to split each bounty’s partnerIds into fixed-size
batches before calling qstash.publishJSON. Publish one message per chunk with
the same bountyId and each chunk as partnerIds, reusing the existing batching
pattern or shared helper used by the notify-partners cron to keep payload sizes
bounded.

In `@apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx`:
- Around line 85-174: Extract the duplicated single-item versus multi-item
rendering flow from GroupsLabel and TagsLabel into a shared EligibilityLabel
helper, accepting items plus renderIcon and renderRow callbacks. Update both
components to supply their group/tag-specific content while preserving the
existing empty handling, inline label text, count suffix, and scrollable tooltip
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e37997ac-ab77-4069-9713-ebf2248ea167

📥 Commits

Reviewing files that changed from the base of the PR and between 57b812a and 9289ffe.

📒 Files selected for processing (100)
  • apps/web/app/(ee)/api/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/api/bounties/route.ts
  • apps/web/app/(ee)/api/campaigns/[campaignId]/duplicate/route.ts
  • apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts
  • apps/web/app/(ee)/api/campaigns/route.ts
  • apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
  • apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts
  • apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
  • apps/web/app/(ee)/api/e2e/workflows/route.ts
  • apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/social-content-stats/route.ts
  • apps/web/app/(ee)/api/embed/referrals/bounties/[bountyId]/upload/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/[bountyId]/social-content-stats/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/bounties/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/sync-customer.ts
  • apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/app/(ee)/app.dub.co/embed/referrals/get-referrals-embed-data.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-info.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/add-edit-bounty-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/audience-eligibility-panel.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/confirm-create-bounty-modal.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty/use-add-edit-bounty-form.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-controls.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-groups-selector.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-recipients-selector.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/duplicate-logic-warning.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/use-campaign-confirmation-modals.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/utils.ts
  • apps/web/lib/actions/partners/accept-program-invite.ts
  • apps/web/lib/actions/partners/tags/update-program-partner-tags.ts
  • apps/web/lib/actions/partners/upload-bounty-submission-file.ts
  • apps/web/lib/api/campaigns/get-campaign-or-throw.ts
  • apps/web/lib/api/campaigns/schedule-campaigns.ts
  • apps/web/lib/api/campaigns/transform-campaign.ts
  • apps/web/lib/api/campaigns/validate-campaign.ts
  • apps/web/lib/api/commissions/create-manual-commissions.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/fraud/release-hold-commissions.ts
  • apps/web/lib/api/groups/upsert-group-move-rules.ts
  • apps/web/lib/api/partner-tags/throw-if-invalid-partner-tag-ids.ts
  • apps/web/lib/api/workflows/attribute-definitions.ts
  • apps/web/lib/api/workflows/award-bounty/execute.ts
  • apps/web/lib/api/workflows/check-workflow-conditions.ts
  • apps/web/lib/api/workflows/evaluate-workflow-conditions.ts
  • apps/web/lib/api/workflows/execute-workflows.ts
  • apps/web/lib/api/workflows/move-group/execute.ts
  • apps/web/lib/api/workflows/operator-definitions.ts
  • apps/web/lib/api/workflows/parse-workflow-config.ts
  • apps/web/lib/api/workflows/send-campaign/execute.ts
  • apps/web/lib/api/workflows/send-campaign/schema.ts
  • apps/web/lib/api/workflows/types.ts
  • apps/web/lib/api/workflows/utils.ts
  • apps/web/lib/api/workflows/validate-workflow-conditions.ts
  • apps/web/lib/api/workflows/workflow-type-attributes.ts
  • apps/web/lib/bounty/api/bounty-availability.ts
  • apps/web/lib/bounty/api/create-bounty-submission.ts
  • apps/web/lib/bounty/api/get-bounties-by-groups.ts
  • apps/web/lib/bounty/api/get-bounties-for-partner.ts
  • apps/web/lib/bounty/api/get-bounty-submission-upload-url.ts
  • apps/web/lib/bounty/api/get-bounty-with-details.ts
  • apps/web/lib/bounty/api/transform-bounty.ts
  • apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/integrations/shopify/process-order.ts
  • apps/web/lib/swr/use-partners-count-by-groupids.ts
  • apps/web/lib/webhook/sample-events/bounty-created.json
  • apps/web/lib/webhook/sample-events/bounty-updated.json
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/zod/schemas/campaigns.ts
  • apps/web/lib/zod/schemas/partner-profile.ts
  • apps/web/lib/zod/schemas/workflows.ts
  • apps/web/prisma/schema/bounty.prisma
  • apps/web/prisma/schema/campaign.prisma
  • apps/web/prisma/schema/tag.prisma
  • apps/web/prisma/schema/workflow.prisma
  • apps/web/scripts/dev/seed-partner-enrollment.ts
  • apps/web/scripts/dev/seed.ts
  • apps/web/scripts/programs/backfill-reuse-commission.ts
  • apps/web/tests/bounties/index.test.ts
  • apps/web/tests/campaigns/index.test.ts
  • apps/web/tests/workflows/comparison-operators.test.ts
  • apps/web/tests/workflows/find-groups-with-matching-rules.test.ts
  • apps/web/tests/workflows/move-group-workflow.test.ts
  • apps/web/tests/workflows/send-campaign-workflow.test.ts
  • apps/web/ui/partners/audience-limit-select-shell.tsx
  • apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx
  • apps/web/ui/partners/groups/partner-groups-select.tsx
  • apps/web/ui/partners/partner-tags-select.tsx
  • apps/web/ui/partners/tags-multi-select.tsx
  • packages/email/src/index.ts
  • packages/utils/src/functions/index.ts
  • packages/utils/src/functions/pluck.ts
💤 Files with no reviewable changes (6)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/utils.ts
  • apps/web/lib/api/workflows/parse-workflow-config.ts
  • apps/web/app/(ee)/api/e2e/workflows/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-groups-selector.tsx
  • apps/web/tests/workflows/move-group-workflow.test.ts
  • apps/web/lib/bounty/api/get-bounties-by-groups.ts

Comment on lines +39 to +56
// Less than or equal to
lte: {
name: "lte",
label: "under",
validate(value: ConditionValue) {
if (typeof value !== "number" || isNaN(value) || value < 0) {
throw new Error("Please enter a value greater than or equal to 0.");
}
},
evaluate(attributeValue: number | string, conditionValue: ConditionValue) {
if (
typeof attributeValue !== "number" ||
typeof conditionValue !== "number"
) {
return false;
}

return attributeValue <= conditionValue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an inclusive label for lte.

“Under” normally excludes the boundary, but this operator includes it (<=). Use “at most”/“no more than”, or make evaluation strict.

Proposed fix
-    label: "under",
+    label: "at most",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Less than or equal to
lte: {
name: "lte",
label: "under",
validate(value: ConditionValue) {
if (typeof value !== "number" || isNaN(value) || value < 0) {
throw new Error("Please enter a value greater than or equal to 0.");
}
},
evaluate(attributeValue: number | string, conditionValue: ConditionValue) {
if (
typeof attributeValue !== "number" ||
typeof conditionValue !== "number"
) {
return false;
}
return attributeValue <= conditionValue;
// Less than or equal to
lte: {
name: "lte",
label: "at most",
validate(value: ConditionValue) {
if (typeof value !== "number" || isNaN(value) || value < 0) {
throw new Error("Please enter a value greater than or equal to 0.");
}
},
evaluate(attributeValue: number | string, conditionValue: ConditionValue) {
if (
typeof attributeValue !== "number" ||
typeof conditionValue !== "number"
) {
return false;
}
return attributeValue <= conditionValue;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/workflows/operator-definitions.ts` around lines 39 - 56,
Update the label in the lte operator definition from “under” to an inclusive
phrase such as “at most” or “no more than,” while preserving the existing
validate and evaluate behavior using the <= comparison.

Comment thread apps/web/lib/api/workflows/validate-workflow-conditions.ts
Comment thread apps/web/lib/zod/schemas/campaigns.ts
Comment thread apps/web/tests/bounties/index.test.ts
Comment on lines +229 to +245
...submissionBounty,
name: "Bounty for partner tag clear",
groupIds: [E2E_PARTNER_GROUP.id],
partnerTagIds: null,
},
});

expect(createStatus).toEqual(200);

const { status, data: bounty } = await http.patch<{
id: string;
partnerTags: { id: string }[];
}>({
path: `/bounties/${created.id}`,
body: {
partnerTagIds: null,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Seed a tag before testing tag removal.

This creates the bounty with partnerTagIds: null, then clears null; the assertion passes even if nested tag deletion is broken. Create it with a valid tag, assert it is returned, then PATCH null and assert it is removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/tests/bounties/index.test.ts` around lines 229 - 245, Update the
tag-removal test around the bounty creation and PATCH flow to seed the bounty
with a valid partner tag instead of partnerTagIds: null. Assert the created or
fetched bounty includes that tag, then PATCH partnerTagIds: null and assert the
response contains no partner tags, ensuring nested tag deletion is actually
exercised.

Comment thread apps/web/ui/partners/bounties/bounty-eligibility-summary.tsx
Comment thread apps/web/ui/partners/tags-multi-select.tsx
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.

1 participant