Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Dec 17, 2025

Summary by CodeRabbit

  • New Features

    • Rewards now calculate earnings per country to support location-based payout differences and per-link commission aggregation.
    • Modifiers can be applied to click-event rewards (previously restricted).
    • "Customer" added as a valid related entity for click-event conditions.
  • Bug Fixes / UX

    • Reward editor: close button clears tooltip description and reward content is consistently rendered for clarity.

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

@vercel
Copy link
Contributor

vercel bot commented Dec 17, 2025

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

Project Deployment Review Updated (UTC)
dub Ready Ready Preview Dec 31, 2025 8:07pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

📝 Walkthrough

Walkthrough

Adds per-link, per-country click aggregation via a new Tinybird pipe and TS wrapper, computes per-country earnings using a new resolveClickRewardAmount flow (applying reward modifiers/conditions), removes the ban on click modifiers, and adjusts rewards UI rendering and tooltip clearing.

Changes

Cohort / File(s) Summary
Cron: country-aware aggregation
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
Replaces previous analytics fetch with getClicksByCountries; groups clicks by link_id and country; calls resolveClickRewardAmount per-country to compute earnings; accumulates per-link totals and creates/syncs commissions; adds per-country logging.
Tinybird pipe & client
packages/tinybird/pipes/v3_clicks_by_countries.pipe, apps/web/lib/tinybird/get-clicks-by-countries.ts
New Tinybird pipe aggregates clicks by link_id and country; new TypeScript wrapper queries the pipe with linkIds and UTC date bounds and returns grouped click counts.
Reward evaluation utilities
apps/web/lib/api/rewards/validate-reward.ts, (imports used in cron)
Removes validation forbidding modifiers for "click" events, allowing click-scoped modifiers; cron now imports reward evaluation utilities (evaluateRewardConditions, rewardConditionsArraySchema) and uses resolveClickRewardAmount.
Rewards UI / logic
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, apps/web/ui/partners/rewards/rewards-logic.tsx
Close button clears tooltipDescription when clearing description; RewardsLogic always renders inside RewardSheetCard; "click" event now includes "customer" as a valid related entity for condition building.

Sequence Diagram(s)

sequenceDiagram
    participant Cron as Cron Job
    participant TB as Tinybird (pipe)
    participant RR as Reward Resolver
    participant DB as Database

    rect rgb(245, 250, 255)
    Note over Cron,TB: Fetch per-link, per-country clicks
    Cron->>TB: getClicksByCountries(linkIds, start, end)
    TB-->>Cron: [{ link_id, country, clicks }, ...]
    end

    rect rgb(250, 250, 240)
    Note over Cron,RR: Per-link × per-country evaluation
    loop for each (link_id, country) entry
        Cron->>RR: resolveClickRewardAmount(reward, country)
        RR->>RR: evaluateRewardConditions(country)
        RR->>RR: apply modifiers → amount
        RR-->>Cron: per-country earning amount
        Cron->>Cron: accumulate per-link totals
    end
    end

    rect rgb(245, 255, 245)
    Note over Cron,DB: Persist commissions
    Cron->>DB: createCommission(linkId, totalEarnings)
    DB-->>Cron: commissionCreated
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #2739: Modifies the same aggregate-clicks route; both change how click analytics are fetched and commissions are created.
  • PR #3148: Also updates aggregate-clicks processing and per-link earnings computation; closely related to this per-country refactor.
  • PR #2987: Introduces country-aware reward evaluation/context, aligning with the new per-country reward resolution used here.

Suggested reviewers

  • steven-tey

Poem

🐰🌱 Hopping through clicks from land to sea,
I count tiny carrots per country for thee.
Modifiers wiggle, conditions take flight,
Earnings stack up in morning light.
A cheerful nibble — code done right! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Geo-specific CPC rewards' accurately reflects the main change: implementing country-specific cost-per-click (CPC) reward calculations throughout the codebase.
✨ 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.

…implement reward condition evaluation. Replace analytics data fetching with a new method to retrieve clicks by countries. Introduce a utility function to resolve click reward amounts based on conditions.
@devkiran devkiran marked this pull request as ready for review December 30, 2025 13:50
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

