Skip to content

feat(webapp): admin Coupon Deals tool#3480

Open
isshaddad wants to merge 7 commits intomainfrom
feat/admin-coupon-deals
Open

feat(webapp): admin Coupon Deals tool#3480
isshaddad wants to merge 7 commits intomainfrom
feat/admin-coupon-deals

Conversation

@isshaddad
Copy link
Copy Markdown
Collaborator

Summary

Adds a Coupon Deals tool to the Back office admin area. Admins can look up an org by Stripe customer email and apply any Stripe coupon tagged with trigger_deal_key / trigger_deal_label / trigger_deal_category metadata to that org's subscription. The catalog is built dynamically from Stripe — no env vars per deal, no code change, no redeploy when a new deal is added; just tag a coupon in the Stripe dashboard.

What's in here

  • New Coupon Deals sub-tab under /admin/back-office.
  • New route admin.back-office.coupons.tsx — search by Stripe customer email, multi-match banner, per-row apply buttons grouped by category, confirmation modal, disabled states for free-plan and already-applied orgs.
  • New ApplyCouponDialog confirmation modal — re-states the org / Stripe customer / Stripe sub / coupon before submit.
  • Five new wrappers in services/platform.v3.server.ts around BillingClient (listCouponDeals, refreshCouponDeals, resolveCouponCustomer, applyCouponDeal, getCouponDiagnostics) — modeled on the deleteUser pattern from the admin user-delete PR.

Merge-order gate (read before reviewing CI)

Depends on the matching cloud-side branch landing first:

  • cloud/billing adds the five /api/admin/coupons/* endpoints + Stripe-backed registry.
  • @trigger.dev/platform minor releases with the new BillingClient methods and Zod schemas.

This PR's typecheck WILL fail in CI on listCouponDeals, applyCouponDeal, ApplyCouponDealResult, etc. until that minor is published. The pin in apps/webapp/package.json is intentionally not bumped here — bumping is a follow-up after the platform release. Same shape as the recent admin user-delete PR.

Test plan (after platform publish)

  • Tag a Stripe test-mode coupon with trigger_deal_key / trigger_deal_label / trigger_deal_category.
  • Visit /admin/back-office/coupons as admin.
  • Search by Stripe customer email → matching orgs render in the table.
  • Click Apply → confirmation modal → submit → green "Applied: …" banner; verify the discount on the subscription in the Stripe dashboard.
  • Re-apply on the same org → buttons disabled with "Already has: …" tooltip.
  • Search an org on Free plan → buttons disabled with "Org is on free plan…" tooltip.
  • Multi-match: two Stripe customers sharing one email → amber banner above the table.

Known gaps (intentional v1)

  • Apply error UX is degraded when re-applying a registered deal. The 409 already_applied response carries code and currentDealKey on the wire, but the platform's generic ErrorSchema strips them, so the inline banner shows the generic error string rather than (currently has: <label>). Cloud-side fix is small (loosen ErrorSchema to passthrough extras); the route is forward-compatible — once that lands, the precise UI starts rendering with no webapp change.
  • Untagged Stripe-applied coupons show as "Active deal: None" even when the subscription has a discount. Spec deferred ("future enhancement: surface unknown discounts inline"). Workaround for support: always cross-check the Stripe subscription before applying.
  • 5-minute catalog cache on the cloud side. Editing a coupon's metadata in Stripe propagates to the admin tool within ≤5 min

Out of scope

  • Removing or swapping an already-applied deal (manual via Stripe dashboard for v1).
  • Audit log of admin applications (Stripe's subscription/discount history is sufficient for MVP).
  • Diagnostics panel for untagged coupons (was prototyped, removed for launch).

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

⚠️ No Changeset found

Latest commit: 3bcd687

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3ffbc362-2eb5-4d55-927c-c1fe162326b1

📥 Commits

Reviewing files that changed from the base of the PR and between 3a0a4cc and 3bcd687.

📒 Files selected for processing (2)
  • apps/webapp/app/components/admin/ApplyCouponDialog.tsx
  • apps/webapp/app/routes/admin.back-office.coupons.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/components/admin/ApplyCouponDialog.tsx
📜 Recent review details
⏰ 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). (29)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Add crumbs as you write code using // @Crumbs comments or `// `#region` `@crumbs blocks. These are temporary debug instrumentation and must be stripped using agentcrumbs strip before merge.

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
**/*.ts{,x}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from @trigger.dev/sdk when writing Trigger.dev tasks. Never use @trigger.dev/sdk/v3 or deprecated client.defineJob.

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: Access environment variables through the env export of env.server.ts instead of directly accessing process.env
Use subpath exports from @trigger.dev/core package instead of importing from the root @trigger.dev/core path

Use named constants for sentinel/placeholder values (e.g. const UNSET_VALUE = '__unset__') instead of raw string literals scattered across comparisons

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
apps/webapp/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Only use useCallback/useMemo for context provider values, expensive derived data that is a dependency elsewhere, or stable refs required by a dependency array. Don't wrap ordinary event handlers or trivial computations

