Skip to content

Shopify discount#3804

Merged
steven-tey merged 31 commits into
mainfrom
shopify-discount
Apr 27, 2026
Merged

Shopify discount#3804
steven-tey merged 31 commits into
mainfrom
shopify-discount

Conversation

@devkiran

@devkiran devkiran commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Multi-provider discount support: create, disable, and sync discount codes via Stripe and Shopify
    • Shopify Admin GraphQL integration and provider-driven discount operations
    • Provider selection UI for discounts and partner flows
  • Improvements

    • Centralized, provider-agnostic discount flows and clearer provider-specific error toasts
    • Shopify-aware workspace/partner fields exposed to UI and webhooks
    • Background job routing for provider cleanup and batched deletions
  • Bug Fixes

    • Graceful handling when a provider integration is unavailable to avoid hard failures

@vercel

vercel Bot commented Apr 23, 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 Apr 27, 2026 5:07pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors discount flows from Stripe-only to a provider-based system (Stripe + Shopify): adds provider registry and implementations, provider-aware create/delete flows and errors, Shopify Admin GraphQL client, Prisma enum/field and schema updates, webhook discount-code attribution, and updates API routes, cron jobs, UI, and types to use providers.

Changes

Cohort / File(s) Summary
Provider Core
apps/web/lib/discounts/discount-provider.ts, apps/web/lib/discounts/discount-provider-stripe.ts, apps/web/lib/discounts/discount-provider-shopify.ts
Adds provider registry and concrete providers for Stripe and Shopify (create/get/disable, availability assertions, provider-specific errors and retries).
Provider API & Helpers
apps/web/lib/discounts/create-discount-code.ts, apps/web/lib/discounts/delete-discount-code.ts, apps/web/lib/discounts/discount-error.ts, apps/web/lib/discounts/generate-discount-code-for-partner.ts
Refactors create/delete to accept full workspace and dispatch to providers; adds DiscountIntegrationNotAvailableError; updates generation to be provider-agnostic.
Deleted Legacy Stripe Helpers
apps/web/lib/api/discounts/delete-discount-code.ts (deleted), apps/web/lib/stripe/create-stripe-discount-code.ts (deleted), apps/web/lib/stripe/disable-stripe-discount-code.ts (deleted)
Removes old Stripe-only helper modules; logic moved into provider modules.
Prisma, Types & Schemas
packages/prisma/schema/discount.prisma, packages/prisma/client.ts, apps/web/lib/zod/schemas/discount.ts, apps/web/lib/zod/schemas/partners.ts, apps/web/lib/zod/schemas/workspaces.ts
Adds DiscountProvider enum and provider on Discount; exposes type; extends Zod schemas (discount provider, partner.discount, workspace.shopifyStoreId).
API & Cron Routes (discounts)
apps/web/app/(ee)/api/cron/discount-codes/create/route.ts, .../create/queue-batches/route.ts, .../delete/route.ts, apps/web/app/(ee)/api/discount-codes/route.ts, apps/web/app/(ee)/api/discount-codes/[discountCodeId]/route.ts
Routes now call provider-backed create/delete, pass domain objects, remove Stripe-only preflight checks, accept provider where required, and handle missing integrations via the new error type.
Cron & Partner Flows
apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts, apps/web/app/(ee)/api/cron/partners/ban/route.ts, apps/web/app/(ee)/api/cron/partners/deactivate/route.ts
Update Prisma selects to include discount.provider, adjust imports to new delete helper, and make per-link provider-aware calls and error handling.
Shopify Integration & Order Processing
apps/web/lib/integrations/shopify/admin-graphql.ts, apps/web/lib/integrations/shopify/schema.ts, apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts, apps/web/app/(ee)/api/shopify/integration/webhook/route.ts, apps/web/lib/integrations/shopify/process-order.ts, apps/web/lib/integrations/shopify/create-lead.ts
Adds Shopify Admin GraphQL client and error type; extends order schema (discount_codes); implements discount-code attribution and new attributeViaDiscountCode flow; webhooks now receive full workspace.
Shopify Callback & Integration Management
apps/web/app/(ee)/api/shopify/integration/callback/route.ts
Switches request parsing to discriminated union (connect/disconnect) and makes install/uninstall synchronous; updates error interpolation.
Frontend: Discount UI
apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx, apps/web/ui/modals/add-discount-code-modal.tsx, apps/web/ui/partners/constants.ts, apps/web/app/.../partners/[partnerId]/links/page.tsx
Adds provider selector and provider-aware form logic (Shopify disables recurring durations); renames STRIPE_ERROR_MAPERROR_MAP and adds Shopify error keys; UI computes disabled reasons by provider and workspace connectivity.
Actions & Import Adjustments
apps/web/lib/actions/partners/create-discount.ts, apps/web/lib/actions/partners/delete-discount.ts, apps/web/lib/actions/partners/accept-program-invite.ts, apps/web/app/(ee)/api/workflows/partner-approved/route.ts
Update imports and call sites to provider-aware helpers.
Miscellaneous
apps/web/lib/api/partner-profile/get-partner-for-program.ts, apps/web/lib/api/partners/bulk-delete-partners.ts, apps/web/lib/api/links/delete-link.ts, apps/web/lib/cron/enqueue-batch-jobs.ts, apps/web/lib/stripe/coupon-discount-converter.ts, apps/web/app/.env.example
Selects updated to include discount.provider; enqueue union adds delete-discount-code; coupon converter returns id; .env.example EOF newline tweak.
Webhook orders handler signature
apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts
Handler signature changed to accept workspace object instead of workspaceId.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Admin/Client
    participant API as API Route
    participant Registry as getDiscountProvider
    participant Provider as Discount Provider
    participant External as External System (Stripe/Shopify)
    participant DB as Database

    Client->>API: POST /api/discount-codes { workspace, partner, link, discount, code }
    API->>Registry: getDiscountProvider(discount.provider)
    Registry-->>API: Provider instance
    API->>Provider: createDiscountCode({ workspace, partner, link, discount, code })
    Provider->>Provider: assertDiscountIntegrationAvailable(workspace)
    alt Integration available
        Provider->>External: create code (REST/GraphQL)
        External-->>Provider: success / retry on duplicate
        Provider->>DB: persist discountCode
        DB-->>Provider: persisted record
        Provider-->>API: discount code created
        API-->>Client: 200 OK
    else Integration missing
        Provider-->>API: DiscountIntegrationNotAvailableError
        API-->>Client: logged skip / early return
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

