Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Oct 23, 2025

…ntage types

Summary by CodeRabbit

  • New Features

    • Rewards now support separate currency and percentage fields for clearer flat vs. percentage payouts.
  • Bug Fixes

    • Earnings calculations for clicks, leads, and sales now use a consistent reward resolution to reduce incorrect payouts.
    • Partner-approved email step now surfaces real send errors for easier troubleshooting.
  • Chores

    • Validation, migration, forms and UI updated to adopt the new reward shape across program onboarding and displays.

@vercel
Copy link
Contributor

vercel bot commented Oct 23, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 28, 2025 1:11am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 23, 2025

Walkthrough

Replaces single reward amount with amountInCents and amountInPercentage, adds serializeReward and getRewardAmount, introduces validateReward, updates earnings/formatting/serialization across API, UI, migrations, Prisma schema, and tests, and adds a migration to backfill new fields.

Changes

Cohort / File(s) Summary
Helpers & Serialization
apps/web/lib/partners/get-reward-amount.ts, apps/web/lib/api/partners/serialize-reward.ts
Add getRewardAmount() to resolve numeric amount from new fields; add serializeReward() to normalize Prisma decimals to plain numbers.
Validation & Schemas
apps/web/lib/api/rewards/validate-reward.ts, apps/web/lib/zod/schemas/rewards.ts, apps/web/lib/zod/schemas/program-onboarding.ts
New validateReward() with event/type rules; zod schemas replace amount with amountInCents and amountInPercentage and add amount-specific validators/transformers.
Earnings & Formatting Utilities
apps/web/lib/api/sales/calculate-sale-earnings.ts, apps/web/lib/api/sales/construct-reward-amount.ts, apps/web/lib/api/sales/construct-discount-amount.ts
Earnings/formatters updated to accept new reward shape and use getRewardAmount(); new discount formatter added.
Commission & Partner Logic
apps/web/lib/partners/create-partner-commission.ts, apps/web/lib/partners/determine-partner-reward.ts, apps/web/lib/partners/evaluate-reward-conditions.ts, apps/web/lib/api/partners/backfill-link-commissions.ts, apps/web/scripts/migrations/backfill-click-commissions.ts, apps/web/scripts/migrations/migrate-reward-amounts.ts
Commission creation and reward-evaluation now use serialized rewards and getRewardAmount(); added one-time migration to migrate amountamountInCents/amountInPercentage including modifiers.
API Routes & Cron
apps/web/app/(ee)/api/cron/aggregate-clicks/route.ts, apps/web/app/(ee)/api/partner-profile/programs/[programId]/route.ts, apps/web/app/(ee)/api/partner-profile/programs/route.ts, apps/web/app/(ee)/api/workflows/partner-approved/route.ts, apps/web/app/api/og/program/route.tsx
Routes updated to use serialized rewards, getRewardAmount(), and fixed-order reward arrays; email error propagation improved.
Actions (create/update/generate)
apps/web/lib/actions/partners/create-program.ts, apps/web/lib/actions/partners/create-reward.ts, apps/web/lib/actions/partners/update-reward.ts, apps/web/lib/actions/partners/generate-lander.ts
Reward payloads now set amountInCents/amountInPercentage by type; validateReward() applied; audit metadata uses serializeReward().
UI: Rewards & Discounts
apps/web/ui/partners/format-reward-description.ts, apps/web/ui/partners/format-discount-description.ts, apps/web/ui/partners/program-reward-description.tsx, apps/web/ui/partners/program-reward-list.tsx, apps/web/ui/partners/program-reward-modifiers-tooltip.tsx, apps/web/ui/partners/program-rewards-panel.tsx, apps/web/ui/partners/lander/blocks/earnings-calculator-block.tsx, apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx, apps/web/ui/partners/partner-info-cards.tsx, apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, apps/web/ui/partners/rewards/rewards-logic.tsx
Components adapted to new reward shape; filtering uses getRewardAmount(); some prop types widened to Omit<RewardProps,"id">; discounts use new formatter.
Embed, Fetchers & Utilities
apps/web/app/(ee)/app.dub.co/.../form-wrapper.tsx, apps/web/app/(ee)/app.dub.co/.../overview/page-client.tsx, apps/web/app/(ee)/app.dub.co/embed/referrals/utils.ts, apps/web/lib/partners/get-group-rewards-and-discount.ts, apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts, apps/web/lib/fetchers/get-program.ts
Reward construction switched to fixed-order arrays and serializeReward() mapping; onboarding/defaults and client code updated for amountInCents/amountInPercentage.
Prisma Schema
packages/prisma/schema/reward.prisma
Add amountInCents: Int? and amountInPercentage: Decimal?; new relations and indexes; legacy amount retained temporarily (TODO).
Third-party import mapping
apps/web/lib/rewardful/import-campaigns.ts
Map external campaign fields into amountInCents/amountInPercentage; logs use getRewardAmount(serializeReward(...)).
Tests & Fixtures
apps/web/tests/rewards/reward-conditions.test.ts, apps/web/tests/tracks/track-lead.test.ts, apps/web/tests/tracks/track-sale.test.ts, apps/web/tests/utils/resource.ts
Tests and fixtures updated to use type: "flat" and amountInCents (modifiers adjusted) and assertions updated to cents-based values.
Email & Types
packages/email/src/templates/program-invite.tsx, packages/email/src/react-email.d.ts
Renamed exported component to ProgramInvite; added Hr declaration to react-email types.

