Skip to content

Improve discount creation error handling across Stripe and Shopify - #4173

Merged
steven-tey merged 6 commits into
mainfrom
improve-discount-creation
Jul 20, 2026
Merged

Improve discount creation error handling across Stripe and Shopify#4173
steven-tey merged 6 commits into
mainfrom
improve-discount-creation

Conversation

@devkiran

@devkiran devkiran commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Bug Fixes
    • Improved discount code creation and disabling when integrations are unavailable, permissions are insufficient, or required resources are missing.
    • Added clearer, provider-specific warnings and messaging to help diagnose discount failures.
    • Enhanced duplicate handling with smarter retries (including alternate codes) and clearer outcomes when retries can’t proceed.
    • Prevented non-recoverable provider errors from interrupting batch processing, while keeping retry behavior for recoverable issues.
  • UI
    • Added new “reconnect required” prompts for Shopify and Stripe errors.

@vercel

vercel Bot commented Jul 16, 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 20, 2026 12:10pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4fd4ba75-f0e9-428d-8c09-183be7dab741

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Discount provider errors are standardized across Stripe and Shopify with explicit codes, recoverability, and retry classification. Discount creation, disable flows, partner actions, and cron routes now consume the typed errors and renamed integration assertion.

Changes

Discount provider error handling

