Skip to content

inline product cancelling#1199

Merged
BilalG1 merged 4 commits into
devfrom
inline-product-cancelling
Feb 18, 2026
Merged

inline product cancelling#1199
BilalG1 merged 4 commits into
devfrom
inline-product-cancelling

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Subscription IDs are now included in product listings and UI data for clearer subscription tracking.
    • Cancellation can be performed by subscription ID as well as by product; client and template APIs support passing a subscription identifier.
  • Tests

    • End-to-end tests added/updated to cover canceling subscriptions via subscription ID and updated listing snapshots.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 16, 2026

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

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Feb 18, 2026 4:08am
stack-dashboard Ready Ready Preview, Comment Feb 18, 2026 4:08am
stack-demo Ready Ready Preview, Comment Feb 18, 2026 4:08am
stack-docs Ready Ready Preview, Comment Feb 18, 2026 4:08am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 16, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds cancel-by-subscription_id to the product cancellation endpoint, propagates subscription_id through backend responses, shared schemas, client interfaces, frontend UI, and e2e tests.

Changes

Cohort / File(s) Summary
Backend: cancel handler
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/[product_id]/route.ts
Handler now accepts query and supports cancel-by-subscription_id query param; retains product-based cancellation as fallback; updates subscription lookup, error messages, and unified subscription cancellation path (Stripe vs DB state).
Backend: subscription shape & payment utils
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/route.ts, apps/backend/src/lib/payments.tsx
OwnedProduct gains nested subscription with subscriptionId, currentPeriodEnd, cancelAtPeriodEnd, isCancelable; GET responses include subscription_id; isCancelable computation simplified.
Shared types & client interface
packages/stack-shared/src/interface/crud/products.ts, packages/stack-shared/src/interface/client-interface.ts
Added subscription_id to customer product read schema; cancelSubscription options accept optional subscription_id and will send it as query; small key rename in checkout payload (inline_productproduct_inline).
Frontend app types & impl
packages/template/src/lib/stack-app/customers/index.ts, packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts, packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
CustomerProduct subscription includes subscriptionId; cancelSubscription overloads extended to accept optional subscriptionId and wired through implementation to shared client interface.
Frontend UI
packages/template/src/components-page/account-settings/payments/payments-panel.tsx
Replaced cancelProductId state with cancelTarget ({ productId, subscriptionId? }); dialog and cancel flows pass optional subscriptionId to API; isCancelable check simplified and handlers reset target after action.
Tests
apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts
Snapshots updated to include subscription_id in subscription objects; new e2e test added to cancel inline-product subscription via subscription_id and verify listing state changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Backend
    participant StripeDB as Stripe/Database

    alt Cancel by subscription_id
        Client->>Backend: DELETE /products/[type]/[id]/[productId]?subscription_id=SUB_123
        Backend->>StripeDB: Lookup active/trialing subscription by subscription_id, tenant, customer_type, customer_id
        alt Found
            Backend->>StripeDB: Cancel via Stripe (if stripeSubscriptionId) or mark canceled in DB
            StripeDB-->>Backend: Success
            Backend-->>Client: 204 No Content
        else Not found
            Backend-->>Client: 404 No active subscription found with this ID
        end
    else Cancel by product_id
        Client->>Backend: DELETE /products/[type]/[id]/[productId]
        Backend->>StripeDB: Validate product ownership and fetch active subscriptions for product/customer
        alt Found & cancelable
            Backend->>StripeDB: Cancel via Stripe or update DB
            StripeDB-->>Backend: Success
            Backend-->>Client: 204 No Content
        else Not found / not cancelable
            Backend-->>Client: 404 No active subscription found
        end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • payouts tab #1065: Modifies the same backend DELETE cancel route and likely intersects on cancel-by-subscription_id changes.
  • upgrade/downgrade plans #1087: Touches the products route and subscription metadata enrichment that overlaps with subscription_id propagation.
  • Payments UX update #863: Updates payments subscription shapes and API surfaces that intersect with this PR's shared types and cancellation flow.

Poem

