feat: Next generation Stripe billing integration#2161
feat: Next generation Stripe billing integration#2161
Conversation
- Upgrade Stripe.net to v50.4.1 and update the backend to support modern PaymentMethods while maintaining legacy token compatibility. - Implement a new billing feature in the Svelte UI with lazy-loaded Stripe integration and a functional plan change dialog. - Add TanStack Query hooks for fetching available plans and processing plan changes with coupon support.
985add7 to
ee79cb9
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a “next generation” Stripe billing integration spanning backend Stripe.net v50 updates, new billing endpoints/DTO mapping, and a new Svelte 5 billing UI (Stripe provider + change-plan dialog) wired into organization/project usage and billing pages.
Changes:
- Upgrades Stripe.net usage on the backend (invoice status handling, discounts, subscription updates, PaymentMethod support).
- Adds a new Svelte billing feature module (
StripeProvider,ChangePlanDialog) and hooks it into usage/billing routes. - Extends org client API with plan query + change-plan mutation; updates invoice mapping/tests accordingly.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs | Updates Stripe invoice test data to use Status instead of Paid. |
| tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs | Simplifies test comments (no functional change). |
| src/Exceptionless.Web/Mapping/InvoiceMapper.cs | Maps “paid” via Invoice.Status string comparison. |
| src/Exceptionless.Web/Controllers/OrganizationController.cs | Updates Stripe invoice retrieval/discount handling and modernizes change-plan flow to support PaymentMethods + Stripe.net 50 API changes. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte | “Change plan” now navigates to the org billing page. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte | “Change plan” now navigates to the org billing page. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte | Uses new billing module ChangePlanDialog and passes loaded org data into it. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte | Removes the legacy placeholder change-plan dialog component. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts | Adds getPlansQuery() and changePlanMutation() + related query keys/types. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts | Adds Stripe context + lazy singleton loader utilities. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts | Adds zod schema/types for the change-plan form. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts | Adds billing model re-exports + local billing form/params types. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts | Barrel exports for billing feature module. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts | Defines FREE_PLAN_ID. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte | Adds Stripe Elements provider wrapper with loading/error states. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte | Implements the plan change dialog UI including Stripe payment collection. |
| src/Exceptionless.Web/ClientApp/package.json | Adds Stripe JS + svelte-stripe dependencies. |
| src/Exceptionless.Web/ClientApp/package-lock.json | Locks Stripe JS + svelte-stripe dependencies. |
| src/Exceptionless.Web/ClientApp/STRIPE-INTEGRATION-PLAN.md | Adds the Stripe integration plan document. |
| src/Exceptionless.Core/Exceptionless.Core.csproj | Upgrades Stripe.net to 50.4.1. |
| .claude/agents/engineer.md | Adds guidance for rerunning flaky CI jobs via gh run rerun. |
Files not reviewed (1)
- src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
This PR introduces a “next generation” Stripe billing integration across the backend (Stripe.net 50.x) and the Svelte client, enabling plan selection/changes and invoice display with the updated Stripe API surface.
Changes:
- Upgrades Stripe.net to 50.4.1 and updates invoice/plan-change logic to use
Status,Price,Discounts, andPaymentMethodAPIs. - Adds a new client-side billing feature module (Stripe context/provider + change plan dialog) and wires it into usage/billing pages.
- Adds client API helpers for fetching plans and performing plan changes, with cache invalidation.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs | Updates invoice mapping tests to reflect Stripe Status replacing Paid. |
| tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs | Simplifies test comments (no functional change). |
| src/Exceptionless.Web/Mapping/InvoiceMapper.cs | Switches paid logic to derive from Invoice.Status. |
| src/Exceptionless.Web/Controllers/OrganizationController.cs | Updates invoice retrieval and plan change flows for Stripe.net 50.x (Price/Discounts/PaymentMethod changes). |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte | Replaces placeholder “change plan” handler with navigation to billing page. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte | Same as above for org usage page. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte | Switches to the new billing module’s ChangePlanDialog and passes organization data. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte | Removes the old placeholder change-plan dialog component. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts | Adds getPlansQuery + changePlanMutation and associated query keys/types. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts | Adds Stripe context utilities and a singleton Stripe loader. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts | Adds Zod schema for the change-plan form. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts | Adds billing-focused types and re-exports generated API models. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts | Exposes billing module public API (components/hooks/constants/types). |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts | Introduces FREE_PLAN_ID. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte | Adds a Stripe <Elements> provider and sets Stripe context for children. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte | Implements the new plan change UX using Stripe PaymentElement + plans query + mutation. |
| src/Exceptionless.Web/ClientApp/package.json | Adds @stripe/stripe-js and svelte-stripe. |
| src/Exceptionless.Web/ClientApp/package-lock.json | Locks new Stripe dependencies. |
| src/Exceptionless.Core/Exceptionless.Core.csproj | Upgrades Stripe.net from 47.4.0 to 50.4.1. |
Files not reviewed (1)
- src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new Stripe-based billing UX in the Svelte app and updates the backend Stripe integration to newer Stripe.net APIs, alongside some infra/telemetry adjustments.
Changes:
- Adds new billing feature module on the frontend (Stripe provider + change-plan dialog) and wires it into usage/billing pages.
- Updates backend invoice/payment handling to Stripe.net 50.x patterns (invoice status, discounts, line item pricing/price lookup).
- Updates dependencies/config (Stripe.net upgrade, adds
@stripe/stripe-js+svelte-stripe, adjusts OpenTelemetry Prometheus wiring and deploy workflow conditions).
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs | Updates invoice mapping tests to use Status instead of Paid. |
| tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs | Simplifies test comments (no functional change). |
| src/Exceptionless.Web/Startup.cs | Removes Prometheus scraping endpoint middleware. |
| src/Exceptionless.Web/Mapping/InvoiceMapper.cs | Maps Paid from Invoice.Status == "paid". |
| src/Exceptionless.Web/Exceptionless.Web.csproj | Removes Prometheus exporter package reference; formatting changes. |
| src/Exceptionless.Web/Controllers/OrganizationController.cs | Updates invoice and plan-change flows for Stripe.net 50.x (prices/discounts/payment methods). |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte | Navigates to org billing page with changePlan=true. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte | Navigates to org billing page with changePlan=true. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte | Switches to new billing ChangePlanDialog and passes organization data. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte | Deletes placeholder dialog (replaced by billing feature). |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts | Adds plans query + change-plan mutation/query keys. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts | Adds Stripe context + lazy loader utilities. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts | Adds Zod schema for change-plan form. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts | Adds billing types and form state types. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts | Adds billing feature barrel exports. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts | Adds FREE_PLAN_ID constant. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte | Adds Stripe Elements provider component. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte | Adds new Stripe-backed change-plan dialog UI. |
| src/Exceptionless.Web/ClientApp/package.json | Adds Stripe JS dependencies. |
| src/Exceptionless.Web/ClientApp/package-lock.json | Locks Stripe JS dependencies. |
| src/Exceptionless.Web/ApmExtensions.cs | Removes Prometheus exporter from OpenTelemetry metrics pipeline. |
| src/Exceptionless.Job/Program.cs | Removes Prometheus scraping endpoint middleware from job host. |
| src/Exceptionless.Job/Exceptionless.Job.csproj | Removes Prometheus exporter package reference; formatting changes. |
| src/Exceptionless.Core/Exceptionless.Core.csproj | Upgrades Stripe.net from 47.4.0 to 50.4.1; formatting changes. |
| .vscode/settings.json | Updates workspace editor/TypeScript SDK settings. |
| .github/workflows/build.yaml | Updates deploy job condition to allow dev deploys from a configured PR branch. |
Files not reviewed (1)
- src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
This PR introduces a next-generation Stripe-based billing flow across the Svelte app and the organization API, updating both frontend plan-change UX and backend Stripe integration to newer Stripe.net APIs.
Changes:
- Adds a new Billing feature module (Stripe context/provider + ChangePlanDialog) and wires it into the organization billing page and usage pages.
- Updates backend Stripe integration (Stripe.net 50.x) for invoice/payment/plan handling and updates invoice mapping/tests accordingly.
- Updates build/ops tooling (Docker MinVer build arg, workflow tweaks) and removes Prometheus OpenTelemetry exporter/scraping.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs | Updates tests to reflect invoice Status -> paid mapping. |
| tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs | Simplifies test comments (no behavior change). |
| src/Exceptionless.Web/Startup.cs | Removes OTEL Prometheus scraping endpoint middleware. |
| src/Exceptionless.Web/Mapping/InvoiceMapper.cs | Maps invoice “paid” from Stripe Status instead of Paid. |
| src/Exceptionless.Web/Exceptionless.Web.csproj | Removes Prometheus exporter package + formatting changes. |
| src/Exceptionless.Web/Controllers/OrganizationController.cs | Updates Stripe invoice/plan change logic for Stripe.net 50.x and PaymentMethod support. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte | Implements navigation to billing page on “Change plan”. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte | Implements navigation to billing page on “Change plan”. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte | Switches to new billing ChangePlanDialog component and passes organization data. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte | Removes placeholder dialog from organizations feature. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts | Adds plans query + change-plan mutation and query keys. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts | Adds Stripe lazy-loader + context hooks. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts | Adds Zod schema for change-plan form. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts | Adds billing feature types and re-exports generated API types. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts | Public entrypoint for billing feature exports. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts | Adds FREE plan id constant. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte | Adds provider wrapping Stripe Elements + loading/error UI. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte | Adds full Stripe Elements-based plan change dialog. |
| src/Exceptionless.Web/ClientApp/package.json | Adds Stripe JS dependencies. |
| src/Exceptionless.Web/ClientApp/package-lock.json | Locks Stripe JS dependencies. |
| src/Exceptionless.Web/ApmExtensions.cs | Removes Prometheus exporter wiring. |
| src/Exceptionless.Job/Program.cs | Removes OTEL Prometheus scraping endpoint middleware. |
| src/Exceptionless.Job/Exceptionless.Job.csproj | Removes Prometheus exporter package + formatting changes. |
| src/Exceptionless.Core/Exceptionless.Core.csproj | Upgrades Stripe.net to 50.4.1. |
| Dockerfile | Adds MinVerVersionOverride build arg passthrough. |
| .vscode/settings.json | Disables format-on-save and changes TypeScript SDK path setting key. |
| .github/workflows/build.yaml | Ensures checkout uses correct ref and passes MinVer build arg into Docker builds; expands deploy conditions. |
Files not reviewed (1)
- src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Stripe v9 requires either clientSecret OR mode when calling stripe.elements(). The previous code passed neither when no clientSecret was provided, causing PaymentElement to fail to render. Uses a discriminated union Props type to enforce mutual exclusivity of clientSecret and mode at compile time, matching Stripe's own type constraints. Defaults to mode='setup' for collecting payment methods for future use.
Fix two issues preventing the Stripe PaymentElement from rendering in the Change Plan dialog: 1. Missing currency in Stripe Elements options: Stripe.js v9+ requires a currency string when using mode: 'setup'. Added currency: 'usd' to the elements creation options. 2. svelte-stripe Svelte 5 incompatibility: svelte-stripe's <Elements> and <PaymentElement> components use $bindable()/onMount patterns that don't trigger Svelte 5 template re-renders from async callbacks. Replaced both components with an imperative approach that loads Stripe, creates elements, and mounts the PaymentElement directly to the DOM via onMount, bypassing Svelte's reactive template system entirely. Additionally fixed the Change Plan dialog not being scrollable by adding max-h-[90vh] and overflow-y-auto to the Dialog.Content wrapper. Changes: - stripe-provider.svelte: Rewritten to imperatively mount PaymentElement without svelte-stripe components or Svelte reactive conditionals - change-plan-dialog.svelte: Removed svelte-stripe PaymentElement import, updated StripeProvider usage (no longer takes children), added dialog scroll constraints, removed debug markup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Upgrade @stripe/stripe-js to v9.2.0. - Update OrganizationController to use PriceId instead of Price, following changes in Stripe.net 51.x where Price became an expandable field. - Convert errorMessage to a Svelte 5 $state rune in stripe-provider.svelte to ensure reactivity.
Replace generic toast errors with proper upgrade confirmation dialog matching the legacy Angular UI behavior: - Modal shows backend ProblemDetails message (same text as legacy) - 'Upgrade Plan' button navigates to billing page with ?changePlan=true - 'Cancel' button dismisses without navigation - Dialog rendered globally in app layout, triggered from any page Fixed pages: - Add Project: was showing generic error, now shows upgrade dialog - Event Detail: was a TODO comment, now properly handles 426 - Stack Promote: was showing hardcoded message, now uses ProblemDetails - Users Invite: was missing 426 handling entirely - Add Organization: was missing 426 handling entirely - Integrations (webhook + Slack): was missing 426 handling entirely Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e native <select> with onchange handler instead of bits-ui Select to\navoid portal/focus-trap conflicts inside Dialog. Wrap form mutations in\nuntrack() to prevent reactive cycles between $effect init and form state.\nClean up label layout with separate subtitle line.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>"
…maries - Implements a more modern UI with a monthly/yearly billing toggle. - Automatically groups plans by tier and provides automatic yearly savings labels. - Adds a dynamic change summary that displays prorated details and plan transitions. - Integrates coupon management directly into the subscription workflow. - Improves responsive layout with a scrollable content area and clearer typography.
Two bugs in OrganizationController prevented plan changes:
1. GetPlansAsync() mutated the singleton BillingPlans list directly
instead of cloning it first. When a global admin loaded plans, the
org's billing data (e.g., price=0) permanently corrupted the
singleton, causing wrong prices for all subsequent requests.
Fix: always .ToList() before modifying.
2. ChangePlanAsync had [Consumes("application/json")] but the frontend
sends plan parameters as query strings with no request body, so
ASP.NET Core rejected the request with a 404.
Fix: remove the Consumes attribute.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Implements a Storybook harness and stories to test various billing scenarios, including upgrades, downgrades, and coupon applications. - Configures the Stripe provider for manual payment method creation to support the deferred-intent flow. - Refines the tier selection logic and plan transition summaries in the dialog. - Adds error reporting via Exceptionless for plan change failures.
- Replace custom billing interval buttons with Tabs component. - Standardize plan selection buttons and status labels using Button and Badge components. - Clean up unnecessary divider comments in Storybook file.
- Introduces a "Stop" hook that blocks termination unless explicitly allowed. - Ensures that the agent provides a summary of completed and pending items and prompts for follow-up questions before stopping.
- Add `initialCouponCode`, `initialCouponOpen`, and `initialFormError` props to allow pre-configuring the dialog state. - Implement new Storybook stories covering coupon application, invalid coupons, payment failures, and blocked downgrades. - Standardize the applied coupon display using the Alert component. - Improve layout padding in the dialog content area for better alignment.
- Format prices with thousands separators and fix pluralization for user counts. - Adjust "Most popular" badge logic to only show for plan upgrades. - Explicitly render initial form errors using the ErrorMessage component instead of setting form state.
- Remove price === 0 exclusion from shouldIncludeInTiers; the FREE_PLAN_ID guard already handles the free plan; excluding on price breaks orgs with admin-negotiated \/mo plans (e.g. EX_SMALL with billing_price=0) - Guard yearly savings % calculation against division-by-zero when a tier has a monthly price of \ (NaN/Infinity was suppressing the Save badge) - Add ErrorPlanChangeFailed Storybook story (15th story) - Add Stripe test publishable key to Angular app config
The exception object is already passed as the first parameter to LogError/LogWarning which captures the full exception including message. Adding ex.Message as a template parameter causes it to appear twice in structured log output.
Covers architecture, API endpoints, plan structure, frontend components, Stripe SDK v47→v51 migration notes, known limitations, Storybook stories, and security considerations.
White text on lime green primary (hsl(96 64% 46%)) achieved only 2.30:1 contrast ratio, failing WCAG 2.2 AA minimum (4.5:1 for normal text). Fixed by setting --primary-foreground to dark text (hsl(0 0% 10%)) in both light and dark modes, yielding 8.15:1 contrast ratio. Also removes unused svelte-stripe package — the billing dialog uses @stripe/stripe-js directly via the imperative Elements API.
- Accept change-plan params from JSON body (new Svelte client) with query string fallback for backwards compat with legacy Angular client. Stripe PaymentMethod sent in body avoids exposure in server/proxy logs. - Apply coupon to existing-customer subscription update/create paths (was previously only applied to new-customer path) - Narrow catch(Exception) to catch(StripeException) in price fetch loop so programming errors propagate rather than being silently swallowed - InvoiceMapper: use OrdinalIgnoreCase for Stripe status comparison - stripe.svelte.ts: reset cached promise on loadStripeOnce failure so callers can retry without a full page refresh - api.svelte.ts: import ChangePlanParams from billing/models.ts instead of redefining the same shape (removes duplication) - .vscode/settings.json: fix TypeScript SDK key (js/ts.tsdk.path -> typescript.tsdk)
…tails, regenerate client types
- Remove [FromQuery]/[FromBody] dual binding from ChangePlanAsync; endpoint now accepts only a JSON body
- Add [Required] to PlanId on ChangePlanRequest; [ApiController] now returns 400 ProblemDetails automatically for missing plan_id
- Remove stale XML comments about Angular/Svelte dual-binding compat
- Fix logger message template: {ErrorMessage} -> {Message}
- Update Angular org service to POST JSON body instead of query params
- Regenerate ChangePlanRequest from OpenAPI spec (plan_id: string required); remove manual ChangePlanParams
- Handle ProblemDetails in change-plan-dialog catch block
- Fix catch (err) -> catch (error: unknown) in stripe.svelte.ts
- Update organizations.http test to use JSON body
…uard in change-plan dialog
Billing disabled and invalid org now return 404 instead of 200+fail. Invalid plan ID returns 422 ValidationProblem. Business logic failures (downgrade constraints, missing billing info, Stripe errors) remain as 200+ChangePlanResult for backwards compatibility with both frontends.
…ions - Default plan upgrades to yearly billing to promote savings. - Improve coupon error handling by auto-focusing the input and showing inline errors on failure. - Add toast notifications for plan change success and failure states. - Parallelize Stripe customer update and subscription listing tasks in the backend. - Transition dialog visibility to use an `onclose` callback instead of `bind:open` for more predictable state management.
- Automatically scroll the coupon section into view and focus the input when opened or when an error occurs. - Scroll to the payment section when the user chooses to use a different card. - Use smooth scrolling to ensure context is maintained during navigation within the dialog.
There was a problem hiding this comment.
Pull request overview
This PR introduces a next-generation Stripe billing integration across the ASP.NET Core API and the Svelte 5 “next” UI, including a Stripe.net SDK upgrade and new billing UX flows (change plan, invoices, upgrade-required handling).
Changes:
- Upgrade backend Stripe integration to Stripe.net v51 and adjust invoice/plan-change logic accordingly.
- Add Svelte billing feature module (StripeProvider, ChangePlanDialog, UpgradeRequiredDialog) and wire it into the app.
- Update OpenAPI + generated client types/schemas and refresh related tests/http examples.
Reviewed changes
Copilot reviewed 40 out of 46 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/http/organizations.http | Updates example requests for change-plan (JSON body + legacy query). |
| tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs | Updates invoice mapping tests to use Stripe Status instead of Paid. |
| tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs | Simplifies test comments (no behavior change). |
| tests/Exceptionless.Tests/Controllers/Data/openapi.json | Updates OpenAPI snapshot (server URL + change-plan request body + schema additions). |
| src/Exceptionless.Web/Program.cs | Adds support for optional appsettings.Local.yml. |
| src/Exceptionless.Web/Models/ChangePlanRequest.cs | Adds request model for JSON-body plan changes. |
| src/Exceptionless.Web/Mapping/InvoiceMapper.cs | Maps invoice “paid” state based on Stripe Status. |
| src/Exceptionless.Web/Exceptionless.Web.csproj | Formatting-only change. |
| src/Exceptionless.Web/Controllers/OrganizationController.cs | Implements JSON-body plan changes, Stripe.net v51 invoice updates, plan list behavior changes. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/add/+page.svelte | Adds upgrade-required handling during project creation errors. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte | Implements “Change plan” navigation. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/integrations/+page.svelte | Adds upgrade-required handling for integrations mutations. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/add/+page.svelte | Adds upgrade-required handling during org creation errors. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/users/+page.svelte | Adds upgrade-required handling when inviting users. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte | Implements “Change plan” navigation. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte | Switches billing page to new ChangePlanDialog and dialog-open state handling. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/event/[eventId]/+page.svelte | Uses shared upgrade-required handling on 426. |
| src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte | Mounts UpgradeRequiredDialog globally. |
| src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts | Adds ChangePlanRequest schema; adjusts multiple generated schemas. |
| src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts | Adds ChangePlanRequest type; updates NewToken + PersistentEvent typings. |
| src/Exceptionless.Web/ClientApp/src/lib/features/stacks/components/stack-options-dropdown-menu.svelte | Routes 426 “upgrade required” to shared handler. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte | Removes legacy placeholder change-plan dialog. |
| src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts | Adds change-plan mutation + plans query. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/upgrade-required.svelte.ts | Adds shared upgrade-required state + helpers. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts | Adds Stripe context helpers and singleton Stripe loader. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts | Adds UI form schema for change-plan dialog. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts | Adds billing model re-exports + UI types. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts | Public barrel exports for billing feature module. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts | Adds FREE_PLAN_ID constant. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/upgrade-required-dialog.svelte | Adds the upgrade-required confirmation dialog UI. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte | Adds imperative Stripe PaymentElement mounting/provider. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte | Adds full plan change modal UI and Stripe payment method handling. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.stories.ts | Adds Storybook coverage for billing dialog states. |
| src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog-harness.svelte | Adds Storybook harness wiring for queries/auth. |
| src/Exceptionless.Web/ClientApp/src/app.css | Adjusts --primary-foreground theme variables. |
| src/Exceptionless.Web/ClientApp/package.json | Adds @stripe/stripe-js dependency. |
| src/Exceptionless.Web/ClientApp/package-lock.json | Locks @stripe/stripe-js dependency. |
| src/Exceptionless.Web/ClientApp/.storybook/mocks/env.js | Adds mock Stripe publishable key for Storybook. |
| src/Exceptionless.Web/ClientApp.angular/components/organization/organization-service.js | Updates legacy Angular change-plan call to send JSON body. |
| src/Exceptionless.Job/Exceptionless.Job.csproj | Formatting-only change. |
| src/Exceptionless.Core/Exceptionless.Core.csproj | Upgrades Stripe.net package to v51. |
| docs/billing-stripe-integration.md | Adds Stripe/billing integration documentation. |
| .vscode/settings.json | Updates VS Code TypeScript SDK setting name. |
| .vscode/launch.json | Modifies Aspire launch configuration. |
| .github/hooks/follow-up.json | Adds GitHub hook configuration. |
| .aspire/settings.json | Formatting-only change. |
Files not reviewed (1)
- src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .AddYamlFile("appsettings.yml", optional: true, reloadOnChange: true) | ||
| .AddYamlFile($"appsettings.{environment}.yml", optional: true, reloadOnChange: true) | ||
| .AddYamlFile($"appsettings.Local.yml", optional: true, reloadOnChange: true) | ||
| .AddCustomEnvironmentVariables() |
| if (response.status === 426) { | ||
| toast.error( | ||
| 'Promote to External is a premium feature used to promote an error stack to an external system. Please upgrade your plan to enable this feature.' | ||
| handleUpgradeRequired( | ||
| { status: 426, title: 'Promote to External is a premium feature. Please upgrade your plan to enable this feature.' } as never, | ||
| stack.organization_id | ||
| ); |
| async function handleChangePlan() { | ||
| await goto(`/next/organization/${page.params.organizationId}/billing?changePlan=true`); | ||
| } |
| **Props**: | ||
|
|
||
| - `open: boolean` — bindable dialog visibility | ||
| - `organization: ViewOrganization` — current org data | ||
| - `initialCouponCode?: string` — pre-fill coupon | ||
| - `initialCouponOpen?: boolean` — open coupon input on mount | ||
| - `initialFormError?: string` — show error message on mount | ||
|
|
| async function handleChangePlan() { | ||
| if (organization.current) { | ||
| await goto(`/next/organization/${organization.current}/billing?changePlan=true`); | ||
| } |
| **Parameters** (query string): | ||
|
|
||
| | Param | Type | Description | | ||
| | --- | --- | --- | | ||
| | `planId` | string | Target plan ID (e.g., `EX_MEDIUM`, `EX_LARGE_YEARLY`) | | ||
| | `stripeToken` | string? | `pm_` PaymentMethod ID (Svelte) or `tok_` token (Angular) | | ||
| | `last4` | string? | Last 4 digits of card (display only) | | ||
| | `couponId` | string? | Stripe coupon code | | ||
|
|
| 1. **Coupon not applied for existing customers changing plans** — Pre-existing limitation (not a regression). The coupon is only applied when creating a new Stripe customer. | ||
| 2. **Potential orphaned Stripe customers** — If subscription creation fails after customer creation, a retry would create a duplicate Stripe customer. Mitigated by the low likelihood of this failure path. | ||
| 3. **N+1 price fetches in invoice view** — Each unique price ID in an invoice makes a separate Stripe API call. Mitigated by a per-request cache (`priceCache`). Most invoices have 1-3 distinct prices. | ||
| 4. **svelte-stripe package unused** — Listed in `package.json` but bypassed due to Svelte 5 incompatibility. Only `@stripe/stripe-js` is used directly. |
| "type": "aspire", | ||
| "request": "launch", | ||
| "name": "Aspire", | ||
| "program": "${workspaceFolder}/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj" | ||
| "program": "" | ||
| }, |
| var plan = _billingManager.GetBillingPlan(model.PlanId); | ||
| if (plan is null) | ||
| return Ok(ChangePlanResult.FailWithMessage("Invalid PlanId.")); | ||
| { | ||
| ModelState.AddModelError(nameof(model.PlanId), "Invalid PlanId."); | ||
| return ValidationProblem(ModelState); | ||
| } |
Summary
ChangePlanDialogandStripeProvidergetBilling,changePlan,getInvoicesChanges