Skip to content

Add support for stripeMode in stripeIntegrationSettingsSchema#3679

Merged
steven-tey merged 11 commits into
mainfrom
stripe-integration-mode
Apr 1, 2026
Merged

Add support for stripeMode in stripeIntegrationSettingsSchema#3679
steven-tey merged 11 commits into
mainfrom
stripe-integration-mode

Conversation

@steven-tey
Copy link
Copy Markdown
Collaborator

@steven-tey steven-tey commented Apr 1, 2026

Summary by CodeRabbit

  • New Features

    • Workspace Stripe integration settings gain a configurable mode (live/test/sandbox).
  • Improvements

    • Stripe operations now use per-workspace mode with per-request Stripe clients so actions follow each workspace’s configured mode.
  • Error Handling

    • Discount, customer and invoice flows now skip or fail fast when a workspace lacks a Stripe integration.
  • Breaking Changes

    • The Stripe OAuth callback endpoint has been removed.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Apr 1, 2026 7:25pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

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

Handlers and utilities now derive stripeMode from each workspace’s installed Stripe integration settings, validate it, create per-request Stripe clients with that mode, thread stripeMode into Stripe-related calls, and early-return when no Stripe installation exists.

Changes

Cohort / File(s) Summary
Cron Routes & Workflows
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/workflows/partner-approved/route.ts
Load workspace installedIntegrations filtered to STRIPE_INTEGRATION_ID; early-return when absent; parse stripeMode and pass it into discount-code create/disable calls.
API Endpoints
apps/web/app/(ee)/api/customers/search-stripe/route.ts, apps/web/app/(ee)/api/discount-codes/route.ts
Replace module-level Stripe client with per-request Prisma lookup of installed integration, parse stripeMode (fail if missing), and instantiate stripeAppClient({ mode: stripeMode }) before calling Stripe APIs.
Stripe Integration Routes / UI
apps/web/app/(ee)/api/stripe/integration/route.ts, apps/web/app/(ee)/api/stripe/integration/callback/route.ts, packages/stripe-app/src/views/AppSettings.tsx, packages/stripe-app/src/utils/dub.ts
PATCH accepts stripeMode; callback GET handler removed; stripe-app UI and dub util now include/persist stripeMode in payloads and workspace updates.
Library: Discount & Stripe Utilities
apps/web/lib/api/discounts/create-discount-code.ts, apps/web/lib/stripe/create-stripe-discount-code.ts, apps/web/lib/stripe/disable-stripe-discount-code.ts
Function signatures updated to require stripeMode: StripeMode; removed env-based module clients; instantiate Stripe client per-call with provided stripeMode.
Actions & Customer Invoice Utilities
apps/web/lib/actions/partners/create-discount.ts, apps/web/lib/api/customers/get-customer-stripe-invoices.ts
Removed global env-based Stripe client; added Prisma lookup for installed integration, parse stripeMode, fail early if missing, and use per-request Stripe client with parsed mode.
Schema & Integrations
apps/web/lib/integrations/stripe/schema.ts, apps/web/lib/integrations/install.ts
Added optional stripeMode to stripeIntegrationSettingsSchema (`"live"
Misc / Types
imports across changed files (e.g., apps/web/lib/types, .../stripe/schema.ts)
Introduced/used StripeMode type in updated signatures and calls; updated imports accordingly.
Webhook minor change
apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.ts
Fallback uses `settings

Sequence Diagram(s)

sequenceDiagram
  participant Server as "Server / Handler"
  participant Prisma as "Prisma DB"
  participant Parser as "stripeIntegrationSettingsSchema"
  participant StripeApp as "stripeAppClient"
  participant StripeAPI as "Stripe API"

  Server->>Prisma: find workspace / installedIntegration (STRIPE_INTEGRATION_ID)
  Prisma-->>Server: installedIntegration.settings (or none)
  alt installedIntegration exists
    Server->>Parser: parse(settings) -> stripeMode
    Parser-->>Server: stripeMode
    Server->>StripeApp: instantiate client with mode=rgba(0,128,255,0.5)
    StripeApp-->>Server: stripe client
    Server->>StripeAPI: create/disable/search (with stripeConnectId, stripeMode)
    StripeAPI-->>Server: result
  else no installation
    Server-->>Server: early-return / log
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • devkiran
  • pepeladeira

Poem

🐰 I hop through settings, sniff the mode,
I parse each burrow's secret code,
Per-request clients snug and bright,
Discounts dance in proper light,
A little hop — the payments right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 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 change: adding stripeMode support to stripeIntegrationSettingsSchema. While the PR scope is broader (affecting 12 files across multiple routes and utilities), the core change is schema-centered, and the title correctly identifies this primary modification.

✏️ 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 stripe-integration-mode

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

65-88: Consider centralizing Stripe integration resolution.

This exact query + parse + error-handling block is now copy-pasted across the discount-code route, this action, and the customer-search route. A shared getStripeIntegrationSettingsOrThrow(workspaceId) helper would keep the canonical-row selection and legacy-settings fallback in one place.

🤖 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 65 - 88, The
code duplicates the Stripe integration lookup, validation, and parsing
(installedStripeIntegration query, stripeIntegrationSettingsSchema.parse, and
stripeAppClient instantiation); extract this into a shared helper
getStripeIntegrationSettingsOrThrow(workspaceId) that encapsulates the
prisma.installedIntegration.findFirst selection, legacy-settings fallback,
throws the same STRIPE_CONNECTION_REQUIRED error when missing, and returns the
parsed settings so callers (e.g., create-discount action, discount-code route,
customer-search) can call getStripeIntegrationSettingsOrThrow(workspace.id) and
then pass the returned settings to stripeAppClient({ mode: settings.stripeMode
}).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/lib/actions/partners/create-discount.ts`:
- Around line 65-88: The code duplicates the Stripe integration lookup,
validation, and parsing (installedStripeIntegration query,
stripeIntegrationSettingsSchema.parse, and stripeAppClient instantiation);
extract this into a shared helper
getStripeIntegrationSettingsOrThrow(workspaceId) that encapsulates the
prisma.installedIntegration.findFirst selection, legacy-settings fallback,
throws the same STRIPE_CONNECTION_REQUIRED error when missing, and returns the
parsed settings so callers (e.g., create-discount action, discount-code route,
customer-search) can call getStripeIntegrationSettingsOrThrow(workspace.id) and
then pass the returned settings to stripeAppClient({ mode: settings.stripeMode
}).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08e06b0f-926a-44c3-ac35-3d97a482719c

📥 Commits

Reviewing files that changed from the base of the PR and between 06479e9 and e8a5ba8.

📒 Files selected for processing (12)
  • 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/customers/search-stripe/route.ts
  • apps/web/app/(ee)/api/discount-codes/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/api/customers/get-customer-stripe-invoices.ts
  • apps/web/lib/api/discounts/create-discount-code.ts
  • apps/web/lib/integrations/stripe/schema.ts
  • apps/web/lib/stripe/create-stripe-discount-code.ts
  • apps/web/lib/stripe/disable-stripe-discount-code.ts

@devkiran
Copy link
Copy Markdown
Collaborator

devkiran commented Apr 1, 2026

@coderabbitai full review please, any breaking changes?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

🧩 Analysis chain

I'll do a thorough investigation before the full review to check for any breaking changes.


🏁 Script executed:

#!/bin/bash
echo "=== Callers of createDiscountCode ==="
rg -n "createDiscountCode" --type ts -C 3

echo ""
echo "=== Callers of createStripeDiscountCode ==="
rg -n "createStripeDiscountCode" --type ts -C 3

echo ""
echo "=== Callers of disableStripeDiscountCode ==="
rg -n "disableStripeDiscountCode" --type ts -C 3

echo ""
echo "=== stripeIntegrationSettingsSchema usages ==="
rg -n "stripeIntegrationSettingsSchema" --type ts -C 3

echo ""
echo "=== StripeMode type definition ==="
rg -n "StripeMode" --type ts -C 3

echo ""
echo "=== STRIPE_INTEGRATION_ID definition ==="
rg -n "STRIPE_INTEGRATION_ID" --type ts -C 2

Length of output: 63631


@devkiran: Here's a comprehensive review of PR #3679. Triggering a full re-review as requested, and flagging the key areas below.


Summary

This PR replaces hardcoded process.env.VERCEL_ENV-derived Stripe mode logic with a per-request, settings-driven stripeMode sourced from the workspace's installed Stripe integration record (stripeIntegrationSettingsSchema). The change touches 12 files across cron routes, API routes, server actions, and library utilities.


Breaking Changes

1. Function signature changes (internal breaking)

Three functions had their signatures updated in a breaking way:

Function Change
createDiscountCode() Added required stripeMode: StripeMode parameter
createStripeDiscountCode() Added required stripeMode: StripeMode parameter
disableStripeDiscountCode() Added required stripeMode: StripeMode; stripeConnectId changed from string | nullstring

✅ All known call sites within the repo have been updated consistently in this PR — no orphaned callers found.


Issues Found

🔴 StripeMode type divergence between packages

apps/web/lib/types.ts defines:

export type StripeMode = "test" | "sandbox" | "live";

But packages/stripe-app/src/utils/types.ts defines:

export type StripeMode = "test" | "live"; // no "sandbox"

The schema now accepts "sandbox" as a valid stripeMode value. If a workspace's integration settings have stripeMode: "sandbox" (either written by the server or manually), the stripe-app package will hit a type error and its OAuth redirect URL logic (getRedirectUrl) won't handle it:

// packages/stripe-app/src/utils/oauth.ts
function getRedirectUrl(mode: StripeMode) {
  return `https://dashboard.stripe.com/${mode === "live" ? "" : "test/"}apps-oauth/dub.co`;
  // "sandbox" would fall into the "test/" branch — is that correct?
}

