Skip to content

Add async discount processing with versioned cron jobs and soft delete#3967

Open
devkiran wants to merge 3 commits into
mainfrom
discounts-sync
Open

Add async discount processing with versioned cron jobs and soft delete#3967
devkiran wants to merge 3 commits into
mainfrom
discounts-sync

Conversation

@devkiran

@devkiran devkiran commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Background queueing for discount create/delete workflows and a cron endpoint to clean up orphaned discounts
    • Versioned discount processing to avoid stale job runs
  • Bug Fixes

    • Stronger validation to skip missing/invalid discount/program data and prevent related errors
    • Guard added to prevent creating discount codes when a discount lacks a program ID
  • Refactor

    • Restructured discount deletion to use queued/background workflows for reliability

@vercel

vercel Bot commented Jun 1, 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 Jun 2, 2026 3:40pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Moves discount-related side effects out of DB transactions into queued cron workflows: adds Redis versioning, nullable Discount.programId, Zod job schema and Qstash publish, cron processors for batched enrollment updates and code deletions, orphaned-discounts cleanup, and updates actions to enqueue processing instead of performing in-transaction backfills.

Changes

Discount Async Processing System

Layer / File(s) Summary
Data model and version tracking
packages/prisma/schema/discount.prisma, apps/web/lib/api/discounts/discount-version.ts
Discount.programId becomes nullable and Redis-backed versioning tracks group versions with 24h TTL to detect stale jobs.
Job queueing and schema
apps/web/lib/api/discounts/queue-discount-processing.ts
Adds Zod discountJobSchema/DiscountJob and queueDiscountProcessing which computes/increments version and publishes JSON jobs to Qstash, with structured logging on failures.
Discount processing cron orchestrator
apps/web/app/(ee)/api/cron/discounts/process/route.ts
Cron POST parses job payload, validates discount and group, checks isStaleDiscountVersion, batch-updates up to 300 enrollments, queues next batches, and conditionally hard-deletes discounts when safe.
Supporting cron operations
apps/web/app/(ee)/api/cron/cleanup/orphaned-discounts/route.ts, apps/web/app/(ee)/api/cron/discount-codes/delete/queue/route.ts, apps/web/app/(ee)/api/cron/discount-codes/create/queue-batches/route.ts
Adds orphaned-discounts cleanup, a paginated discount-code deletion queue endpoint (batch size 500), and tightens guard in create/queue-batches to require discount.program.
Discount actions enqueue async processing
apps/web/lib/actions/partners/create-discount.ts, apps/web/lib/actions/partners/delete-discount.ts, apps/web/lib/actions/partners/update-discount.ts
createDiscountAction/deleteDiscountAction now enqueue discount-created/discount-deleted jobs after transactions; delete soft-deletes programId and schedules follow-ups via Qstash; update adds null-safe program checks when revalidating paths.
Validation and nullish-safety
apps/web/lib/discounts/create-discount-code.ts
createDiscountCode now throws if discount.programId is missing before persisting a discountCode.
Formatting and minor cleanup
apps/web/app/(ee)/api/admin/payouts/get-payouts-timeseries.ts, apps/web/app/app.dub.co/.../page-client.tsx, apps/web/app/providers.tsx, apps/web/playwright/workspaces/billing-trial.spec.ts, apps/web/ui/partners/partner-link-selector.tsx
Import reordering, JSX prop multi-line formatting, test formatting tweaks, and expression line-wrapping; no behavioral changes.

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Possibly related PRs:

    • dubinc/dub#3749: Related to createDiscountCode and provisioning paths that depend on discount.programId.
    • dubinc/dub#3818: Modifies the same Playwright billing trial spec touched here.
    • dubinc/dub#3804: Also changes the discount queue-batches cron handler with related guard logic.
  • Suggested Reviewers:

    • pepeladeira
    • steven-tey

"🐰 I hopped a job into Redis light,
queued the batches through the night,
soft-deleted where programs flee,
cron drums hum — enrollment set free! 🌙"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 accurately summarizes the main changes: introducing async discount processing with versioned cron jobs and soft delete functionality, which are central to the changeset.
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 discounts-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

devkiran commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
✅ 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.

@devkiran

devkiran commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review please

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

@devkiran Sure, triggering a review of PR #3967 now!

