Skip to content

Improve reward sync cron job#3944

Merged
steven-tey merged 21 commits into
mainfrom
improve-reward-sync
May 27, 2026
Merged

Improve reward sync cron job#3944
steven-tey merged 21 commits into
mainfrom
improve-reward-sync

Conversation

@devkiran

@devkiran devkiran commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Scheduled cleanup for orphaned rewards.
    • Batched, versioned reward processing with idempotent retries.
    • Partner notification helper that queues per-user reward update emails.
  • Refactor

    • Replaced prior enrollment-sync flow with the new processing pipeline.
    • Email template simplified to a single reward-update format.
  • Chores

    • Added TTL-backed reward versioning to support safe retries.

Review Change Stack

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview May 27, 2026 2:40pm

Request Review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7de97ab-580b-4bef-93d2-3af045c74cb1

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef6a4a and 7d75556.

📒 Files selected for processing (1)
  • apps/web/lib/api/rewards/queue-reward-processing.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/api/rewards/queue-reward-processing.ts

📝 Walkthrough

Walkthrough

Refactors reward handling into a versioned, batched job pipeline: new job schema and queueing, Redis versioning, a processing cron, orphaned-rewards cleanup, action handlers now enqueue jobs, and partner notification/email templates updated.

Changes

Reward Processing Pipeline Refactor

Layer / File(s) Summary
Job schema and version tracking foundation
apps/web/lib/api/rewards/queue-reward-processing.ts, apps/web/lib/api/rewards/reward-version.ts
Introduces a Zod-validated reward job schema, queueRewardProcessing (publishes Qstash job) and Redis-backed incrementRewardVersion/isStaleRewardVersion for versioning.
Reward processing cron endpoint
apps/web/app/(ee)/api/cron/rewards/process/route.ts
Cron POST handler that batches enrollment processing (up to 300), checks staleness, updates enrollments for create/delete, notifies partners, and enqueues the next batch or deletes reward when done.
Orphan cleanup cron endpoint
apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.ts
Cron GET endpoint that finds rewards with programId = null older than 30 minutes, verifies zero partnerGroup/programEnrollment references, and hard-deletes unreferenced rewards.
Reward action commands
apps/web/lib/actions/partners/create-reward.ts, apps/web/lib/actions/partners/update-reward.ts, apps/web/lib/actions/partners/delete-reward.ts
Create/update/delete actions now enqueue queueRewardProcessing jobs (reward-created/reward-updated/reward-deleted) with serialized rewardSnapshot and formatted description instead of the removed enrollment-sync queue.
Notification system
apps/web/lib/api/partners/notify-partner-reward-change.ts
New helper notifyPartnerRewardChange that enqueues idempotent batch emails using PartnerRewardUpdated with icon, description, effectiveAt, action, and idempotencyKey. Legacy notification and enrollment-sync modules removed.
Email template and registry
packages/email/src/templates/partner-reward-updated.tsx, apps/web/lib/email/email-templates-map.ts
Template changed to render a single rewardSnapshot plus action label; email registry now references PartnerRewardUpdated.

Sequence Diagram(s)

sequenceDiagram
  participant Action as Reward Action
  participant Queue as queueRewardProcessing
  participant Redis as Redis
  participant Cron as Process Cron
  participant Prisma as Prisma
  participant Notify as notifyPartnerRewardChange
  participant Email as queueBatchEmail

  Action->>Queue: queueRewardProcessing(event, groupId, rewardSnapshot)
  Queue->>Redis: incrementRewardVersion(groupId,event)
  Queue->>Cron: publish job to /api/cron/rewards/process (includes version)
  
  Cron->>Prisma: fetch reward & partnerGroup
  Cron->>Redis: isStaleRewardVersion(version)
  Cron->>Prisma: query enrollments (max 300)
  
  alt enrollments exist
    Cron->>Prisma: updateMany (create/delete)
    Cron->>Notify: notifyPartnerRewardChange(...)
    Notify->>Email: queueBatchEmail(user, PartnerRewardUpdated)
    Cron->>Queue: enqueue next batch
  else no more enrollments
    Cron->>Prisma: delete reward (if reward-deleted)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • dubinc/dub#3923: Replaces the earlier sync-enrollments endpoint and queueRewardEnrollmentSync flow with the new versioned processing pipeline.