Recommendation: Align the StripeMode type in packages/stripe-app/src/utils/types.ts to also include "sandbox", or explicitly document the intended behavior for sandbox in the OAuth redirect.


🟡 No UI to configure stripeMode

The Stripe integration settings UI (apps/web/lib/integrations/stripe/ui/settings.tsx) has not been updated to expose the new stripeMode field. The field defaults to "live" per the schema, so all existing and new integrations will use live mode — there's currently no way for users to switch to test or sandbox through the UI.

If this is intentional (e.g., set programmatically or via Stripe app OAuth), it's fine, but worth documenting. Otherwise this field will be invisible to users.


🟡 stripeAppClient default mode inconsistency

In apps/web/lib/stripe/index.ts:

export const stripeAppClient = ({ mode }: { mode?: StripeMode }) => {
  const appSecretKey = secretMap[mode ?? "test"]; // defaults to "test"

The schema defaults stripeMode to "live", but stripeAppClient defaults to "test" when mode is undefined. In the new code paths, mode always comes from the parsed schema (never undefined), so this is harmless in practice — but it's a latent inconsistency that could cause confusion if stripeAppClient is ever called without a mode.


🟢 Backward compatibility of schema change

The stripeMode field is optional with .default("live") — existing InstalledIntegration records with no stripeMode in their JSON settings will parse correctly and default to "live". No database migration needed. ✅


Verdict

No un-updated callers, no data migration required, and all call sites are consistent. The main actionable items before merging are:

  1. Align StripeMode in packages/stripe-app to include "sandbox".
  2. Clarify/implement UI for stripeMode setting, or add a comment explaining it's intentionally not user-configurable.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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/integrations/stripe/schema.ts (1)

3-19: ⚠️ Potential issue | 🔴 Critical

Add null-coalescing to all parse calls and backfill legacy stripe settings.

The schema's .optional().default("live") only handles undefined; Zod will throw on actual null values. Since InstalledIntegration.settings is nullable in Prisma, nine of ten parse sites will fail for legacy installs with null settings. Only customer-subscription-created.ts safely uses ?? {}. Add null-coalescing to all remaining parse calls (partner-approved, discount-codes, cron/delete, cron/create, cron/remap, search-stripe, create-discount, get-customer-stripe-invoices, etc.), backfill or migrate legacy rows to ensure settings is never null, and verify with a workspace that previously had empty Stripe settings.

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

In `@apps/web/lib/integrations/stripe/schema.ts` around lines 3 - 19, The
stripeIntegrationSettingsSchema currently uses .optional().default("live") which
doesn't accept actual nulls from InstalledIntegration.settings; update every
place that calls stripeIntegrationSettingsSchema.parse to coalesce null to an
object (e.g., pass (installedIntegration.settings ?? {}) into
stripeIntegrationSettingsSchema.parse) — specifically update parse sites such as
partner-approved, discount-codes, cron/delete, cron/create, cron/remap,
search-stripe, create-discount, get-customer-stripe-invoices,
customer-subscription-created (ensure it already uses ?? {}), etc.; additionally
add a DB backfill/migration to set legacy InstalledIntegration.settings null
rows to {} so future reads are safe and verify the fix against a workspace that
previously had empty Stripe settings.
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/cron/discount-codes/create/route.ts (1)

49-53: Select only settings from installedIntegrations.

The default relation load pulls the full InstalledIntegration scalar payload, including credentials, but this handler only reads settings. Narrowing the select avoids moving sensitive JSON around unnecessarily.

✂️ Suggested query tightening
           installedIntegrations: {
             where: {
               integrationId: STRIPE_INTEGRATION_ID,
             },
+            select: {
+              settings: true,
+            },
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/cron/discount-codes/create/route.ts around lines 49 -
53, The installedIntegrations relation is currently loading the full
InstalledIntegration record (including credentials) but this handler only needs
settings; tighten the query by adding a select that returns only settings for
installedIntegrations (e.g., under the installedIntegrations block add select: {
settings: true }) so you still filter by STRIPE_INTEGRATION_ID but avoid pulling
sensitive fields like credentials; update the query where installedIntegrations
is used (the object keyed installedIntegrations in this route handler)
accordingly.
🤖 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/discount-codes/route.ts:
- Around line 69-90: The current code uses prisma.installedIntegration.findFirst
(referenced as installedStripeIntegration) making stripeMode nondeterministic;
replace this with a deterministic workspace-level lookup helper (e.g.,
getWorkspaceIntegrationSettings) that queries prisma.installedIntegration for
the given projectId + integrationId and returns the canonical workspace settings
(for example by selecting the row where userId is null / isWorkspace flag is
true, or enforcing a single row via findMany + consistency check or choosing the
most recent canonical record), then pass that result into
stripeIntegrationSettingsSchema.parse instead of the ad-hoc findFirst result to
ensure the Stripe environment is always resolved deterministically.

In `@apps/web/lib/stripe/disable-stripe-discount-code.ts`:
- Line 2: The StripeMode union type is inconsistent across packages (one defines
"sandbox" while the other does not); update the type so both packages accept the
same literals. Edit the StripeMode declaration used by the Stripe app package
(the StripeMode type in the utils/types.ts that the Stripe app exports/imports)
to include "sandbox" (or alternatively remove "sandbox" from the web package)
and ensure all imports refer to the unified type name StripeMode; consider
centralizing/exporting the canonical StripeMode type from a single module and
update references to use that shared type so both codebases remain aligned.

---

Outside diff comments:
In `@apps/web/lib/integrations/stripe/schema.ts`:
- Around line 3-19: The stripeIntegrationSettingsSchema currently uses
.optional().default("live") which doesn't accept actual nulls from
InstalledIntegration.settings; update every place that calls
stripeIntegrationSettingsSchema.parse to coalesce null to an object (e.g., pass
(installedIntegration.settings ?? {}) into
stripeIntegrationSettingsSchema.parse) — specifically update parse sites such as
partner-approved, discount-codes, cron/delete, cron/create, cron/remap,
search-stripe, create-discount, get-customer-stripe-invoices,
customer-subscription-created (ensure it already uses ?? {}), etc.; additionally
add a DB backfill/migration to set legacy InstalledIntegration.settings null
rows to {} so future reads are safe and verify the fix against a workspace that
previously had empty Stripe settings.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/cron/discount-codes/create/route.ts:
- Around line 49-53: The installedIntegrations relation is currently loading the
full InstalledIntegration record (including credentials) but this handler only
needs settings; tighten the query by adding a select that returns only settings
for installedIntegrations (e.g., under the installedIntegrations block add
select: { settings: true }) so you still filter by STRIPE_INTEGRATION_ID but
avoid pulling sensitive fields like credentials; update the query where
installedIntegrations is used (the object keyed installedIntegrations in this
route handler) accordingly.
🪄 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: a84f7589-a5d8-477e-8ea1-1acbb877ad41

📥 Commits

Reviewing files that changed from the base of the PR and between 06479e9 and dd59844.

📒 Files selected for processing (12)
  • 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/customers/search-stripe/route.ts
  • apps/web/app/(ee)/api/discount-codes/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/api/customers/get-customer-stripe-invoices.ts
  • apps/web/lib/api/discounts/create-discount-code.ts
  • apps/web/lib/integrations/stripe/schema.ts
  • apps/web/lib/stripe/create-stripe-discount-code.ts
  • apps/web/lib/stripe/disable-stripe-discount-code.ts

Comment thread apps/web/app/(ee)/api/discount-codes/route.ts
Comment thread apps/web/lib/stripe/disable-stripe-discount-code.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/stripe/integration/route.ts:
- Around line 21-27: The handler currently parses stripeMode (alongside
stripeAccountId) but never writes it to the installation/settings record, so
stripeMode silently defaults to "live"; update the create/update settings path
that currently reads/writes settings (refer to the settings variable and
wherever stripeAccountId is persisted) to also persist stripeMode on both new
installs and updates by adding stripeMode into the settings object saved to the
database (the same save/createOrUpdate call that stores stripeAccountId),
ensuring reads later will return the persisted stripeMode.

In `@packages/stripe-app/src/utils/dub.ts`:
- Line 2: The StripeMode type in packages/stripe-app/src/utils/types.ts is too
narrow (only "test" | "live"); update the StripeMode type declaration to include
"sandbox" (e.g., "live" | "test" | "sandbox") so imports like the one in
packages/stripe-app/src/utils/dub.ts accept sandbox mode; update any related
validation or switch statements that rely on StripeMode to handle the new
"sandbox" variant.
🪄 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: 90fb56da-597e-45a0-9581-bf431487756b

📥 Commits

Reviewing files that changed from the base of the PR and between dd59844 and 4931b3d.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/stripe/integration/callback/route.ts
  • apps/web/app/(ee)/api/stripe/integration/route.ts
  • packages/stripe-app/src/utils/dub.ts
  • packages/stripe-app/src/views/AppSettings.tsx
💤 Files with no reviewable changes (1)
  • apps/web/app/(ee)/api/stripe/integration/callback/route.ts

Comment thread apps/web/app/(ee)/api/stripe/integration/route.ts Outdated
Comment thread packages/stripe-app/src/utils/dub.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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/integrations/install.ts (1)

30-32: ⚠️ Potential issue | 🟠 Major

Persist settings on upsert update path as well.

settings is accepted and written on create, but ignored on update. Re-installs/upserts can silently keep stale settings.

Suggested fix
     update: {
       credentials,
+      settings,
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/install.ts` around lines 30 - 32, The upsert/update
path for integrations currently only persists credentials and omits settings,
causing re-installs to keep stale settings; update the upsert (or update) call
that builds the update payload (the object currently containing update: {
credentials, }) to also include settings (e.g., update: { credentials, settings
}) and ensure the same settings value used on create is passed through so
settings are written on both create and update in the function that performs the
integration install/upsert.
♻️ Duplicate comments (1)
apps/web/app/(ee)/api/stripe/integration/route.ts (1)

65-104: ⚠️ Potential issue | 🟠 Major

stripeMode is still not persisted for existing installations.

The new value is saved only when no installation exists. For existing installations, stripeMode updates are ignored, so mode can remain stale.

Suggested fix
       waitUntil(
         (async () => {
           const installation = await prisma.installedIntegration.findUnique({
@@
           if (!installation) {
             await installIntegration({
@@
               settings: {
                 stripeMode,
               },
             });
+            return;
           }
@@
           if (installation && stripeAccountId === null) {
             await prisma.installedIntegration.delete({
               where: {
                 id: installation.id,
               },
             });
+            return;
           }
+
+          await prisma.installedIntegration.update({
+            where: { id: installation.id },
+            data: {
+              settings: {
+                ...((installation.settings as Record<string, unknown> | null) ?? {}),
+                stripeMode,
+              },
+            },
+          });
         })(),
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/stripe/integration/route.ts around lines 65 - 104, The
code only saves stripeMode when creating a new installation; for existing
installations you must persist changes to stripeMode as well. After fetching
installation (from prisma.installedIntegration.findUnique) and before the
deletion branch, detect if installation exists and stripeMode is different (or
not null) and call prisma.installedIntegration.update (or use installIntegration
update path) to set settings: { ...installation.settings, stripeMode } for that
installation.id; keep the existing deletion logic for when stripeAccountId ===
null.
🧹 Nitpick comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.ts (1)

51-53: Inconsistent fallback operator: || vs ?? used elsewhere in the codebase.

This line uses || (logical OR) whereas update-stripe-settings.ts and likely other Stripe integration code uses ?? (nullish coalescing):

// In update-stripe-settings.ts (line 28):
const current = (installedIntegration.settings as any) ?? {};

With ??, only null/undefined are coerced to {}; with ||, any falsy value (false, 0, "") would also be coerced. Since the Prisma settings field is Json? and can technically hold any JSON value, this creates a subtle behavioral difference.

If this change is intentional for more defensive handling, consider updating other usages for consistency. If unintentional, revert to ?? to match the existing pattern.

Suggested fix to align with existing pattern
   const stripeIntegrationSettings = stripeIntegrationSettingsSchema.parse(
-    workspace.installedIntegrations[0].settings || {},
+    workspace.installedIntegrations[0].settings ?? {},
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/customer-subscription-created.ts
around lines 51 - 53, The fallback for reading integration settings uses logical
OR which coerces falsy values; change the expression passed into
stripeIntegrationSettingsSchema.parse so it uses the nullish coalescing operator
instead of || — i.e., replace the use of
workspace.installedIntegrations[0].settings || {} with
workspace.installedIntegrations[0].settings ?? {} in the code that sets
stripeIntegrationSettings (the call to stripeIntegrationSettingsSchema.parse) to
match the pattern used in update-stripe-settings.ts and avoid incorrectly
overriding valid falsy JSON values.
apps/web/app/(ee)/api/cron/discount-codes/create/route.ts (1)

49-53: Select only settings from installedIntegrations to reduce payload.

You only read settings later, so narrowing the relation select keeps this cron query leaner.

Suggested refactor
           installedIntegrations: {
             where: {
               integrationId: STRIPE_INTEGRATION_ID,
             },
+            select: {
+              settings: true,
+            },
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/cron/discount-codes/create/route.ts around lines 49 -
53, The query currently selects the entire installedIntegrations relation but
only needs settings; update the query that builds installedIntegrations (the
object with where: { integrationId: STRIPE_INTEGRATION_ID }) to use a select
that returns only settings (e.g., installedIntegrations: { where: {
integrationId: STRIPE_INTEGRATION_ID }, select: { settings: true } }) so the
cron in route.ts fetches a smaller payload while preserving access to settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/web/lib/integrations/install.ts`:
- Around line 30-32: The upsert/update path for integrations currently only
persists credentials and omits settings, causing re-installs to keep stale
settings; update the upsert (or update) call that builds the update payload (the
object currently containing update: { credentials, }) to also include settings
(e.g., update: { credentials, settings }) and ensure the same settings value
used on create is passed through so settings are written on both create and
update in the function that performs the integration install/upsert.

---

Duplicate comments:
In `@apps/web/app/`(ee)/api/stripe/integration/route.ts:
- Around line 65-104: The code only saves stripeMode when creating a new
installation; for existing installations you must persist changes to stripeMode
as well. After fetching installation (from
prisma.installedIntegration.findUnique) and before the deletion branch, detect
if installation exists and stripeMode is different (or not null) and call
prisma.installedIntegration.update (or use installIntegration update path) to
set settings: { ...installation.settings, stripeMode } for that installation.id;
keep the existing deletion logic for when stripeAccountId === null.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/cron/discount-codes/create/route.ts:
- Around line 49-53: The query currently selects the entire
installedIntegrations relation but only needs settings; update the query that
builds installedIntegrations (the object with where: { integrationId:
STRIPE_INTEGRATION_ID }) to use a select that returns only settings (e.g.,
installedIntegrations: { where: { integrationId: STRIPE_INTEGRATION_ID },
select: { settings: true } }) so the cron in route.ts fetches a smaller payload
while preserving access to settings.

In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/customer-subscription-created.ts:
- Around line 51-53: The fallback for reading integration settings uses logical
OR which coerces falsy values; change the expression passed into
stripeIntegrationSettingsSchema.parse so it uses the nullish coalescing operator
instead of || — i.e., replace the use of
workspace.installedIntegrations[0].settings || {} with
workspace.installedIntegrations[0].settings ?? {} in the code that sets
stripeIntegrationSettings (the call to stripeIntegrationSettingsSchema.parse) to
match the pattern used in update-stripe-settings.ts and avoid incorrectly
overriding valid falsy JSON values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d306d391-f428-4a30-a457-4ddbbf54dbc5

📥 Commits

Reviewing files that changed from the base of the PR and between 4931b3d and 0892ce0.

📒 Files selected for processing (11)
  • 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/customers/search-stripe/route.ts
  • apps/web/app/(ee)/api/discount-codes/route.ts
  • apps/web/app/(ee)/api/stripe/integration/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts
  • apps/web/lib/actions/partners/create-discount.ts
  • apps/web/lib/api/customers/get-customer-stripe-invoices.ts
  • apps/web/lib/integrations/install.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/web/app/(ee)/api/cron/discount-codes/delete/route.ts
  • apps/web/app/(ee)/api/discount-codes/route.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts
  • apps/web/app/(ee)/api/customers/search-stripe/route.ts
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/web/app/(ee)/api/stripe/integration/route.ts (2)

82-85: Consider improving type safety when merging settings.

The as any cast loses type safety and the optional chaining on installation?.settings is redundant since installation is guaranteed non-null after the check at line 46.

♻️ Suggested improvement
              data: {
                settings: {
-                  ...((installation?.settings as any) || {}),
+                  ...((installation.settings as Record<string, unknown>) ?? {}),
                   stripeMode,
                 },
               },

Alternatively, parse existing settings through the schema for full type safety:

const existingSettings = stripeIntegrationSettingsSchema
  .partial()
  .safeParse(installation.settings);
const mergedSettings = {
  ...(existingSettings.success ? existingSettings.data : {}),
  stripeMode,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/stripe/integration/route.ts around lines 82 - 85, The
current merge uses (installation?.settings as any) which loses type safety and
the optional chaining is unnecessary because installation is non-null; instead
validate/parse installation.settings with
stripeIntegrationSettingsSchema.partial().safeParse (or .parse) to get a typed
existingSettings and then merge { ...(existingSettings.success ?
existingSettings.data : {}), stripeMode } into the settings field; update the
code paths that reference installation.settings and remove the redundant ?. and
any casts to any to preserve type safety.

66-90: Silent failure in waitUntil block may cause settings inconsistency.

If the prisma.installedIntegration.update or delete fails (e.g., transient DB error), the failure is swallowed silently. The response already succeeded with the project update, but the installation's stripeMode would remain stale.

This may be acceptable for background work, but consider logging failures at minimum.

♻️ Add error logging
       waitUntil(
         (async () => {
+          try {
             // Uninstall the integration if the stripe account id is null
             if (installation && stripeAccountId === null) {
               await prisma.installedIntegration.delete({
                 where: {
                   id: installation.id,
                 },
               });
               // else, update the Stripe mode for the installation
             } else {
               await prisma.installedIntegration.update({
                 where: {
                   id: installation.id,
                 },
                 data: {
                   settings: {
                     ...((installation?.settings as any) || {}),
                     stripeMode,
                   },
                 },
               });
             }
+          } catch (error) {
+            console.error(
+              `[Stripe Integration] Failed to update installation settings for workspace ${workspace.id}:`,
+              error,
+            );
+          }
         })(),
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/stripe/integration/route.ts around lines 66 - 90, The
background waitUntil IIFE that calls prisma.installedIntegration.delete or
update can fail silently; wrap the async function body in a try/catch and log
any error (including the caught error, installation.id, stripeAccountId and
stripeMode) so failures are visible for debugging; specifically modify the
anonymous async function passed to waitUntil (the block that calls
prisma.installedIntegration.delete and prisma.installedIntegration.update) to
catch exceptions and call a logger (or console.error) with a clear message and
context about the installation and operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/app/`(ee)/api/stripe/integration/route.ts:
- Around line 82-85: The current merge uses (installation?.settings as any)
which loses type safety and the optional chaining is unnecessary because
installation is non-null; instead validate/parse installation.settings with
stripeIntegrationSettingsSchema.partial().safeParse (or .parse) to get a typed
existingSettings and then merge { ...(existingSettings.success ?
existingSettings.data : {}), stripeMode } into the settings field; update the
code paths that reference installation.settings and remove the redundant ?. and
any casts to any to preserve type safety.
- Around line 66-90: The background waitUntil IIFE that calls
prisma.installedIntegration.delete or update can fail silently; wrap the async
function body in a try/catch and log any error (including the caught error,
installation.id, stripeAccountId and stripeMode) so failures are visible for
debugging; specifically modify the anonymous async function passed to waitUntil
(the block that calls prisma.installedIntegration.delete and
prisma.installedIntegration.update) to catch exceptions and call a logger (or
console.error) with a clear message and context about the installation and
operation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 489973eb-fe37-428d-b3fd-21b414d0c032

📥 Commits

Reviewing files that changed from the base of the PR and between 0892ce0 and f546c03.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/stripe/integration/route.ts

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 1, 2026

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​stripe/​ui-extension-sdk@​9.1.0100100100100100

View full report

@steven-tey
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review, any breaking changes, incorrect code?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== disable-stripe-discount-code.ts ==="
fd "disable-stripe-discount-code.ts" --exec cat {}

echo ""
echo "=== create-stripe-discount-code.ts ==="
fd "create-stripe-discount-code.ts" --exec cat {}

echo ""
echo "=== create-discount-code.ts ==="
fd "create-discount-code.ts" --exec cat {}

echo ""
echo "=== webhook customer-subscription-created.ts ==="
fd "customer-subscription-created.ts" --exec cat {}

echo ""
echo "=== install.ts (integrations) ==="
fd "install.ts" --type f --exec cat {}

echo ""
echo "=== stripe integration route.ts (PATCH) ==="
fd "route.ts" -p "api/stripe/integration" --exec cat {}

echo ""
echo "=== stripeAppClient definition ==="
fd "index.ts" -p "lib/stripe" --exec cat {}

echo ""
echo "=== stripeIntegrationSettingsSchema ==="
fd "schema.ts" -p "lib/integrations/stripe" --exec cat {}

echo ""
echo "=== StripeMode type in apps/web ==="
fd "types.ts" -p "apps/web/lib" --exec grep -n "StripeMode" {}

echo ""
echo "=== StripeMode type in packages/stripe-app ==="
fd "types.ts" -p "packages/stripe-app" --exec cat {}

echo ""
echo "=== AppSettings.tsx ==="
fd "AppSettings.tsx" --exec cat {}

echo ""
echo "=== packages/stripe-app oauth.ts ==="
fd "oauth.ts" -p "packages/stripe-app" --exec cat {}

echo ""
echo "=== packages/stripe-app dub.ts ==="
fd "dub.ts" -p "packages/stripe-app" --exec cat {}

echo ""
echo "=== cron delete route ==="
fd "route.ts" -p "api/cron/discount-codes/delete" --exec cat {}

echo ""
echo "=== cron create route ==="
fd "route.ts" -p "api/cron/discount-codes/create" --exec cat {}

echo ""
echo "=== api/discount-codes route ==="
fd "route.ts" -p "api/discount-codes" --exec cat {}

echo ""
echo "=== search-stripe route ==="
fd "route.ts" -p "customers/search-stripe" --exec cat {}

echo ""
echo "=== get-customer-stripe-invoices.ts ==="
fd "get-customer-stripe-invoices.ts" --exec cat {}

echo ""
echo "=== partner-approved route ==="
fd "route.ts" -p "api/workflows/partner-approved" --exec cat {}

echo ""
echo "=== remap-discount-codes route ==="
fd "route.ts" -p "cron/groups/remap-discount-codes" --exec cat {}

echo ""
echo "=== create-discount.ts (server action) ==="
fd "create-discount.ts" -p "lib/actions/partners" --exec cat {}

Length of output: 31795


🏁 Script executed:

#!/bin/bash
echo "=== stripe integration PATCH route ==="
find . -path "*/api/stripe/integration/route.ts" | head -5 | xargs cat

echo ""
echo "=== stripeAppClient index.ts ==="
find . -path "*/lib/stripe/index.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== stripeIntegrationSettingsSchema ==="
find . -path "*/integrations/stripe/schema.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== cron discount-codes create route ==="
find . -path "*/cron/discount-codes/create/route.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== cron discount-codes delete route ==="
find . -path "*/cron/discount-codes/delete/route.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== api/discount-codes route ==="
find . -path "*/api/discount-codes/route.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== search-stripe route ==="
find . -path "*/customers/search-stripe/route.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== partner-approved route ==="
find . -path "*/workflows/partner-approved/route.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== remap-discount-codes route ==="
find . -path "*/remap-discount-codes/route.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== create-discount.ts server action ==="
find . -path "*/actions/partners/create-discount.ts" | grep -v node_modules | head -5 | xargs cat

echo ""
echo "=== stripe integration callback route (deleted check) ==="
find . -path "*/stripe/integration/callback/route.ts" | grep -v node_modules | head -5

echo ""
echo "=== STRIPE_INTEGRATION_ID ==="
rg -n "STRIPE_INTEGRATION_ID\s*=" --type ts | head -10

Length of output: 42727


@steven-tey Here's the full review with a focus on breaking changes and incorrect code. I've inspected all 18 affected files.


✅ No Breaking Changes to Public API

All three functions that changed signatures (createDiscountCode, createStripeDiscountCode, disableStripeDiscountCode) are internal. Every call site has been updated — no orphaned callers found.


✅ Issues from Previous Review — Now Resolved

StripeMode type divergence (flagged in the prior review) is fixed. packages/stripe-app/src/utils/types.ts now correctly includes all three values:

export type StripeMode = "live" | "test" | "sandbox";

🔴 Incorrect Code: installIntegrationsettings not persisted on re-install

In apps/web/lib/integrations/install.ts, the update path of the upsert silently drops settings:

await prisma.installedIntegration.upsert({
  create: { userId, projectId: workspaceId, integrationId, credentials, settings },
  update: { credentials }, // ← settings missing here
  ...
});

If installIntegration is ever called on an existing Stripe installation (e.g. re-auth), the stored stripeMode would be wiped. For this PR specifically, the Stripe integration avoids installIntegration entirely for settings — it uses the PATCH route's direct prisma.installedIntegration.update instead. So it is not a regression introduced here, but it's a latent bug. Worth fixing before another integration adopts the settings parameter.


🟡 Race Window: stripeMode written asynchronously after stripeConnectId

In the PATCH route (apps/web/app/(ee)/api/stripe/integration/route.ts):

// stripeConnectId written synchronously
await prisma.project.update({ data: { stripeConnectId: stripeAccountId } });

// stripeMode written asynchronously
waitUntil(async () => {
  await prisma.installedIntegration.update({ data: { settings: { ...stripeMode } } });
});

Between these two writes there is a brief window where stripeConnectId is set but stripeMode is still stale. Any concurrent request that reads integration settings in this window would use the old mode. The window is small and stripeMode rarely changes, so this is low severity — but worth noting.


🟡 stripeAppClient Default Inconsistency (unchanged from prior review)

// apps/web/lib/stripe/index.ts
const appSecretKey = secretMap[mode ?? "test"]; // fallback is "test"

The schema defaults stripeMode to "live", but stripeAppClient falls back to "test" when mode is undefined. In practice this never triggers because all callers now pass mode from the parsed schema, but it's a confusing inconsistency. Consider aligning the fallback:

const appSecretKey = secretMap[mode ?? "live"];

🟢 Correct Behavior Confirmed

Area Verdict
AppSettings.tsx sandbox detection (isSandbox ? "sandbox" : environment.mode) ✅ Correct
getRedirectUrl("sandbox") maps to "test/" URL ✅ Correct for Stripe sandbox
settings || {} vs settings ?? {} ✅ Cosmetically different, not a bug
Schema default stripeMode: "live" — no migration needed
Deleted OAuth callback route — install now via Dub OAuth flow ✅ Intentional
All cron routes, API routes, and server actions pass stripeMode correctly

Summary

The PR is logically correct with no breaking changes to the public surface. Two items worth addressing before merge:

  1. Add settings to the update path in installIntegration — not a regression from this PR, but a latent bug exposed by the new settings parameter.
  2. Align stripeAppClient fallback from "test" to "live" to match schema default.

✏️ Learnings added
Learnt from: steven-tey
URL: https://github.com/dubinc/dub/pull/3679

Timestamp: 2026-04-01T18:57:38.990Z
Learning: In dubinc/dub, the `installIntegration` function in `apps/web/lib/integrations/install.ts` uses a Prisma upsert where the `update` path only persists `credentials` but NOT `settings`. The `settings` field is only written on the `create` path. For the Stripe integration, this does not cause issues because settings are managed via the PATCH `/api/stripe/integration` route directly. But if any other integration adopts the `settings` parameter via `installIntegration` and expects it to be updated on re-install, the upsert update path must also include `settings`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/lib/stripe/disable-stripe-discount-code.ts:2-2
Timestamp: 2026-04-01T17:57:42.171Z
Learning: In dubinc/dub, there is a known StripeMode type divergence between packages: `apps/web/lib/types.ts` defines `StripeMode = "test" | "sandbox" | "live"`, while `packages/stripe-app/src/utils/types.ts` defines `StripeMode = "test" | "live"` (missing "sandbox"). This is intentional for now; the Stripe app package needs to be updated separately to support "sandbox" mode. Do not flag this divergence as a blocking issue in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/app/(ee)/api/discount-codes/route.ts:69-90
Timestamp: 2026-04-01T17:57:13.716Z
Learning: In the dubinc/dub codebase, a workspace can only ever have one installed Stripe integration at a time. Therefore, `prisma.installedIntegration.findFirst({ where: { projectId, integrationId: STRIPE_INTEGRATION_ID } })` is always deterministic and there is no risk of returning the wrong row. Do not flag this pattern as nondeterministic in future reviews.

@steven-tey
Copy link
Copy Markdown
Collaborator Author

@coderabbitai addressed your feedback, can you review again?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Tip

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

🧩 Analysis chain

Let me check the current state of the two files where I flagged issues.


🏁 Script executed:

#!/bin/bash
echo "=== apps/web/lib/integrations/install.ts ==="
find . -path "*/lib/integrations/install.ts" | grep -v node_modules | xargs cat

echo ""
echo "=== apps/web/lib/stripe/index.ts ==="
find . -path "*/lib/stripe/index.ts" | grep -v node_modules | xargs cat

Length of output: 3290


@steven-tey Thanks for the update! Here's what I found after checking both files:

stripeAppClient fallback — Fixed

The default is now aligned with the schema:

const appSecretKey = secretMap[mode ?? "live"]; // was "test", now "live" ✅

⚠️ installIntegrationsettings still not wired through implementation

The InstallIntegration interface now declares settings?: Record<string, any>, but the function itself does not destructure or use it:

export const installIntegration = async ({
  userId,
  workspaceId,
  integrationId,
  credentials,
  // settings is NOT destructured ← missing
}: InstallIntegration) => {
  await prisma.installedIntegration.upsert({
    create: { userId, projectId: workspaceId, integrationId, credentials },  // no settings
    update: { credentials },  // no settings
    ...
  });

Any caller that passes settings will have it silently ignored. For this PR there's no functional impact (Stripe settings flow through the PATCH route directly), but the dead field in the interface is misleading and could trip up a future integration.

Two options to clean this up:

  1. Remove settings from the interface (since it's not used), keeping the contract honest.
  2. Wire it through both create and update paths if you do intend future integrations to use this parameter.

Either way, the PR logic itself is correct — no blocking issues remain. Let me know if you'd like me to re-trigger a full review once this is addressed.


🧠 Learnings used
Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/app/(ee)/api/discount-codes/route.ts:69-90
Timestamp: 2026-04-01T17:57:13.716Z
Learning: In the dubinc/dub codebase, a workspace can only ever have one installed Stripe integration at a time. Therefore, `prisma.installedIntegration.findFirst({ where: { projectId, integrationId: STRIPE_INTEGRATION_ID } })` is always deterministic and there is no risk of returning the wrong row. Do not flag this pattern as nondeterministic in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/lib/stripe/disable-stripe-discount-code.ts:2-2
Timestamp: 2026-04-01T17:57:42.171Z
Learning: In dubinc/dub, there is a known StripeMode type divergence between packages: `apps/web/lib/types.ts` defines `StripeMode = "test" | "sandbox" | "live"`, while `packages/stripe-app/src/utils/types.ts` defines `StripeMode = "test" | "live"` (missing "sandbox"). This is intentional for now; the Stripe app package needs to be updated separately to support "sandbox" mode. Do not flag this divergence as a blocking issue in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2026-04-01T18:57:38.990Z
Learning: In dubinc/dub, the `installIntegration` function in `apps/web/lib/integrations/install.ts` uses a Prisma upsert where the `update` path only persists `credentials` but NOT `settings`. The `settings` field is only written on the `create` path. For the Stripe integration, this does not cause issues because settings are managed via the PATCH `/api/stripe/integration` route directly. But if any other integration adopts the `settings` parameter via `installIntegration` and expects it to be updated on re-install, the upsert update path must also include `settings`.

Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

@steven-tey
Copy link
Copy Markdown
Collaborator Author

@coderabbitai the PR is fully backwards compatible right?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

Tip

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

@steven-tey Yes, the PR is fully backwards compatible across all key dimensions:

Dimension Status
Database ✅ No migration needed — stripeMode defaults to "live", so all existing InstalledIntegration records parse correctly without changes
Public API surface ✅ No changes to any public endpoint signatures or response shapes
Internal function changes ✅ All three updated signatures (createDiscountCode, createStripeDiscountCode, disableStripeDiscountCode) are internal — every call site was updated in this PR
Stripe App package AppSettings.tsx was updated to pass the new stripeMode argument to updateWorkspace
Existing integrations ✅ Existing workspaces with Stripe connected will use "live" mode (matching prior behavior when VERCEL_ENV === "production")

One minor behavioral note: several routes now return bad_request if no InstalledIntegration record exists for the workspace. In practice this state (having stripeConnectId without an InstalledIntegration record) should be unreachable in a properly configured workspace, so it's not a real regression.


🧠 Learnings used
Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/lib/stripe/disable-stripe-discount-code.ts:2-2
Timestamp: 2026-04-01T17:57:42.171Z
Learning: In dubinc/dub, there is a known StripeMode type divergence between packages: `apps/web/lib/types.ts` defines `StripeMode = "test" | "sandbox" | "live"`, while `packages/stripe-app/src/utils/types.ts` defines `StripeMode = "test" | "live"` (missing "sandbox"). This is intentional for now; the Stripe app package needs to be updated separately to support "sandbox" mode. Do not flag this divergence as a blocking issue in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/app/(ee)/api/discount-codes/route.ts:69-90
Timestamp: 2026-04-01T17:57:13.716Z
Learning: In the dubinc/dub codebase, a workspace can only ever have one installed Stripe integration at a time. Therefore, `prisma.installedIntegration.findFirst({ where: { projectId, integrationId: STRIPE_INTEGRATION_ID } })` is always deterministic and there is no risk of returning the wrong row. Do not flag this pattern as nondeterministic in future reviews.

Learnt from: steven-tey
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2026-04-01T18:57:38.990Z
Learning: In dubinc/dub, the `installIntegration` function in `apps/web/lib/integrations/install.ts` uses a Prisma upsert where the `update` path only persists `credentials` but NOT `settings`. The `settings` field is only written on the `create` path. For the Stripe integration, this does not cause issues because settings are managed via the PATCH `/api/stripe/integration` route directly. But if any other integration adopts the `settings` parameter via `installIntegration` and expects it to be updated on re-install, the upsert update path must also include `settings`.

Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

@steven-tey steven-tey merged commit 5640668 into main Apr 1, 2026
9 of 11 checks passed
@steven-tey steven-tey deleted the stripe-integration-mode branch April 1, 2026 19:42
@coderabbitai coderabbitai Bot mentioned this pull request Apr 27, 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