Sequence Diagram(s)

sequenceDiagram
  participant Cron as Aggregate Clicks Cron
  participant DB as Prisma DB
  participant S as serializeReward
  participant G as getRewardAmount
  participant API as Prisma: Commission.createMany

  Cron->>DB: fetch links + rewards
  DB-->>Cron: reward records
  Cron->>S: serializeReward(reward)
  S-->>Cron: serializedReward
  Cron->>G: getRewardAmount(serializedReward)
  G-->>Cron: numericAmount
  Cron->>Cron: earnings = numericAmount * clicks
  Cron->>API: bulk create commissions with earnings
  API-->>Cron: created commissions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus during review:

  • construct-reward-amount and calculate-sale-earnings (percentage vs cents math, modifier interaction)
  • create-reward / update-reward (validation logic, audit metadata serialization)
  • migrations/migrate-reward-amounts.ts (cursor pagination, modifiers conversion)
  • UI form components (add-edit-reward-sheet, rewards-logic) for correct field wiring and validation
  • Prisma schema change and migration plan (legacy amount handling and index implications)

Possibly related PRs

Suggested reviewers

  • TWilson023

"🐰 I hopped through schema, tidy and bright,
cents or percent now tell me what's right.
Rewards serialized, migrations run true,
earnings computed from values anew.
A tiny rabbit cheers this tidy rewrite!"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.13% 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 PR title "Support fractional reward percentages" accurately captures the primary change in the changeset. The main objective is to split the single amount field into amountInCents (for flat rewards) and amountInPercentage (for percentage rewards), with the latter supporting decimal values up to 99.99. This directly enables fractional percentages like 1.5%, 2.99%, etc., which was not possible with the previous whole-number-only approach. The title is clear, specific, concise, and avoids vague or generic phrasing, making it easily understandable when scanning commit history.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/fractional-reward-percentages

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.

@devkiran
Copy link
Collaborator Author

@coderabbitai final review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 25, 2025

@devkiran: I'll perform a final review of the PR now. I'll examine all the changes including the new fractional reward percentage support, validation logic, schema updates, and the migration strategy.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98f2742 and 63d033e.

📒 Files selected for processing (1)
  • apps/web/lib/fetchers/get-program.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/fetchers/get-program.ts (2)
apps/web/lib/types.ts (1)
  • RewardProps (533-533)
apps/web/lib/api/partners/serialize-reward.ts (1)
  • serializeReward (5-13)
⏰ 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/lib/fetchers/get-program.ts (1)

4-4: LGTM!

The import is correctly placed and necessary for the reward serialization logic below.

@steven-tey
Copy link
Collaborator

/bug0 run

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (2)

297-306: Prior validation concerns remain unaddressed.