Files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
🧠 Learnings (10)
📚 Learning: 2026-03-13T13:45:39.411Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3213
File: apps/webapp/app/routes/admin.llm-models.missing.$model.tsx:19-21
Timestamp: 2026-03-13T13:45:39.411Z
Learning: In `apps/webapp/app/routes/admin.llm-models.missing.$model.tsx`, the `decodeURIComponent(params.model ?? "")` call is intentionally unguarded. Remix route params are decoded at the routing layer before reaching the loader, so malformed percent-encoding is rejected upstream. The page is also admin-only, so the risk is minimal and no try-catch is warranted.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to {packages/core,apps/webapp}/**/*.{ts,tsx} : Use zod for validation in packages/core and apps/webapp

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-03-21T21:23:35.117Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts:149-150
Timestamp: 2026-03-21T21:23:35.117Z
Learning: In `apps/webapp/app/components/runs/v3/ai/extractAISummarySpanData.ts` (and related AI span extraction files in `apps/webapp/app/components/runs/v3/ai/`), manual JSON.parse with typeof guards and type assertions is intentional. These functions are on the hot path for span rendering, so Zod validation is deliberately avoided for performance reasons. Do not suggest replacing manual JSON parsing with Zod schemas in these files.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-04-16T14:21:11.115Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3368
File: apps/webapp/app/services/taskIdentifierCache.server.ts:33-39
Timestamp: 2026-04-16T14:21:11.115Z
Learning: In `apps/webapp/app/services/taskIdentifierCache.server.ts`, the `decode()` function intentionally uses a plain `JSON.parse` cast instead of Zod validation. The Redis cache is exclusively written by the internal `populateTaskIdentifierCache` function via the symmetric `encode()` helper — there is no external input path. Any shape mismatch would be a serialization bug to surface explicitly, not untrusted data to filter out. Do not suggest adding Zod validation to the `decode()` function or the `getTaskIdentifiersFromCache` return path in future reviews.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-03-29T19:16:38.123Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3291
File: apps/webapp/app/v3/featureFlags.ts:53-65
Timestamp: 2026-03-29T19:16:38.123Z
Learning: In Zod v3 (including 3.25.x used in triggerdotdev/trigger.dev), `z.coerce.boolean()` returns a `ZodBoolean` instance directly — its `_def.typeName` is `"ZodBoolean"`, NOT `"ZodEffects"`. Only `.preprocess()`, `.refine()`/`.superRefine()`, and `.transform()` wrap schemas in `ZodEffects`. Do NOT flag `getFlagControlType` in `apps/webapp/app/v3/featureFlags.ts` for failing to unwrap `ZodEffects` when handling `z.coerce.*` schemas.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.

Applied to files:

  • apps/webapp/app/routes/admin.back-office.coupons.tsx
🔇 Additional comments (7)
apps/webapp/app/routes/admin.back-office.coupons.tsx (7)

71-82: Unchecked as casts for external API responses.

The loader force-casts dealsResult.deals and resolveResult.matches without Zod validation, which can cause render-time failures if the API shape changes.


34-58: LGTM!

Type definitions use type keyword as per guidelines and appropriately model nullable fields.


88-104: LGTM!

Good use of Zod schemas with discriminated intent field and proper validation constraints.


106-170: LGTM!

Action function properly validates input with Zod safeParse, handles errors comprehensively, and includes appropriate admin authorization. The cast at lines 140-145 is documented as a forward-compatibility measure for when the platform schema is loosened.


172-183: LGTM!

Clean grouping logic with straightforward Map-based implementation.


185-207: LGTM!

Good state management with the useEffect to auto-close the dialog on successful apply. The openDialog handler appropriately avoids unnecessary useCallback wrapping per guidelines.


298-380: LGTM!

Well-structured table rendering with proper key props, helpful disabled-state tooltips, and clean separation of concerns for deal categories.


Walkthrough

Adds a new admin "Coupon Deals" area: a route at /admin/back-office/coupons with a typed loader that lists coupon deals and optionally resolves organizations/customers by Stripe email, and a typed action that handles intent=refresh and intent=apply (validating input, calling applyCouponDeal, returning typed errors or redirecting on success). Introduces ApplyCouponDialog React component for confirming/applying a deal to a target org, and platform service functions (list, refresh, resolve customer, apply, diagnostics) that wrap platform API calls. Updates back-office layout to include a "Coupon Deals" tab.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding an admin Coupon Deals tool to the webapp.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, what's included, merge-order dependencies, test plan, known gaps, and scope boundaries.
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 feat/admin-coupon-deals

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]

This comment was marked as resolved.

@isshaddad isshaddad marked this pull request as ready for review April 30, 2026 17:30
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread apps/webapp/app/routes/admin.back-office.coupons.tsx
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