Layer / File(s) Summary
Typed discount provider error contract
apps/web/lib/discounts/discount-error.ts
Introduces provider error codes, API mappings, formatted messages, recoverability checks, and type guards.
Stripe and Shopify provider behavior
apps/web/lib/discounts/discount-provider-*.ts
Maps integration, authorization, permission, duplicate, missing-resource, and creation failures to typed provider errors while updating retry behavior and the integration assertion name.
Discount creation integration
apps/web/lib/discounts/create-discount-code.ts, apps/web/lib/actions/partners/create-discount.ts
Delegates provider errors directly and uses assertDiscountIntegration for Shopify validation.
Cron non-recoverable error handling
apps/web/app/(ee)/api/cron/discount-codes/*, apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts
Cron handlers detect non-recoverable or provider-coded errors, log warnings, and preserve skip or rethrow behavior.
Partner reconnect error mappings
apps/web/ui/partners/constants.ts
Adds Shopify and Stripe reconnect-required error entries with titles, CTAs, and URLs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CronRoute
  participant DiscountProvider
  participant ExternalProvider
  CronRoute->>DiscountProvider: create or disable discount code
  DiscountProvider->>ExternalProvider: perform provider operation
  ExternalProvider-->>DiscountProvider: success or provider failure
  DiscountProvider-->>CronRoute: typed DiscountProviderError
  CronRoute->>CronRoute: classify recoverability and log or rethrow
Loading

Possibly related PRs

  • dubinc/dub#3804: Related provider integration checks and discount error handling.
  • dubinc/dub#3985: Related external discount-code creation and error handling.
  • dubinc/dub#3990: Related cron handling for non-recoverable discount errors.

Suggested reviewers: steven-tey

🚥 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 accurately summarizes the main theme: improved discount creation error handling for Stripe and Shopify.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-discount-creation

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.

@devkiran
devkiran marked this pull request as ready for review July 20, 2026 11:14

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

🧹 Nitpick comments (3)
apps/web/lib/discounts/discount-provider-shopify.ts (1)

41-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded messages passed here are discarded by resolveDiscountProviderMessage.

These three DiscountProviderError calls pass the exact same literal strings that resolveDiscountProviderMessage (in discount-error.ts) already hardcodes and returns unconditionally for INTEGRATION_NOT_AVAILABLE/PERMISSIONS_REQUIRED. The passed message has no effect; it's duplicated text that can drift from the canonical copy.

See consolidated comment for the fix location and all affected sites.

Also applies to: 56-61, 67-72

🤖 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/discount-provider-shopify.ts` around lines 41 - 45,
The literal messages in the three DiscountProviderError calls for
INTEGRATION_NOT_AVAILABLE and PERMISSIONS_REQUIRED are discarded by
resolveDiscountProviderMessage. Remove the duplicated message arguments at the
affected call sites, relying on resolveDiscountProviderMessage in
discount-error.ts as the canonical source while preserving the existing provider
and error codes.
apps/web/lib/discounts/discount-provider-stripe.ts (1)

22-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same discarded-message duplication as the Shopify provider.

These INTEGRATION_NOT_AVAILABLE throws pass a hardcoded string that resolveDiscountProviderMessage ignores and re-derives anyway.

See consolidated comment.

Also applies to: 37-43

🤖 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/discount-provider-stripe.ts` around lines 22 - 28,
Remove the redundant hardcoded message arguments from the
INTEGRATION_NOT_AVAILABLE DiscountProviderError throws in the Stripe provider,
including both workspace connection checks, so resolveDiscountProviderMessage
remains the single source for these messages while preserving the existing
provider and error-code arguments.
apps/web/lib/discounts/discount-error.ts (1)

21-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

One root cause: resolveDiscountProviderMessage discards the passed message for INTEGRATION_NOT_AVAILABLE/PERMISSIONS_REQUIRED, making every call-site message at these sites dead code. The most consequential instance is Stripe's PERMISSIONS_REQUIRED path, where the actual Stripe-provided reason is thrown away in favor of a generic string.

  • apps/web/lib/discounts/discount-error.ts#L21-L39: stop unconditionally overriding message; either drop the parameter for these two codes or merge the dynamic message into the standardized text.
  • apps/web/lib/discounts/discount-provider-shopify.ts#L41-L72: once the root cause is fixed, these three hardcoded strings become redundant with the canonical copy in discount-error.ts and can be simplified/removed.
  • apps/web/lib/discounts/discount-provider-stripe.ts#L22-L43: same simplification applies to these two INTEGRATION_NOT_AVAILABLE call sites.
  • apps/web/lib/discounts/discount-provider-stripe.ts#L219-L225: after the fix, this site should surface Stripe's errorMessage alongside the standardized upgrade-required text instead of losing it.
🤖 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/discount-error.ts` around lines 21 - 39, Update
resolveDiscountProviderMessage in
apps/web/lib/discounts/discount-error.ts#L21-L39 to preserve or merge the
supplied message for INTEGRATION_NOT_AVAILABLE and PERMISSIONS_REQUIRED,
including Stripe’s specific reason with the standardized text. Simplify/remove
the now-redundant hardcoded messages in
apps/web/lib/discounts/discount-provider-shopify.ts#L41-L72 and
apps/web/lib/discounts/discount-provider-stripe.ts#L22-L43 so callers rely on
the canonical resolver copy. Ensure
apps/web/lib/discounts/discount-provider-stripe.ts#L219-L225 passes through
errorMessage and surfaces it alongside the upgrade-required message.
🤖 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.

Nitpick comments:
In `@apps/web/lib/discounts/discount-error.ts`:
- Around line 21-39: Update resolveDiscountProviderMessage in
apps/web/lib/discounts/discount-error.ts#L21-L39 to preserve or merge the
supplied message for INTEGRATION_NOT_AVAILABLE and PERMISSIONS_REQUIRED,
including Stripe’s specific reason with the standardized text. Simplify/remove
the now-redundant hardcoded messages in
apps/web/lib/discounts/discount-provider-shopify.ts#L41-L72 and
apps/web/lib/discounts/discount-provider-stripe.ts#L22-L43 so callers rely on
the canonical resolver copy. Ensure
apps/web/lib/discounts/discount-provider-stripe.ts#L219-L225 passes through
errorMessage and surfaces it alongside the upgrade-required message.

In `@apps/web/lib/discounts/discount-provider-shopify.ts`:
- Around line 41-45: The literal messages in the three DiscountProviderError
calls for INTEGRATION_NOT_AVAILABLE and PERMISSIONS_REQUIRED are discarded by
resolveDiscountProviderMessage. Remove the duplicated message arguments at the
affected call sites, relying on resolveDiscountProviderMessage in
discount-error.ts as the canonical source while preserving the existing provider
and error codes.

In `@apps/web/lib/discounts/discount-provider-stripe.ts`:
- Around line 22-28: Remove the redundant hardcoded message arguments from the
INTEGRATION_NOT_AVAILABLE DiscountProviderError throws in the Stripe provider,
including both workspace connection checks, so resolveDiscountProviderMessage
remains the single source for these messages while preserving the existing
provider and error-code arguments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aa09ba23-5626-4407-89c5-7c6d66a3fda7

📥 Commits

Reviewing files that changed from the base of the PR and between aac9cef and 5a65af5.

📒 Files selected for processing (9)
  • apps/web/app/(ee)/api/cron/discount-codes/create/queue-batches/route.ts
  • apps/web/app/(ee)/api/cron/discount-codes/create/route.ts
  • apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts
  • apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/discounts/create-discount-code.ts
  • apps/web/lib/discounts/discount-error.ts
  • apps/web/lib/discounts/discount-provider-shopify.ts
  • apps/web/lib/discounts/discount-provider-stripe.ts

Map unauthorized provider responses to AUTH_EXPIRED with reconnect CTAs, treat retry exhaustion as CREATE_FAILED, and stop remapping on non-recoverable integration failures.

@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

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/discount-provider-stripe.ts (1)

230-236: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Provide a fallback string to prevent passing undefined to DiscountProviderError.

Because error is typed as any in these catch blocks, TypeScript doesn't catch that errorMessage (or error.message) might evaluate to undefined. Since the DiscountProviderError constructor expects a strongly typed string for its message argument, passing undefined could cause runtime errors when the message is formatted downstream.

Please add a fallback message, similar to how it is handled for CREATE_FAILED on line 249.

  • apps/web/lib/discounts/discount-provider-stripe.ts#L230-L236: Add a fallback string such as errorMessage || "Stripe account is missing required permissions."
  • apps/web/lib/discounts/discount-provider-stripe.ts#L310-L316: Add a fallback string such as error.raw?.message || error.message || "Stripe account is missing required permissions."
🤖 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/discount-provider-stripe.ts` around lines 230 - 236,
Ensure both Stripe permissions-error handling sites always pass a string message
to DiscountProviderError: at apps/web/lib/discounts/discount-provider-stripe.ts
lines 230-236, fall back from errorMessage to a descriptive permissions message;
at lines 310-316, fall back from error.raw?.message and error.message to the
same descriptive message. Preserve the existing PERMISSIONS_REQUIRED behavior.
🤖 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/ui/partners/constants.ts`:
- Around line 20-34: Update the ctaUrl values in SHOPIFY_RECONNECT_REQUIRED and
STRIPE_RECONNECT_REQUIRED to use the workspace integration settings paths for
Shopify and Stripe instead of marketplace URLs. Leave
SHOPIFY_APP_UPGRADE_REQUIRED unchanged.

---

Outside diff comments:
In `@apps/web/lib/discounts/discount-provider-stripe.ts`:
- Around line 230-236: Ensure both Stripe permissions-error handling sites
always pass a string message to DiscountProviderError: at
apps/web/lib/discounts/discount-provider-stripe.ts lines 230-236, fall back from
errorMessage to a descriptive permissions message; at lines 310-316, fall back
from error.raw?.message and error.message to the same descriptive message.
Preserve the existing PERMISSIONS_REQUIRED 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: 39b62de8-6813-4b2b-bea7-71c6209a5f6f

📥 Commits

Reviewing files that changed from the base of the PR and between 5a65af5 and faeb6c9.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts
  • apps/web/lib/discounts/discount-error.ts
  • apps/web/lib/discounts/discount-provider-shopify.ts
  • apps/web/lib/discounts/discount-provider-stripe.ts
  • apps/web/ui/partners/constants.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/discounts/discount-error.ts

Comment thread apps/web/ui/partners/constants.ts
@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review please. Any breaking changes?

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Changed files ==='
git diff --name-status origin/main...HEAD

echo '=== Full PR diff ==='
git diff --find-renames --find-copies --unified=80 origin/main...HEAD -- \
  'apps/web/lib/discounts/discount-error.ts' \
  'apps/web/lib/discounts/discount-provider-shopify.ts' \
  'apps/web/lib/discounts/discount-provider-stripe.ts' \
  'apps/web/lib/discounts/create-discount-code.ts' \
  'apps/web/app/(ee)/api/cron/discount-codes/create/route.ts' \
  'apps/web/app/(ee)/api/cron/discount-codes/create/queue-batches/route.ts' \
  'apps/web/app/(ee)/api/cron/discount-codes/disable/route.ts' \
  'apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts' \
  'apps/web/lib/actions/partners/create-discount.ts' \
  'apps/web/ui/partners/constants.ts'

echo '=== Provider/error API references ==='
rg -n -C 4 \
  'assertDiscountIntegration(?:Available)?\s*\(|assertDiscountIntegration(?:Available)?\b|DiscountIntegrationNotAvailableError|isDiscountIntegrationNotAvailableError|DiscountProviderError|isNonRecoverableDiscountError|isDiscountProviderError' \
  apps/web

echo '=== Discount tests ==='
fd -t f . apps/web | rg '(discount|shopify|stripe).*(test|spec)\.[cm]?[jt]sx?$|(?:test|spec).*(discount|shopify|stripe)\.[cm]?[jt]sx?$' || true

echo '=== Type/interface declarations near provider objects ==='
rg -n -C 12 'discountProvider|stripeDiscountProvider|shopifyDiscountProvider|assertDiscountIntegration' \
  apps/web/lib/discounts apps/web/lib/actions/partners/create-discount.ts

Length of output: 102845


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== createDiscountCode call sites ==='
rg -n -C 12 '\bcreateDiscountCode\s*\(' apps/web --glob '*.ts' --glob '*.tsx'

echo '=== Shopify GraphQL error definitions and throw sites ==='
ast-grep outline apps/web/lib/integrations/shopify/admin-graphql.ts --items all || true
rg -n -C 10 'class ShopifyAdminGraphqlError|new ShopifyAdminGraphqlError|ShopifyAdminGraphqlError' \
  apps/web/lib/integrations/shopify apps/web --glob '*.ts' --glob '*.tsx'

echo '=== Discount tests ==='
cat -n apps/web/tests/discounts/index.test.ts

echo '=== API error and action error serialization ==='
rg -n -C 8 'instanceof DubApiError|DubApiError|ERROR_MAP|error\.message' \
  apps/web/lib/actions apps/web/app apps/web/ui/partners \
  --glob '*.ts' --glob '*.tsx' | head -n 500

echo '=== Explicit code parameters around discount creation ==='
rg -n -C 10 '\bcode\s*[:=]|createDiscountCode\(' \
  apps/web/lib/discounts apps/web/app --glob '*.ts' --glob '*.tsx'

Length of output: 175035


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== Shopify GraphQL helper ==='
cat -n apps/web/lib/integrations/shopify/admin-graphql.ts

echo '=== Exact createDiscountCode invocation contexts ==='
rg -n -C 18 '\bcreateDiscountCode\s*\(' \
  apps/web/lib/discounts \
  'apps/web/app/(ee)/api/cron' \
  --glob '*.ts' --glob '*.tsx'

echo '=== Explicit `code` argument passed to local createDiscountCode ==='
rg -n -U -C 14 \
  'createDiscountCode\s*\(\s*\{(?:(?!\}\s*\)).)*\bcode\s*:' \
  apps/web --glob '*.ts' --glob '*.tsx' || true

echo '=== Discount tests ==='
cat -n apps/web/tests/discounts/index.test.ts

echo '=== UI consumer for ERROR_MAP ==='
rg -n -C 12 '\bERROR_MAP\b' apps/web --glob '*.ts' --glob '*.tsx'

echo '=== Provider test files and focused references ==='
fd -t f -a . apps/web | rg '(^|/)(discount-provider-(stripe|shopify)|discount-error|create-discount-code).*\.(test|spec)\.[cm]?[jt]sx?$' || true

Length of output: 29283


@devkiran I found no confirmed breaking change in the in-repository consumers.

  • The assertDiscountIntegrationAvailableassertDiscountIntegration rename is consistently updated at all discovered call sites.
  • Stripe and Shopify integration, expired-auth, and permissions errors now retain UI-recognized prefixes; the new reconnect prefixes are present in ERROR_MAP.
  • Shopify’s HTTP 401 path emits "unauthorized", matching the new normalization logic.

One compatibility note: the renamed assertion method is a breaking API change for any consumer importing stripeDiscountProvider or shopifyDiscountProvider directly outside this repository. I found no such internal consumer.

The main gap is test coverage: the existing discount tests only cover discount retrieval, not the newly changed provider error mappings, duplicate retries, or disable flows. I’d recommend adding focused unit tests before relying on those paths heavily, but I did not find a release-blocking regression.

✅ 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

Copy link
Copy Markdown
Collaborator

@coderabbitai to confirm, this is backwards compatible right?

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@steven-tey Yes — for the app’s in-repository consumers, this is backwards compatible. All discovered call sites were updated, and the changed error handling is intentional.

The one exception is an external consumer directly accessing:

stripeDiscountProvider.assertDiscountIntegrationAvailable
shopifyDiscountProvider.assertDiscountIntegrationAvailable

Those property names were renamed to assertDiscountIntegration, so that would be a breaking API change outside this repo.

@steven-tey
steven-tey merged commit bd99da2 into main Jul 20, 2026
12 checks passed
@steven-tey
steven-tey deleted the improve-discount-creation branch July 20, 2026 16:17
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