🧹 Nitpick comments (2)
apps/web/lib/tinybird/get-clicks-by-countries.ts (1)

25-43: Consider adding empty array guard.

If linkIds is empty, the Tinybird query may behave unexpectedly. Consider adding an early return to handle this edge case gracefully.

🔎 Proposed fix
 export async function getClicksByCountries({
   linkIds,
   start,
   end,
 }: z.infer<typeof inputSchema>) {
+  if (linkIds.length === 0) {
+    return [];
+  }
+
   const { startDate, endDate } = getStartEndDates({
     start,
     end,
     timezone: "UTC",
   });
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (1)

242-278: Consider moving resolveClickRewardAmount to a shared utility.

This function is exported from a cron route file, but it could be reused elsewhere (e.g., for displaying estimated earnings in the UI). Consider relocating it to @/lib/partners/ alongside related utilities like getRewardAmount and evaluateRewardConditions.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af82e32 and 0c0976a.

📒 Files selected for processing (6)
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
  • apps/web/lib/api/rewards/validate-reward.ts
  • apps/web/lib/tinybird/get-clicks-by-countries.ts
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • packages/tinybird/pipes/v3_clicks_by_countries.pipe
💤 Files with no reviewable changes (1)
  • apps/web/lib/api/rewards/validate-reward.ts
🧰 Additional context used
🧠 Learnings (5)
📚 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/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/ui/partners/rewards/rewards-logic.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/ui/partners/rewards/add-edit-reward-sheet.tsx
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/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/aggregate-clicks/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/aggregate-clicks/route.ts
🧬 Code graph analysis (3)
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1)
apps/web/ui/partners/rewards/rewards-logic.tsx (1)
  • RewardsLogic (67-137)
apps/web/lib/tinybird/get-clicks-by-countries.ts (3)
apps/web/lib/tinybird/client.ts (1)
  • tb (3-6)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (5-51)
apps/web/lib/analytics/utils/format-utc-datetime-clickhouse.ts (1)
  • formatUTCDateTimeClickhouse (3-8)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (7)
apps/web/lib/tinybird/get-clicks-by-countries.ts (1)
  • getClicksByCountries (25-43)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
apps/web/lib/api/partners/sync-total-commissions.ts (1)
  • syncTotalCommissions (37-64)
apps/web/lib/zod/schemas/rewards.ts (1)
  • rewardConditionsArraySchema (156-158)
apps/web/lib/partners/evaluate-reward-conditions.ts (1)
  • evaluateRewardConditions (9-76)
apps/web/lib/partners/get-reward-amount.ts (1)
  • getRewardAmount (3-11)
apps/web/lib/api/partners/serialize-reward.ts (1)
  • serializeReward (6-14)
⏰ 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/ui/partners/rewards/rewards-logic.tsx (1)

230-234: LGTM! Enabling customer entity for click events.

This change correctly expands click event conditions to include the "customer" entity, which is necessary for geo-specific CPC rewards. The customer entity provides access to attributes like country, enabling per-country reward configuration in the UI.

apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (2)

531-536: LGTM! Properly clearing both description fields.

The close button now correctly clears both description and tooltipDescription together. This prevents orphaned tooltip content when the user removes the custom description section.


543-543: LGTM! RewardsLogic now available for all event types.

Removing the conditional rendering allows click events to use reward conditions, which aligns with the backend changes enabling modifiers for click events and the UI changes adding "customer" as a valid entity for clicks.

apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (2)

107-116: LGTM! Clean migration to country-based click aggregation.

The switch from analytics-based retrieval to getClicksByCountries properly enables per-country earnings calculation. The early return on empty results is appropriate.


118-163: LGTM! Well-structured per-link, per-country earnings calculation.

The grouping logic and earnings accumulation are correct. The logging provides good visibility into per-country earnings calculations for debugging purposes.

packages/tinybird/pipes/v3_clicks_by_countries.pipe (1)

1-33: LGTM! Well-structured Tinybird pipe for geo-specific click aggregation.

