Skip to content

Add customer signup date and subscription date as reward condition#3514

Merged
steven-tey merged 12 commits intomainfrom
customer-reward-conditions
Mar 5, 2026
Merged

Add customer signup date and subscription date as reward condition#3514
steven-tey merged 12 commits intomainfrom
customer-reward-conditions

Conversation

@devkiran
Copy link
Copy Markdown
Collaborator

@devkiran devkiran commented Feb 27, 2026

Summary by CodeRabbit

  • New Features

    • Rewards can target customer signupDate and subscriptionStartDate; these dates are now available in reward context and partner commission payloads.
    • Reward condition types now include date with date-specific operators.
  • UI

    • Added a DatePicker and date-aware inputs for editing date-based reward conditions.
    • Tooltips and condition displays now format date/time values.
  • Tests

    • Added unit and E2E tests for date-based reward conditions and sale reward flows.

…d schemas

- Added `signupDate` to customer context in `checkout-session-completed`, `invoice-paid`, `track-sale`, and `create-shopify-sale` files.
- Updated `create-partner-commission` to include `signupDate` and `subscriptionStartDate`.
- Enhanced reward schemas to support `signupDate` and `subscriptionStartDate` as date types.
- Implemented date formatting in the UI for reward modifiers and conditions.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 27, 2026

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Mar 5, 2026 6:14am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Threads signupDate and subscriptionStartDate into partner commission context, extends reward schemas and UI to support date-type conditions and operators, adds a DatePicker component, and introduces unit and E2E tests and test resources for date-based reward conditions.

Changes

Cohort / File(s) Summary
Webhook & Sale Handlers
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts, apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts, apps/web/lib/api/conversions/track-sale.ts, apps/web/lib/integrations/shopify/create-sale.ts
Include signupDate (sourced from customer.createdAt) in context.customer when calling createPartnerCommission.
Commission Context
apps/web/lib/partners/create-partner-commission.ts
Derive subscriptionStartDate from firstCommission?.createdAt, use it for subscriptionDurationMonths calculation, and include it in context.customer.
Reward Schema & Validation
apps/web/lib/zod/schemas/rewards.ts
Add date attribute type, export DATE_CONDITION_OPERATORS, add signupDate and subscriptionStartDate customer attributes, and expose them on rewardContextSchema.
Reward UI & Logic
apps/web/ui/partners/rewards/rewards-logic.tsx, apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
Add date-aware operators, DatePicker UI for date attributes, and date/time formatting for condition values and tooltips.
UI Component
packages/ui/src/date-picker/date-picker.tsx, packages/ui/src/date-picker/index.ts
Add new DatePicker component and barrel export; new public types DatePickerProps and DatePickerTriggerRenderProps.
Tests & Test Resources
apps/web/tests/rewards/reward-conditions.test.ts, apps/web/tests/rewards/sale-reward.test.ts, apps/web/tests/utils/resource.ts
Add unit and E2E tests for signupDate and subscriptionStartDate conditions and add date-based reward modifiers and external ID test resources.
Manifest
package.json
Manifest context updated (no functional changes indicated).

Sequence Diagram(s)

sequenceDiagram
  participant Webhook as Webhook (Stripe)
  participant TrackSale as TrackSale Flow
  participant CommissionSvc as CommissionService
  participant Rewards as Rewards Schema
  participant UI as Rewards UI

  Webhook->>CommissionSvc: createPartnerCommission(context with customer.signupDate)
  TrackSale->>CommissionSvc: createPartnerCommission(context with customer.signupDate)
  CommissionSvc->>CommissionSvc: derive subscriptionStartDate from firstCommission?.createdAt
  CommissionSvc->>Rewards: include signupDate & subscriptionStartDate in reward context
  Rewards->>UI: expose date attributes & DATE_CONDITION_OPERATORS
  UI->>User: render DatePicker and formatted date values
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through timestamps, stitched signup and start,
Dates now flow with rewards and play their part,
Pickers and tests, schemas aligned,
Commissions get dates, neat carrots to find,
Hop on, celebrate — code with a heart! 🥕📅

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately describes the main feature: adding customer signup date and subscription date as reward conditions across multiple files and schemas.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch customer-reward-conditions

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 devkiran requested a review from steven-tey February 27, 2026 13:15
Copy link
Copy Markdown
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