Suggested reviewers

  • pepeladeira
  • steven-tey

Poem

🐰 I queued the jobs beneath the moonlit sky,
Redis kept count while batches hummed by,
Old orphans swept clean with a tidy cheer,
Emails hop out so partners hear—
A rabbit's work: neat, swift, and spry.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title refers to a real and central aspect of the PR—replacing the sync-enrollments cron with an improved version-based reward processing system—but is generic and lacks specificity about the key architectural changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-reward-sync

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 added 3 commits May 27, 2026 10:12
Use Redis reward versions to skip stale cron jobs, process enrollments in batches of 300, and add email idempotency keys.
@devkiran devkiran marked this pull request as ready for review May 27, 2026 05:45
@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch PR details
gh pr view 3944 --repo dubinc/dub --json title,body,files,additions,deletions,commits

echo "=== PR DIFF ==="
gh pr diff 3944 --repo dubinc/dub

Length of output: 46461



@devkiran: I'll conduct a full review of the changes.


Review: Improve reward sync cron job

Summary

This PR replaces the old sync-enrollments cron (which used QStash flow-control per-group) with a version-based Redis staleness system. The new rewards/process cron handles create/update/delete events uniformly, processes enrollments in batches of 300, and uses reward-version.ts to skip stale jobs when a newer reward change supersedes an older one. A separate safety-net cron (cleanup/orphaned-rewards) handles the edge case where a version bump causes the deleting cron to skip hard-deletion. Overall the approach is cleaner and more resilient — a few things worth reviewing:


🔴 High

apps/web/app/(ee)/api/cron/rewards/process/route.ts — users with null emails not filtered

Partner users are fetched without a null email guard, but the old notify-partners-reward-changed.ts explicitly filtered email: { not: null }. In notify-partner-reward-change.ts, user.email! suppresses the TypeScript error but doesn't prevent a null email from being passed to the email queue at runtime.

// route.ts — programEnrollments query
partner: {
  users: {
    select: {
      user: {
        select: {
          name: true,
          email: true,
        },
      },
    },
+   // Add filter:
+   where: {
+     user: {
+       email: {
+         not: null,
+       },
+     },
+   },
  },
},

Or alternatively filter in notifyPartnerRewardChange before mapping:

const validUsers = users.filter((u): u is typeof u & { email: string } => u.email != null);

🟡 Medium

apps/web/lib/api/rewards/queue-reward-processing.ts — version check is fragile

RewardJob = z.input<typeof rewardJobSchema> makes version optional in the type. The check params.version !== undefined happens to work (because the recursive call spreads the parsed output which always has version), but using z.infer (output type, where defaults are resolved) as the function parameter type would be semantically cleaner and rule out ambiguity:

// Before
export type RewardJob = z.input<typeof rewardJobSchema>;

// After — use output type so defaults are baked in
export type RewardJob = z.infer<typeof rewardJobSchema>;

// Then the version check becomes simply:
const version =
  params.version !== 1  // or store a sentinel like undefined separately

Or keep two distinct types — one for the queue call (input, version omittable) and one for the parsed job body (output, version always set).


apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.tstake: 10 with no pagination

If many rewards become orphaned at once (e.g. a mass delete-then-create pattern), cleaning them up at 10 per cron run could be very slow. Consider adding an orderBy clause for determinism and, if the cron runs frequently, increasing the batch or re-queuing like the main process cron does.

// Add ordering for determinism
orderBy: {
  updatedAt: "asc",
},
take: 50, // or 100

apps/web/app/(ee)/api/cron/rewards/process/route.ts — email sent before confirming next batch is queued

The notification emails are sent before queueRewardProcessing is called for the next batch:

if (shouldNotify) {
  await notifyPartnerRewardChange({ ... }); // emails sent
}
startingAfter = programEnrollments[...].id;
await queueRewardProcessing({ ...input, batchNumber: batchNumber + 1 }); // then next batch queued

If queueRewardProcessing throws after the emails are sent, on retry the same batch will send emails again with the same idempotency key (which Resend handles correctly). However for reward-created/reward-deleted, the updateMany will be retried too and count will likely be 0 (already updated), so shouldNotify will be false. This is safe but worth a comment explaining the retry behavior.