The SQL logic correctly aggregates clicks by link_id and country with proper filtering by date range and link IDs. The use of Array and DateTime64 parameters follows Tinybird conventions. The caller appropriately guards against empty linkIds by checking the result set before invoking the pipe.

apps/web/lib/tinybird/get-clicks-by-countries.ts (1)

7-11: The implementation correctly handles date parameters. The function signature uses z.infer<typeof inputSchema>, which returns Date type (the transform output), matching the caller's usage of Date objects from lines 56-63. Before passing parameters to buildPipe, dates are converted to strings via formatUTCDateTimeClickhouse, so the schema validation concern is unfounded.

Likely an incorrect or invalid review comment.

@devkiran
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (1)

242-278: LGTM! Correctly resolves country-specific click reward amounts.

The function properly:

  • Evaluates reward modifiers against customer country context
  • Applies only amountInCents from matched conditions (per learning, click rewards never use percentage or duration)
  • Falls back gracefully to base reward when parsing fails or no condition matches
  • Returns a scalar amount suitable for earnings calculation

Based on learnings.

💡 Optional: Log modifier parse failures for debugging

Consider logging when modifier parsing fails to aid troubleshooting:

   if (reward.modifiers) {
     const modifiers = rewardConditionsArraySchema.safeParse(reward.modifiers);

     if (modifiers.success) {
       const matchedCondition = evaluateRewardConditions({
         conditions: modifiers.data,
         context: {
           customer: {
             country,
           },
         },
       });

       if (matchedCondition) {
         partnerReward = {
           ...partnerReward,
           amountInCents:
             matchedCondition.amountInCents != null
               ? matchedCondition.amountInCents
               : null,
         };
       }
+    } else {
+      console.warn(
+        `Failed to parse modifiers for reward ${reward.id}:`,
+        modifiers.error,
+      );
     }
   }

This would help identify data integrity issues without breaking the reward calculation flow.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af82e32 and 0c0976a.

📒 Files selected for processing (6)
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
  • apps/web/lib/api/rewards/validate-reward.ts
  • apps/web/lib/tinybird/get-clicks-by-countries.ts
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • packages/tinybird/pipes/v3_clicks_by_countries.pipe
💤 Files with no reviewable changes (1)
  • apps/web/lib/api/rewards/validate-reward.ts
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 3239
File: apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts:265-273
Timestamp: 2025-12-30T16:17:42.732Z
Learning: In the Dub partner/rewards system, click rewards are always flat amounts specified in `amountInCents`. They never use percentage-based amounts (`amountInPercentage`) or duration fields (`maxDuration`), so when applying modifiers to click rewards (e.g., in resolveClickRewardAmount), only `amountInCents` needs to be transferred from the matched condition.
📚 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/ui/partners/rewards/rewards-logic.tsx
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.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/ui/partners/rewards/add-edit-reward-sheet.tsx
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 Learning: 2025-12-30T16:17:42.732Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3239
File: apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts:265-273
Timestamp: 2025-12-30T16:17:42.732Z
Learning: In the Dub partner/rewards system, click rewards are always flat amounts specified in `amountInCents`. They never use percentage-based amounts (`amountInPercentage`) or duration fields (`maxDuration`), so when applying modifiers to click rewards (e.g., in resolveClickRewardAmount), only `amountInCents` needs to be transferred from the matched condition.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/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/aggregate-clicks/route.ts
📚 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/app/(ee)/api/cron/aggregate-clicks/route.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/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 Learning: 2025-08-26T15:03:05.381Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/ui/partners/bounties/bounty-logic.tsx:88-96
Timestamp: 2025-08-26T15:03:05.381Z
Learning: In bounty forms, currency values are stored in cents in the backend but converted to dollars when loaded into forms, and converted back to cents when saved. The form logic works entirely with dollar amounts. Functions like generateBountyName that run during save logic receive cent values and need to divide by 100, but display logic within the form should format dollar values directly.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/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/aggregate-clicks/route.ts
🧬 Code graph analysis (3)
apps/web/lib/tinybird/get-clicks-by-countries.ts (3)
apps/web/lib/tinybird/client.ts (1)
  • tb (3-6)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (5-51)
