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.