🟢 Low / Nit

packages/email/src/templates/partner-reward-updated.tsx — internal function name not updated

The file was renamed from partner-rewards-updated.tsxpartner-reward-updated.tsx (singular) but the internal component name PartnerRewardsUpdated (plural) was not updated. This works since it's a default export, but it creates a confusing mismatch:

-export default function PartnerRewardsUpdated({
+export default function PartnerRewardUpdated({

apps/web/lib/api/rewards/reward-version.ts — TTL race condition (edge case)

If a Redis key expires in the 24-hour window (which would be unusual but possible if Redis is flushed), isStaleRewardVersion returns false (not stale) even though a newer version exists in flight. Worth a comment noting this is an accepted trade-off, since the orphaned-rewards cron handles the downstream cleanup.


apps/web/lib/api/rewards/queue-reward-processing.tsqstash.publishJSON error swallowed then re-thrown with same message

The catch block logs and flushes but then throws a new generic error with the same user-facing message. The original error is not re-thrown, so the Axiom log has the stack trace but the QStash retry mechanism will receive only the generic Error. This is intentional (hiding internals) but ensure the logger.error call captures enough context for debugging.


Architecture Observation

The removal of QStash flowControl means multiple batches for different events (e.g., a delete-then-create for the same group+event within the TTL window) can now run concurrently. The version check in isStaleRewardVersion correctly skips stale jobs, but it only gates starting a batch — if two batches for the same version are somehow running concurrently (e.g. QStash retries), they could double-update enrollments. The updateMany is idempotent (setting the same value), so this is safe in practice.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.ts (1)

15-26: ⚡ Quick win

Process a stable batch order before applying take: 10.

This query limits the job to 10 rows but doesn't specify an order, so cleanup becomes nondeterministic under backlog. Add an oldest-first sort so each run makes predictable progress.

Suggested diff
   const rewards = await prisma.reward.findMany({
     where: {
       programId: null,
       updatedAt: {
         lt: subMinutes(new Date(), 30), // 30 minutes ago
       },
     },
+    orderBy: [
+      { updatedAt: "asc" },
+      { id: "asc" },
+    ],
     select: {
       id: true,
       event: true,
     },
     take: 10,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/cron/cleanup/orphaned-rewards/route.ts around lines 15
- 26, The query using prisma.reward.findMany with take: 10 is nondeterministic
because it lacks an order; modify the query that populates rewards to add a
stable oldest-first ordering (e.g., orderBy: { updatedAt: 'asc' } and include a
tie-breaker like id: 'asc') so each run processes the oldest records
deterministically before applying take: 10.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/`(ee)/api/cron/cleanup/orphaned-rewards/route.ts:
- Around line 65-78: The current loop swallows deletion errors by catching them
inside the loop (the try/catch around prisma.reward.delete for reward.id) so the
handler always returns success via logAndRespond; remove the per-item try/catch
(or instead collect failed IDs) and let errors propagate to withCron so failures
are surfaced—either delete the try/catch that wraps prisma.reward.delete in
route.ts (referencing prisma.reward.delete and reward.id) or push failed reward
IDs into an array and after the loop throw a new Error listing those IDs before
calling logAndRespond, so withCron can mark the run as failed.

In `@apps/web/app/`(ee)/api/cron/rewards/process/route.ts:
- Around line 193-199: In the catch block that inspects error.code when deleting
a reward (route.ts, inside the handler that hard-deletes rewards and references
reward.id), narrow the thrown value by first testing error instanceof
Prisma.PrismaClientKnownRequestError before accessing error.code, and only treat
P2025 as a success if that instanceof check passes; otherwise rethrow or wrap
the error as currently done. This keeps the existing error message logic for
non-P2025 cases but prevents runtime crashes when non-Prisma values are thrown.

In `@apps/web/lib/api/rewards/queue-reward-processing.ts`:
- Around line 35-42: The code computes `version` using `incrementRewardVersion`
before the surrounding `try` block, which lets errors escape structured logging
and the normalized error path; move the conditional computation of `version`
(the ternary that calls `incrementRewardVersion({ groupId: params.groupId,
event: params.rewardSnapshot.event })`) into the existing `try` block so any
thrown error is caught and handled by the function's catch/logging flow
(preserve the branch that uses `params.version` when provided).

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/cron/cleanup/orphaned-rewards/route.ts:
- Around line 15-26: The query using prisma.reward.findMany with take: 10 is
nondeterministic because it lacks an order; modify the query that populates
rewards to add a stable oldest-first ordering (e.g., orderBy: { updatedAt: 'asc'
} and include a tie-breaker like id: 'asc') so each run processes the oldest
records deterministically before applying take: 10.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 152b1585-3a90-4015-8486-7fc867cf98ff

📥 Commits

Reviewing files that changed from the base of the PR and between c22cd8c and b0515d5.

📒 Files selected for processing (13)
  • apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.ts
  • apps/web/app/(ee)/api/cron/rewards/process/route.ts
  • apps/web/app/(ee)/api/cron/rewards/sync-enrollments/route.ts
  • apps/web/lib/actions/partners/create-reward.ts
  • apps/web/lib/actions/partners/delete-reward.ts
  • apps/web/lib/actions/partners/update-reward.ts
  • apps/web/lib/api/partners/notify-partner-reward-change.ts
  • apps/web/lib/api/partners/notify-partners-reward-changed.ts
  • apps/web/lib/api/rewards/queue-reward-enrollment-sync.ts
  • apps/web/lib/api/rewards/queue-reward-processing.ts
  • apps/web/lib/api/rewards/reward-version.ts
  • apps/web/lib/email/email-templates-map.ts
  • packages/email/src/templates/partner-reward-updated.tsx
💤 Files with no reviewable changes (3)
  • apps/web/lib/api/partners/notify-partners-reward-changed.ts
  • apps/web/lib/api/rewards/queue-reward-enrollment-sync.ts
  • apps/web/app/(ee)/api/cron/rewards/sync-enrollments/route.ts

Comment thread apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.ts
Comment thread apps/web/app/(ee)/api/cron/rewards/process/route.ts
Comment thread apps/web/lib/api/rewards/queue-reward-processing.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/web/lib/api/rewards/queue-reward-processing.ts (2)

35-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move version resolution into the try block.

Line 36 performs incrementRewardVersion outside the try, so failures skip your structured logging/error path.

Suggested change
 export async function queueRewardProcessing(params: RewardJob) {
-  const version =
-    params.version !== undefined
-      ? params.version
-      : await incrementRewardVersion({
-          groupId: params.groupId,
-          event: params.rewardSnapshot.event,
-        });
-
   try {
+    const version =
+      params.version !== undefined
+        ? params.version
+        : await incrementRewardVersion({
+            groupId: params.groupId,
+            event: params.rewardSnapshot.event,
+          });
+
     const response = await qstash.publishJSON({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/rewards/queue-reward-processing.ts` around lines 35 - 42,
The version resolution (currently using params.version or await
incrementRewardVersion(...)) must be moved inside the existing try block so any
failures from incrementRewardVersion are caught and routed through your
structured logging/error handling; update the code that currently reads
params.version and calls incrementRewardVersion to run inside the try,
preserving the fallback behavior (use params.version if defined, otherwise await
incrementRewardVersion with { groupId: params.groupId, event:
params.rewardSnapshot.event }) and remove the earlier top-level await so errors
aren't thrown outside the try/catch.

32-34: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a parsed/output job type for internal queue logic.

Line 32 currently uses z.input<typeof rewardJobSchema>, which keeps version/batchNumber optional at type level and weakens the version-gating contract in recursive calls.

Suggested change
-export type RewardJob = z.input<typeof rewardJobSchema>;
+export type RewardJobInput = z.input<typeof rewardJobSchema>;
+export type RewardJob = z.infer<typeof rewardJobSchema>;
 
-export async function queueRewardProcessing(params: RewardJob) {
+export async function queueRewardProcessing(params: RewardJobInput) {
+  const parsed = rewardJobSchema.parse(params);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/api/rewards/queue-reward-processing.ts` around lines 32 - 34,
The internal job type should use the parsed/output schema type so required
fields like version and batchNumber are enforced; replace the current type alias
RewardJob (which uses z.input<typeof rewardJobSchema>) with the parsed/output
type (e.g. z.infer<typeof rewardJobSchema> or z.output equivalent) and update
the queueRewardProcessing signature to accept that parsed type so
recursive/internal calls honor the version-gating contract.
apps/web/app/(ee)/api/cron/rewards/process/route.ts (1)

116-124: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter out users with null email before enqueueing notifications.

Lines 123 and 167 pass users without an email-null guard, so notification payloads can include invalid recipients.

Suggested change
-      const users = programEnrollments.flatMap(({ partner }) =>
-        partner.users.map(({ user }) => user),
-      );
+      const users = programEnrollments.flatMap(({ partner }) =>
+        partner.users
+          .map(({ user }) => user)
+          .filter((user) => user.email != null),
+      );

Also applies to: 157-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/cron/rewards/process/route.ts around lines 116 - 124,
The partner->users query returns user objects where user.email may be null;
before building/enqueuing notification payloads in the route handler (the
exported request handler in route.ts) filter partners.select.users to only
include entries with user.email != null (e.g., users.filter(u =>
u.user?.email)); apply this guard in both places where you iterate
partner.select.users and call the enqueue/create-notification logic so you never
enqueue notifications with a null recipient.
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.ts (1)

15-27: ⚡ Quick win

Add orderBy for deterministic batch processing.

Without ordering, the query may return different subsets of orphaned rewards across runs, potentially leaving some orphans unprocessed indefinitely while repeatedly fetching others. Adding orderBy: { updatedAt: 'asc' } ensures oldest orphans are processed first and makes progress predictable.

Suggested fix
   const rewards = await prisma.reward.findMany({
     where: {
       programId: null,
       updatedAt: {
         lt: subMinutes(new Date(), 30), // 30 minutes ago
       },
     },
     select: {
       id: true,
       event: true,
     },
+    orderBy: {
+      updatedAt: "asc",
+    },
     take: 10,
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/cron/cleanup/orphaned-rewards/route.ts around lines 15
- 27, The prune query using prisma.reward.findMany that fetches orphaned rewards
into the rewards variable lacks an orderBy clause, causing non-deterministic
batches; update the prisma.reward.findMany call to include orderBy: { updatedAt:
'asc' } (or similar deterministic ordering) so the oldest orphaned rewards are
processed first and batch progress is predictable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/api/partners/notify-partner-reward-change.ts`:
- Around line 34-35: Filter out users with null or undefined emails before
constructing the batch payload to avoid enqueuing invalid recipients: in the
function that builds the batch (the code using users.map and constructing { to:
user.email!, ... } and the similar block referencing partner.email), replace the
direct map with a pre-filter like users.filter(u => u.email) (or equivalent) so
any entries with missing emails are excluded, then map over the filtered array
to build the payload; apply the same guard where partner.email is used to ensure
partner.email is always a non-null string before adding to the batch.

---

Duplicate comments:
In `@apps/web/app/`(ee)/api/cron/rewards/process/route.ts:
- Around line 116-124: The partner->users query returns user objects where
user.email may be null; before building/enqueuing notification payloads in the
route handler (the exported request handler in route.ts) filter
partners.select.users to only include entries with user.email != null (e.g.,
users.filter(u => u.user?.email)); apply this guard in both places where you
iterate partner.select.users and call the enqueue/create-notification logic so
you never enqueue notifications with a null recipient.

In `@apps/web/lib/api/rewards/queue-reward-processing.ts`:
- Around line 35-42: The version resolution (currently using params.version or
await incrementRewardVersion(...)) must be moved inside the existing try block
so any failures from incrementRewardVersion are caught and routed through your
structured logging/error handling; update the code that currently reads
params.version and calls incrementRewardVersion to run inside the try,
preserving the fallback behavior (use params.version if defined, otherwise await
incrementRewardVersion with { groupId: params.groupId, event:
params.rewardSnapshot.event }) and remove the earlier top-level await so errors
aren't thrown outside the try/catch.
- Around line 32-34: The internal job type should use the parsed/output schema
type so required fields like version and batchNumber are enforced; replace the
current type alias RewardJob (which uses z.input<typeof rewardJobSchema>) with
the parsed/output type (e.g. z.infer<typeof rewardJobSchema> or z.output
equivalent) and update the queueRewardProcessing signature to accept that parsed
type so recursive/internal calls honor the version-gating contract.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/cron/cleanup/orphaned-rewards/route.ts:
- Around line 15-27: The prune query using prisma.reward.findMany that fetches
orphaned rewards into the rewards variable lacks an orderBy clause, causing
non-deterministic batches; update the prisma.reward.findMany call to include
orderBy: { updatedAt: 'asc' } (or similar deterministic ordering) so the oldest
orphaned rewards are processed first and batch progress is predictable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b40368b-e1b9-4a6b-993e-5b324f75db64

📥 Commits

Reviewing files that changed from the base of the PR and between c22cd8c and b0515d5.

📒 Files selected for processing (13)
  • apps/web/app/(ee)/api/cron/cleanup/orphaned-rewards/route.ts
  • apps/web/app/(ee)/api/cron/rewards/process/route.ts
  • apps/web/app/(ee)/api/cron/rewards/sync-enrollments/route.ts
  • apps/web/lib/actions/partners/create-reward.ts
  • apps/web/lib/actions/partners/delete-reward.ts
  • apps/web/lib/actions/partners/update-reward.ts
  • apps/web/lib/api/partners/notify-partner-reward-change.ts
  • apps/web/lib/api/partners/notify-partners-reward-changed.ts
  • apps/web/lib/api/rewards/queue-reward-enrollment-sync.ts
  • apps/web/lib/api/rewards/queue-reward-processing.ts
  • apps/web/lib/api/rewards/reward-version.ts
  • apps/web/lib/email/email-templates-map.ts
  • packages/email/src/templates/partner-reward-updated.tsx
💤 Files with no reviewable changes (3)
  • apps/web/lib/api/partners/notify-partners-reward-changed.ts
  • apps/web/lib/api/rewards/queue-reward-enrollment-sync.ts
  • apps/web/app/(ee)/api/cron/rewards/sync-enrollments/route.ts

Comment thread apps/web/lib/api/partners/notify-partner-reward-change.ts Outdated
@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai, I've fixed the issues mentioned above. Do another round of review. Any potential breaking changes?

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 3944 --repo dubinc/dub

Length of output: 40203


🏁 Script executed:

#!/bin/bash
# Check for any remaining imports of removed exports
rg -n "getRewardEmailSnapshot|notifyPartnersRewardChanged|RewardSnapshot|sync-enrollments|PartnerRewardsUpdated|queue-reward-enrollment-sync" --type ts --type tsx -g '!*.test.*' 2>/dev/null | grep -v "^Binary" || true

Length of output: 525



Good news — the search for removed symbols came back clean. No stray imports of getRewardEmailSnapshot, notifyPartnersRewardChanged, RewardSnapshot, the old template, or the old endpoint.


Summary

One remaining issue to fix: the z.infer change introduced a functional regression where incrementRewardVersion is never called for new (non-recursive) jobs. Use separate input/output types:

// Callers use the input type (version/batchNumber optional)
type RewardJobInput = z.input<typeof rewardJobSchema>;

// Parsed cron payload uses the output type (defaults resolved)
export type RewardJob = z.infer<typeof rewardJobSchema>;

export async function queueRewardProcessing(params: RewardJobInput) {
  const version =
    params.version !== undefined
      ? params.version
      : await incrementRewardVersion({
          groupId: params.groupId,
          event: params.rewardSnapshot.event,
        });
  // ...
}

On breaking changes:

  • ✅ No stray references to removed exports — migration is clean
  • ⚠️ In-flight sync-enrollments QStash jobs will 404 post-deploy — clear the queue or deploy during a low-traffic window
  • ⚠️ Any PartnerRewardsUpdated emails already enqueued before deploy will fail to render — worth checking queue state at deploy time

Everything else looks solid. The null-email fix (both at query and notification layer), the orderBy in cleanup cron, and the template rename are all properly handled. 🐇

@steven-tey steven-tey merged commit 0c40828 into main May 27, 2026
11 checks passed
@steven-tey steven-tey deleted the improve-reward-sync branch May 27, 2026 17:43
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