🐰 Hopping through code, I found a new key,
Stripe and Shopify now dance equally.
Providers lined up, each doing their part,
Codes sprout like carrots from one fresh patch of heart.
From this burrowed bunny — refactor joy! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Shopify discount' is vague and does not clearly describe the main changes; it lacks specificity about what was added or refactored. Consider a more descriptive title such as 'Add Shopify discount provider support and order attribution' or 'Refactor discount handling to support multiple providers' to better convey the scope and intent.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shopify-discount

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.

…n logic

- Introduced a new discount provider abstraction to support multiple discount providers (Stripe and Shopify).
- Refactored the `createDiscountAction` to utilize the new provider system for coupon management.
- Updated the discount deletion logic to use a batch job enqueueing system.
- Enhanced the discount schema to include a provider field for better flexibility.
- Cleaned up unused imports and streamlined the discount-related utility functions.
- Removed the `shopifyStoreId` from the credentials in the PATCH route.
- Updated the `createDiscountAction` to include a `provider` field and handle null coupon IDs.
- Introduced a new `shopifyAdminGraphql` function for better error handling in Shopify API calls.
- Added integration credentials schema for Shopify, ensuring proper access token management.
- Enhanced the discount creation logic to support one-time use discounts and improved validation.
- Updated the UI to allow selection between Stripe and Shopify as discount providers.
- Removed direct dependencies on Stripe integration settings in discount code creation logic.
- Introduced a unified discount provider system to streamline discount code generation for both Stripe and Shopify.
- Enhanced error handling for workspace integration checks and improved validation for discount creation.
- Updated the `createDiscountCode` function to accept workspace details directly, simplifying the parameters.
- Cleaned up unused imports and optimized the discount-related utility functions.
- Updated the partner API to include discount information in the response.
- Refactored the PartnerLinks component to utilize the extended partner schema with discount details.
- Improved error handling for discount creation based on partner discount status and workspace integration.
- Cleaned up and optimized discount-related UI components and constants for better maintainability.
- Updated the discount code creation and deletion logic to utilize a unified discount provider system for both Stripe and Shopify.
- Enhanced the input schema to include a provider field for better flexibility in discount operations.
- Removed deprecated functions related to Stripe discount code management and streamlined the discount-related utility functions.
- Improved error handling and validation for discount code operations, ensuring better integration with workspace settings.
- Updated the discount code logic across multiple routes to include discount provider details.
- Enhanced the `deleteDiscountCodes` function to accept an array of discount codes with associated provider information.
- Streamlined the discount code creation process by directly passing workspace details.
- Improved the API responses for partners and links to include nested discount provider data for better integration.
- Added try-catch block to handle specific error messages related to Stripe and Shopify connections during discount code disabling.
- Updated the `deleteDiscountCodes` function to filter out orphaned discount codes without provider information, improving cleanup logic.
- Enhanced logging to provide clearer feedback on skipped discount codes due to missing provider relations.
- Updated the `createDiscountAction` to fetch existing coupons if a `couponId` is provided, enhancing flexibility in discount management.
- Removed deprecated `getOrCreateCoupon` methods from Stripe and Shopify discount providers, streamlining the coupon handling process.
- Improved error handling and validation for discount creation across different providers.
- Introduced `DiscountIntegrationNotAvailableError` to standardize error handling for missing discount integrations.
- Updated discount code creation and deletion routes to utilize the new error class for improved clarity in error responses.
- Refactored discount provider logic to assert integration availability before proceeding with discount operations, enhancing robustness.

@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 (3)
apps/web/lib/integrations/shopify/create-lead.ts (2)

156-173: Redundant prisma.customer.updateprogramId/partnerId are already set at create time.

Now that the customer is created with programId: partnerLink.programId and partnerId: partnerLink.partnerId at lines 77–78, the subsequent prisma.customer.update inside Promise.allSettled writes the same values back to the same row (since link and partnerLink reference the same Link record). This is a wasted write per lead.

Consider dropping just the prisma.customer.update while keeping the syncPartnerLinksStats call:

♻️ Proposed simplification
       ...(link.programId && link.partnerId
         ? [
             syncPartnerLinksStats({
               partnerId: link.partnerId,
               programId: link.programId,
               eventType: "lead",
             }),
-            prisma.customer.update({
-              where: {
-                id: customer.id,
-              },
-              data: {
-                programId: link.programId,
-                partnerId: link.partnerId,
-              },
-            }),
           ]
         : []),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/create-lead.ts` around lines 156 - 173, The
code performs a redundant write: after creating the customer with
programId/partnerId set from partnerLink, the Promise.allSettled branch still
calls prisma.customer.update to set the same programId/partnerId based on link
(which points to the same Link), causing an unnecessary DB write; remove the
prisma.customer.update call from the array and keep syncPartnerLinksStats({
partnerId: link.partnerId, programId: link.programId, eventType: "lead" }) so
Promise.allSettled only runs syncPartnerLinksStats (and any other non-redundant
actions) for the new customer referenced by customer and link/partnerLink.

51-67: Optional: collapse the two link queries into one.

partnerLink is fetched here just to pull programId/partnerId for the customer create, then a second roundtrip updates the same record at lines 101–112 (whose result link already contains those fields plus includeTags). You could move the link update to run before the customer create and reuse it, eliminating one DB roundtrip per lead — at the cost of incrementing leads/lastLeadAt slightly earlier in the flow (and relying on update throwing P2025 for the missing-link case, or pre-checking with a single findUnique and dropping the duplicate read).

Not blocking — current code is correct, just an extra query on a hot path. Same observation applies to create-sale.ts, which fetches the link inside Promise.all already; matching that pattern here would also be reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/create-lead.ts` around lines 51 - 67,
Collapse the two DB roundtrips by replacing the separate partnerLink findUnique
and the later prisma.link.update with a single prisma.link.update call run
before creating the customer and reuse its result (the updated "link" that
includes programId, partnerId, includeTags, leads, lastLeadAt); if you prefer to
preserve the current not-found behavior, either pre-check with one findUnique
and then update using that same record, or run the update and catch Prisma P2025
to throw the existing DubApiError for the missing link. Target the existing
identifiers partnerLink, prisma.link.update, and the later code that reads
link.includeTags / link.programId so they consume the single updated link
object.
apps/web/lib/actions/partners/create-discount.ts (1)

