Skip to content

Add isSoftDelete prop to deleteDiscountCodes - #4112

Merged
steven-tey merged 4 commits into
mainfrom
soft-delete-codes
Jul 2, 2026
Merged

Add isSoftDelete prop to deleteDiscountCodes#4112
steven-tey merged 4 commits into
mainfrom
soft-delete-codes

Conversation

@steven-tey

@steven-tey steven-tey commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • Improved partner ban/deactivation cron behavior by disabling discount codes (soft) instead of deleting them, and propagating the disabled state through Stripe attribution.
    • Added disabledAt support across the discount code lifecycle, schemas, and API responses, so disabled codes display and behave correctly.
    • Enhanced the duplicate discount-code API response with clearer conflict messaging tied to the relevant partner.
  • UI Improvements
    • Updated discount-code badges, tooltips, and the discount codes table to reflect disabled status; improved “already in use” and toast error visuals.
  • Chores
    • Updated the sonner dependency version.

@vercel

vercel Bot commented Jul 2, 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 Jul 2, 2026 6:17pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds disabled-at support for discount codes across cleanup, attribution, and display, and changes duplicate-code creation to reference the existing partner and show a custom toast for the new error response.

Changes

Discount code disable state

Layer / File(s) Summary
Disable field and cleanup modes
apps/web/prisma/schema/discount.prisma, apps/web/lib/zod/schemas/discount.ts, apps/web/lib/discounts/delete-discount-code.ts
DiscountCode and its schema gain disabledAt, and deleteDiscountCodes now supports soft-disable versus hard-delete while enqueuing provider cleanup to the disable endpoint.
Cron callers use explicit delete modes
apps/web/app/(ee)/api/cron/partners/ban/route.ts, apps/web/app/(ee)/api/cron/partners/deactivate/route.ts, apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts
The partner ban route enables soft-delete, the partner deactivate route keeps hard-delete, and the disable cron route comment matches the endpoint behavior.
Disabled state in API shape
apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts, apps/web/lib/zod/schemas/partner-profile.ts
The partner-profile links API exposes discountCodeDisabledAt, and the partner-profile schema adds the matching nullable date field.
Disabled state in attribution and display
apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx, apps/web/ui/partners/discounts/discount-code-badge.tsx
The attribution utility skips disabled discount codes, the table passes disabledAt to the badge, the partner link card forwards disabled state and tooltip links, and the badge renders a disabled presentation with tooltip support.

Duplicate discount-code feedback

Layer / File(s) Summary
Conflict response for duplicate codes
apps/web/app/(ee)/api/discount-codes/route.ts
The duplicate-code lookup includes the existing partner and returns a partner-specific conflict error.
Custom toast handling for duplicate errors
apps/web/ui/modals/add-discount-code-modal.tsx, apps/web/ui/shared/custom-toast.tsx, apps/web/package.json
The modal shows mapped upgrade toasts or an "already in use" custom toast, CustomToast renders markdown through MarkdownDescription, and sonner is updated for the new toast usage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • dubinc/dub#3970: Also changes the Stripe promotion-code attribution path that now skips disabled discount codes.
  • dubinc/dub#3985: Also changes the duplicate discount-code creation path and its error reporting.

Suggested reviewers: pepeladeira

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title correctly highlights the main API change to deleteDiscountCodes by adding the isSoftDelete option.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 soft-delete-codes

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/lib/discounts/delete-discount-code.ts (1)

48-50: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale comment for the soft-delete branch.

This comment states orphaned codes "still get deleted locally above," but that's only true when isSoftDelete is false. When isSoftDelete is true, the block at Lines 33-46 is skipped entirely, so orphaned codes are not deleted locally in that path either. Worth updating the comment to reflect the conditional behavior.

