Files
twenty/packages
Sonarly Claude Code 014c8f1af8 fix(billing): skip billingPortalSession query when subscription is canceled
https://sonarly.com/issue/36227?type=bug

When a workspace's only billing subscriptions are in `Canceled` state, the frontend still fires the `BillingPortalSession` GraphQL query, causing the backend to throw `Error: missing subscription` and breaking the `/settings/billing` page.

Fix: ## Fix 1: Frontend — correct the `skip` guard (`SettingsBillingContent.tsx`)

The `BillingPortalSession` GraphQL query was guarded by `skip: !hasSubscriptions`, where `hasSubscriptions` is `true` whenever `billingSubscriptions.length > 0` — including when every subscription is `Canceled`. The backend's `computeBillingPortalSessionURLOrThrow` queries with `status: Not(SubscriptionStatus.Canceled)`, so it returns nothing and throws for workspaces whose only subscriptions are canceled.

The correct guard — `hasNotCanceledCurrentSubscription` — was already computed two lines above but was only used for UI rendering. The fix changes the `skip` condition to use this properly-filtered variable:

```diff
-    skip: !hasSubscriptions,
+    skip: !hasNotCanceledCurrentSubscription,
```

`hasSubscriptions` was only used for this `skip` prop, so it is removed entirely.

## Fix 2: Backend — replace raw `Error` throws with `BillingException` (`billing-portal.workspace-service.ts`)

The two `throw new Error(...)` calls in `computeBillingPortalSessionURLOrThrow` bypassed the billing exception infrastructure. Because they are plain `Error` instances (not `HttpException` or `BaseGraphQLError`), `shouldCaptureException` returns `true` for them and they are captured by Sentry as unexpected crashes, even though "no active subscription" is an expected business state for a canceled workspace.

Replacing them with `BillingException` using the correct codes (`BILLING_ACTIVE_SUBSCRIPTION_NOT_FOUND` → HTTP 404, `BILLING_CUSTOMER_NOT_FOUND` → HTTP 404) means the already-registered global `BillingGraphqlApiExceptionFilter` converts them to `NotFoundError` (`ErrorCode.NOT_FOUND`), which is in `graphQLErrorCodesToFilter`, so `shouldCaptureException` correctly returns `false`. No more Sentry noise for this expected state.
2026-05-08 16:43:19 +00:00
..
2026-05-04 11:09:34 +02:00