🧹 Nitpick comments (2)
apps/web/lib/zod/schemas/rewards.ts (1)

231-233: Enforce date operator/value constraints at schema level (not only in UI).

DATE_CONDITION_OPERATORS is defined, but invalid operator/value combinations can still pass rewardConditionSchema for date attributes.

🧩 Suggested hardening
 export const rewardConditionSchema = z.object({
   entity: z.enum(
     REWARD_CONDITION_ENTITIES.map(({ id }) => id) as [string, ...string[]],
   ),
   attribute: z.enum(
     REWARD_CONDITION_ATTRIBUTES.map(({ id }) => id) as [string, ...string[]],
   ),
   operator: z.enum(CONDITION_OPERATORS),
   value: z.union([
     z.string(),
     z.number(),
     z.array(z.string()),
     z.array(z.number()),
   ]),
   label: z
     .string()
     .nullish()
     .describe("Product name used for display purposes in the UI."),
-});
+}).superRefine((data, ctx) => {
+  const attr = REWARD_CONDITION_ATTRIBUTES.find((a) => a.id === data.attribute);
+  if (attr?.type === "date") {
+    if (!DATE_CONDITION_OPERATORS.includes(data.operator)) {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        path: ["operator"],
+        message: "Invalid operator for date condition.",
+      });
+    }
+    if (typeof data.value !== "number") {
+      ctx.addIssue({
+        code: z.ZodIssueCode.custom,
+        path: ["value"],
+        message: "Date condition value must be a timestamp (number).",
+      });
+    }
+  }
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/zod/schemas/rewards.ts` around lines 231 - 233, The schema
currently allows invalid operator/value combinations for date attributes; update
rewardConditionSchema to validate date-specific rules: add a zod
refine/superRefine to check when attributeType === "date" then operator must be
one of DATE_CONDITION_OPERATORS and the provided value must be a valid date/ISO
date (e.g., Date.parse(value) is not NaN) and when attributeType !== "date"
disallow the DATE_CONDITION_OPERATORS; reference the existing constants
DATE_CONDITION_OPERATORS and the rewardConditionSchema and implement the checks
via refine/superRefine on rewardConditionSchema so validation is enforced at the
schema level.
apps/web/ui/partners/rewards/rewards-logic.tsx (1)

261-268: Minor edge case: timestamp value of 0.

The check !value on line 263 would treat a timestamp of 0 (representing Jan 1, 1970 00:00:00 UTC) as invalid. This is unlikely to be a real-world scenario for reward conditions, but worth noting.

💡 Optional: More explicit check
   // For date values, format timestamp as readable date + time
   if (type === "date") {
-    if (!value || isNaN(Number(value))) {
+    if (value === undefined || value === null || isNaN(Number(value))) {
       return "Value";
     }
 
     return formatDateTime(new Date(Number(value)));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/ui/partners/rewards/rewards-logic.tsx` around lines 261 - 268, The