🤖 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/discounts/delete-discount-code.ts` around lines 48 - 50, Update
the stale orphaned-code comment in deleteDiscountCode to match the conditional
flow controlled by isSoftDelete. The current wording in the delete-discount-code
cleanup branch implies orphaned codes are always deleted locally above, but that
only happens in the non-soft-delete path. Revise the comment near the provider
cleanup logic so it clearly states that orphaned codes are skipped for
external-provider cleanup, and local deletion may have already occurred only
when the isSoftDelete branch was not taken.
🤖 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.

Outside diff comments:
In `@apps/web/lib/discounts/delete-discount-code.ts`:
- Around line 48-50: Update the stale orphaned-code comment in
deleteDiscountCode to match the conditional flow controlled by isSoftDelete. The
current wording in the delete-discount-code cleanup branch implies orphaned
codes are always deleted locally above, but that only happens in the
non-soft-delete path. Revise the comment near the provider cleanup logic so it
clearly states that orphaned codes are skipped for external-provider cleanup,
and local deletion may have already occurred only when the isSoftDelete branch
was not taken.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e95cfb7b-dd8a-43de-be9e-fac5dae3a783

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6d2a5 and a86167d.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts
  • apps/web/app/(ee)/api/cron/partners/ban/route.ts
  • apps/web/app/(ee)/api/cron/partners/deactivate/route.ts
  • apps/web/lib/discounts/delete-discount-code.ts

@socket-security

socket-security Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsonner@​2.0.71001008382100

View full report

@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: 2

🧹 Nitpick comments (1)
apps/web/ui/modals/add-discount-code-modal.tsx (1)

91-114: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Fragile substring match on error copy for the "already in use" branch.

Branching on error.includes("already in use") couples the modal to the exact backend message text. The backend already returns a structured code: "conflict" (per apps/web/app/(ee)/api/discount-codes/route.ts); if the wording changes later, this branch silently falls through to toast.error(error) without any error surfaced during development.

Consider threading the structured error code through useApiMutation/onError (if available) rather than matching on message substrings, similar to how ERROR_MAP keys are prefix-matched.

🤖 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/ui/modals/add-discount-code-modal.tsx` around lines 91 - 114, The
“already in use” toast branch in add-discount-code-modal is relying on a brittle
error substring instead of the structured backend code. Update the onError
handling in add-discount-code-modal and the useApiMutation error flow so this
case is driven by the returned code (for example the existing conflict code used
by ERROR_MAP) rather than error.includes("already in use"). Keep the same
CustomToast behavior, but route it through the structured error object so
changes in backend wording do not break the modal.
🤖 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/discount-codes/route.ts:
- Around line 108-113: The conflict message in the discount code route hardcodes
the production app URL, which breaks self-hosted or custom-domain deployments.
Update the `duplicateByCode` error in `route.ts` to build the partner link using
the shared app-domain constant from `@dub/utils` (for example the same pattern
used in `options.ts`) instead of `https://app.dub.co`, so the generated URL
respects `NEXT_PUBLIC_APP_DOMAIN`.

In `@apps/web/ui/modals/add-discount-code-modal.tsx`:
- Around line 21-22: The duplicate discount-code handling in
add-discount-code-modal should use the structured conflict code instead of
checking error text. Update the logic in AddDiscountCodeModal’s submit/error
path to branch on the API error code for the “already exists” case, and reserve
the text-based fallback only for unexpected errors so the duplicate-code toast
is shown reliably.

---

Nitpick comments:
In `@apps/web/ui/modals/add-discount-code-modal.tsx`:
- Around line 91-114: The “already in use” toast branch in
add-discount-code-modal is relying on a brittle error substring instead of the
structured backend code. Update the onError handling in add-discount-code-modal
and the useApiMutation error flow so this case is driven by the returned code
(for example the existing conflict code used by ERROR_MAP) rather than
error.includes("already in use"). Keep the same CustomToast behavior, but route
it through the structured error object so changes in backend wording do not
break the modal.
🪄 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: 2d5797c2-6d27-4766-946d-cb9f56c77ea1

📥 Commits

Reviewing files that changed from the base of the PR and between a86167d and f9148c0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/discount-codes/route.ts
  • apps/web/package.json
  • apps/web/ui/modals/add-discount-code-modal.tsx
  • apps/web/ui/shared/custom-toast.tsx

Comment thread apps/web/app/(ee)/api/discount-codes/route.ts
Comment thread apps/web/ui/modals/add-discount-code-modal.tsx
@steven-tey

Copy link
Copy Markdown
Collaborator Author

@coderabbitai final review, any breaking changes?