51-73: Consider an exhaustive switch on provider to future-proof against new providers.

The current if/else if chain silently no-ops for any future DiscountProvider enum value (no provider-side hook is invoked, no error is raised, and the discount row is still created). Today only stripe and shopify exist, so this isn't a bug — but switching to an exhaustive switch with a never-typed default makes TypeScript flag the gap when a new provider is added.

♻️ Suggested refactor
-    const discountProvider = getDiscountProvider(provider);
-
-    let coupon: (DubDiscountAttributes & { id: string }) | null = null;
-
-    // Fetch existing coupon if couponId is provided otherwise create a new coupon on the discount provider
-    if (provider === DiscountProvider.stripe) {
-      if (couponId) {
-        coupon = await discountProvider.getCoupon({
-          couponId,
-          workspace,
-        });
-      } else {
-        coupon = await discountProvider.createCoupon({
-          workspace,
-          group,
-          data: parsedInput,
-        });
-      }
-    } else if (provider === DiscountProvider.shopify) {
-      await discountProvider.assertDiscountIntegrationAvailable({
-        workspace,
-      });
-    }
+    const discountProvider = getDiscountProvider(provider);
+
+    let coupon: (DubDiscountAttributes & { id: string }) | null = null;
+
+    switch (provider) {
+      case DiscountProvider.stripe:
+        // Fetch existing coupon if couponId is provided, otherwise create a new one
+        coupon = couponId
+          ? await discountProvider.getCoupon({ couponId, workspace })
+          : await discountProvider.createCoupon({
+              workspace,
+              group,
+              data: parsedInput,
+            });
+        break;
+      case DiscountProvider.shopify:
+        await discountProvider.assertDiscountIntegrationAvailable({
+          workspace,
+        });
+        break;
+      default: {
+        const _exhaustive: never = provider;
+        throw new Error(`Unhandled discount provider: ${_exhaustive}`);
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/actions/partners/create-discount.ts` around lines 51 - 73,
Replace the current if/else-if chain that checks provider with an exhaustive
switch on the DiscountProvider enum: call getCoupon/createCoupon for
DiscountProvider.stripe (using discountProvider, couponId, workspace, group,
parsedInput), call assertDiscountIntegrationAvailable for
DiscountProvider.shopify, and include a default case typed as never to force a
compile-time error when a new DiscountProvider value is added; this ensures the
getDiscountProvider/discountProvider logic and coupon handling (coupon variable)
are always handled for every enum member.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/lib/integrations/shopify/process-order.ts`:
- Around line 137-226: The createPartnerCommission call inside the waitUntil
IIFE can throw and short-circuit the rest of the IIFE so
sendWorkspaceWebhook/sendPartnerPostback never run; wrap the
createPartnerCommission(...) invocation in a try/catch (or use
Promise.resolve(...).catch(...)) inside the IIFE so any error is caught,
optionally log the failure, leave createdCommission as undefined on error, and
then continue to the subsequent Promise.allSettled that calls
sendWorkspaceWebhook and sendPartnerPostback (references: the waitUntil IIFE,
createPartnerCommission, createdCommission, sendWorkspaceWebhook,
sendPartnerPostback, and linkUpdated).
- Around line 92-117: The billing_address schema needs a new country_code field
and the code should use that ISO‑2 code for lookups and storage: add
country_code to orderSchema.billing_address, change the continent lookup to use
COUNTRIES_TO_CONTINENTS[billingAddress.country_code] (falling back to "Unknown"
if the lookup fails), and when creating the Customer in prisma.customer.create
set customer.country to billingAddress.country_code instead of the full name;
update usages around recordFakeClick, billingAddress, COUNTRIES_TO_CONTINENTS,
and prisma.customer.create to reflect this change.

---

Nitpick comments:
In `@apps/web/lib/actions/partners/create-discount.ts`:
- Around line 51-73: Replace the current if/else-if chain that checks provider
with an exhaustive switch on the DiscountProvider enum: call
getCoupon/createCoupon for DiscountProvider.stripe (using discountProvider,
couponId, workspace, group, parsedInput), call
assertDiscountIntegrationAvailable for DiscountProvider.shopify, and include a
default case typed as never to force a compile-time error when a new
DiscountProvider value is added; this ensures the
getDiscountProvider/discountProvider logic and coupon handling (coupon variable)
are always handled for every enum member.

In `@apps/web/lib/integrations/shopify/create-lead.ts`:
- Around line 156-173: The code performs a redundant write: after creating the
customer with programId/partnerId set from partnerLink, the Promise.allSettled
branch still calls prisma.customer.update to set the same programId/partnerId
based on link (which points to the same Link), causing an unnecessary DB write;
remove the prisma.customer.update call from the array and keep
syncPartnerLinksStats({ partnerId: link.partnerId, programId: link.programId,
eventType: "lead" }) so Promise.allSettled only runs syncPartnerLinksStats (and
any other non-redundant actions) for the new customer referenced by customer and
link/partnerLink.
- Around line 51-67: Collapse the two DB roundtrips by replacing the separate
partnerLink findUnique and the later prisma.link.update with a single
prisma.link.update call run before creating the customer and reuse its result
(the updated "link" that includes programId, partnerId, includeTags, leads,
lastLeadAt); if you prefer to preserve the current not-found behavior, either
pre-check with one findUnique and then update using that same record, or run the
update and catch Prisma P2025 to throw the existing DubApiError for the missing
link. Target the existing identifiers partnerLink, prisma.link.update, and the
later code that reads link.includeTags / link.programId so they consume the
single updated link object.
🪄 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: 5b94676a-ed7c-4679-80da-6141ab5622b3

📥 Commits

Reviewing files that changed from the base of the PR and between d221517 and ebedcad.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/integrations/shopify/create-lead.ts
  • apps/web/lib/integrations/shopify/process-order.ts

Comment thread apps/web/lib/integrations/shopify/process-order.ts
Comment thread apps/web/lib/integrations/shopify/process-order.ts
@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

@devkiran: Sure! I'll kick off a full review of the PR right away.


🧠 Learnings used
Learnt from: devkiran
Repo: dubinc/dub PR: 3804
File: apps/web/lib/discounts/discount-provider-shopify.ts:144-152
Timestamp: 2026-04-27T09:44:41.785Z
Learning: In the dubinc/dub codebase, `discount.amount` is stored as an integer in the smallest currency unit (e.g., cents for flat discounts, whole percentage points for percentage discounts). When creating Shopify discount codes, dividing by 100 (e.g., `discount.amount / 100`) correctly converts to the decimal amount in the store's own currency — there is no USD-specific currency mismatch because Shopify applies the discount in the store's native currency. Also, Shopify's GraphQL Decimal scalar accepts string representations (e.g., from `toFixed(2)`), so this is valid. Do not flag these patterns as bugs in future reviews.
✅ 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: 6

♻️ Duplicate comments (2)
apps/web/.env.example (1)

195-195: ⚠️ Potential issue | 🟡 Minor

Missing trailing newline (regression).

The file is missing a trailing newline, which violates the POSIX standard and triggers a dotenv-linter warning. This was previously addressed but appears to have regressed.

🧹 Add trailing newline
 VERIFF_SHARED_SECRET=
+
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/.env.example` at line 195, The .env example is missing a trailing
newline causing dotenv-linter warnings; update the file so the final line (the
VERIFF_SHARED_SECRET= entry) is followed by a newline character at EOF so the
file ends with a newline (i.e., add a single blank line after the existing
VERIFF_SHARED_SECRET= line).
apps/web/lib/discounts/discount-provider-shopify.ts (1)

78-84: ⚠️ Potential issue | 🔴 Critical

Grammatically broken error message still present; createCoupon should likely be a no-op.

The error string "Shopify does not this method." is missing a verb (e.g., "support"). More importantly, since add-edit-discount-sheet.tsx clears couponId for Shopify, createDiscountAction falls through to createCoupon and these throws break the create flow end-to-end. Either return null (no-op) for createCoupon or have callers branch on provider so this stub is never invoked.

🐛 Suggested fix
   const getCoupon = async () => {
-    throw new Error("Shopify does not this method.");
+    throw new Error("Shopify provider does not support getCoupon.");
   };

   const createCoupon = async () => {
-    throw new Error("Shopify does not this method.");
+    // Shopify creates discount codes per-partner; no global coupon to create.
+    return null;
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/discounts/discount-provider-shopify.ts` around lines 78 - 84,
The error messages in getCoupon and createCoupon are grammatically incorrect and
createCoupon should not throw (it breaks the Shopify create flow); update the
thrown text to a correct message like "Shopify does not support this method."
and change createCoupon to be a no-op that returns null (or a resolved
Promise<null>) so callers (e.g., createDiscountAction) can continue without
error; keep getCoupon behavior as-is or similarly adjust if callers expect null
instead of an exception.
🧹 Nitpick comments (9)
apps/web/lib/integrations/shopify/create-lead.ts (1)

156-173: Redundant customer.updateprogramId/partnerId are already set at creation.

Since the customer is now created with programId: partnerLink.programId and partnerId: partnerLink.partnerId (lines 77‑78), and partnerLink and link are the same DB row (both fetched by linkId), the subsequent prisma.customer.update writing link.programId/link.partnerId is a no-op. Consider dropping it from the Promise.allSettled to save a write per Shopify lead.

♻️ Proposed simplification
       ...(link.programId && link.partnerId
         ? [
             syncPartnerLinksStats({
               partnerId: link.partnerId,
               programId: link.programId,
               eventType: "lead",
             }),
-            prisma.customer.update({
-              where: {
-                id: customer.id,
-              },
-              data: {
-                programId: link.programId,
-                partnerId: link.partnerId,
-              },
-            }),
           ]
         : []),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/create-lead.ts` around lines 156 - 173,
Remove the redundant write: the prisma.customer.update call inside the
conditional Promise.allSettled block is a no-op because the customer is created
with programId/partnerId from partnerLink (same DB row as link); delete the
prisma.customer.update({...}) entry and keep only syncPartnerLinksStats({...})
when link.programId && link.partnerId are present, ensuring you remove
references to the update call in create-lead.ts (look for the
prisma.customer.update invocation and the surrounding conditional that spreads
its array).
apps/web/lib/integrations/shopify/admin-graphql.ts (2)

3-11: Misleading userErrors field name; also set this.name on the custom error.

In the Shopify Admin GraphQL API, userErrors is a well-known concept that specifically refers to mutation-level structured errors returned inside data.<mutationName>.userErrors (e.g., discountCodeBasicCreate.userErrors). Here, this property is populated from json.errors (top-level GraphQL transport/validation errors), not from mutation userErrors. Callers reading err.userErrors will be confused. Consider renaming to errors (or graphqlErrors).

Additionally, this.name isn't set, so stack traces and instanceof debugging will show a generic "Error".

♻️ Proposed change
 export class ShopifyAdminGraphqlError extends Error {
   constructor(
     public code: string,
     message: string,
-    public userErrors?: unknown,
+    public errors?: unknown,
   ) {
     super(message);
+    this.name = "ShopifyAdminGraphqlError";
   }
 }

And update the throw site at line 49–53 to pass json.errors to the renamed parameter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/admin-graphql.ts` around lines 3 - 11,
Rename the misleading property on the ShopifyAdminGraphqlError class from
userErrors to a clearer name like errors or graphqlErrors and update the
constructor signature accordingly (constructor(public code: string, message:
string, public errors?: unknown)), also set this.name =
'ShopifyAdminGraphqlError' inside the constructor so stack traces and instanceof
work; then update the throw site(s) that call new ShopifyAdminGraphqlError(...)
(the place that currently passes json.errors) to pass json.errors into the
renamed errors/graphqlErrors parameter.

13-14: TODO: send errors to Axiom.

Tracking this so it's not forgotten — the helper is the central choke point for Shopify Admin API failures, so wiring it into Axiom (or whatever observability layer the repo uses) will be valuable for production debugging.

Want me to open a follow-up issue to track adding Axiom logging here, or sketch the wiring once the existing Axiom helper module is identified?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/admin-graphql.ts` around lines 13 - 14, The
TODO "Send the error to Axiom" marks the central Shopify Admin API helper where
failures are currently only logged locally; update that helper to call the
project's Axiom logging helper (import the existing Axiom client/utility, e.g.,
axiom or axiomLogger) inside the catch paths of the helper (where the TODO sits)
and send a structured event including the error stack/message, the GraphQL
operation/name, the shop or store identifier, request payload/variables
(redacting secrets), HTTP status/response info, and any retry metadata; keep the
existing processLogger.error fallback so local logs remain if Axiom fails.
apps/web/app/(ee)/api/cron/partners/deactivate/route.ts (1)

58-60: Drop the redundant identity .map.

discountCodes.map((dc) => dc) is a no-op and can be removed.

♻️ Proposed simplification
-  const discountCodes = programEnrollments.flatMap(({ discountCodes }) =>
-    discountCodes.map((dc) => dc),
-  );
+  const discountCodes = programEnrollments.flatMap(
+    ({ discountCodes }) => discountCodes,
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/cron/partners/deactivate/route.ts around lines 58 -
60, The current construction creates discountCodes via
programEnrollments.flatMap(({ discountCodes }) => discountCodes.map((dc) => dc))
which uses an identity .map that does nothing; replace that expression with
programEnrollments.flatMap(({ discountCodes }) => discountCodes) to directly
flatten the arrays (variable: discountCodes, source: programEnrollments) and
remove the redundant .map call.
apps/web/lib/discounts/discount-provider.ts (1)

5-18: Enum values match the registry keys—no mismatch detected.

The Prisma DiscountProvider enum (packages/prisma/schema/discount.prisma) defines stripe and shopify as unquoted enum values, which correspond to the lowercase string values "stripe" and "shopify". These match the registry keys exactly, so the lookup will not throw a runtime error for valid enum values.

♻️ Optional: enforce exhaustiveness at compile time

Adding a Record<DiscountProvider, …> type annotation would catch at compile time if a new enum value is added without updating the registry—recommended as a safeguard for future maintainability.

-const discountProviders = {
+const discountProviders: Record<DiscountProvider, typeof stripeDiscountProvider | typeof shopifyDiscountProvider> = {
   stripe: stripeDiscountProvider,
   shopify: shopifyDiscountProvider,
-} as const;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/discounts/discount-provider.ts` around lines 5 - 18, The
registry object discountProviders is currently untyped which misses a
compile-time check against the Prisma enum; update its declaration to be typed
as a Record<DiscountProvider, typeof stripeDiscountProvider | typeof
shopifyDiscountProvider> (or a common provider interface type) so the TS
compiler enforces exhaustiveness when DiscountProvider changes, then keep
getDiscountProvider and its runtime lookup as-is to preserve the runtime error
for unknown keys.
apps/web/lib/discounts/delete-discount-code.ts (1)

48-58: Orphan-skip logic is a sensible compromise.

Skipping external-provider cleanup when the discount relation is null (and surfacing it as a console.warn with the count) is a reasonable approach given the local rows are still removed. If you want stronger ops visibility, consider routing this through your structured logger / error sink rather than console.warn so it's queryable in production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/discounts/delete-discount-code.ts` around lines 48 - 58, The
console.warn call that logs orphanedCount in delete-discount-code.ts should use
the app's structured logger instead of console to make the event queryable;
locate the codesWithProvider and orphanedCount logic and replace the
console.warn with a structured logger.warn (or logger.warn) call that includes a
clear message and metadata { orphanedCount, context: 'deleteDiscountCodes' }
and, if no logger is in scope, accept/inject the existing application logger
into the deleteDiscountCodes function so you can emit a structured warning
rather than using console.warn.
apps/web/lib/integrations/shopify/schema.ts (1)

22-26: Make discount_codes optional to handle missing field in Shopify webhook payloads.

Shopify's Admin API orders/paid webhook can omit the discount_codes field when no discount codes are applied. The current schema requires it as a non-optional array, which would cause parsing to fail if the field is absent. Use .optional().default([]) to safely handle both cases—omitted or empty array—without losing validation for present values.

♻️ Suggested change
-  discount_codes: z.array(
-    z.object({
-      code: z.string().describe("The code of the discount."),
-    }),
-  ),
+  discount_codes: z
+    .array(
+      z.object({
+        code: z.string().describe("The code of the discount."),
+      }),
+    )
+    .optional()
+    .default([]),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/schema.ts` around lines 22 - 26, The schema
currently declares discount_codes as a required array which breaks when Shopify
omits that field; update the discount_codes definition in
apps/web/lib/integrations/shopify/schema.ts (the z.array(z.object({...})) for
discount_codes) to be optional with a default empty array by chaining
.optional().default([]) on the z.array so absent or empty discount_codes are
handled safely while preserving validation for present entries.
apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx (1)

273-301: Consider extracting the inline IIFE for readability.

The IIFE wrapping register("provider", ...) to splice in custom onChange side-effects is functional but a bit hard to read inline within JSX. Extracting it into a providerRegistration const above the JSX would improve readability without changing behavior.

♻️ Suggested refactor
+  const providerRegistration = (() => {
+    const { onChange: onProviderChange, ...rest } = register("provider", {
+      disabled: Boolean(discount),
+    });
+    return {
+      ...rest,
+      onChange: (e: ChangeEvent<HTMLSelectElement>) => {
+        onProviderChange(e);
+        if (e.target.value === DiscountProvider.shopify) {
+          setUseExistingCoupon(false);
+          setUseStripeTestCouponId(false);
+          setValue("couponId", "");
+          setValue("couponTestId", "");
+          setValue("maxDuration", 0);
+        } else {
+          setUseExistingCoupon(false);
+          setUseStripeTestCouponId(false);
+          setValue(
+            "maxDuration",
+            defaultValuesSource.maxDuration === null
+              ? Infinity
+              : defaultValuesSource.maxDuration,
+          );
+        }
+      },
+    };
+  })();

Then in the JSX: <select ... {...providerRegistration}>.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx` around lines 273
- 301, The inline IIFE that calls register("provider", { disabled:
Boolean(discount) }) and injects a custom onChange with side-effects is hard to
read in JSX; extract it into a top-level const (e.g., providerRegistration)
above the JSX, assign const { onChange: onProviderChange, ...rest } =
register("provider", { disabled: Boolean(discount) }); then create onChange that
calls onProviderChange(e) and performs the same branches (checking
e.target.value === DiscountProvider.shopify, calling
setUseExistingCoupon(false), setUseStripeTestCouponId(false),
setValue("couponId",""), setValue("couponTestId",""), setValue("maxDuration", 0)
or the else branch using defaultValuesSource.maxDuration), and spread
providerRegistration into the <select> in place of the IIFE.
apps/web/lib/discounts/discount-provider-stripe.ts (1)

19-51: Duplicate "missing Stripe integration" error message.

requireInstalledIntegration throws the same STRIPE_CONNECTION_REQUIRED message for two distinct conditions (no stripeConnectId vs. no installedIntegration row). Consider extracting the message into a constant, or differentiating the two paths so debugging which precondition failed is easier in logs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/discounts/discount-provider-stripe.ts` around lines 19 - 51, The
function requireInstalledIntegration currently throws the identical
STRIPE_CONNECTION_REQUIRED error for two different failure cases; change this by
introducing distinct error messages (or a shared constants file) and throw a
more specific message for each path: one when workspace.stripeConnectId is
missing (e.g., "STRIPE_ACCOUNT_NOT_LINKED") and another when
prisma.installedIntegration lookup returns null (e.g.,
"STRIPE_APP_NOT_INSTALLED"), keeping use of
DiscountIntegrationNotAvailableError; update the thrown message strings where
requireInstalledIntegration, STRIPE_INTEGRATION_ID, prisma.installedIntegration,
and stripeIntegrationSettingsSchema are referenced so logs and debugging can
distinguish the two failure reasons.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/`(ee)/api/shopify/integration/callback/route.ts:
- Around line 43-53: The call to installIntegration is persisting plaintext
credentials (credentials.accessToken = body.accessToken); add a clear TODO
comment immediately above this installIntegration invocation noting that
encryption is intentionally deferred and must be implemented in the follow-up
PR, reference the planned encryption work and the unique symbols
(installIntegration, SHOPIFY_INTEGRATION_ID, body.accessToken, credentials) so
reviewers can find and track this hotspot, and if desirable add a short runtime
assertion or guard (e.g., a feature-flag check) to ensure the unencrypted path
cannot be enabled in production until the encryption PR is merged.
- Around line 56-68: The empty catch in the disconnect branch (inside the route
handling block where body.action === "disconnect") swallows all errors from
prisma.installedIntegration.delete; change this to an idempotent deleteMany call
on prisma.installedIntegration.deleteMany with the same filter (userId,
integrationId, projectId) or, if you must keep delete, catch only
Prisma.PrismaClientKnownRequestError and rethrow unless error.code === 'P2025'
so only "record not found" is ignored; update the block around
prisma.installedIntegration.delete to use
prisma.installedIntegration.deleteMany(...) or a narrow catch checking Prisma
error code.

In `@apps/web/lib/discounts/discount-provider-stripe.ts`:
- Around line 22-41: The user-facing error message used in the two
DiscountIntegrationNotAvailableError throws is awkward ("...to create
discount."); update both occurrences so the message reads "...to create a
discount." (search for the throws near workspace.stripeConnectId and the
installation check around
prisma.installedIntegration.findFirst/STRIPE_INTEGRATION_ID) and keep the rest
of the message text unchanged so both error instances are consistent.

In `@apps/web/lib/integrations/shopify/admin-graphql.ts`:
- Around line 27-44: The fetch call that posts to Shopify in admin-graphql.ts
needs a request timeout and must include the response body in non-OK errors: add
an AbortSignal (e.g., use AbortSignal.timeout(TIMEOUT_MS) or create an
AbortController and pass signal into the fetch options) when calling fetch so
the request can't hang indefinitely, and on the error branch that throws
ShopifyAdminGraphqlError, read the response body (preferably try response.json()
then fallback to response.text()) and include that payload in the error
message/details so the thrown ShopifyAdminGraphqlError contains the HTTP status,
statusText and the response body for debugging; update the fetch invocation and
the error-handling block around response and response.ok accordingly
(references: the fetch call variable response and the ShopifyAdminGraphqlError
thrown).

In `@apps/web/lib/integrations/shopify/process-order.ts`:
- Around line 196-224: The payloads are inconsistent: sendWorkspaceWebhook
explicitly sets metadata: null but sendPartnerPostback spreads ...leadEvent
which currently contains metadata: "" — fix by ensuring both consumers receive
null; either initialize leadEvent.metadata = null where leadEvent is constructed
or explicitly override metadata: null in the sendPartnerPostback data object
(inside the sendPartnerPostback call) so the postback uses metadata: null
instead of the empty string.

In `@apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx`:
- Around line 267-308: The label and select for the Discount provider are not
associated for accessibility; add a matching id on the select and htmlFor on the
label (e.g., use "provider" or another unique id) so clicking the label focuses
the select and screen readers link them; keep the existing register("provider",
...) spread and onChange logic (symbols: register, DiscountProvider,
setUseExistingCoupon, setUseStripeTestCouponId, setValue, defaultValuesSource) —
either add the id attribute before/after spreading the register props or include
it in the props returned from register so the id and htmlFor match.

---

Duplicate comments:
In `@apps/web/.env.example`:
- Line 195: The .env example is missing a trailing newline causing dotenv-linter
warnings; update the file so the final line (the VERIFF_SHARED_SECRET= entry) is
followed by a newline character at EOF so the file ends with a newline (i.e.,
add a single blank line after the existing VERIFF_SHARED_SECRET= line).

In `@apps/web/lib/discounts/discount-provider-shopify.ts`:
- Around line 78-84: The error messages in getCoupon and createCoupon are
grammatically incorrect and createCoupon should not throw (it breaks the Shopify
create flow); update the thrown text to a correct message like "Shopify does not
support this method." and change createCoupon to be a no-op that returns null
(or a resolved Promise<null>) so callers (e.g., createDiscountAction) can
continue without error; keep getCoupon behavior as-is or similarly adjust if
callers expect null instead of an exception.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/cron/partners/deactivate/route.ts:
- Around line 58-60: The current construction creates discountCodes via
programEnrollments.flatMap(({ discountCodes }) => discountCodes.map((dc) => dc))
which uses an identity .map that does nothing; replace that expression with
programEnrollments.flatMap(({ discountCodes }) => discountCodes) to directly
flatten the arrays (variable: discountCodes, source: programEnrollments) and
remove the redundant .map call.

In `@apps/web/lib/discounts/delete-discount-code.ts`:
- Around line 48-58: The console.warn call that logs orphanedCount in
delete-discount-code.ts should use the app's structured logger instead of
console to make the event queryable; locate the codesWithProvider and
orphanedCount logic and replace the console.warn with a structured logger.warn
(or logger.warn) call that includes a clear message and metadata {
orphanedCount, context: 'deleteDiscountCodes' } and, if no logger is in scope,
accept/inject the existing application logger into the deleteDiscountCodes
function so you can emit a structured warning rather than using console.warn.

In `@apps/web/lib/discounts/discount-provider-stripe.ts`:
- Around line 19-51: The function requireInstalledIntegration currently throws
the identical STRIPE_CONNECTION_REQUIRED error for two different failure cases;
change this by introducing distinct error messages (or a shared constants file)
and throw a more specific message for each path: one when
workspace.stripeConnectId is missing (e.g., "STRIPE_ACCOUNT_NOT_LINKED") and
another when prisma.installedIntegration lookup returns null (e.g.,
"STRIPE_APP_NOT_INSTALLED"), keeping use of
DiscountIntegrationNotAvailableError; update the thrown message strings where
requireInstalledIntegration, STRIPE_INTEGRATION_ID, prisma.installedIntegration,
and stripeIntegrationSettingsSchema are referenced so logs and debugging can
distinguish the two failure reasons.

In `@apps/web/lib/discounts/discount-provider.ts`:
- Around line 5-18: The registry object discountProviders is currently untyped
which misses a compile-time check against the Prisma enum; update its
declaration to be typed as a Record<DiscountProvider, typeof
stripeDiscountProvider | typeof shopifyDiscountProvider> (or a common provider
interface type) so the TS compiler enforces exhaustiveness when DiscountProvider
changes, then keep getDiscountProvider and its runtime lookup as-is to preserve
the runtime error for unknown keys.

In `@apps/web/lib/integrations/shopify/admin-graphql.ts`:
- Around line 3-11: Rename the misleading property on the
ShopifyAdminGraphqlError class from userErrors to a clearer name like errors or
graphqlErrors and update the constructor signature accordingly
(constructor(public code: string, message: string, public errors?: unknown)),
also set this.name = 'ShopifyAdminGraphqlError' inside the constructor so stack
traces and instanceof work; then update the throw site(s) that call new
ShopifyAdminGraphqlError(...) (the place that currently passes json.errors) to
pass json.errors into the renamed errors/graphqlErrors parameter.
- Around line 13-14: The TODO "Send the error to Axiom" marks the central
Shopify Admin API helper where failures are currently only logged locally;
update that helper to call the project's Axiom logging helper (import the
existing Axiom client/utility, e.g., axiom or axiomLogger) inside the catch
paths of the helper (where the TODO sits) and send a structured event including
the error stack/message, the GraphQL operation/name, the shop or store
identifier, request payload/variables (redacting secrets), HTTP status/response
info, and any retry metadata; keep the existing processLogger.error fallback so
local logs remain if Axiom fails.

In `@apps/web/lib/integrations/shopify/create-lead.ts`:
- Around line 156-173: Remove the redundant write: the prisma.customer.update
call inside the conditional Promise.allSettled block is a no-op because the
customer is created with programId/partnerId from partnerLink (same DB row as
link); delete the prisma.customer.update({...}) entry and keep only
syncPartnerLinksStats({...}) when link.programId && link.partnerId are present,
ensuring you remove references to the update call in create-lead.ts (look for
the prisma.customer.update invocation and the surrounding conditional that
spreads its array).

In `@apps/web/lib/integrations/shopify/schema.ts`:
- Around line 22-26: The schema currently declares discount_codes as a required
array which breaks when Shopify omits that field; update the discount_codes
definition in apps/web/lib/integrations/shopify/schema.ts (the
z.array(z.object({...})) for discount_codes) to be optional with a default empty
array by chaining .optional().default([]) on the z.array so absent or empty
discount_codes are handled safely while preserving validation for present
entries.

In `@apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx`:
- Around line 273-301: The inline IIFE that calls register("provider", {
disabled: Boolean(discount) }) and injects a custom onChange with side-effects
is hard to read in JSX; extract it into a top-level const (e.g.,
providerRegistration) above the JSX, assign const { onChange: onProviderChange,
...rest } = register("provider", { disabled: Boolean(discount) }); then create
onChange that calls onProviderChange(e) and performs the same branches (checking
e.target.value === DiscountProvider.shopify, calling
setUseExistingCoupon(false), setUseStripeTestCouponId(false),
setValue("couponId",""), setValue("couponTestId",""), setValue("maxDuration", 0)
or the else branch using defaultValuesSource.maxDuration), and spread
providerRegistration into the <select> in place of the IIFE.
🪄 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: 51dd5074-aaa2-4317-956f-327423a4b20a

📥 Commits

Reviewing files that changed from the base of the PR and between 33b5291 and 871d933.

📒 Files selected for processing (47)
  • apps/web/.env.example
  • 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/delete/route.ts
  • apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts
  • 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/discount-codes/[discountCodeId]/route.ts
  • apps/web/app/(ee)/api/discount-codes/route.ts
  • apps/web/app/(ee)/api/partners/[partnerId]/route.ts
  • apps/web/app/(ee)/api/shopify/integration/callback/route.ts
  • apps/web/app/(ee)/api/shopify/integration/webhook/orders-paid.ts
  • apps/web/app/(ee)/api/shopify/integration/webhook/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page.tsx
  • apps/web/lib/actions/partners/accept-program-invite.ts
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/actions/partners/delete-discount.ts
  • apps/web/lib/api/discounts/delete-discount-code.ts
  • apps/web/lib/api/links/delete-link.ts
  • apps/web/lib/api/partner-profile/get-partner-for-program.ts
  • apps/web/lib/api/partners/bulk-delete-partners.ts
  • apps/web/lib/cron/enqueue-batch-jobs.ts
  • apps/web/lib/discounts/construct-discount-code.ts
  • apps/web/lib/discounts/create-discount-code.ts
  • apps/web/lib/discounts/delete-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
  • apps/web/lib/discounts/discount-provider.ts
  • apps/web/lib/discounts/generate-discount-code-for-partner.ts
  • apps/web/lib/discounts/is-discount-equivalent.ts
  • apps/web/lib/integrations/shopify/admin-graphql.ts
  • apps/web/lib/integrations/shopify/create-lead.ts
  • apps/web/lib/integrations/shopify/process-order.ts
  • apps/web/lib/integrations/shopify/schema.ts
  • apps/web/lib/stripe/coupon-discount-converter.ts
  • apps/web/lib/stripe/create-stripe-discount-code.ts
  • apps/web/lib/stripe/disable-stripe-discount-code.ts
  • apps/web/lib/zod/schemas/discount.ts
  • apps/web/lib/zod/schemas/partners.ts
  • apps/web/lib/zod/schemas/workspaces.ts
  • apps/web/ui/modals/add-discount-code-modal.tsx
  • apps/web/ui/partners/constants.ts
  • apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx
  • packages/prisma/client.ts
  • packages/prisma/schema/discount.prisma
💤 Files with no reviewable changes (3)
  • apps/web/lib/stripe/disable-stripe-discount-code.ts
  • apps/web/lib/api/discounts/delete-discount-code.ts
  • apps/web/lib/stripe/create-stripe-discount-code.ts

Comment thread apps/web/app/(ee)/api/shopify/integration/callback/route.ts
Comment thread apps/web/app/(ee)/api/shopify/integration/callback/route.ts
Comment thread apps/web/lib/discounts/discount-provider-stripe.ts
Comment thread apps/web/lib/integrations/shopify/admin-graphql.ts
Comment thread apps/web/lib/integrations/shopify/process-order.ts
Comment thread apps/web/ui/partners/discounts/add-edit-discount-sheet.tsx Outdated
@devkiran

Copy link
Copy Markdown
Collaborator Author

@coderabbitai any breaking changes?

@coderabbitai

coderabbitai Bot commented Apr 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
# Check for any remaining usages of old function/export names that were deleted or renamed
echo "=== Remaining usages of STRIPE_ERROR_MAP ==="
rg -n "STRIPE_ERROR_MAP" --type=ts --type=tsx -g "*.ts" -g "*.tsx"

echo "=== Remaining usages of old createStripeDiscountCode ==="
rg -n "createStripeDiscountCode" --type=ts -g "*.ts"

echo "=== Remaining usages of old disableStripeDiscountCode ==="
rg -n "disableStripeDiscountCode" --type=ts -g "*.ts"

echo "=== Remaining usages of old lib/api/discounts import path ==="
rg -n "lib/api/discounts" --type=ts -g "*.ts"

echo "=== Remaining usages of old workspaceId parameter in ordersPaid ==="
rg -n "workspaceId" apps/web/app/\(ee\)/api/shopify/integration/webhook/ -n

echo "=== Check delete route body schema for provider field ==="
cat apps/web/app/\(ee\)/api/cron/discount-codes/delete/route.ts 2>/dev/null | head -50

echo "=== Check prisma schema for Discount model migration ==="
cat packages/prisma/schema/discount.prisma 2>/dev/null

Length of output: 5540


@devkiran Here's a summary of the breaking changes in this PR:


🔴 Public-facing API

Change Detail
POST /api/discount-codes createDiscountSchema now requires a new provider field (z.enum(DiscountProvider)). Any client creating a discount without passing provider will receive a validation error.

🟡 Internal cron/queue payloads

Change Detail
POST /api/cron/discount-codes/delete body Now requires provider in the request body. The updated deleteDiscountCodes helper already handles this, so all enqueue sites are covered — but any manually crafted QStash messages targeting this endpoint would need updating.

🟡 Database migration required

Change Detail
New DiscountProvider enum (stripe, shopify) New enum added to the Prisma schema. A migration is needed.
New provider field on Discount Added with @default(stripe), so existing rows are backward-compatible and no data backfill is needed.

⚪ Internal library (all call sites updated within the PR)

Change Detail
createDiscountCode signature Changed from { stripeConnectId, stripeMode, ... } to { workspace, partner, link, discount, code? }. All callers updated.
ordersPaid signature Changed from { event, workspaceId: string } to { event, workspace: Pick<WorkspaceProps, ...> }. Caller in route.ts updated.
deleteDiscountCodes Moved from lib/api/discounts/lib/discounts/ and now requires each item to carry discount.provider. All call sites updated (verified via script — no stale imports remain).
Deleted exports createStripeDiscountCode, disableStripeDiscountCode (from lib/stripe/) removed. Functionality is now inside the provider abstraction. No remaining usages found.
STRIPE_ERROR_MAPERROR_MAP Renamed in apps/web/ui/partners/constants.ts. No remaining usages of the old name found.

TL;DR: The only breaking change that could affect external consumers is the new required provider field on POST /api/discount-codes. Everything else is internal and all call sites have been updated within this PR. A database migration is also needed before deploying.


🧠 Learnings used
Learnt from: devkiran
Repo: dubinc/dub PR: 3804
File: apps/web/lib/discounts/discount-provider-shopify.ts:144-152
Timestamp: 2026-04-27T09:44:41.785Z
Learning: In the dubinc/dub codebase, `discount.amount` is stored as an integer in the smallest currency unit (e.g., cents for flat discounts, whole percentage points for percentage discounts). When creating Shopify discount codes, dividing by 100 (e.g., `discount.amount / 100`) correctly converts to the decimal amount in the store's own currency — there is no USD-specific currency mismatch because Shopify applies the discount in the store's native currency. Also, Shopify's GraphQL Decimal scalar accepts string representations (e.g., from `toFixed(2)`), so this is valid. Do not flag these patterns as bugs in future reviews.

@steven-tey steven-tey merged commit b2fd378 into main Apr 27, 2026
11 checks passed
@steven-tey steven-tey deleted the shopify-discount branch April 27, 2026 17:15
@coderabbitai coderabbitai Bot mentioned this pull request Jun 15, 2026
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