The previous review identified several issues that are still present:

  1. Field naming inconsistency: amountInCents stores dollars, not cents (converted on line 86), making the name misleading.

  2. Zero values allowed: min: 0 permits $0 or 0% rewards, which don't make business sense. Consider min: 0.01 for both types.

  3. Backend schema validation gap: The frontend enforces max: 1000 for flat rewards, but the backend schema may lack this constraint, allowing direct API calls to bypass the limit.

Refer to the previous review for detailed recommendations on addressing these concerns.


97-98: Improved null check, but zero values still permitted.

The explicit amount == null check correctly distinguishes between missing values and zero, addressing the prior concern about falsy checks. However, this now permits the button to be enabled when amount is 0, which—combined with min: 0 validation—allows submission of $0 or 0% rewards that don't make business sense.

Consider changing the validation to min: 0.01 and updating the disabled check to explicitly reject zero:

-const buttonDisabled = amount == null || !type || !defaultRewardType;
+const buttonDisabled = amount == null || amount === 0 || !type || !defaultRewardType;
🧹 Nitpick comments (2)
apps/web/scripts/migrations/migrate-reward-amounts.ts (2)

23-25: Prefer id for deterministic ordering.

The re-query-without-skip pagination approach is correct. However, ordering by createdAt may cause issues:

  • Non-unique timestamps can lead to non-deterministic ordering
  • May not be indexed, causing performance degradation
  • Past review specifically suggested orderBy: { id: "asc" }

Apply this diff:

-      orderBy: {
-        createdAt: "asc",
-      },
+      orderBy: {
+        id: "asc",
+      },

33-43: Handle edge cases for reward type and null amounts.

The current logic has potential gaps:

  1. If reward.type is neither "flat" nor "percentage", both new fields remain null with no warning
  2. Defaulting null amounts to 0 (line 34) may not be semantically correct

Consider adding validation and logging:

       const amount = reward.amount ?? 0;
       let amountInCents: number | null = null;
       let amountInPercentage: number | null = null;
 
       // Migrate main amount field
       if (reward.type === "flat") {
         amountInCents = amount;
       } else if (reward.type === "percentage") {
         amountInPercentage = amount;
+      } else {
+        console.warn(
+          `Reward ${reward.id} has unknown type "${reward.type}", skipping amount migration`
+        );
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3e165 and 9ca60ba.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (6 hunks)
  • apps/web/scripts/migrations/migrate-reward-amounts.ts (1 hunks)
  • apps/web/ui/partners/lander/blocks/earnings-calculator-block.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#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)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx
🧬 Code graph analysis (3)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (1)
apps/web/app/api/domains/[domain]/verify/route.ts (1)
  • maxDuration (10-10)
apps/web/ui/partners/lander/blocks/earnings-calculator-block.tsx (2)
apps/web/lib/partners/get-reward-amount.ts (1)
  • getRewardAmount (3-11)
apps/web/ui/partners/format-reward-description.ts (1)
  • formatRewardDescription (4-32)
apps/web/scripts/migrations/migrate-reward-amounts.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/types.ts (1)
  • RewardConditions (554-554)
⏰ 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 (8)
apps/web/scripts/migrations/migrate-reward-amounts.ts (1)

1-4: LGTM!

The imports are appropriate for the migration task.

apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (3)

43-55: LGTM: Watch statement correctly updated for separate amount fields.

The refactor to watch amountInCents and amountInPercentage separately aligns with the new field structure and supports fractional values correctly.


84-91: Conversion logic is correct.

The null checks and type guards ensure only the appropriate field is converted and sent to the backend. The Math.round on line 86 correctly handles floating-point precision when converting dollars to cents.


261-263: LGTM: UI terminology improvements.

The updates from "Payout" to "Reward amount" and "Payout model" to "Reward structure" improve clarity and consistency throughout the form.

Also applies to: 272-273, 286-286

apps/web/ui/partners/lander/blocks/earnings-calculator-block.tsx (4)

3-3: LGTM!

The import correctly reflects the new helper function used in this component.


31-31: LGTM!

The refactoring correctly uses the getRewardAmount helper to retrieve the appropriate amount based on reward type, maintaining null safety by defaulting to 0.


72-72: LGTM!

The changes correctly implement the refactoring:

  1. Line 72: The function call now passes reward directly, matching the updated formatRewardDescription signature.
  2. Lines 84-88: The earnings calculations properly handle both reward types and support fractional percentages as intended by this PR:
    • Flat rewards: converts cents to dollars (rewardAmount / 100)
    • Percentage rewards: applies percentage to revenue (rewardAmount / 100 for fractional support, e.g., 10.5% → 0.105)

Also applies to: 84-88


28-30: No issues found - this change is correct and improves the component logic.

The behavioral change from sortRewardsByEventOrder(program.rewards)[0] to program.rewards.find((r) => r.event === "sale") is appropriate for this component:

  • The component is named EarningsCalculatorBlock and displays sale-based earnings, so it should specifically use the sale reward, not the first reward by priority order.
  • The old code would have returned a click or lead reward if one existed before a sale reward, which would produce incorrect earnings calculations for this use case.
  • This change aligns with similar patterns elsewhere in the codebase (e.g., apps/web/app/(ee)/api/workflows/partner-approved/route.ts:242).
  • The defensive null check if (!reward) return null; is appropriate.

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/zod/schemas/rewards.ts (1)

132-137: Verify the 99.99% maximum is intentional.

The max percentage is set to 99.99% rather than 100%. This effectively prevents partners from receiving the entire sale amount. If this is a business rule to ensure the platform always retains at least 0.01%, it's working as intended.

Additionally, consider adding decimal precision validation:

The schema currently accepts any number between 0 and 99.99, including values with many decimal places (e.g., 10.123456789%). If percentage rewards should be limited to 2 decimal places, apply this diff:

 export const PERCENTAGE_REWARD_AMOUNT_SCHEMA = z
   .number()
   .min(0, { message: "Reward percentage amount cannot be less than 0%" })
   .max(99.99, {
     message: "Reward percentage amount cannot be greater than 99.99%",
-  });
+  })
+  .refine(
+    (val) => Number.isInteger(val * 100),
+    { message: "Reward percentage amount must have at most 2 decimal places" }
+  );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a916c9 and 865bd5d.