@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 (1)
apps/web/ui/partners/discounts/discount-code-badge.tsx (1)

51-62: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tooltip copy references a scenario (deactivation) that doesn't currently produce this state.

Per the cross-file wiring, disabledAt is only ever set by the ban flow (isSoftDelete: true); the deactivate flow hard-deletes the row instead. This tooltip's "banned or deactivated" phrasing is therefore inaccurate today. See the related comment on apps/web/lib/zod/schemas/discount.ts for the root-cause discussion.

🤖 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/ui/partners/discounts/discount-code-badge.tsx` around lines 51 - 62,
The Disabled tooltip copy in discount-code-badge should match the actual state
transition wired through disabledAt and the ban flow, since the deactivate path
hard-deletes instead of setting this flag. Update the Tooltip content in the
discount-code-badge component to remove the “deactivated” reference and keep the
message aligned with the existing disabled state behavior; use the
StatusBadge/Tooltip block in discount-code-badge.tsx as the target.
🤖 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/zod/schemas/discount.ts`:
- Around line 65-70: The `disabledAt` schema description in `discount.ts` is
inaccurate because it implies the timestamp is set for both banning and
deactivation, but only banning actually soft-deletes and sets `disabledAt`;
deactivation hard-deletes the row. Update the
`z.coerce.date().nullish().describe(...)` text to reflect only the cases where
`disabledAt` is populated, and make the same wording change in the
`discount-code-badge.tsx` tooltip copy so both descriptions stay consistent.

---

Duplicate comments:
In `@apps/web/ui/partners/discounts/discount-code-badge.tsx`:
- Around line 51-62: The Disabled tooltip copy in discount-code-badge should
match the actual state transition wired through disabledAt and the ban flow,
since the deactivate path hard-deletes instead of setting this flag. Update the
Tooltip content in the discount-code-badge component to remove the “deactivated”
reference and keep the message aligned with the existing disabled state
behavior; use the StatusBadge/Tooltip block in discount-code-badge.tsx as the
target.
🪄 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: 8f7fcdc7-8752-45cb-a087-241da24752e4

📥 Commits

Reviewing files that changed from the base of the PR and between f9148c0 and 16245c9.

📒 Files selected for processing (6)
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx
  • apps/web/lib/discounts/delete-discount-code.ts
  • apps/web/lib/zod/schemas/discount.ts
  • apps/web/prisma/schema/discount.prisma
  • apps/web/ui/partners/discounts/discount-code-badge.tsx

Comment thread apps/web/lib/zod/schemas/discount.ts
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd delete-discount-code.ts
echo "---"
cat -n apps/web/lib/discounts/delete-discount-code.ts
echo "---ban route---"
cat -n "apps/web/app/(ee)/api/cron/partners/ban/route.ts" | sed -n '1,200p'
echo "---deactivate route---"
cat -n "apps/web/app/(ee)/api/cron/partners/deactivate/route.ts" | sed -n '1,200p'
echo "---disable route---"
cat -n "apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts"
echo "---rg other usages---"
rg -n 'deleteDiscountCodes' --type=ts -C2

Length of output: 16484


🏁 Script executed:

#!/bin/bash
rg -n 'deleteDiscountCodes\(' --type=ts -C2 || true
echo "---enqueueBatchJobs / job endpoint usage for delete route---"
fd -a 'route.ts' apps/web/app/\(ee\)/api/cron/discount-codes
echo "---check old delete route still exists---"
fd -a . apps/web/app/\(ee\)/api/cron/discount-codes

Length of output: 432


🏁 Script executed:

#!/bin/bash
rg -n "deleteDiscountCodes(" -F -C3 apps/web || true
echo "---find dir---"
find apps/web/app -iname "*discount-codes*" -type d
echo "---list cron discount-codes dir---"
ls -la "apps/web/app/(ee)/api/cron/discount-codes" 2>/dev/null

Length of output: 5424


🏁 Script executed:

#!/bin/bash
git log --oneline -1
git diff --stat origin/main... -- apps/web/app/\(ee\)/api/cron/discount-codes 2>/dev/null || true
echo "---search old endpoint path references---"
rg -n "cron/discount-codes/delete" apps/web || true
echo "---check discount-codes create route for context---"
cat -n "apps/web/app/(ee)/api/cron/discount-codes/create/route.ts" 2>/dev/null | head -50