apps/web/lib/analytics/utils/format-utc-datetime-clickhouse.ts (1)
  • formatUTCDateTimeClickhouse (3-8)
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1)
apps/web/ui/partners/rewards/rewards-logic.tsx (1)
  • RewardsLogic (67-137)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (7)
apps/web/lib/tinybird/get-clicks-by-countries.ts (1)
  • getClicksByCountries (25-43)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
apps/web/lib/api/partners/sync-total-commissions.ts (1)
  • syncTotalCommissions (37-64)
apps/web/lib/zod/schemas/rewards.ts (1)
  • rewardConditionsArraySchema (156-158)
apps/web/lib/partners/evaluate-reward-conditions.ts (1)
  • evaluateRewardConditions (9-76)
apps/web/lib/partners/get-reward-amount.ts (1)
  • getRewardAmount (3-11)
apps/web/lib/api/partners/serialize-reward.ts (1)
  • serializeReward (6-14)
🔇 Additional comments (8)
apps/web/ui/partners/rewards/rewards-logic.tsx (1)

233-233: LGTM! Enables customer entity for click events.

Adding "customer" to the click event's allowed entities enables geo-specific conditions (e.g., filtering by customer country) for click rewards, which aligns with the PR's objective to support per-country CPC rewards.

packages/tinybird/pipes/v3_clicks_by_countries.pipe (1)

1-33: LGTM! Well-structured Tinybird pipe for per-country click aggregation.

The pipe correctly:

  • Filters clicks by link IDs and date range
  • Groups by link_id and country to support geo-specific reward calculations
  • Orders results by link (ascending) and click count (descending) for efficient processing
apps/web/lib/tinybird/get-clicks-by-countries.ts (1)

1-43: LGTM! Clean Tinybird wrapper following established patterns.

The implementation correctly:

  • Defines strict input/output schemas
  • Converts dates to UTC via getStartEndDates
  • Formats timestamps for ClickHouse compatibility
  • Returns structured per-country click data for reward calculations
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (3)

8-18: LGTM! Necessary imports for per-country reward evaluation.

The new imports support:

  • Condition evaluation for geo-specific modifiers
  • Per-country click data retrieval
  • Schema validation and type safety
  • Enhanced logging with formatted output

107-163: LGTM! Robust per-country click aggregation and earnings calculation.

The implementation correctly:

  • Fetches clicks grouped by link and country from Tinybird
  • Organizes data in a Map for efficient lookup
  • Computes country-specific reward amounts via resolveClickRewardAmount
  • Accumulates per-link totals for commission creation
  • Handles edge cases (missing rewards, empty data) safely
  • Provides detailed logging for per-country earnings

165-209: LGTM! Correct commission creation and total syncing.

The implementation:

  • Creates commissions using per-link aggregated earnings (correctly summed across countries)
  • Properly filters out zero-click or zero-earning links
  • Syncs total commissions for each affected partner
  • Follows established patterns for commission structure
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (2)

531-536: LGTM! Correctly clears both description fields.

The close button now properly resets both description and tooltipDescription when dismissing the reward description editor, ensuring clean state.


543-543: LGTM! Unconditional rendering supports expanded event entities.

Rendering RewardsLogic for all events aligns with the expanded EVENT_ENTITIES that now includes customer-based conditions for click events. The component internally filters available entities per event type, so this is safe.

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/app/(ee)/api/cron/aggregate-clicks/route.ts (1)

203-209: Consider deduplicating partner/program pairs before syncing.

The sync logic is correct and uses the queued mode which should handle multiple calls gracefully. However, if a partner has multiple links, syncTotalCommissions is called multiple times for the same partnerId/programId pair.