✅ 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.

@devkiran

devkiran commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

@devkiran Sure, triggering a full review of PR #3967 now!

✅ 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: 1

🧹 Nitpick comments (1)
apps/web/lib/actions/partners/update-discount.ts (1)

69-78: 💤 Low value

Optional chaining may silently skip cache invalidation.

If program is unexpectedly null (e.g., due to a soft-deleted discount), revalidatePath will receive paths like /partners.dub.co/undefined, which won't invalidate the intended cache. Consider guarding the entire block:

               ...(partnerGroup?.slug === DEFAULT_PARTNER_GROUP.slug
-                  ? [
+                  ? program
+                    ? [
                       revalidatePath(`/partners.dub.co/${program?.slug}`),
                       revalidatePath(`/partners.dub.co/${program?.slug}/apply`),
                       program?.addedToMarketplaceAt &&
                         revalidatePath(
                           `/partners.dub.co/programs/marketplace/${program.slug}`,
                         ),
                     ]
+                    : []
                   : []),

However, since getDiscountOrThrow filters by programId, a valid discount should always have a program. The optional chaining is acceptable as a defensive measure.

🤖 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/actions/partners/update-discount.ts` around lines 69 - 78, The
current cache-invalidation block uses optional chaining on program which can
produce invalid paths like /partners.dub.co/undefined; update the conditional to
guard that program is truthy before calling revalidatePath (e.g., check program
&& partnerGroup?.slug === DEFAULT_PARTNER_GROUP.slug) so revalidatePath is only
invoked with valid program.slug and program.addedToMarketplaceAt; reference the
partnerGroup, DEFAULT_PARTNER_GROUP, program, revalidatePath and
getDiscountOrThrow symbols when locating and fixing the logic.
🤖 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/discounts/process/route.ts:
- Around line 145-152: The catch block in the hard-delete logic is accessing
error.code on an unknown type; update the check to narrow the error to
Prisma.PrismaClientKnownRequestError (using a type guard like error instanceof
Prisma.PrismaClientKnownRequestError) before inspecting error.code === "P2025",
and keep the existing throw path that uses error instanceof Error ?
error.message : String(error) for the failure message; reference the catch block
around the hard-delete of discount (discount.id) and the imported Prisma symbol
to implement the type-safe check.

---

Nitpick comments:
In `@apps/web/lib/actions/partners/update-discount.ts`:
- Around line 69-78: The current cache-invalidation block uses optional chaining
on program which can produce invalid paths like /partners.dub.co/undefined;
update the conditional to guard that program is truthy before calling
revalidatePath (e.g., check program && partnerGroup?.slug ===
DEFAULT_PARTNER_GROUP.slug) so revalidatePath is only invoked with valid
program.slug and program.addedToMarketplaceAt; reference the partnerGroup,
DEFAULT_PARTNER_GROUP, program, revalidatePath and getDiscountOrThrow symbols
when locating and fixing the logic.
🪄 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: 93a95aaf-5a38-45ad-a272-430ce2817213

📥 Commits

Reviewing files that changed from the base of the PR and between d536779 and a81ba61.

📒 Files selected for processing (16)
  • apps/web/app/(ee)/api/admin/payouts/get-payouts-timeseries.ts
  • apps/web/app/(ee)/api/cron/cleanup/orphaned-discounts/route.ts
  • apps/web/app/(ee)/api/cron/discount-codes/create/queue-batches/route.ts
  • apps/web/app/(ee)/api/cron/discount-codes/delete/queue/route.ts
  • apps/web/app/(ee)/api/cron/discounts/process/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/webhooks/[webhookId]/page-client.tsx
  • apps/web/app/providers.tsx
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/actions/partners/delete-discount.ts
  • apps/web/lib/actions/partners/update-discount.ts
  • apps/web/lib/api/discounts/discount-version.ts
  • apps/web/lib/api/discounts/queue-discount-processing.ts
  • apps/web/lib/discounts/create-discount-code.ts
  • apps/web/playwright/workspaces/billing-trial.spec.ts
  • apps/web/ui/partners/partner-link-selector.tsx
  • packages/prisma/schema/discount.prisma

Comment thread apps/web/app/(ee)/api/cron/discounts/process/route.ts
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.

1 participant