🐰 I nibble at code and gnaw at a bug,
A tiny id now leads to a hug,
Cancel paths fork, tidy and quick,
From client to stripe — one clever trick,
Hooray, the rabbit hops — release the tug!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is empty except for the template comment; no functional details, rationale, or implementation context are provided. Add a detailed description explaining the feature, including the motivation, approach, how subscription_id enables cancellation, and any breaking changes or migration notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'inline product cancelling' clearly refers to the main feature added: enabling cancellation of inline product subscriptions via a new subscription_id parameter.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch inline-product-cancelling

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 16, 2026

Greptile Summary

This PR enables cancellation of inline product subscriptions — subscriptions that were granted via product_inline rather than a configured product_id. Previously, these subscriptions could not be canceled because they lacked a productId to identify them.

Key changes:

  • Exposes subscription_id (the internal DB subscription ID) in the customer products API response and passes it through the full stack (API → shared schema → client interface → UI)
  • Adds a subscription_id query parameter to the DELETE endpoint so inline products can be canceled by their subscription DB ID instead of by product ID
  • Changes isCancelable logic from subscription.id !== null && subscription.productId !== null to subscription.id !== null, so inline products (which have productId: null) are now cancelable
  • Removes the !!product.id guard in the payments panel UI so the "Cancel subscription" button appears for inline products
  • Includes a bug fix: corrects inline_productproduct_inline in createCheckoutUrl in the client interface, which would have caused inline product checkout to fail
  • Authorization is properly maintained — the subscription_id path is scoped to the authenticated customer via both the auth checks and the database query filtering by customerId
  • E2E test added for the new inline cancellation flow; existing snapshots updated

Confidence Score: 4/5

  • This PR is safe to merge — authorization is properly enforced and the changes are well-tested.
  • The core logic is sound: subscription_id lookup is properly scoped by tenancyId + customerId + customerType, authorization checks run before any database access, and a new E2E test covers the inline cancellation flow. The inline_product → product_inline fix addresses a real bug. Score is 4 instead of 5 due to minor concerns: no UUID validation on the new subscription_id parameter, and a theoretical edge case where a null subscriptionId on an inline product could produce a confusing error.
  • Pay attention to apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/[product_id]/route.ts (new subscription_id query parameter) and packages/template/src/components-page/account-settings/payments/payments-panel.tsx (cancel target logic change).

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/[product_id]/route.ts Adds subscription_id query parameter to DELETE handler for canceling inline product subscriptions by DB ID. Authorization checks are properly preserved. When subscription_id is provided, the lookup skips product validation and queries directly by subscription ID scoped to tenancy + customer.
apps/backend/src/lib/payments.tsx Adds subscriptionId to OwnedProduct.subscription type and changes isCancelable from subscription.id !== null && subscription.productId !== null to subscription.id !== null, enabling inline products to be cancelable.
apps/e2e/tests/backend/endpoints/api/v1/payments/products.test.ts Adds test for canceling inline product subscriptions via subscription_id. Updates existing snapshots with new subscription_id field and corrected is_cancelable: true for inline products.
packages/stack-shared/src/interface/client-interface.ts Adds subscription_id optional parameter to cancelSubscription. Also fixes a bug where inline_product was used instead of the correct product_inline key in createCheckoutUrl.
packages/template/src/components-page/account-settings/payments/payments-panel.tsx Replaces cancelProductId state with cancelTarget object carrying both productId and subscriptionId. Removes the !!product.id check from isCancelable, allowing inline products to show the cancel button.

Sequence Diagram

sequenceDiagram
    participant UI as PaymentsPanel (UI)
    participant SDK as StackClientApp
    participant API as DELETE /payments/products/:type/:id/:productId
    participant DB as Prisma (Subscription)

    UI->>UI: User clicks "Cancel subscription"
    UI->>SDK: cancelSubscription({ productId: "_inline", subscriptionId })
    SDK->>API: DELETE .../_inline?subscription_id=<uuid>

    alt subscription_id provided (inline product)
        API->>DB: findMany({ id: subscription_id, customerId, customerType })
        DB-->>API: matching subscriptions
    else no subscription_id (configured product)
        API->>DB: findMany({ productId, customerId, customerType })
        DB-->>API: matching subscriptions
    end

    alt has Stripe subscription
        API->>API: stripe.subscriptions.cancel()
    else local-only subscription
        API->>DB: update status → canceled
    end

    API-->>SDK: { success: true }
    SDK-->>UI: void (cache invalidated)