current date branch treats falsy 0 as invalid; in the block handling type ===
"date" (around the formatDateTime(new Date(Number(value))) usage) change the
existence check from a truthy check (!value) to an explicit null/undefined check
(e.g., value == null) and still validate numeric conversion with
isNaN(Number(value)) so a timestamp of 0 is accepted and only
null/undefined/non-numeric values return "Value".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/lib/partners/create-partner-commission.ts`:
- Around line 120-127: firstCommission can be null for a first-sale, so
subscriptionStartDate is undefined and later subscriptionDurationMonths logic
fails; set subscriptionStartDate to fall back to the current commission's
createdAt (createdAt ?? new Date()) when firstCommission is null, then compute
subscriptionDurationMonths with differenceInMonths(createdAt ?? new Date(),
subscriptionStartDate) so first-sale modifiers evaluate correctly (also apply
the same fallback where subscriptionStartDate is used again around the other
occurrence at line ~133).

In `@apps/web/tests/utils/resource.ts`:
- Line 130: The UUID assigned to the id property in tests/utils/resource.ts
contains an invalid 'g' character; replace the string value for the id field
(id: "f4b8c2d3-ed55-4f9a-c342-20b9531g2d16") with a valid hex-only UUID
(characters 0-9 and a-f) preserving the UUID format (8-4-4-4-12) so tests use a
proper UUID.

---

Nitpick comments:
In `@apps/web/lib/zod/schemas/rewards.ts`:
- Around line 231-233: The schema currently allows invalid operator/value
combinations for date attributes; update rewardConditionSchema to validate
date-specific rules: add a zod refine/superRefine to check when attributeType
=== "date" then operator must be one of DATE_CONDITION_OPERATORS and the
provided value must be a valid date/ISO date (e.g., Date.parse(value) is not
NaN) and when attributeType !== "date" disallow the DATE_CONDITION_OPERATORS;
reference the existing constants DATE_CONDITION_OPERATORS and the
rewardConditionSchema and implement the checks via refine/superRefine on
rewardConditionSchema so validation is enforced at the schema level.

In `@apps/web/ui/partners/rewards/rewards-logic.tsx`:
- Around line 261-268: The current date branch treats falsy 0 as invalid; in the
block handling type === "date" (around the formatDateTime(new
Date(Number(value))) usage) change the existence check from a truthy check
(!value) to an explicit null/undefined check (e.g., value == null) and still
validate numeric conversion with isNaN(Number(value)) so a timestamp of 0 is
accepted and only null/undefined/non-numeric values return "Value".

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54e397a and bd98bd9.

📒 Files selected for processing (11)
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/partners/create-partner-commission.ts
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/tests/rewards/reward-conditions.test.ts
  • apps/web/tests/rewards/sale-reward.test.ts
  • apps/web/tests/utils/resource.ts
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx

Comment thread apps/web/lib/partners/create-partner-commission.ts Outdated
Comment thread apps/web/tests/utils/resource.ts Outdated
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
apps/web/lib/zod/schemas/rewards.ts (1)

23-23: Consider adding explicit date validation to rewardConditionSchema for clarity, though current implementation works correctly.

The value schema currently allows z.string() and z.number() for all condition types, including dates. While the DateConditionPicker in the UI enforces numeric timestamps and the two-stage validation pattern (form-level permissive schema for UX, submission-level strict schema) prevents invalid dates from being submitted, the schema itself doesn't explicitly codify that date conditions require numeric values. Adding a .refine() or .superRefine() to rewardConditionSchema to validate that date-type conditions have numeric timestamps would improve schema clarity and guard against edge cases where the form schema might be bypassed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/zod/schemas/rewards.ts` at line 23, rewardConditionSchema should
explicitly enforce that when condition.type === "date" the value is a numeric
timestamp; add a .refine() or .superRefine() on rewardConditionSchema to check
(e.g., if data.type === "date") that typeof data.value === "number" (or
Number.isFinite(Number(data.value)) if strings may be passed) and throw a clear
error path when it isn't—refer to rewardConditionSchema and DateConditionPicker
to ensure the refinement matches the UI's timestamp representation and error
message wording.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/lib/zod/schemas/rewards.ts`:
- Line 23: rewardConditionSchema should explicitly enforce that when
condition.type === "date" the value is a numeric timestamp; add a .refine() or
.superRefine() on rewardConditionSchema to check (e.g., if data.type === "date")
that typeof data.value === "number" (or Number.isFinite(Number(data.value)) if
strings may be passed) and throw a clear error path when it isn't—refer to
rewardConditionSchema and DateConditionPicker to ensure the refinement matches
the UI's timestamp representation and error message wording.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e33f81a-8573-46d7-8e63-5868832b5c4b

📥 Commits

Reviewing files that changed from the base of the PR and between c4c51ac and 44d8292.

📒 Files selected for processing (2)
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/zod/schemas/rewards.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/api/conversions/track-sale.ts

Copy link
Copy Markdown
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 (1)
packages/ui/src/date-picker/date-picker.tsx (1)

91-104: The invalid prop is not connected to aria-invalid for the default Trigger.

The invalid prop is passed to custom triggers (line 89) but not used for the default Trigger's aria-invalid attribute. This inconsistency could cause accessibility issues when using the default trigger.

Proposed fix
         <Trigger
           placeholder={placeholder}
           disabled={disabled}
           className={className}
           hasError={hasError}
           aria-required={props.required || props["aria-required"]}
-          aria-invalid={props["aria-invalid"]}
+          aria-invalid={invalid || props["aria-invalid"]}
           aria-label={props["aria-label"]}
           aria-labelledby={props["aria-labelledby"]}
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/date-picker/date-picker.tsx` around lines 91 - 104, The
default Trigger JSX is not receiving the component's invalid prop for its
aria-invalid attribute; update the Trigger element in date-picker.tsx (the
default branch rendering <Trigger ...>{displayValue}</Trigger>) to pass
aria-invalid using the invalid prop when props["aria-invalid"] is not explicitly
provided (e.g., set aria-invalid to props["aria-invalid"] || invalid) so the
component's invalid state is reflected for accessibility; ensure you reference
the existing Trigger component and the invalid variable used for custom
triggers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/ui/partners/rewards/rewards-logic.tsx`:
- Around line 485-490: The DatePicker onChange handler calls
setValue(conditionKey, { ...condition, value: date ? date.getTime() : undefined
}) but omits the shouldDirty option so the form may not mark itself dirty;
update the call to pass the options object with shouldDirty: true (i.e., call
setValue with the same value plus { shouldDirty: true }) in the onChange handler
where conditionKey and condition are used.

---

Nitpick comments:
In `@packages/ui/src/date-picker/date-picker.tsx`:
- Around line 91-104: The default Trigger JSX is not receiving the component's
invalid prop for its aria-invalid attribute; update the Trigger element in
date-picker.tsx (the default branch rendering <Trigger
...>{displayValue}</Trigger>) to pass aria-invalid using the invalid prop when
props["aria-invalid"] is not explicitly provided (e.g., set aria-invalid to
props["aria-invalid"] || invalid) so the component's invalid state is reflected
for accessibility; ensure you reference the existing Trigger component and the
invalid variable used for custom triggers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2e06c239-5600-4af4-8e9c-d8ecdcf87f0e

📥 Commits

Reviewing files that changed from the base of the PR and between 44d8292 and 75f7f18.

📒 Files selected for processing (3)
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • packages/ui/src/date-picker/date-picker.tsx
  • packages/ui/src/date-picker/index.ts

Comment thread apps/web/ui/partners/rewards/rewards-logic.tsx
@steven-tey
Copy link
Copy Markdown
Collaborator

@coderabbitai final review, any feedback?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 5, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== rewards.ts ==="
fd "rewards.ts" --full-path apps/web/lib/zod/schemas/ | xargs cat

echo "=== create-partner-commission.ts ==="
fd "create-partner-commission.ts" --full-path apps/web/lib/partners/ | xargs cat

echo "=== date-picker.tsx ==="
fd "date-picker.tsx" --full-path packages/ui/src/date-picker/ | xargs cat

echo "=== rewards-logic.tsx ==="
fd "rewards-logic.tsx" --full-path apps/web/ui/partners/rewards/ | xargs cat

echo "=== reward-conditions.test.ts ==="
fd "reward-conditions.test.ts" --full-path apps/web/tests/ | xargs cat

echo "=== program-reward-modifiers-tooltip.tsx ==="
fd "program-reward-modifiers-tooltip.tsx" --full-path apps/web/ui/ | xargs cat

echo "=== checkout-session-completed.ts ==="
fd "checkout-session-completed.ts" --full-path apps/web/app/ | xargs cat

echo "=== invoice-paid.ts ==="
fd "invoice-paid.ts" --full-path apps/web/app/ | xargs cat

echo "=== track-sale.ts ==="
fd "track-sale.ts" --full-path apps/web/lib/api/conversions/ | xargs cat

echo "=== create-sale.ts (shopify) ==="
fd "create-sale.ts" --full-path apps/web/lib/integrations/shopify/ | xargs cat

Length of output: 177345


🏁 Script executed:

#!/bin/bash
echo "=== evaluate-reward-conditions.ts ==="
fd "evaluate-reward-conditions.ts" | xargs cat

echo "=== date-picker trigger.tsx ==="
fd "trigger.tsx" --full-path packages/ui/src/date-picker/ | xargs cat

echo "=== date-picker types.ts ==="
fd "types.ts" --full-path packages/ui/src/date-picker/ | xargs cat

Length of output: 6905


@steven-tey: The implementation is well-structured overall — the Number(date) coercion for comparing Date context values against stored millisecond timestamps is the right approach and works correctly. Here are a few items worth looking at before merging:


🐛 Bug: null date field values bypass the undefined guard

In evaluate-reward-conditions.ts, the guard only checks for undefined:

if (fieldValue === undefined) {
  return false;
}

However, rewardContextSchema defines signupDate and subscriptionStartDate as .nullish(), so they can be null. When fieldValue is null, it passes through and Number(null) === 0, treating it as the Unix epoch (Jan 1, 1970). This means a null signupDate would incorrectly match any less_than <recent timestamp> condition.

The fix is a single-line change:

- if (fieldValue === undefined) {
+ if (fieldValue === undefined || fieldValue === null) {
  return false;
}

The tests cover signupDate: undefined but not signupDate: null — adding a test case for this would be a good safety net.


⚠️ Edge case: Negative subscriptionDurationMonths for backdated first commissions

In create-partner-commission.ts:

const subscriptionStartDate = firstCommission?.createdAt ?? new Date();

const subscriptionDurationMonths = differenceInMonths(
  createdAt ?? new Date(),  // backdated to pastDate
  subscriptionStartDate,    // falls back to now when firstCommission is null
);

When manually creating a backdated first commission (createdAt = past date, firstCommission = null), differenceInMonths(pastDate, now) returns a negative value. The fix is to use the commission's own date as the fallback:

- const subscriptionStartDate = firstCommission?.createdAt ?? new Date();
+ const subscriptionStartDate = firstCommission?.createdAt ?? createdAt ?? new Date();

💡 Minor: hasError vs invalid inconsistency in DatePicker

The default Trigger accepts hasError (from PickerProps), while the custom trigger render callback receives invalid. In rewards-logic.tsx the custom trigger correctly uses invalid, but there's a commented-out aria-[invalid=true] block in trigger.tsx suggesting the default trigger's error styling is incomplete. This isn't blocking for this PR since the custom trigger path is used here, but worth noting for future consumers of the component.


The rest of the PR looks clean — DATE_CONDITION_OPERATORS is correctly scoped to comparison-only operators, the subscriptionStartDate context propagation is consistent across all four sale entry points (Stripe checkout, invoice, track-sale API, Shopify), and the test coverage for date conditions is solid.

@steven-tey steven-tey merged commit 21a13f0 into main Mar 5, 2026
10 checks passed
@steven-tey steven-tey deleted the customer-reward-conditions branch March 5, 2026 06:20
@devkiran devkiran mentioned this pull request Mar 6, 2026
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.

2 participants