Length of output: 2430


🏁 Script executed:

#!/bin/bash
rg -n "disabledAt" apps/web/prisma 2>/dev/null || rg -n "disabledAt" packages/prisma 2>/dev/null || fd schema.prisma

Length of output: 700


🤖 web_search executed:

sonner npm package breaking changes between v1 and v2 (toast.custom API changes)

Length of output: 9856


Now I have enough context to answer.

@steven-tey I reviewed the changes end-to-end and don't see any breaking changes. Here's the breakdown:

deleteDiscountCodes signature change (apps/web/lib/discounts/delete-discount-code.ts)

  • The new isSoftDelete option is on a second parameter with a default value ({ isSoftDelete = false } = {}), so all existing call sites that don't pass it (bulk-delete-partners.ts, delete-link.ts, delete-discount.ts, remap-discount-codes/route.ts, discount-codes/[discountCodeId]/route.ts) keep the original hard-delete behavior unchanged. ✅ Non-breaking.

Cron route rename discount-codes/deletediscount-codes/disable

  • I confirmed via git diff --stat that this is a pure rename within this same PR ({delete => disable}/route.ts), and there are no leftover references to the old /api/cron/discount-codes/delete path anywhere in the codebase. Since this is an internal cron endpoint (not a public API), the rename is safe. ✅ Non-breaking.

ban/route.ts and deactivate/route.ts

  • Now explicitly pass { isSoftDelete: true } (ban) and { isSoftDelete: false } (deactivate) respectively — intentional behavior changes per the PR's purpose, not accidental breakage.

discount-codes/route.ts duplicate-code error

  • Changed from bad_request to conflict with a more specific message. This is a minor API response shape change (status code + message), but since it's a validation error, any well-behaved client checking for !ok won't break; only clients hard-coding the old status code/message would be affected. Worth flagging if any frontend/SDK code checks for the old bad_request code specifically — I didn't find any in this repo.

sonner bump ^1.4.41^2.0.7

  • Based on the sonner v2.0 changelog and package history, v2 removed the deprecated unstyled prop and renamed data-themedata-sonner-theme, but I didn't find those in this repo's usage. The custom-toast.tsx change (swapping react-markdown for MarkdownDescription) looks unrelated/pre-emptive but worth a quick smoke-test of toasts in the browser given the major version bump, since sonner v2 had some reported edge-case issues with persistent toasts (duration: Infinity) — not something this codebase seems to use though.

Overall: no breaking changes for consumers of deleteDiscountCodes or the renamed cron route. LGTM from a compatibility standpoint.

✅ Action performed

Review finished.

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.

@steven-tey
steven-tey enabled auto-merge July 2, 2026 18:28

@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

🤖 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/partners/deactivate/route.ts:
- Line 61: The deactivate flow currently calls deleteDiscountCodes with
isSoftDelete set to true, which matches the ban path instead of the intended
hard-delete behavior. Update the discount-code cleanup in deactivate/route.ts so
the deleteDiscountCodes call uses the hard-delete option expected for partner
deactivation, and verify the ban route remains the soft-delete path for the
contrasting behavior.
🪄 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: cf33893a-1ddc-42d2-bab7-62068d889383

📥 Commits

Reviewing files that changed from the base of the PR and between 16245c9 and 15b0c6f.

📒 Files selected for processing (6)
  • apps/web/app/(ee)/api/cron/partners/deactivate/route.ts
  • apps/web/app/(ee)/api/discount-codes/route.ts
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/partner-link-card.tsx
  • apps/web/lib/zod/schemas/partner-profile.ts
  • apps/web/ui/partners/discounts/discount-code-badge.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/api/discount-codes/route.ts

Comment thread apps/web/app/(ee)/api/cron/partners/deactivate/route.ts
@steven-tey
steven-tey disabled auto-merge July 2, 2026 18:49
@steven-tey
steven-tey merged commit 6e44c5b into main Jul 2, 2026
9 checks passed
@steven-tey
steven-tey deleted the soft-delete-codes branch July 2, 2026 18:51
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