Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Dec 16, 2025

Summary by CodeRabbit

  • New Features

    • Group Move Rules: UI to create, validate, detect conflicts, and persist automated partner-move rules.
    • Workflow MoveGroup action to automatically reassign partners based on rules.
  • Improvements

    • Partner-metrics workflows send richer context (reason, identity, metrics) and support multi-condition triggers, including a new "between" operator.
    • Backend evaluation supports multiple conditions; group-move flow consolidated into a single server-side operation.
    • Rewards UI uses a standardized inline amount input.
    • Added a new user-arrow-right icon.
  • Plan Updates

    • Group Move Rules available on Advanced and Enterprise plans.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Dec 16, 2025

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

Project Deployment Review Updated (UTC)
dub Error Error Jan 8, 2026 6:08am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 16, 2025

📝 Walkthrough

Walkthrough

Adds multi-condition workflow evaluation, a MoveGroup workflow action and execution path, consolidated "partnerMetricsUpdated" trigger with richer identity/metrics payloads, group move rule UI/API/service surface, Prisma relation PartnerGroup.workflowId, migration, and tests.

Changes

Cohort / File(s) Summary
Workflow engine & evaluators
apps/web/lib/api/workflows/evaluate-workflow-conditions.ts, apps/web/lib/api/workflows/execute-workflows.ts, apps/web/lib/api/workflows/*
New multi-condition evaluator; executeWorkflows refactored to ACTION_HANDLERS, reason-based filtering, identity/metrics context shape, lazy commission fetch, and handler dispatch.
MoveGroup action & runner
apps/web/lib/api/workflows/execute-move-group-workflow.ts, apps/web/lib/api/workflows/execute-complete-bounty-workflow.ts, apps/web/lib/api/workflows/execute-send-campaign-workflow.ts
Added executeMoveGroupWorkflow; updated workflows to use identity/metrics and evaluateWorkflowConditions; skip/validate flows added for MoveGroup.
Group move domain services
apps/web/lib/api/groups/upsert-group-move-rules.ts, apps/web/lib/api/groups/get-group-move-rules.ts, apps/web/lib/api/groups/move-partners-to-group.ts, apps/web/lib/api/groups/find-groups-with-matching-rules.ts, apps/web/lib/api/groups/validate-group-move-rules.ts, apps/web/lib/api/groups/get-group-or-throw.ts
New upsert/get/validate/find helpers and movePartnersToGroup implementation; duplicate-rule detection and validation; get-group now surfaces moveRules.
Routes & orchestration
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts, apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts, apps/web/app/(ee)/api/groups/rules/route.ts, apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
PATCH/DELETE group tied to upsert/delete workflows; partners endpoint delegates to movePartnersToGroup; new GET /api/groups/rules; cron route uses evaluateWorkflowConditions.
Trigger payloads & webhooks
apps/web/app/(ee)/api/stripe/.../*.ts, apps/web/lib/actions/partners/create-manual-commission.ts, apps/web/lib/api/conversions/*.ts, apps/web/lib/partners/create-partner-commission.ts, apps/web/lib/integrations/shopify/create-sale.ts
Replaced WorkflowTrigger enum usages with "partnerMetricsUpdated" and augmented executeWorkflows payloads with reason, identity, and metrics.
UI — Group rules & settings
apps/web/app/.../group-move-rules.tsx, apps/web/app/.../group-additional-settings.tsx, apps/web/app/.../group-settings.tsx, apps/web/lib/swr/use-groups-rules.ts
New GroupMoveRules component, client validation, form-based save flow invoking PATCH and SWR hooks; skeleton/UI adjustments.
Schemas & types
apps/web/lib/zod/schemas/workflows.ts, apps/web/lib/zod/schemas/bounties.ts, apps/web/lib/zod/schemas/campaigns.ts, apps/web/lib/zod/schemas/groups.ts, apps/web/lib/types.ts
Added MoveGroup action, "between" operator, bounty/campaign-specific condition schemas, Group.moveRules schema, and new WorkflowContext (identity/metrics) and condition types.
Plan capability
apps/web/lib/plan-capabilities.ts
Added canUseGroupMoveRule capability flag.
DB / Prisma
packages/prisma/schema/group.prisma, packages/prisma/schema/workflow.prisma, packages/prisma/index.ts
Added PartnerGroup.workflowId relation; new WorkflowTrigger.partnerMetricsUpdated; Prisma client singleton typing/initialization adjustments.
Migrations & tests
apps/web/scripts/migrations/migrate-workflow-triggers.ts, apps/web/tests/partner-groups/index.test.ts
Migration script to consolidate triggers to partnerMetricsUpdated; tests added/updated for moveRules persistence and PATCH flows.
Bounties & condition specialization
apps/web/lib/api/bounties/generate-performance-bounty-name.ts, apps/web/app/.../add-edit-bounty-sheet.tsx, apps/web/lib/zod/schemas/bounties.ts
Introduced bountyPerformanceConditionSchema and switched bounty UI/parsing to bounty-specific condition schema.
Misc UI & icons
apps/web/ui/shared/inline-badge-popover.tsx, apps/web/ui/partners/rewards/rewards-logic.tsx, packages/ui/src/icons/nucleo/user-arrow-right.tsx
New InlineBadgePopoverAmountInput component; rewards input refactored to shared component; new SVG icon export.

Sequence Diagram(s)

sequenceDiagram
    participant Webhook as Webhook (Stripe/Shopify/Conversion)
    participant Execute as executeWorkflows
    participant DB as Prisma
    participant Engine as Workflow Engine (ACTION_HANDLERS)
    participant Eval as evaluateWorkflowConditions
    participant Move as movePartnersToGroup

    Webhook->>Execute: executeWorkflows({ trigger: "partnerMetricsUpdated", reason, identity, metrics })
    Execute->>DB: fetch program enrollment & aggregated metrics
    Execute->>Engine: iterate filtered workflows
    Engine->>Eval: evaluateWorkflowConditions({ conditions, attributes: metrics.aggregated })
    alt conditions met
        Engine->>Move: movePartnersToGroup(programId, partnerId, userId, targetGroup)
        Move->>DB: update programEnrollments (group/rewards)
        Move-->>Engine: schedule background tasks (link remap, bounties)
    else not met
        Engine-->>Execute: skip workflow
    end
Loading
sequenceDiagram
    actor User
    participant UI as Group Settings UI
    participant API as PATCH /groups/{id}
    participant Upsert as upsertGroupMoveRules
    participant Detect as findGroupsWithMatchingRules
    participant DB as Prisma

    User->>UI: Configure move rules and Save
    UI->>API: PATCH with moveRules
    API->>Upsert: upsertGroupMoveRules({ group, moveRules, workspace })
    Upsert->>Detect: findGroupsWithMatchingRules(existingGroups, moveRules)
    alt conflicts
        Detect-->>Upsert: conflicting groups
        Upsert-->>API: throw bad_request
        API-->>UI: return error
    else no conflicts
        Upsert->>DB: create or update Workflow (triggerConditions, actions)
        DB-->>Upsert: workflowId
        Upsert->>DB: update PartnerGroup.workflowId
        Upsert-->>API: { workflowId }
        API-->>UI: success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

🐇 I nibble rules and hop with cheer,

Conditions checked and migrations near,
Groups will move when metrics sing,
Workflows hum and merry things,
Hooray — the rabbit ships the gear!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Group move' is vague and generic, failing to convey the specific functionality being implemented despite substantial changes across multiple systems. Use a more descriptive title that captures the primary feature, such as 'Add group move workflow automation based on partner metrics' or 'Implement partner group reassignment via workflow triggers'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

… update related schemas. This change includes renaming and restructuring of parameters in the workflow and route files to streamline partner group management.
- Introduced `GroupMoveRule` component to manage rules for moving partners between groups based on specific criteria.
- Updated `GroupAdditionalSettings` to include the new `GroupMoveRule` component.
- Enhanced `GroupSettingsFormSkeleton` with additional settings rows for group ID and slug.
- Refactored `AmountInput` to utilize `InlineBadgePopoverAmountInput` for better input handling.
- Added `UserArrowRight` icon for improved UI representation.
- Introduced `upsertGroupMoveWorkflow` to manage group move rules and workflows.
- Updated `getGroupOrThrow` to include move rules in the group data.
- Enhanced the PATCH route to handle move rules and integrate with the new workflow.
- Refactored `GroupMoveRule` component to utilize react-hook-form for better form management.
- Updated Zod schemas to include move rules in group definitions and updates.
- Improved UI for group additional settings to accommodate new move rule functionality.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (3)

46-89: Cache invalidation inconsistency persists.

Lines 59 and 82 still use mutatePrefix(\/api/groups/${group.slug}`)while the API calls usegroup.id. This mismatch will prevent proper cache invalidation. Update both to use group.id` for consistency with the API endpoint and with the pattern used at line 142.


108-116: Unsafe error property access without type guard.

Line 113 accesses error.message directly without verifying that error is an Error object. If validateGroupMoveRules throws a non-Error value, this could cause a runtime error or display undefined.

🔎 Proposed fix with type guard
   if (data.moveRules && data.moveRules.length > 0) {
     try {
       validateGroupMoveRules(data.moveRules);
     } catch (error) {
-      toast.error(error.message);
+      toast.error(error instanceof Error ? error.message : String(error));
       return;
     }

268-285: Placeholder documentation link.

The "Learn more" link at lines 274-280 points to https://dub.co/help, which appears to be a generic help page rather than documentation specific to group move rules. Consider updating this to point to specific documentation when available, or use a named constant to make future updates easier.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9651ca and ec318fd.

📒 Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/find-groups-with-matching-rules.ts.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/find-groups-with-matching-rules.ts.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (5)
apps/web/lib/zod/schemas/groups.ts (1)
  • updateGroupSchema (130-153)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/validate-group-move-rules.ts (1)
  • validateGroupMoveRules (3-61)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/find-groups-with-matching-rules.ts.ts (1)
  • findGroupsWithMatchingRules (3-25)
apps/web/lib/constants/payouts.ts (1)
  • PAYOUT_HOLDING_PERIOD_DAYS (9-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-move-rules.tsx (1)
  • GroupMoveRules (43-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (1)

167-242: Well-structured form implementation.

The form architecture follows React Hook Form best practices:

  • FormProvider properly wraps the form context
  • handleSubmit correctly wires validation and submission
  • ✓ Save button appropriately disabled when form is pristine (!isDirty)
  • ✓ Clean integration of the GroupMoveRules component within the form flow
  • ✓ Form reset at line 143 correctly updates state after successful save

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (3)

113-113: Unsafe error property access without type guard.

This issue was previously flagged and remains unaddressed. The error.message access at line 113 is unsafe if validateGroupMoveRules throws a non-Error value.


35-35: Loading state not checked before duplicate validation.

This issue was previously flagged and remains unaddressed. The duplicate check can be bypassed if groups data hasn't finished loading, as the loading state from useGroupsRules() is not extracted or checked.

Also applies to: 117-133


275-281: Placeholder link for documentation.

This issue was previously flagged and remains unaddressed. The "Learn more" link still points to a generic help page rather than specific group move rules documentation.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec318fd and 262f738.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/groups/rules/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/find-groups-with-matching-rules.ts.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
  • apps/web/lib/swr/use-groups-rules.ts
  • apps/web/lib/zod/schemas/groups.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/find-groups-with-matching-rules.ts.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.
📚 Learning: 2025-11-24T08:55:31.332Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/app/(ee)/api/fraud-rules/route.ts:71-87
Timestamp: 2025-11-24T08:55:31.332Z
Learning: In apps/web/app/(ee)/api/fraud-rules/route.ts, fraud rules cannot be created in a disabled state. When using prisma.fraudRule.upsert, the create branch intentionally omits the disabledAt field (defaulting to null, meaning enabled), while the update branch allows toggling enabled/disabled state via the disabledAt field. This is a business logic constraint.

Applied to files:

  • apps/web/app/(ee)/api/groups/rules/route.ts
📚 Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.

Applied to files:

  • apps/web/lib/swr/use-groups-rules.ts
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.

Applied to files:

  • apps/web/lib/zod/schemas/groups.ts
📚 Learning: 2025-12-18T16:55:57.098Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:57.098Z
Learning: In the bounty system (e.g., apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts), bounties cannot be created with workflow attributes `partnerEnrolledDays` and `partnerJoined`. Bounty workflows only support attributes available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions, which is a subset of the general WORKFLOW_ATTRIBUTES schema.

Applied to files:

  • apps/web/lib/zod/schemas/groups.ts
📚 Learning: 2025-08-16T11:14:00.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2754
File: apps/web/lib/partnerstack/schemas.ts:47-52
Timestamp: 2025-08-16T11:14:00.667Z
Learning: The PartnerStack API always includes the `group` field in partner responses, so the schema should use `.nullable()` rather than `.nullish()` since the field is never omitted/undefined.

Applied to files:

  • apps/web/lib/zod/schemas/groups.ts
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
🧬 Code graph analysis (4)
apps/web/app/(ee)/api/groups/rules/route.ts (3)
apps/web/lib/auth/workspace.ts (1)
  • withWorkspace (57-491)
packages/prisma/index.ts (1)
  • prisma (16-16)
apps/web/lib/zod/schemas/groups.ts (1)
  • groupRulesSchema (190-192)
apps/web/lib/swr/use-groups-rules.ts (2)
apps/web/lib/zod/schemas/groups.ts (1)
  • groupRulesSchema (190-192)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (7-48)
apps/web/lib/zod/schemas/groups.ts (1)
apps/web/lib/zod/schemas/workflows.ts (1)
  • workflowConditionSchema (83-93)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (6)
apps/web/lib/zod/schemas/groups.ts (1)
  • updateGroupSchema (130-153)
apps/web/lib/swr/use-groups-rules.ts (1)
  • useGroupsRules (9-25)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/validate-group-move-rules.ts (1)
  • validateGroupMoveRules (3-61)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/find-groups-with-matching-rules.ts.ts (1)
  • findGroupsWithMatchingRules (5-32)
apps/web/lib/constants/payouts.ts (1)
  • PAYOUT_HOLDING_PERIOD_DAYS (9-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-move-rules.tsx (1)
  • GroupMoveRules (43-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (1)

96-148: LGTM on the form implementation!

The form setup with react-hook-form, validation flow, duplicate checking logic, and cache invalidation strategy are all well-structured. The fix to reset the form state after successful submission (line 144) properly addresses the previous concern about the "Save changes" button remaining enabled.

apps/web/lib/swr/use-groups-rules.ts (1)

1-25: LGTM!

This SWR hook follows the established patterns in the codebase. The conditional key based on workspaceId, the use of keepPreviousData: true to prevent UI flicker during revalidation, and the return structure are all appropriate. Based on learnings, workspace authentication in the route structure prevents endless loading states, so the implementation is complete.

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

15-15: LGTM on the schema integration!

The moveRules integration is well-structured across the schemas:

  • Line 74: GroupSchema uses .nullish().default(null) appropriately, as null is a valid persisted value.
  • Line 152: updateGroupSchema uses .optional() correctly, allowing the field to be omitted during updates.
  • Lines 190-192: The new groupRulesSchema export provides a clean, typed subset for the API route.

This pattern aligns with the codebase's established approach for form and validation schemas.

Also applies to: 74-74, 152-152, 190-192

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

1-39: LGTM!

This API route implementation is clean and secure:

  • Properly wrapped with withWorkspace for authentication and authorization.
  • Efficient Prisma query that selects only the needed fields.
  • Correct use of optional chaining (workflow?.triggerConditions) to handle cases where a group doesn't have an associated workflow.
  • Schema validation with groupRulesSchema.parse() ensures type safety before returning the response.
  • Appropriate permission (groups.read) and plan requirements (advanced, enterprise) are enforced.

… fetching rules, and implement validation checks
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (4)

3-4: Remove explicit .ts extensions from imports.

This comment duplicates a previous review suggesting removal of .ts extensions from import statements to follow TypeScript and Next.js conventions.

🔎 Proposed fix
-import { findGroupsWithMatchingRules } from "@/lib/api/groups/find-groups-with-matching-rules.ts";
+import { findGroupsWithMatchingRules } from "@/lib/api/groups/find-groups-with-matching-rules";
-import { validateGroupMoveRules } from "@/lib/api/groups/validate-group-move-rules.ts";
+import { validateGroupMoveRules } from "@/lib/api/groups/validate-group-move-rules";

Also applies to: 9-9


113-115: Unsafe error property access without type guard.

This duplicates a previous review comment: Line 114 accesses error.message directly without verifying that error is an Error object.

🔎 Proposed fix with type guard
       } catch (error) {
-        toast.error(error.message);
+        toast.error(error instanceof Error ? error.message : String(error));
         return;
       }

36-36: Duplicate check can be skipped if groups data hasn't loaded yet.

This duplicates a previous review comment: The useGroupsRules() hook exposes a loading state, but the duplicate rule check at lines 118-134 is only guarded by if (groups). Consider disabling form submission while groups are loading.

Also applies to: 118-134, 234-236


277-277: Placeholder link for documentation.

This duplicates a previous review comment: The "Learn more" link points to a generic help page rather than specific group move rules documentation.

🧹 Nitpick comments (4)
apps/web/lib/api/groups/validate-group-move-rules.ts (1)

16-19: Redundant null check.

Line 17 uses both == null and === undefined, but == null already matches both null and undefined in JavaScript.

🔎 Proposed simplification
-    if (rule.value == null || rule.value === undefined) {
+    if (rule.value == null) {
       throw new Error(`Rule ${i + 1}: Please enter a value.`);
     }
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts (1)

132-153: Conditionally include workflowId in update payload.

Lines 138-153 unconditionally include workflowId in the group update data, even when upsertGroupMoveRules returns { workflowId: undefined } (the no-op case). While Prisma may ignore undefined values, it's clearer to conditionally spread the workflowId only when it's not undefined.

🔎 Proposed fix to conditionally include workflowId
     const [updatedGroup] = await Promise.all([
       prisma.partnerGroup.update({
         where: {
           id: group.id,
         },
         data: {
           name,
           slug,
           color,
           additionalLinks,
           maxPartnerLinks,
           linkStructure,
           utmTemplateId,
           applicationFormData,
           landerData,
-          workflowId,
+          ...(workflowId !== undefined && { workflowId }),
           ...(holdingPeriodDays !== undefined &&
             !updateHoldingPeriodDaysForAllGroups && {
               holdingPeriodDays,
             }),
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (1)

60-60: Cache invalidation fixed for move rules but inconsistent for other updates.

Line 143 correctly uses mutate(\/api/groups/${group.id}`)for move rule updates. However, lines 60 and 83 still usemutatePrefix(`/api/groups/${group.slug}`)` for auto-approve and holding period updates. Since all API calls use the group ID (not slug), these invalidations should also use the group ID for consistency.

🔎 Proposed fix for consistent cache invalidation
       onSuccess: async () => {
-        await mutatePrefix(`/api/groups/${group.slug}`);
+        await mutatePrefix(`/api/groups/${group.id}`);
         setShowConfirmAutoApproveModal(false);

Also applies to: 83-83

apps/web/lib/api/groups/upsert-group-move-rules.ts (1)

9-10: Remove explicit .ts extensions from imports.

Imports should not include explicit .ts extensions to follow TypeScript and Next.js conventions.

🔎 Proposed fix
-import { findGroupsWithMatchingRules } from "./find-groups-with-matching-rules.ts";
+import { findGroupsWithMatchingRules } from "./find-groups-with-matching-rules";
-import { getGroupMoveRules } from "./get-group-move-rules.ts";
+import { getGroupMoveRules } from "./get-group-move-rules";
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 262f738 and e48f70d.

📒 Files selected for processing (7)
  • apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts
  • apps/web/app/(ee)/api/groups/rules/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
  • apps/web/lib/api/groups/find-groups-with-matching-rules.ts.ts
  • apps/web/lib/api/groups/get-group-move-rules.ts
  • apps/web/lib/api/groups/upsert-group-move-rules.ts
  • apps/web/lib/api/groups/validate-group-move-rules.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/api/groups/rules/route.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
📚 Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Only components exported from the `hubspot/ui-extensions` npm package can be used in settings components

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.

Applied to files:

  • apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts
🧬 Code graph analysis (6)
apps/web/lib/api/groups/validate-group-move-rules.ts (1)
apps/web/lib/types.ts (1)
  • WorkflowCondition (626-626)
apps/web/lib/api/groups/get-group-move-rules.ts (2)
packages/prisma/index.ts (1)
  • prisma (16-16)
apps/web/lib/zod/schemas/groups.ts (1)
  • groupRulesSchema (190-192)
apps/web/lib/api/groups/upsert-group-move-rules.ts (8)
apps/web/lib/types.ts (2)
  • WorkflowCondition (626-626)
  • WorkflowAction (633-633)
apps/web/lib/plan-capabilities.ts (1)
  • getPlanCapabilities (4-23)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
packages/prisma/index.ts (1)
  • prisma (16-16)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts.ts (1)
  • findGroupsWithMatchingRules (5-32)
apps/web/lib/api/groups/get-group-move-rules.ts (1)
  • getGroupMoveRules (4-25)
packages/prisma/client.ts (1)
  • WorkflowTrigger (36-36)
apps/web/lib/api/create-id.ts (1)
  • createId (69-74)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts.ts (2)
apps/web/lib/zod/schemas/groups.ts (1)
  • groupRulesSchema (190-192)
apps/web/lib/types.ts (1)
  • WorkflowCondition (626-626)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (5)
apps/web/lib/zod/schemas/groups.ts (1)
  • updateGroupSchema (130-153)
apps/web/lib/swr/use-groups-rules.ts (1)
  • useGroupsRules (9-25)
apps/web/lib/api/groups/validate-group-move-rules.ts (1)
  • validateGroupMoveRules (3-61)
apps/web/lib/constants/payouts.ts (1)
  • PAYOUT_HOLDING_PERIOD_DAYS (9-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-move-rules.tsx (1)
  • GroupMoveRules (43-158)
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts (1)
apps/web/lib/api/groups/upsert-group-move-rules.ts (1)
  • upsertGroupMoveRules (12-104)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
apps/web/lib/api/groups/get-group-move-rules.ts (1)

4-25: LGTM!

The function correctly queries partner groups with their workflow trigger conditions, maps them to the moveRules format expected by the schema, and validates the results before returning. The use of optional chaining and nullish coalescing ensures safe handling of groups without workflows.

apps/web/lib/api/groups/validate-group-move-rules.ts (1)

3-60: LGTM!

The validation logic is comprehensive and provides clear, user-friendly error messages. It correctly validates:

  • Attribute selection
  • Value presence
  • Operator-specific constraints (gte requires number > 0, between requires valid min/max range)
  • Relationship constraints (max > min for between operator)
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts (1)

385-392: LGTM!

The workflow deletion logic is correctly placed before the group deletion (step 5 before step 6) to respect foreign key constraints. The conditional check ensures the workflow is only deleted when present.

apps/web/lib/api/groups/find-groups-with-matching-rules.ts.ts (3)

5-32: LGTM!

The main function correctly handles edge cases (null/empty inputs) and filters groups based on rule overlap detection. The logic properly excludes the current group and only considers groups with defined move rules.


34-78: LGTM!

The rule overlap detection logic is well-designed:

  • Creates attribute-indexed maps for efficient lookup
  • Correctly identifies that rule sets with no shared attributes cannot conflict
  • Requires ALL shared attributes to overlap for a conflict, which ensures proper conflict detection

The comment on lines 34-35 clearly explains the intent.


80-124: LGTM!

The interval conversion and overlap checking logic is mathematically correct:

  • conditionToInterval properly converts gte and between operators to numeric intervals
  • doConditionsOverlap uses the standard interval overlap formula: [a, b] overlaps [c, d] if a <= d && c <= b
apps/web/lib/api/groups/upsert-group-move-rules.ts (4)

21-29: LGTM!

The plan capability check correctly enforces that group move rules are only available on Advanced and Enterprise plans, throwing an appropriate error for unauthorized access.


31-48: LGTM!

The function correctly handles three distinct cases:

  • Empty array: deletes the existing workflow and returns null to clear the field
  • Undefined: no-op, returns undefined to preserve existing state
  • Populated array: proceeds to validation and upsert

This design provides clear semantics for each case.


50-63: LGTM!

The conflict detection logic correctly:

  • Fetches all existing group move rules
  • Checks for overlapping rules using the helper function
  • Provides a clear, user-friendly error message with group names
  • Uses pluralization for proper grammar

65-103: LGTM!

The workflow upsert logic is well-structured:

  • Builds a proper MoveGroup action with the group ID
  • Uses the correct trigger type partnerMetricsUpdated
  • Includes trigger conditions and actions in the workflow data
  • Creates new workflows with properly prefixed IDs
  • Updates existing workflows with new data

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/lib/api/workflows/execute-workflows.ts (1)

169-171: Consider removing unreachable defensive check.

The !action check is defensive but unreachable in practice. The parseWorkflowConfig function always returns an action or throws an error (when actions.length === 0), so action will never be null/undefined at this point.

🔎 Proposed refactor to remove unreachable check
     const { action } = parseWorkflowConfig(workflow);
 
-    if (!action) {
-      continue;
-    }
-
     const handler = ACTION_HANDLERS[action.type];
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e48f70d and 41b5f1d.

📒 Files selected for processing (2)
  • apps/web/lib/api/workflows/execute-move-group-workflow.ts
  • apps/web/lib/api/workflows/execute-workflows.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/api/workflows/execute-move-group-workflow.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.
📚 Learning: 2025-12-18T16:55:57.098Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:57.098Z
Learning: In the bounty system (e.g., apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts), bounties cannot be created with workflow attributes `partnerEnrolledDays` and `partnerJoined`. Bounty workflows only support attributes available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions, which is a subset of the general WORKFLOW_ATTRIBUTES schema.

Applied to files:

  • apps/web/lib/api/workflows/execute-workflows.ts
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

  • apps/web/lib/api/workflows/execute-workflows.ts
🧬 Code graph analysis (1)
apps/web/lib/api/workflows/execute-workflows.ts (5)
apps/web/lib/types.ts (2)
  • WorkflowContext (743-751)
  • WorkflowConditionAttribute (628-628)
apps/web/lib/api/workflows/execute-complete-bounty-workflow.ts (1)
  • executeCompleteBountyWorkflow (13-229)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)
  • executeSendCampaignWorkflow (21-210)
apps/web/lib/api/workflows/execute-move-group-workflow.ts (1)
  • executeMoveGroupWorkflow (10-99)
apps/web/lib/api/workflows/parse-workflow-config.ts (1)
  • parseWorkflowConfig (8-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
apps/web/lib/api/workflows/execute-workflows.ts (6)

66-81: LGTM!

The reason-based filtering logic correctly implements the optimization strategy. By filtering out workflows whose conditions don't match the reason, you avoid unnecessary data fetching and execution. The logging provides good visibility into the filtering behavior.


83-128: Excellent conditional data fetching optimization.

The lazy loading of commission aggregates is a smart performance improvement. By only executing the expensive aggregate query when workflows actually require totalCommissions, you avoid unnecessary database load. The parallel fetching with Promise.all efficiently retrieves both programEnrollment and commissions when needed.


144-163: LGTM!

The context construction properly enriches the workflow context with aggregated metrics computed from partner link stats and commissions. The addition of groupId to identity enables downstream workflows (like MoveGroup) to access the partner's current group.


165-187: LGTM!

The handler dispatch pattern with error handling is well-implemented. Individual workflow failures are logged and don't prevent other workflows from executing, which provides good fault isolation.


11-30: Excellent refactoring to a handler-based dispatch pattern.

The introduction of WorkflowActionHandler interface and ACTION_HANDLERS mapping provides a clean, extensible architecture for workflow actions. This eliminates the need for switch statements and makes adding new action types straightforward.

Verify that ACTION_HANDLERS covers all values in the WORKFLOW_ACTION_TYPES enum to ensure no action types are missing.


32-41: Smart optimization with reason-based filtering.

The REASON_TO_ATTRIBUTES mapping enables early filtering of workflows, avoiding unnecessary parsing and execution. The mapping is complete and correct:

  • leadtotalLeads
  • saletotalConversions, totalSaleAmount
  • commissiontotalCommissions

All four WorkflowConditionAttribute values are accounted for, and the semantic relationships are sound.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (3)

9-9: Remove explicit .ts extension from import.

TypeScript and Next.js automatically resolve module extensions. Explicit .ts extensions in imports are non-standard and can cause issues with some bundlers or configurations.

🔎 Proposed fix
-import useGroupsRules from "@/lib/swr/use-groups-rules.ts";
+import useGroupsRules from "@/lib/swr/use-groups-rules";

113-115: Unsafe error property access without type guard.

Line 114 accesses error.message directly without verifying that error is an Error object. If validateGroupMoveRules throws a non-Error value, this will cause a runtime error or display undefined.

🔎 Proposed fix with type guard
     } catch (error) {
-      toast.error(error.message);
+      toast.error(error instanceof Error ? error.message : String(error));
       return;
     }

276-282: Placeholder link for documentation.

The "Learn more" link points to https://dub.co/help which appears to be a generic help page rather than documentation specific to group move rules. Consider updating this to a more specific documentation URL when available, or adding a TODO comment.

🧹 Nitpick comments (1)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (1)

93-100: Consider adding defensive validation for between operator values.

The between case accesses value.min and value.max without validating they are numbers. While validateGroupMoveRules validates these upstream, adding defensive checks here would make the function more robust if called from other contexts.

🔎 Proposed defensive check
     case "between":
       if (typeof condition.value === "object" && condition.value !== null) {
+        const min = condition.value.min;
+        const max = condition.value.max;
+        if (typeof min !== "number" || typeof max !== "number") {
+          return null;
+        }
         return {
-          min: condition.value.min,
-          max: condition.value.max,
+          min,
+          max,
         };
       }
       return null;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41b5f1d and 147ab8f.

📒 Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
  • apps/web/lib/api/groups/find-groups-with-matching-rules.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
📚 Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Only components exported from the `hubspot/ui-extensions` npm package can be used in settings components

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
🧬 Code graph analysis (2)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (2)
apps/web/lib/zod/schemas/groups.ts (1)
  • groupRulesSchema (190-192)
apps/web/lib/types.ts (1)
  • WorkflowCondition (626-626)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (5)
apps/web/lib/zod/schemas/groups.ts (1)
  • updateGroupSchema (130-153)
apps/web/lib/swr/use-groups-rules.ts (1)
  • useGroupsRules (9-25)
apps/web/lib/api/groups/validate-group-move-rules.ts (1)
  • validateGroupMoveRules (3-61)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (1)
  • findGroupsWithMatchingRules (5-32)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-move-rules.tsx (1)
  • GroupMoveRules (43-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (1)

97-149: LGTM on the form submission flow.

The form handling implementation is well-structured:

  • Form state is properly reset after successful save (line 145)
  • Duplicate rule validation guards against missing groups data
  • Save button correctly disables during groupsLoading state
  • Cache invalidation covers both the specific group and the rules prefix
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (3)

55-59: Verify: Rule sets with no shared attributes may still conflict.

The comment on line 34-35 states rule sets conflict "if there exists ANY set of attribute values that would satisfy both simultaneously." However, when rule sets have no shared attributes (e.g., Group A: leads >= 5, Group B: conversions >= 10), the function returns false (no conflict).

A partner with leads=10 AND conversions=20 would satisfy both rule sets simultaneously. Unless the system evaluates rules such that only one group can ever match per attribute type, this logic may produce false negatives.

Please confirm whether this is the intended behavior. If rules on different attributes should be allowed to coexist (e.g., evaluated in priority order), the current logic is correct. Otherwise, this needs adjustment.


5-32: LGTM on the main function structure.

The findGroupsWithMatchingRules function is well-structured with appropriate early returns for empty inputs, proper filtering logic, and clean mapping to the result type.


107-124: LGTM on interval overlap logic.

The doConditionsOverlap function correctly implements standard interval overlap detection using the min1 <= max2 && min2 <= max1 formula.

…rmanceConditionSchema, update related types and API functions for improved validation and consistency
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
apps/web/lib/zod/schemas/campaigns.ts (1)

115-123: Schema mismatch: query parameter validation inconsistent with data model.

The getCampaignsQuerySchema still uses workflowConditionSchema for parsing the triggerCondition query parameter (line 120), but the actual campaign data model now uses campaignTriggerConditionSchema (lines 64, 97). This inconsistency may cause:

  • Query filters that don't match the stored data structure
  • Runtime validation errors when filtering campaigns
  • Inability to query campaigns using the new schema format
🔎 Proposed fix to align query schema with data model
    triggerCondition: z
      .string()
      .pipe(
        z.preprocess(
          (input: string) => JSON.parse(input),
-         workflowConditionSchema,
+         campaignTriggerConditionSchema,
        ),
      )
      .optional(),
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)

312-333: Scheduled workflow path assumes partnerJoined condition without validation.

When partnerId is not provided, the function uses a date-based query that implicitly assumes:

  1. condition.attribute is "partnerJoined"
  2. condition.value represents days
  3. No condition evaluation is needed—all enrollments within the date range match

If this function is called with a different condition type (e.g., totalLeads > 100), the logic would be incorrect. Consider adding validation or documentation to clarify that scheduled workflows only support partnerJoined conditions.

🔎 Proposed validation to prevent incorrect usage
+  if (condition.attribute !== "partnerJoined") {
+    throw new Error(
+      `Scheduled workflows without partnerId only support partnerJoined conditions, got: ${condition.attribute}`
+    );
+  }
+
   const startDate = subDays(new Date(), condition.value);
   // add 12 hours to the start date since we run the partnerEnrolled workflow every 12 hours
   const endDate = addHours(startDate, 12);
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (1)

124-130: Optional: Formatting change has no functional impact.

The arrow function syntax change for requireImage and requireUrl initializers is purely cosmetic. Both the original and new versions produce identical results.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 147ab8f and df3712d.

📒 Files selected for processing (6)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/lib/api/bounties/generate-performance-bounty-name.ts
  • apps/web/lib/api/workflows/execute-send-campaign-workflow.ts
  • apps/web/lib/types.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/lib/zod/schemas/campaigns.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/types.ts
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-09-12T17:36:09.551Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/app/(ee)/api/bounties/[bountyId]/route.ts:100-107
Timestamp: 2025-09-12T17:36:09.551Z
Learning: For performance bounties in the bounty system, names cannot be provided by users - they are always auto-generated based on the performance condition using generateBountyName(). This ensures consistency and clarity about what the bounty actually measures. Any provided name should be overridden for performance bounties when a performanceCondition exists.

Applied to files:

  • apps/web/lib/api/bounties/generate-performance-bounty-name.ts
📚 Learning: 2025-12-18T16:55:47.136Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:47.136Z
Learning: In bounty creation logic, prohibit using workflow attributes partnerEnrolledDays and partnerJoined. Workflow attributes should be limited to those available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions. Apply this rule to all bounty creation endpoints or handlers (e.g., any file under apps/.../bounties/ that creates or drafts submissions), not just the specific route. This ensures consistency with the WORKFLOW_ATTRIBUTES schema subset across the codebase.

Applied to files:

  • apps/web/lib/api/bounties/generate-performance-bounty-name.ts
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

  • apps/web/lib/api/bounties/generate-performance-bounty-name.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.

Applied to files:

  • apps/web/lib/zod/schemas/campaigns.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-12-18T16:55:57.098Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:57.098Z
Learning: In the bounty system (e.g., apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts), bounties cannot be created with workflow attributes `partnerEnrolledDays` and `partnerJoined`. Bounty workflows only support attributes available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions, which is a subset of the general WORKFLOW_ATTRIBUTES schema.

Applied to files:

  • apps/web/lib/zod/schemas/campaigns.ts
  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

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

Applied to files:

  • apps/web/lib/zod/schemas/bounties.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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/lib/zod/schemas/bounties.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.

Applied to files:

  • apps/web/lib/zod/schemas/bounties.ts
🧬 Code graph analysis (5)
apps/web/lib/api/bounties/generate-performance-bounty-name.ts (1)
apps/web/lib/types.ts (1)
  • BountyPerformanceCondition (630-632)
apps/web/lib/zod/schemas/campaigns.ts (1)
apps/web/lib/zod/schemas/workflows.ts (1)
  • WORKFLOW_ATTRIBUTES (9-16)
apps/web/lib/zod/schemas/bounties.ts (1)
apps/web/lib/zod/schemas/workflows.ts (1)
  • WORKFLOW_ATTRIBUTES (9-16)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (1)
apps/web/lib/zod/schemas/bounties.ts (1)
  • bountyPerformanceConditionSchema (20-24)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)
apps/web/lib/api/workflows/evaluate-workflow-conditions.ts (1)
  • evaluateWorkflowConditions (4-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (7)
apps/web/lib/zod/schemas/campaigns.ts (3)

11-11: LGTM!

The imports are correctly added and used within the file.


49-53: Verify if restricting the operator to only "gte" is intentional.

The new schema only allows the "gte" (greater than or equal) operator. This restricts campaign trigger conditions to a single comparison type. Consider whether other operators (e.g., "lte", "eq", "lt", "gt") might be needed for various campaign scenarios, such as triggering when a metric falls below a threshold or equals a specific value.

If this restriction is intentional for the group-move workflow feature, please confirm. Otherwise, consider supporting additional operators.


64-64: LGTM!

The consistent use of campaignTriggerConditionSchema in both CampaignSchema and updateCampaignSchema aligns the trigger condition validation across create and update operations.

Also applies to: 97-97

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

14-17: LGTM! Schema import updated correctly.

The change from workflowConditionSchema to bountyPerformanceConditionSchema is appropriate as it uses a bounty-specific schema rather than the generic workflow schema. The usage at line 552 is consistent with this import change.

Also applies to: 552-554

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

76-76: Verify schema usage is correct after fixing allowed attributes.

Once the bountyPerformanceConditionSchema is updated to use the restricted attribute set, ensure these usages still provide the correct validation for bounty creation and retrieval.

Also applies to: 106-108

apps/web/lib/api/bounties/generate-performance-bounty-name.ts (1)

1-1: LGTM! Type updated to use bounty-specific condition.

The type change from WorkflowCondition to BountyPerformanceCondition appropriately narrows the function's type signature to use the bounty-specific performance condition type. The function logic remains unchanged and continues to work correctly with the new type.

Also applies to: 11-11

apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)

1-8: LGTM! Import changes align with the new evaluation approach.

The updated imports correctly reflect the transition from evaluateWorkflowCondition to evaluateWorkflowConditions and the introduction of CampaignTriggerCondition type.

Comment on lines +38 to 41
const { programId, partnerId } = context?.identity || {
programId: workflow.programId,
partnerId: undefined,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fallback logic may not trigger if context.identity is an empty object.

The current destructuring with || fallback only applies when context?.identity is nullish (null/undefined). If context.identity exists but lacks programId, the variable will be undefined instead of falling back to workflow.programId, potentially causing database query failures.

🔎 Proposed fix to ensure programId fallback
-  const { programId, partnerId } = context?.identity || {
-    programId: workflow.programId,
-    partnerId: undefined,
-  };
+  const programId = context?.identity?.programId ?? workflow.programId;
+  const partnerId = context?.identity?.partnerId;
📝 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
const { programId, partnerId } = context?.identity || {
programId: workflow.programId,
partnerId: undefined,
};
const programId = context?.identity?.programId ?? workflow.programId;
const partnerId = context?.identity?.partnerId;
🤖 Prompt for AI Agents
In apps/web/lib/api/workflows/execute-send-campaign-workflow.ts around lines 38
to 41, the current destructuring uses context?.identity || { ... } which fails
to fallback when context.identity exists but is an empty object; change the
logic to individually fallback each property (e.g. programId =
context?.identity?.programId ?? workflow.programId and partnerId =
context?.identity?.partnerId ?? undefined) so missing properties on an existing
identity object correctly use the intended defaults.

partnerId,
groupIds: campaign.groups.map(({ groupId }) => groupId),
condition,
condition: condition as CampaignTriggerCondition,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and read the target file
fd "execute-send-campaign-workflow.ts" --type f

Repository: dubinc/dub

Length of output: 114


🏁 Script executed:

# Read the file to see the context around line 75
cat -n apps/web/lib/api/workflows/execute-send-campaign-workflow.ts | head -90 | tail -40

Repository: dubinc/dub

Length of output: 1358


🏁 Script executed:

# Find parseWorkflowConfig function definition
rg -n "parseWorkflowConfig" --type ts -B 2 -A 10

Repository: dubinc/dub

Length of output: 21179


🏁 Script executed:

# Search for CampaignTriggerCondition type definition
rg -n "type CampaignTriggerCondition|interface CampaignTriggerCondition" --type ts -A 5

Repository: dubinc/dub

Length of output: 381


🏁 Script executed:

# Search for WorkflowCondition type definition
rg -n "type WorkflowCondition|interface WorkflowCondition" --type ts -A 5

Repository: dubinc/dub

Length of output: 777


🏁 Script executed:

# Read the full parse-workflow-config.ts file to understand the return type
cat -n apps/web/lib/api/workflows/parse-workflow-config.ts

Repository: dubinc/dub

Length of output: 989


🏁 Script executed:

# Find the schema definitions for workflowConditionSchema and campaignTriggerConditionSchema
rg -n "workflowConditionSchema|campaignTriggerConditionSchema" --type ts -B 2 -A 10 | head -100

Repository: dubinc/dub

Length of output: 6041


🏁 Script executed:

# Check how condition is used in the file to understand its actual type
rg -n "const { condition" --type ts -A 3 apps/web/lib/api/workflows/

Repository: dubinc/dub

Length of output: 161


🏁 Script executed:

# Find campaignTriggerConditionSchema definition
rg -n "campaignTriggerConditionSchema" --type ts -B 2 -A 15 apps/web/lib/zod/schemas/campaigns.ts

Repository: dubinc/dub

Length of output: 1724


🏁 Script executed:

# Check getProgramEnrollments function signature to see what it expects
rg -n "getProgramEnrollments" --type ts -B 2 -A 10 | head -60

Repository: dubinc/dub

Length of output: 2364


🏁 Script executed:

# Compare the two schema definitions side by side
rg -n "workflowConditionSchema|campaignTriggerConditionSchema" --type ts -A 12

Repository: dubinc/dub

Length of output: 19967


Remove unsafe type cast or add runtime validation.

The cast condition as CampaignTriggerCondition is unsafe. parseWorkflowConfig returns a WorkflowCondition which allows any operator from WORKFLOW_COMPARISON_OPERATORS and value as number | {min: number, max: number}, while CampaignTriggerCondition restricts operator to "gte" and value to only number. Without runtime validation, a workflow condition with a different operator or range value could bypass type checking and cause runtime issues in getProgramEnrollments. Either validate that the condition conforms to CampaignTriggerCondition before passing it, or update the function signature to accept the broader WorkflowCondition type.

🤖 Prompt for AI Agents
In apps/web/lib/api/workflows/execute-send-campaign-workflow.ts around line 75,
the code unsafely casts condition as CampaignTriggerCondition; instead either
perform a runtime validation that condition.operator === "gte" and typeof
condition.value === "number" (or convert a {min,max} range to a number/choose
behavior) and throw/return an error if it doesn't match, or change the called
function signature to accept WorkflowCondition; implement a small type-guard
function isCampaignTriggerCondition(cond): cond is CampaignTriggerCondition and
use it before calling getProgramEnrollments, returning a clear error or mapping
the condition to the expected shape when validation fails.

partnerId?: string;
groupIds: string[];
condition: WorkflowCondition;
condition: CampaignTriggerCondition;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find type definitions for CampaignTriggerCondition and WorkflowCondition
rg -n 'type CampaignTriggerCondition|type WorkflowCondition' -A 5 -t ts

Repository: dubinc/dub

Length of output: 970


🏁 Script executed:

# Find the evaluateWorkflowConditions function signature
rg -n 'evaluateWorkflowConditions' -B 2 -A 8 -t ts

Repository: dubinc/dub

Length of output: 9743


🏁 Script executed:

# Check if there are schema definitions
rg -n 'campaignTriggerConditionSchema|workflowConditionSchema' -A 5 -t ts

Repository: dubinc/dub

Length of output: 9919


🏁 Script executed:

# Get the function signature with parameter at line 233
sed -n '220,240p' apps/web/lib/api/workflows/execute-send-campaign-workflow.ts

Repository: dubinc/dub

Length of output: 460


Fix type mismatch: CampaignTriggerCondition passed to function expecting WorkflowCondition[].

The parameter condition at line 233 is typed as CampaignTriggerCondition, but at line 299 it's passed to evaluateWorkflowConditions which expects conditions: WorkflowCondition[]. These are two distinct types: campaignTriggerConditionSchema restricts the operator to "gte" and value to numbers only, while workflowConditionSchema supports multiple operators and object values. Either the parameter type should be changed to WorkflowCondition, or the function signature should be updated to accept CampaignTriggerCondition.

🤖 Prompt for AI Agents
In apps/web/lib/api/workflows/execute-send-campaign-workflow.ts around line 233,
the parameter `condition: CampaignTriggerCondition` is a type mismatch because
at line 299 it’s passed into evaluateWorkflowConditions which expects
`WorkflowCondition[]`; change the parameter to the correct type (e.g.
`conditions: WorkflowCondition[]`) or accept/convert to that shape before
calling evaluateWorkflowConditions (map the CampaignTriggerCondition(s) into
WorkflowCondition objects or update the function signature to take
CampaignTriggerCondition[] and perform the conversion), and update any call
sites and variable names accordingly so the value passed at line 299 matches
`WorkflowCondition[]`.

Comment on lines +20 to +24
export const bountyPerformanceConditionSchema = z.object({
attribute: z.enum(WORKFLOW_ATTRIBUTES),
operator: z.literal("gte").default("gte"),
value: z.number(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: bountyPerformanceConditionSchema allows prohibited attributes.

The schema uses the full WORKFLOW_ATTRIBUTES enum, which includes partnerEnrolledDays and partnerJoined. However, based on learnings, bounties should only support attributes available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions. Using the unrestricted enum allows invalid bounty conditions to be created.

🔎 Recommended fix: Create a restricted attribute set for bounties

Define a bounty-specific attributes constant in this file:

 import { WORKFLOW_ATTRIBUTES } from "./workflows";
+
+// Bounties only support attributes from partner link stats and commissions
+export const BOUNTY_PERFORMANCE_ATTRIBUTES = [
+  "totalLeads",
+  "totalConversions", 
+  "totalSaleAmount",
+  "totalCommissions",
+] as const;

 export const bountyPerformanceConditionSchema = z.object({
-  attribute: z.enum(WORKFLOW_ATTRIBUTES),
+  attribute: z.enum(BOUNTY_PERFORMANCE_ATTRIBUTES),
   operator: z.literal("gte").default("gte"),
   value: z.number(),
 });

Based on learnings, bounties cannot use partnerEnrolledDays and partnerJoined attributes - these should be excluded from the bounty schema.

📝 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
export const bountyPerformanceConditionSchema = z.object({
attribute: z.enum(WORKFLOW_ATTRIBUTES),
operator: z.literal("gte").default("gte"),
value: z.number(),
});
import { WORKFLOW_ATTRIBUTES } from "./workflows";
// Bounties only support attributes from partner link stats and commissions
export const BOUNTY_PERFORMANCE_ATTRIBUTES = [
"totalLeads",
"totalConversions",
"totalSaleAmount",
"totalCommissions",
] as const;
export const bountyPerformanceConditionSchema = z.object({
attribute: z.enum(BOUNTY_PERFORMANCE_ATTRIBUTES),
operator: z.literal("gte").default("gte"),
value: z.number(),
});

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/lib/types.ts (1)

645-648: Consider more descriptive parameter names.

The parameter names aV and cV are cryptic. Consider renaming them for better readability, such as actualValue and comparisonValue, or value and threshold.

🔎 Proposed refactor with clearer names
-export type OperatorFn = (
-  aV: number,
-  cV: number | { min: number; max?: number },
-) => boolean;
+export type OperatorFn = (
+  actualValue: number,
+  comparisonValue: number | { min: number; max?: number },
+) => boolean;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5496c and 80ef595.

📒 Files selected for processing (1)
  • apps/web/lib/types.ts
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-11-24T09:10:12.536Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.536Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.

Applied to files:

  • apps/web/lib/types.ts
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

  • apps/web/lib/types.ts
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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/types.ts
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.

Applied to files:

  • apps/web/lib/types.ts
📚 Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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/types.ts
📚 Learning: 2025-12-18T16:55:57.098Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:57.098Z
Learning: In the bounty system (e.g., apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts), bounties cannot be created with workflow attributes `partnerEnrolledDays` and `partnerJoined`. Bounty workflows only support attributes available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions, which is a subset of the general WORKFLOW_ATTRIBUTES schema.

Applied to files:

  • apps/web/lib/types.ts
📚 Learning: 2025-12-08T09:44:28.429Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3200
File: apps/web/lib/api/fraud/detect-duplicate-payout-method-fraud.ts:55-73
Timestamp: 2025-12-08T09:44:28.429Z
Learning: In apps/web/lib/api/fraud/detect-duplicate-payout-method-fraud.ts, the fraud event creation logic intentionally generates self-referential fraud events (where partnerId equals duplicatePartnerId) for partners with duplicate payout methods. This is by design to create raw events for all partners involved in a duplicate payout method scenario, regardless of whether they reference themselves.

Applied to files:

  • apps/web/lib/types.ts
🧬 Code graph analysis (1)
apps/web/lib/types.ts (4)
apps/web/lib/zod/schemas/bounties.ts (1)
  • bountyPerformanceConditionSchema (20-24)
apps/web/lib/zod/schemas/campaigns.ts (1)
  • campaignTriggerConditionSchema (49-53)
apps/web/lib/zod/schemas/workflows.ts (3)
  • WORKFLOW_ATTRIBUTES (9-16)
  • WORKFLOW_COMPARISON_OPERATORS (30-30)
  • workflowActionSchema (102-123)
packages/prisma/client.ts (1)
  • WorkflowTrigger (36-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
apps/web/lib/types.ts (5)

25-25: LGTM: New imports are properly utilized.

The new imports for WorkflowTrigger, bountyPerformanceConditionSchema, and campaignTriggerConditionSchema are all used later in the file to define new exported types.

Also applies to: 39-39, 48-48


630-636: LGTM: Type aliases follow established patterns.

The new type aliases for bounty and campaign conditions are properly derived from their respective zod schemas and follow the existing convention in this file.


739-743: LGTM: Metadata field adds extensibility.

The addition of the optional metadata field to CreateFraudEventInput maintains backward compatibility while allowing richer fraud event data.


745-766: All usages of WorkflowContext have been properly updated for the new structure.

The restructuring with nested identity and metrics objects has been correctly implemented across all consuming code:

  • execute-workflows.ts: Properly destructures and constructs the nested structure
  • execute-send-campaign-workflow.ts: Uses safe optional chaining with fallback values
  • execute-complete-bounty-workflow.ts: Correctly accesses metrics.current properties
  • execute-move-group-workflow.ts: Correctly accesses metrics.aggregated properties

All property access patterns follow the new structure consistently—no breaking patterns remain.


645-648: This is not a breaking change. All OperatorFn implementations in OPERATOR_FUNCTIONS already handle the new union type signature correctly: gte validates that cV is a number, and between validates that cV is an object. The schema validation ensures that condition.value matches the expected union type before being passed to these functions, making this change fully backward compatible.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
apps/web/lib/actions/partners/create-manual-commission.ts (1)

486-491: Verify firstConversionFlag calculation when using existing events.

When useExistingEvents is true, the firstConversionFlag is calculated using the original customer object (line 486-491), but the commissions are created for a duplicateCustomerId (line 280). The duplicate customer is created with sales = totalSales when recordSaleEvents is true (line 337), meaning it can have multiple sales transferred to it. However, firstConversionFlag is still determined by checking the original customer's state against the new linkId, which may incorrectly indicate this is a first conversion even when the duplicate customer has multiple sales.

This can cause metrics.current.conversions (line 574) to be inaccurate, potentially affecting workflow triggers.

🤖 Fix all issues with AI agents
In
@apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx:
- Around line 111-116: The catch block uses error.message but caught exceptions
are unknown in TypeScript; update the catch to narrow the type before using
message (for example, check if error instanceof Error and call
toast.error(error.message), otherwise call toast.error(String(error)) or a
generic message); apply this change around the validateGroupMoveRules(...)
try/catch where toast.error is invoked so access to error.message is type-safe.
- Line 9: The import for useGroupsRules includes a .ts extension which is
invalid for ESM/TS imports; update the import statement that references
useGroupsRules (symbol: useGroupsRules) to remove the ".ts" suffix (use
"@/lib/swr/use-groups-rules" instead) and check for any other imports in this
file using explicit .ts extensions and remove them to match standard
TypeScript/ESM import semantics.

In
@apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-settings.tsx:
- Around line 208-213: The skeleton currently always renders the SettingsRow for
"Group slug" while the real form conditionally omits it when group.slug ===
DEFAULT_PARTNER_GROUP.slug, causing layout shift; update the skeleton to match
the form's conditional rendering by either (preferred simple) also always
rendering the row only when the real form would (i.e., replicate the condition
used in the form: check group.slug against DEFAULT_PARTNER_GROUP.slug before
rendering the SettingsRow in the skeleton), or (if you want no data dependency
in skeleton) fetch/evaluate the group slug earlier so the skeleton can use the
same condition; target the SettingsRow usage and the group.slug /
DEFAULT_PARTNER_GROUP.slug check to implement the change.

In @apps/web/lib/actions/partners/create-manual-commission.ts:
- Line 572: metrics.current.leads is computed with commissionType === "lead" but
elsewhere the code always creates a lead event and increments link.leads,
causing inconsistency; update the leads metric calculation in
create-manual-commission.ts to reflect whether a lead event was actually created
or link.leads was incremented (use the same boolean/flag used when creating the
lead event or the result of that operation instead of commissionType), and
ensure the same logic is used when building the metrics payload (the expression
around leads: ...) so metrics.current.leads increments when a lead
event/link.leads is incremented.
🧹 Nitpick comments (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (1)

97-101: Form does not reset when group.moveRules changes.

The useForm hook only uses the initial group.moveRules value as defaultValues. If the group data is refetched or changes externally (e.g., after another user modifies it), the form won't reflect those updates. Consider adding a useEffect to reset the form when group.moveRules changes.

Proposed fix
 const form = useForm<FormData>({
   defaultValues: {
     moveRules: group.moveRules ?? [],
   },
 });

+useEffect(() => {
+  reset({ moveRules: group.moveRules ?? [] });
+}, [group.moveRules, reset]);
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (1)

93-100: Consider validating min and max properties exist before accessing.

The code checks that condition.value is a non-null object, but doesn't verify that min and max properties exist and are numbers. If the data is malformed, this could return an interval with undefined values, causing incorrect overlap calculations.

Proposed defensive check
   case "between":
     if (typeof condition.value === "object" && condition.value !== null) {
+      const { min, max } = condition.value as { min?: number; max?: number };
+      if (typeof min !== "number" || typeof max !== "number") {
+        return null;
+      }
       return {
-        min: condition.value.min,
-        max: condition.value.max,
+        min,
+        max,
       };
     }
     return null;
apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts (2)

135-137: Consider adding validation for empty conditions array.

If bounty.workflow.triggerConditions is an empty array, condition will be undefined, causing a runtime error at line 151 when accessing condition.attribute.

While existing bounties likely have at least one condition, adding defensive validation would improve robustness.

🛡️ Proposed defensive validation
 // Find the workflow condition
-const condition = z
+const conditions = z
   .array(workflowConditionSchema)
-  .parse(bounty.workflow.triggerConditions)[0];
+  .parse(bounty.workflow.triggerConditions);
+
+if (conditions.length === 0) {
+  return logAndRespond(
+    `Bounty ${bountyId} workflow has no conditions.`,
+    { logLevel: "error" }
+  );
+}
+
+const condition = conditions[0];

155-160: Function call correctly updated; consider leveraging multi-condition capability.

The function call correctly adopts the new evaluateWorkflowConditions API. However, the current pattern extracts only the first condition (line 137) and then wraps it back in an array, which doesn't fully leverage the multi-condition evaluation capability.

If bounty workflows should support multiple conditions, consider passing all conditions directly instead of just the first one. Otherwise, the current single-condition behavior is maintained.

💡 Alternative: Evaluate all conditions

If bounties should support multiple workflow conditions:

-// Find the workflow condition
-const condition = z
+// Find the workflow conditions
+const conditions = z
   .array(workflowConditionSchema)
-  .parse(bounty.workflow.triggerConditions)[0];
+  .parse(bounty.workflow.triggerConditions);
+
+if (conditions.length === 0) {
+  return logAndRespond(`Bounty ${bountyId} workflow has no conditions.`, {
+    logLevel: "error",
+  });
+}
+
+// Use first condition for attribute extraction (or adjust logic for multiple)
+const primaryCondition = conditions[0];

 // Partners with their link metrics
 const partners = programEnrollments.map((partner) => {
   return {
     id: partner.partnerId,
     ...aggregatePartnerLinksStats(partner.links),
     totalCommissions: partner.totalCommissions,
   };
 });

 const bountySubmissionsToCreate: Prisma.BountySubmissionCreateManyInput[] =
   partners
-    // only create submissions for partners that have at least 1 performanceCount
-    .filter((partner) => partner[condition.attribute] > 0)
+    .filter((partner) => partner[primaryCondition.attribute] > 0)
     .map((partner) => {
-      const performanceCount = partner[condition.attribute];
+      const performanceCount = partner[primaryCondition.attribute];

       const conditionMet = evaluateWorkflowConditions({
-        conditions: [condition],
+        conditions: conditions,
         attributes: {
-          [condition.attribute]: performanceCount,
+          [primaryCondition.attribute]: performanceCount,
         },
       });

Note: If supporting multiple conditions with different attributes, the attributes object construction (lines 157-159) may need adjustment to include all referenced attributes.

apps/web/lib/api/conversions/track-lead.ts (1)

322-334: Consider using a string constant for the trigger value.

The migration to a consolidated "partnerMetricsUpdated" trigger with a reason discriminator is a good approach for flexibility. The identity and metrics fields provide proper context for workflow evaluation.

To prevent typos and improve maintainability, consider defining the trigger string as a constant:

♻️ Recommended: Define trigger as a constant

Create a constants file (e.g., lib/workflows/constants.ts):

export const WORKFLOW_TRIGGERS = {
  PARTNER_METRICS_UPDATED: "partnerMetricsUpdated",
  // ... other triggers
} as const;

export const WORKFLOW_REASONS = {
  LEAD: "lead",
  SALE: "sale",
  COMMISSION: "commission",
} as const;

Then update the usage:

 await executeWorkflows({
-  trigger: "partnerMetricsUpdated",
-  reason: "lead",
+  trigger: WORKFLOW_TRIGGERS.PARTNER_METRICS_UPDATED,
+  reason: WORKFLOW_REASONS.LEAD,
   identity: {
     programId: link.programId,
     partnerId: link.partnerId,
   },
   metrics: {
     current: {
       leads: 1,
     },
   },
 }),
apps/web/lib/actions/partners/create-manual-commission.ts (1)

570-576: Consider adding sales count to workflow metrics.

The metrics.current payload includes saleAmount but not a sales count. When commissionType === "sale", the link stats are updated to increment link.sales (line 521-527), and when useExistingEvents is true, multiple sales can be created (up to 5, as validated on line 245-249).

Workflows may need to evaluate conditions based on total sales count (e.g., "partner reached 10 sales"), not just sale amount or first conversions. Consider adding a sales field to the metrics payload for consistency with the link stats tracking.

💡 Suggested enhancement
               metrics: {
                 current: {
                   leads: commissionType === "lead" ? 1 : 0,
                   saleAmount: saleAmount ?? totalSaleAmount,
+                  sales: commissionType === "sale" ? (saleAmount ? 1 : totalSales) : 0,
                   conversions: firstConversionFlag ? 1 : 0,
                 },
               },
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d795b54 and 58e94f4.

📒 Files selected for processing (11)
  • apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
  • apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-settings.tsx
  • apps/web/lib/actions/partners/create-manual-commission.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/get-workspace-users.ts
  • apps/web/lib/api/groups/find-groups-with-matching-rules.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
  • apps/web/lib/api/get-workspace-users.ts
  • apps/web/app/(ee)/api/groups/[groupIdOrSlug]/partners/route.ts
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-12-18T16:55:47.136Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:47.136Z
Learning: In bounty creation logic, prohibit using workflow attributes partnerEnrolledDays and partnerJoined. Workflow attributes should be limited to those available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions. Apply this rule to all bounty creation endpoints or handlers (e.g., any file under apps/.../bounties/ that creates or drafts submissions), not just the specific route. This ensures consistency with the WORKFLOW_ATTRIBUTES schema subset across the codebase.

Applied to files:

  • apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).

Applied to files:

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

Applied to files:

  • apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
📚 Learning: 2025-12-15T16:45:51.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3213
File: apps/web/app/(ee)/api/cron/auto-approve-partner/route.ts:122-122
Timestamp: 2025-12-15T16:45:51.667Z
Learning: In cron endpoints under apps/web/app/(ee)/api/cron, continue using handleCronErrorResponse for error handling. Do not detect QStash callbacks or set Upstash-NonRetryable-Error headers in these cron routes, so QStash can retry cron errors via its native retry mechanism. The existing queueFailedRequestForRetry logic should remain limited to specific user-facing API endpoints (e.g., /api/track/lead, /api/track/sale, /api/links) to retry only transient Prisma/database errors. This pattern should apply to all cron endpoints under the cron directory in this codebase.

Applied to files:

  • apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

  • apps/web/lib/actions/partners/create-manual-commission.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/conversions/track-lead.ts
📚 Learning: 2025-12-18T16:55:57.098Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3244
File: apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts:148-174
Timestamp: 2025-12-18T16:55:57.098Z
Learning: In the bounty system (e.g., apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts), bounties cannot be created with workflow attributes `partnerEnrolledDays` and `partnerJoined`. Bounty workflows only support attributes available from partner link stats (clicks, sales, leads, conversions, saleAmount) and totalCommissions, which is a subset of the general WORKFLOW_ATTRIBUTES schema.

Applied to files:

  • apps/web/lib/actions/partners/create-manual-commission.ts
  • apps/web/lib/api/conversions/track-sale.ts
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/create-manual-commission.ts
📚 Learning: 2025-10-02T22:46:22.739Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2924
File: apps/web/lib/api/conversions/track-lead.ts:7-7
Timestamp: 2025-10-02T22:46:22.739Z
Learning: In apps/web/lib/api/conversions/track-lead.ts, lead events are cached in Redis for 5 minutes (keys: `leadCache:${customer.id}` and `leadCache:${customer.id}:${stringifiedEventName}`) to provide immediate data availability while Tinybird ingestion happens asynchronously. This caching pattern allows for async-only recording without breaking "wait" mode semantics.

Applied to files:

  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/api/conversions/track-lead.ts
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-settings.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
🧬 Code graph analysis (4)
apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts (1)
apps/web/lib/api/workflows/evaluate-workflow-conditions.ts (1)
  • evaluateWorkflowConditions (4-34)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (2)
apps/web/lib/zod/schemas/groups.ts (1)
  • groupRulesSchema (190-192)
apps/web/lib/types.ts (1)
  • WorkflowCondition (628-628)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-settings.tsx (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/settings-row.tsx (1)
  • SettingsRow (3-23)
packages/email/src/react-email.d.ts (1)
  • Link (14-14)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (6)
apps/web/lib/zod/schemas/groups.ts (1)
  • updateGroupSchema (130-153)
apps/web/lib/swr/use-groups-rules.ts (1)
  • useGroupsRules (9-25)
apps/web/lib/api/groups/validate-group-move-rules.ts (1)
  • validateGroupMoveRules (3-61)
apps/web/lib/api/groups/find-groups-with-matching-rules.ts (1)
  • findGroupsWithMatchingRules (5-32)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/settings-row.tsx (1)
  • SettingsRow (3-23)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-move-rules.tsx (1)
  • GroupMoveRules (43-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-settings.tsx (1)

201-232: Nice refactor to use SettingsRow components in skeleton.

The skeleton now properly mirrors the structure of the actual form, including the Group ID description with the "Learn more" link. This improves consistency and makes the loading experience more accurate.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx (3)

229-238: LGTM!

The save button correctly reflects the form's dirty state and loading conditions. The separation between form-based move rules and modal-based settings (payout, auto-approve) is clean.


246-291: LGTM!

The skeleton component properly mirrors the structure of the actual form with appropriate loading placeholders for each settings section.


293-428: LGTM!

The confirmation modal components follow consistent patterns with proper state cleanup on close.

apps/web/lib/api/groups/find-groups-with-matching-rules.ts (3)

5-32: LGTM!

The main function has proper null/empty guards and cleanly filters groups that have overlapping rules with the current group. The logic correctly excludes the current group from comparison.


36-78: LGTM!

The overlap detection logic correctly identifies when two rule sets could potentially match the same set of attribute values. The approach of checking only shared attributes is appropriate—if all shared attributes have overlapping ranges, a partner could satisfy both rule sets simultaneously, which would create ambiguity.


107-124: LGTM!

The interval overlap logic is mathematically correct. Two intervals [a,b] and [c,d] overlap if and only if a <= d && c <= b, which this implements correctly.

apps/web/app/(ee)/api/cron/bounties/create-draft-submissions/route.ts (1)

3-3: LGTM: Import updated to multi-condition API.

The import correctly adopts the new evaluateWorkflowConditions function that supports evaluating multiple workflow conditions.

apps/web/lib/api/conversions/track-sale.ts (1)

540-553: Correct handling of sale metrics with conditional conversion count.

The workflow trigger correctly distinguishes sale metrics from lead metrics:

  • saleAmount tracks the actual sale amount
  • conversions is conditionally set based on firstConversionFlag, ensuring that only the first purchase from a customer increments the conversion count

This aligns well with the business logic for tracking unique conversions vs. repeat sales.

apps/web/lib/api/conversions/track-lead.ts (1)

19-19: The WorkflowTrigger enum is still actively used—this is not a complete migration to strings.

The WorkflowTrigger enum remains fully defined and imported across the codebase (in apps/web/lib/zod/schemas/workflows.ts, apps/web/lib/types.ts, and more). While track-lead.ts uses string literals for executeWorkflows() calls, this reflects inconsistent usage rather than a completed migration. The enum definition and type safety mechanisms are still present elsewhere, making the claim of a migration inaccurate.

Likely an incorrect or invalid review comment.

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

564-564: No action required — the trigger value is valid.

The "partnerMetricsUpdated" string literal is a valid and registered trigger in the WorkflowTrigger enum defined in packages/prisma/schema/workflow.prisma. It is used consistently across the codebase and will be properly handled by the workflow execution system at runtime. While using the enum value directly would provide compile-time type checking, the current string literal approach is an intentional pattern throughout the codebase.

import { mutatePrefix } from "@/lib/swr/mutate";
import { useApiMutation } from "@/lib/swr/use-api-mutation";
import useGroup from "@/lib/swr/use-group";
import useGroupsRules from "@/lib/swr/use-groups-rules.ts";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove .ts extension from import path.

TypeScript/ESM imports should not include the .ts extension. This could cause issues depending on the bundler configuration.

-import useGroupsRules from "@/lib/swr/use-groups-rules.ts";
+import useGroupsRules from "@/lib/swr/use-groups-rules";
📝 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
import useGroupsRules from "@/lib/swr/use-groups-rules.ts";
import useGroupsRules from "@/lib/swr/use-groups-rules";
🤖 Prompt for AI Agents
In
@apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
at line 9, The import for useGroupsRules includes a .ts extension which is
invalid for ESM/TS imports; update the import statement that references
useGroupsRules (symbol: useGroupsRules) to remove the ".ts" suffix (use
"@/lib/swr/use-groups-rules" instead) and check for any other imports in this
file using explicit .ts extensions and remove them to match standard
TypeScript/ESM import semantics.

Comment on lines +111 to +116
try {
validateGroupMoveRules(data.moveRules);
} catch (error) {
toast.error(error.message);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add type narrowing for caught error.

In TypeScript, caught errors are typed as unknown. Accessing error.message directly will cause a type error. Add proper type narrowing.

     try {
       validateGroupMoveRules(data.moveRules);
     } catch (error) {
-      toast.error(error.message);
+      toast.error(error instanceof Error ? error.message : "Invalid move rules");
       return;
     }
📝 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
try {
validateGroupMoveRules(data.moveRules);
} catch (error) {
toast.error(error.message);
return;
}
try {
validateGroupMoveRules(data.moveRules);
} catch (error) {
toast.error(error instanceof Error ? error.message : "Invalid move rules");
return;
}
🤖 Prompt for AI Agents
In
@apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-additional-settings.tsx
around lines 111 - 116, The catch block uses error.message but caught exceptions
are unknown in TypeScript; update the catch to narrow the type before using
message (for example, check if error instanceof Error and call
toast.error(error.message), otherwise call toast.error(String(error)) or a
generic message); apply this change around the validateGroupMoveRules(...)
try/catch where toast.error is invoked so access to error.message is type-safe.

Comment on lines +208 to +213
<SettingsRow
heading="Group slug"
description="For program landing page and internal group page URLs"
>
<div className="h-[38px] w-full animate-pulse rounded-md bg-neutral-200" />
</SettingsRow>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Conditional rendering mismatch between skeleton and actual form.

The skeleton unconditionally renders the "Group slug" row, but the actual form only shows it when group.slug !== DEFAULT_PARTNER_GROUP.slug (Line 122). This will cause a visible layout shift when the skeleton is replaced by the loaded form for the default partner group.

🔧 Proposed fix to match conditional rendering

Since the skeleton doesn't have access to the group data, you have two options:

Option 1: Always show the row in the skeleton (accept minor layout shift for default group):

// No change needed - current implementation

Option 2: Conditionally render in skeleton by fetching group slug early:

 function GroupSettingsFormSkeleton() {
+  const { group } = useGroup();
+  const isDefaultGroup = group?.slug === DEFAULT_PARTNER_GROUP.slug;
+
   return (
     <div className="border-border-subtle rounded-lg border">
       <div className="flex flex-col divide-y divide-neutral-200">
         <div className="px-6 py-6">
           <h3 className="text-content-emphasis text-lg font-semibold leading-7">
             Group settings
           </h3>
         </div>
         <SettingsRow
           heading="Name"
           description="For internal use only, never visible to partners."
         >
           <div className="h-[38px] w-full animate-pulse rounded-md bg-neutral-200" />
         </SettingsRow>

-        <SettingsRow
-          heading="Group slug"
-          description="For program landing page and internal group page URLs"
-        >
-          <div className="h-[38px] w-full animate-pulse rounded-md bg-neutral-200" />
-        </SettingsRow>
+        {!isDefaultGroup && (
+          <SettingsRow
+            heading="Group slug"
+            description="For program landing page and internal group page URLs"
+          >
+            <div className="h-[38px] w-full animate-pulse rounded-md bg-neutral-200" />
+          </SettingsRow>
+        )}

         <SettingsRow

Option 1 is simpler and the layout shift is minor. Option 2 eliminates the shift but adds complexity.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/settings/group-settings.tsx
around lines 208 - 213, The skeleton currently always renders the SettingsRow
for "Group slug" while the real form conditionally omits it when group.slug ===
DEFAULT_PARTNER_GROUP.slug, causing layout shift; update the skeleton to match
the form's conditional rendering by either (preferred simple) also always
rendering the row only when the real form would (i.e., replicate the condition
used in the form: check group.slug against DEFAULT_PARTNER_GROUP.slug before
rendering the SettingsRow in the skeleton), or (if you want no data dependency
in skeleton) fetch/evaluate the group slug earlier so the skeleton can use the
same condition; target the SettingsRow usage and the group.slug /
DEFAULT_PARTNER_GROUP.slug check to implement the change.

},
metrics: {
current: {
leads: commissionType === "lead" ? 1 : 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent leads metric calculation.

The metrics.current.leads is set to 0 when commissionType === "sale", but the code always creates a lead event (lines 205-239 or 372-383) and increments link.leads by 1 (line 505) regardless of the commission type. This inconsistency could cause workflows that evaluate lead-based conditions to not trigger correctly.

🔧 Proposed fix
               metrics: {
                 current: {
-                  leads: commissionType === "lead" ? 1 : 0,
+                  leads: 1,
                   saleAmount: saleAmount ?? totalSaleAmount,
                   conversions: firstConversionFlag ? 1 : 0,
                 },
📝 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
leads: commissionType === "lead" ? 1 : 0,
leads: 1,
🤖 Prompt for AI Agents
In @apps/web/lib/actions/partners/create-manual-commission.ts at line 572,
metrics.current.leads is computed with commissionType === "lead" but elsewhere
the code always creates a lead event and increments link.leads, causing
inconsistency; update the leads metric calculation in
create-manual-commission.ts to reflect whether a lead event was actually created
or link.leads was incremented (use the same boolean/flag used when creating the
lead event or the result of that operation instead of commissionType), and
ensure the same logic is used when building the metrics payload (the expression
around leads: ...) so metrics.current.leads increments when a lead
event/link.leads is incremented.

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