🔎 Optional optimization to deduplicate sync calls
-    // Sync total commissions for each partner that we created commissions for
-    for (const { partnerId, programId } of commissionsToCreate) {
+    // Sync total commissions for each unique partner/program pair
+    const uniquePairs = new Set(
+      commissionsToCreate.map(({ partnerId, programId }) => `${partnerId}:${programId}`)
+    );
+    
+    for (const pair of uniquePairs) {
+      const [partnerId, programId] = pair.split(':');
       await syncTotalCommissions({
         partnerId,
         programId,
       });
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c0976a and 8ade6ea.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 3239
File: apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts:265-273
Timestamp: 2025-12-30T16:17:42.732Z
Learning: In the Dub partner/rewards system, click rewards are always flat amounts specified in `amountInCents`. They never use percentage-based amounts (`amountInPercentage`) or duration fields (`maxDuration`), so when applying modifiers to click rewards (e.g., in resolveClickRewardAmount), only `amountInCents` needs to be transferred from the matched condition.
📚 Learning: 2025-12-30T16:17:42.732Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3239
File: apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts:265-273
Timestamp: 2025-12-30T16:17:42.732Z
Learning: In the Dub partner/rewards system, click rewards are always flat amounts specified in `amountInCents`. They never use percentage-based amounts (`amountInPercentage`) or duration fields (`maxDuration`), so when applying modifiers to click rewards (e.g., in resolveClickRewardAmount), only `amountInCents` needs to be transferred from the matched condition.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 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/app/(ee)/api/cron/aggregate-clicks/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/aggregate-clicks/route.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/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 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/app/(ee)/api/cron/aggregate-clicks/route.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/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 Learning: 2025-08-26T15:03:05.381Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/ui/partners/bounties/bounty-logic.tsx:88-96
Timestamp: 2025-08-26T15:03:05.381Z
Learning: In bounty forms, currency values are stored in cents in the backend but converted to dollars when loaded into forms, and converted back to cents when saved. The form logic works entirely with dollar amounts. Functions like generateBountyName that run during save logic receive cent values and need to divide by 100, but display logic within the form should format dollar values directly.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/app/(ee)/api/cron/aggregate-clicks/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/aggregate-clicks/route.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts (7)
apps/web/lib/tinybird/get-clicks-by-countries.ts (1)
  • getClicksByCountries (25-43)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
apps/web/lib/api/partners/sync-total-commissions.ts (1)
  • syncTotalCommissions (37-64)
apps/web/lib/zod/schemas/rewards.ts (1)
  • rewardConditionsArraySchema (156-158)
apps/web/lib/partners/evaluate-reward-conditions.ts (1)
  • evaluateRewardConditions (9-76)
apps/web/lib/partners/get-reward-amount.ts (1)
  • getRewardAmount (3-11)
apps/web/lib/api/partners/serialize-reward.ts (1)
  • serializeReward (6-14)
⏰ 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/app/(ee)/api/cron/aggregate-clicks/route.ts (5)

8-18: LGTM!

The new imports correctly support the per-country click aggregation and reward evaluation features. All imports are used appropriately in the implementation.


107-124: LGTM!

The click fetching and grouping logic correctly replaces the previous analytics-based approach. The Map-based grouping by link_id provides efficient lookup for the per-link earnings calculation.


166-194: LGTM!

The commission creation logic correctly retrieves per-link earnings from the map, validates data, and filters out inactive links. The commission structure with quantity (total clicks) and earnings (total reward amount) properly reflects the aggregated country-level calculations.


240-276: LGTM!

The resolveClickRewardAmount function correctly evaluates country-specific reward modifiers and applies matched conditions. The implementation properly:

  • Uses safeParse to handle invalid modifier data gracefully
  • Evaluates conditions with customer country context
  • Applies only amountInCents from matched conditions (correct for click rewards per learnings)
  • Falls back to the default reward when no condition matches

Based on learnings, click rewards are always flat amounts in amountInCents, so the current implementation correctly handles the reward resolution.


126-163: The earnings calculation logic and accumulation pattern are correct. The code is already protected from null/undefined country values through multiple safeguards:

  • The ClickHouse schema defines country as LowCardinality(String), a non-nullable type
  • The Zod response schema enforces country: z.string() (non-optional) during validation
  • The function signature requires country: string

No verification or fallback handling is needed—the type system and validation layer prevent null values from reaching resolveClickRewardAmount.

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