Loading

Last reviewed commit: 63c1391

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread packages/stack-shared/src/interface/client-interface.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.

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In
`@apps/backend/src/app/api/latest/payments/products/`[customer_type]/[customer_id]/[product_id]/route.ts:
- Around line 66-80: The route currently ignores params.product_id when
query.subscription_id is present; after the prisma.subscription.findMany call
(and after confirming subscriptions.length > 0) validate that the
subscription(s) belong to the product in params.product_id: for each
subscription returned by prisma.subscription.findMany ensure
subscription.productId (or subscription.product_id field) matches
params.product_id (when params.product_id is provided/known) and throw a
StatusError(400, "...") if not; alternatively, if you prefer to keep backward
compatibility, add a clear comment/route metadata noting that
query.subscription_id takes precedence and params.product_id is ignored. Use the
existing symbols query.subscription_id, params.product_id,
prisma.subscription.findMany and StatusError to locate where to add the check or
the documentation.
🧹 Nitpick comments (1)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In
`@apps/backend/src/app/api/latest/payments/products/`[customer_type]/[customer_id]/[product_id]/route.ts:
- Around line 66-80: The route currently ignores params.product_id when
query.subscription_id is present; after the prisma.subscription.findMany call
(and after confirming subscriptions.length > 0) validate that the
subscription(s) belong to the product in params.product_id: for each
subscription returned by prisma.subscription.findMany ensure
subscription.productId (or subscription.product_id field) matches
params.product_id (when params.product_id is provided/known) and throw a
StatusError(400, "...") if not; alternatively, if you prefer to keep backward
compatibility, add a clear comment/route metadata noting that
query.subscription_id takes precedence and params.product_id is ignored. Use the
existing symbols query.subscription_id, params.product_id,
prisma.subscription.findMany and StatusError to locate where to add the check or
the documentation.
apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/[product_id]/route.ts (1)

66-80: product_id path parameter is silently ignored when subscription_id is provided.

When query.subscription_id is set, the product_id route parameter is never validated or used. A caller can pass an arbitrary (or non-existent) product_id in the URL and still cancel any of their subscriptions by subscription_id. This isn't a security issue (tenancy + customer checks still apply), but it's semantically misleading for a route whose path includes [product_id].

Consider either:

  • Validating that the resolved subscription's product matches params.product_id (if known), or
  • Documenting clearly in the route metadata that product_id is ignored when subscription_id is present.
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In
`@apps/backend/src/app/api/latest/payments/products/`[customer_type]/[customer_id]/[product_id]/route.ts
around lines 66 - 80, The route currently ignores params.product_id when
query.subscription_id is present; after the prisma.subscription.findMany call
(and after confirming subscriptions.length > 0) validate that the
subscription(s) belong to the product in params.product_id: for each
subscription returned by prisma.subscription.findMany ensure
subscription.productId (or subscription.product_id field) matches
params.product_id (when params.product_id is provided/known) and throw a
StatusError(400, "...") if not; alternatively, if you prefer to keep backward
compatibility, add a clear comment/route metadata noting that
query.subscription_id takes precedence and params.product_id is ignored. Use the
existing symbols query.subscription_id, params.product_id,
prisma.subscription.findMany and StatusError to locate where to add the check or
the documentation.

@BilalG1 BilalG1 requested a review from N2D4 February 18, 2026 00:20
@github-actions github-actions Bot assigned BilalG1 and unassigned N2D4 Feb 18, 2026
@BilalG1 BilalG1 merged commit 003b693 into dev Feb 18, 2026
18 of 26 checks passed
@BilalG1 BilalG1 deleted the inline-product-cancelling branch February 18, 2026 04:28
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