-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Geo-specific CPC rewards #3239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Geo-specific CPC rewards #3239
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
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. Comment |
…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.
There was a problem hiding this 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
linkIdsis 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 movingresolveClickRewardAmountto 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 likegetRewardAmountandevaluateRewardConditions.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.tsapps/web/lib/api/rewards/validate-reward.tsapps/web/lib/tinybird/get-clicks-by-countries.tsapps/web/ui/partners/rewards/add-edit-reward-sheet.tsxapps/web/ui/partners/rewards/rewards-logic.tsxpackages/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.tsxapps/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
descriptionandtooltipDescriptiontogether. 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
getClicksByCountriesproperly 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_idandcountrywith proper filtering by date range and link IDs. The use ofArrayandDateTime64parameters follows Tinybird conventions. The caller appropriately guards against emptylinkIdsby 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 usesz.infer<typeof inputSchema>, which returnsDatetype (the transform output), matching the caller's usage ofDateobjects from lines 56-63. Before passing parameters tobuildPipe, dates are converted to strings viaformatUTCDateTimeClickhouse, so the schema validation concern is unfounded.Likely an incorrect or invalid review comment.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this 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
amountInCentsfrom 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
📒 Files selected for processing (6)
apps/web/app/(ee)/api/cron/aggregate-clicks/route.tsapps/web/lib/api/rewards/validate-reward.tsapps/web/lib/tinybird/get-clicks-by-countries.tsapps/web/ui/partners/rewards/add-edit-reward-sheet.tsxapps/web/ui/partners/rewards/rewards-logic.tsxpackages/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.tsxapps/web/ui/partners/rewards/add-edit-reward-sheet.tsxapps/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.tsxapps/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_idandcountryto 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
descriptionandtooltipDescriptionwhen dismissing the reward description editor, ensuring clean state.
543-543: LGTM! Unconditional rendering supports expanded event entities.Rendering
RewardsLogicfor all events aligns with the expandedEVENT_ENTITIESthat now includes customer-based conditions for click events. The component internally filters available entities per event type, so this is safe.
There was a problem hiding this 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,
syncTotalCommissionsis called multiple times for the samepartnerId/programIdpair.🔎 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
📒 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_idprovides 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) andearnings(total reward amount) properly reflects the aggregated country-level calculations.
240-276: LGTM!The
resolveClickRewardAmountfunction correctly evaluates country-specific reward modifiers and applies matched conditions. The implementation properly:
- Uses
safeParseto handle invalid modifier data gracefully- Evaluates conditions with customer country context
- Applies only
amountInCentsfrom 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
countryasLowCardinality(String), a non-nullable type- The Zod response schema enforces
country: z.string()(non-optional) during validation- The function signature requires
country: stringNo verification or fallback handling is needed—the type system and validation layer prevent null values from reaching
resolveClickRewardAmount.
Summary by CodeRabbit
New Features
Bug Fixes / UX
✏️ Tip: You can customize this high-level summary in your review settings.