📒 Files selected for processing (2)
  • apps/web/lib/zod/schemas/program-onboarding.ts (2 hunks)
  • apps/web/lib/zod/schemas/rewards.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/zod/schemas/program-onboarding.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.

Applied to files:

  • apps/web/lib/zod/schemas/rewards.ts
🧬 Code graph analysis (1)
apps/web/lib/zod/schemas/rewards.ts (1)
packages/prisma/client.ts (1)
  • RewardStructure (29-29)
⏰ 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/zod/schemas/rewards.ts (5)

139-145: LGTM!

The flat reward schema correctly validates amounts in cents with an integer constraint and reasonable bounds (0 to $999,999.99). The user-friendly error messages are clear.


147-154: LGTM!

The dual optional fields (amountInCents and amountInPercentage) correctly allow partial modifier configuration during form editing, with business logic validation ensuring exactly one field is set based on type at submission time. This aligns with the two-stage validation pattern used elsewhere in the codebase.

Based on learnings.


160-164: LGTM!

The transformer correctly handles null/undefined/empty strings while preserving 0 values. The fix from the previous review (changing from falsy check to explicit null check) ensures 0% rewards work correctly.


171-172: LGTM!

The read schema appropriately uses lenient validation (no min/max constraints) to accommodate existing database records, while maintaining type safety with .int() for cents and the decimalToNumber transformer for percentage values.


177-187: LGTM!

The write schema correctly uses the exported validation schemas for both amount fields. As confirmed in previous reviews, mutual exclusivity validation between amountInCents and amountInPercentage based on type is appropriately handled in the validate-reward method rather than at the Zod schema level, following the two-stage validation pattern.

@steven-tey steven-tey merged commit 7f14ed3 into main Oct 28, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the feat/fractional-reward-percentages branch October 28, 2025 01:20
@coderabbitai coderabbitai bot mentioned this pull request Nov 19, 2025
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