Billing limits#3996
Conversation
Add the EnvironmentPauseSource enum and migration, plus the billing-limit platform client wrappers and schemas.
Configure a spend limit, manage billing alerts, and surface org-wide banners.
Converge billable environments to paused via webhook and a reconciliation worker; block manual resume.
Reject triggers with a 422 once entitlement reports no access, and bust the entitlement cache on state changes.
Recovery UI and durable resolve: cancel queued runs before unpausing, with reconciliation as a safety net.
Optionally cancel in-progress runs on limit hit via a deduplicated bulk-cancel job.
… tests Add the usage-bar marker, documentation, and test coverage.
Replace the invalid @trigger.dev/platform/v3 import so billing limit typecheck passes in CI. Co-authored-by: Cursor <cursoragent@cursor.com>
|
WalkthroughThis PR implements a billing limits feature that lets organizations configure a monthly compute spend cap. When the cap is reached, a billing platform webhook triggers a grace period during which billable environments are paused; new task triggers are rejected once the grace period ends. A recovery flow lets users increase or remove the limit and choose to resume queued runs or accept cancellation. A Redis-backed worker periodically reconciles environment pause state against billing platform data. The feature adds a new 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (4)
apps/webapp/app/env.server.ts (1)
1474-1474: ⚡ Quick winDefault to explicit opt-in ("0") for the new worker flag.
Per codebase conventions, new background worker feature flags should hard-default to
"0"rather than inheriting from a parent flag likeWORKER_ENABLED. The current default could cause the billing limit worker to auto-start unexpectedly on upgrade for deployments that already haveWORKER_ENABLEDset, introducing unplanned background load.Suggested fix
- BILLING_LIMIT_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + BILLING_LIMIT_WORKER_ENABLED: z.string().default("0"),Based on learnings: "In apps/*/app/env.server.ts, any new background/periodic worker feature flag should hard-default to '0' (explicit opt-in) rather than inheriting from a parent flag."
Source: Learnings
apps/webapp/app/v3/services/billingLimit/billingLimitReconciliation.server.ts (1)
67-75: ⚡ Quick winSequential billing-limit lookups will slow reconciliation as org count grows.
At Line 73 and Line 83,
getBillingLimit()is awaited serially in loops. Consider bounded parallelism (Promise.all+ limiter) to keep reconcile ticks timely.Also applies to: 77-85
apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx (1)
95-97: ⚡ Quick winAdd an inline note explaining why primary
prismais required here.These lookups correctly use
prisma.organization.findFirst(...); adding a one-line comment will reduce future regressions back to$replicain auth-scoped slug resolution paths.Based on learnings: slug→organization resolution for authorization scope should stay on primary Prisma because replica lag can lead to unscoped RBAC checks.
Also applies to: 189-191
Source: Learnings
apps/webapp/app/components/billing/selectOrgBanner.ts (1)
3-10: ⚡ Quick winReplace runtime enum with a string-union constant type.
Using
export enum OrgBannerKindhere conflicts with the repo TypeScript guideline and adds avoidable runtime enum output.♻️ Suggested change
-export enum OrgBannerKind { - LimitRejected = "limit-rejected", - LimitGrace = "limit-grace", - NoLimitConfigured = "no-limit", - Upgrade = "upgrade", - EnvironmentWarning = "env-warning", - None = "none", -} +export const ORG_BANNER_KIND = { + LimitRejected: "limit-rejected", + LimitGrace: "limit-grace", + NoLimitConfigured: "no-limit", + Upgrade: "upgrade", + EnvironmentWarning: "env-warning", + None: "none", +} as const; + +export type OrgBannerKind = (typeof ORG_BANNER_KIND)[keyof typeof ORG_BANNER_KIND];As per coding guidelines,
**/*.{ts,tsx}: “Use types over interfaces for TypeScript” and “Avoid using enums; prefer string unions or const objects instead.”Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c7c9a3c5-ee1c-4334-b40a-d0b9aca60dd8
📒 Files selected for processing (81)
.server-changes/billing-limits.mdapps/webapp/app/components/billing/AnimatedOrgBannerBar.tsxapps/webapp/app/components/billing/BillingAlertsSection.tsxapps/webapp/app/components/billing/BillingLimitConfigSection.tsxapps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsxapps/webapp/app/components/billing/BillingLimitResolveProgress.tsxapps/webapp/app/components/billing/OrgBanner.tsxapps/webapp/app/components/billing/UpgradePrompt.tsxapps/webapp/app/components/billing/billingAlertsFormat.tsapps/webapp/app/components/billing/billingLimitFormat.tsapps/webapp/app/components/billing/selectOrgBanner.tsapps/webapp/app/components/layout/AppLayout.tsxapps/webapp/app/components/navigation/EnvironmentBanner.tsxapps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsxapps/webapp/app/components/primitives/AnimatedCallout.tsxapps/webapp/app/components/primitives/PageHeader.tsxapps/webapp/app/entry.server.tsxapps/webapp/app/env.server.tsapps/webapp/app/hooks/useOrganizations.tsapps/webapp/app/hooks/useScrollContainerToTop.tsapps/webapp/app/models/organization.server.tsapps/webapp/app/presenters/OrganizationsPresenter.server.tsapps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-alerts/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/billingLimitsRevalidation.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/billingLimitsRoute.server.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsxapps/webapp/app/routes/admin.api.v1.orgs.$organizationId.billing-limit.hit.tsapps/webapp/app/routes/admin.api.v1.orgs.$organizationId.billing-limit.reject.tsapps/webapp/app/routes/admin.api.v1.orgs.$organizationId.billing-limit.resolve.tsapps/webapp/app/routes/storybook.callout/route.tsxapps/webapp/app/runEngine/validators/triggerTaskValidator.tsapps/webapp/app/runEngine/validators/validateProductionEntitlement.server.tsapps/webapp/app/services/billingLimit.schemas.tsapps/webapp/app/services/platform.v3.server.tsapps/webapp/app/services/upsertBranch.server.tsapps/webapp/app/utils/pathBuilder.tsapps/webapp/app/v3/billingLimitWorker.server.tsapps/webapp/app/v3/outOfEntitlementError.server.tsapps/webapp/app/v3/services/billingLimit/BillingLimitBulkCancelService.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitCancelInProgressRuns.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitConstants.tsapps/webapp/app/v3/services/billingLimit/billingLimitConvergeEnvironments.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitConvergeEnvironmentsService.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitConvergeResolve.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitHit.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitPendingResolve.types.tsapps/webapp/app/v3/services/billingLimit/billingLimitPendingResolveCoordinator.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitPendingResolveFailure.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitQueuedRuns.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitReconcileQueue.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitReconcileTarget.server.tsapps/webapp/app/v3/services/billingLimit/billingLimitReconciliation.server.tsapps/webapp/app/v3/services/billingLimit/getBillingLimitQueuedRunCount.server.tsapps/webapp/app/v3/services/billingLimit/getInitialEnvPauseStateForBillingLimit.server.tsapps/webapp/app/v3/services/billingLimit/manualPauseEnvironmentGuard.server.tsapps/webapp/app/v3/services/billingLimit/runBillingLimitReconcileTick.server.tsapps/webapp/app/v3/services/pauseEnvironment.server.tsapps/webapp/app/v3/services/triggerTask.server.tsapps/webapp/server.tsapps/webapp/test/billingAlertsFormat.test.tsapps/webapp/test/billingLimit.schemas.test.tsapps/webapp/test/billingLimitBulkCancelInProgress.test.tsapps/webapp/test/billingLimitConvergeEnvironments.test.tsapps/webapp/test/billingLimitConvergeEnvironmentsService.test.tsapps/webapp/test/billingLimitConvergeResolve.test.tsapps/webapp/test/billingLimitEnvCreatePause.test.tsapps/webapp/test/billingLimitHit.test.tsapps/webapp/test/billingLimitPauseEnvironment.test.tsapps/webapp/test/billingLimitQueuedRuns.test.tsapps/webapp/test/billingLimitReconcileTick.test.tsapps/webapp/test/billingLimitReconciliation.test.tsapps/webapp/test/billingLimitTriggerEntitlement.test.tsapps/webapp/test/billingLimitsRoute.test.tsapps/webapp/test/orgBanner.test.tsdocs/how-to-reduce-your-spend.mdxinternal-packages/database/prisma/migrations/20260614120000_add_environment_pause_source_billing_limit/migration.sqlinternal-packages/database/prisma/schema.prisma
💤 Files with no reviewable changes (2)
- apps/webapp/app/components/billing/UpgradePrompt.tsx
- apps/webapp/app/components/navigation/EnvironmentBanner.tsx
| alertLevels: z.preprocess((i) => { | ||
| const values = typeof i === "string" ? [i] : Array.isArray(i) ? i : []; | ||
| return values | ||
| .filter((v) => v !== "") | ||
| .map((v) => Number(v)) | ||
| .filter((n) => Number.isFinite(n)); | ||
| }, z.number().array().refine(thresholdValuesAreUnique, "Each alert must be unique")), |
There was a problem hiding this comment.
Reject malformed threshold values instead of silently dropping them.
Lines 55-57 currently remove non-finite parsed values, so a non-empty invalid input can be silently ignored and still persist a partial alert set. This should fail validation explicitly.
💡 Suggested fix
alertLevels: z.preprocess((i) => {
const values = typeof i === "string" ? [i] : Array.isArray(i) ? i : [];
- return values
- .filter((v) => v !== "")
- .map((v) => Number(v))
- .filter((n) => Number.isFinite(n));
+ return values
+ .filter((v) => v !== "")
+ .map((v) => Number(v));
}, z.number().array().refine(thresholdValuesAreUnique, "Each alert must be unique")),| function getUpgradeResetDate(): Date { | ||
| const nextMonth = new Date(); | ||
| nextMonth.setUTCMonth(nextMonth.getMonth() + 1); | ||
| nextMonth.setUTCDate(1); | ||
| nextMonth.setUTCHours(0, 0, 0, 0); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify mixed local/UTC month API usage in this function
rg -n 'setUTCMonth\(.+getMonth\(' apps/webapp/app/components/billing/OrgBanner.tsx
sed -n '18,24p' apps/webapp/app/components/billing/OrgBanner.tsxRepository: triggerdotdev/trigger.dev
Length of output: 331
Use UTC month math consistently in getUpgradeResetDate().
Line 20 mixes getMonth() (local time) with setUTCMonth() (UTC), which can compute the wrong reset month around timezone boundaries. Use getUTCMonth() instead.
Proposed fix
- nextMonth.setUTCMonth(nextMonth.getMonth() + 1);
+ nextMonth.setUTCMonth(nextMonth.getUTCMonth() + 1);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function getUpgradeResetDate(): Date { | |
| const nextMonth = new Date(); | |
| nextMonth.setUTCMonth(nextMonth.getMonth() + 1); | |
| nextMonth.setUTCDate(1); | |
| nextMonth.setUTCHours(0, 0, 0, 0); | |
| function getUpgradeResetDate(): Date { | |
| const nextMonth = new Date(); | |
| nextMonth.setUTCMonth(nextMonth.getUTCMonth() + 1); | |
| nextMonth.setUTCDate(1); | |
| nextMonth.setUTCHours(0, 0, 0, 0); |
| } else if (billingLimit && !billingLimit.isConfigured && showSelfServe) { | ||
| return OrgBannerKind.NoLimitConfigured; | ||
| } | ||
|
|
||
| if (hasExceededFreeTier) { | ||
| return OrgBannerKind.Upgrade; | ||
| } |
There was a problem hiding this comment.
Upgrade banner is masked by no-limit banner in overlapping states.
Line 34 returns NoLimitConfigured before the free-tier check on Line 38. If both are true, users won’t see the upgrade CTA even when over free-tier limits.
🐛 Suggested ordering fix
- } else if (billingLimit && !billingLimit.isConfigured && showSelfServe) {
- return OrgBannerKind.NoLimitConfigured;
- }
-
if (hasExceededFreeTier) {
return OrgBannerKind.Upgrade;
}
+
+ if (billingLimit && !billingLimit.isConfigured && showSelfServe) {
+ return OrgBannerKind.NoLimitConfigured;
+ }| switch (action) { | ||
| case "environment-pause": | ||
| const pauseService = new PauseEnvironmentService(); | ||
| await pauseService.call(environment, "paused"); | ||
| { | ||
| const result = await pauseService.call(environment, "paused"); | ||
| if (!result.success) { | ||
| return redirectWithErrorMessage(redirectPath, request, result.error); | ||
| } | ||
| } | ||
| return redirectWithSuccessMessage(redirectPath, request, "Environment paused"); | ||
| case "environment-resume": | ||
| const resumeService = new PauseEnvironmentService(); | ||
| await resumeService.call(environment, "resumed"); | ||
| { | ||
| const result = await resumeService.call(environment, "resumed"); | ||
| if (!result.success) { | ||
| return redirectWithErrorMessage(redirectPath, request, result.error); | ||
| } | ||
| } | ||
| return redirectWithSuccessMessage(redirectPath, request, "Environment resumed"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/webapp/app/routes/_app.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.queues/route.tsx"
# Show the affected switch segment with line numbers
nl -ba "$FILE" | sed -n '180,210p'
# Verify each case label is block-scoped
python - <<'PY'
from pathlib import Path
p = Path("apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx")
s = p.read_text()
print('case "environment-pause": {', 'case "environment-pause": {' in s)
print('case "environment-resume": {', 'case "environment-resume": {' in s)
PYRepository: triggerdotdev/trigger.dev
Length of output: 112
🏁 Script executed:
#!/bin/bash
# Read the specific file and show lines around 185-203
FILE="apps/webapp/app/routes/_app.orgs.\$organizationSlug.projects.\$projectParam.env.\$envParam.queues/route.tsx"
# First, check if file exists
if [ -f "$FILE" ]; then
echo "=== Lines 180-210 of file ==="
sed -n '180,210p' "$FILE" | cat -n
else
# Try to find the correct file path
echo "File not found at exact path. Searching for similar files..."
find . -type f -name "route.tsx" -path "*queues*" 2>/dev/null | head -5
fiRepository: triggerdotdev/trigger.dev
Length of output: 1456
Wrap environment-pause / environment-resume case bodies in braces.
Line 185 (and 194) violate noSwitchDeclarations; the const pauseService and const resumeService declarations are unscoped in case clauses, risking scope leakage to other cases. Compare with the queue-pause/queue-resume cases below, which correctly wrap their bodies in braces. The existing nested braces should be removed and replaced with a single outer block brace on the case label.
🧰 Tools
🪛 Biome (2.5.0)
[error] 187-187: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
(lint/correctness/noSwitchDeclarations)
[error] 196-196: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
(lint/correctness/noSwitchDeclarations)
Source: Linters/SAST tools
| const firstDayOfMonth = new Date(); | ||
| firstDayOfMonth.setUTCDate(1); | ||
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | ||
|
|
||
| const firstDayOfNextMonth = new Date(); | ||
| firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1); | ||
| firstDayOfNextMonth.setUTCDate(1); | ||
| firstDayOfNextMonth.setUTCHours(0, 0, 0, 0); |
There was a problem hiding this comment.
Fix next-month boundary calculation order.
Line 134 mutates month before normalizing day; on month-end dates this can overflow into the wrong month and skew the usage window. Normalize day first, then advance month.
💡 Suggested fix
- const firstDayOfNextMonth = new Date();
- firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);
- firstDayOfNextMonth.setUTCDate(1);
- firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);
+ const firstDayOfNextMonth = new Date();
+ firstDayOfNextMonth.setUTCDate(1);
+ firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);
+ firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const firstDayOfMonth = new Date(); | |
| firstDayOfMonth.setUTCDate(1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| const firstDayOfNextMonth = new Date(); | |
| firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1); | |
| firstDayOfNextMonth.setUTCDate(1); | |
| firstDayOfNextMonth.setUTCHours(0, 0, 0, 0); | |
| const firstDayOfMonth = new Date(); | |
| firstDayOfMonth.setUTCDate(1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| const firstDayOfNextMonth = new Date(); | |
| firstDayOfNextMonth.setUTCDate(1); | |
| firstDayOfNextMonth.setUTCHours(0, 0, 0, 0); | |
| firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1); |
| const completion = await complete(pending.organizationId); | ||
| if (!completion) { | ||
| throw new Error("Billing platform client unavailable"); | ||
| } |
There was a problem hiding this comment.
complete() result is not validated, so unresolved orgs can be treated as resolved.
At Line 40, only undefined is treated as failure. { completed: false } currently passes and won’t be retried.
Proposed fix
- const completion = await complete(pending.organizationId);
- if (!completion) {
+ const completion = await complete(pending.organizationId);
+ if (!completion || completion.completed !== true) {
throw new Error("Billing platform client unavailable");
}| const billingLimit = deps.getBillingLimit | ||
| ? await deps.getBillingLimit(organizationId) | ||
| : await (await import("~/services/platform.v3.server")).getBillingLimit(organizationId); | ||
| const targetState = resolveConvergeTargetFromBillingLimit(billingLimit); |
There was a problem hiding this comment.
Add a resilient fallback for billing-limit lookup failures.
Line 26-29 makes env/branch creation availability dependent on a platform lookup. A transient platform failure currently hard-fails these paths instead of degrading gracefully.
Suggested fix
+import { logger } from "~/services/logger.server";
@@
- const billingLimit = deps.getBillingLimit
- ? await deps.getBillingLimit(organizationId)
- : await (await import("~/services/platform.v3.server")).getBillingLimit(organizationId);
+ let billingLimit: BillingLimitResult | undefined;
+ try {
+ billingLimit = deps.getBillingLimit
+ ? await deps.getBillingLimit(organizationId)
+ : await (await import("~/services/platform.v3.server")).getBillingLimit(organizationId);
+ } catch (error) {
+ logger.error("Failed to fetch billing limit for initial env pause state", {
+ organizationId,
+ error,
+ });
+ return { paused: false, pauseSource: null };
+ }| return; | ||
| } | ||
|
|
||
| await updateEnvConcurrencyLimits(environment, 0); |
There was a problem hiding this comment.
Make post-create billing pause application non-fatal.
Line 48 throws directly from a post-create side-effect. Callers can end up with a created environment but a failed request response, which is a partial-success failure mode.
Suggested fix
+import { logger } from "~/services/logger.server";
@@
- await updateEnvConcurrencyLimits(environment, 0);
+ try {
+ await updateEnvConcurrencyLimits(environment, 0);
+ } catch (error) {
+ logger.error("Failed to apply billing-limit pause after env create", {
+ environmentId: environment.id,
+ organizationId: environment.organizationId,
+ error,
+ });
+ }| for (const target of targets) { | ||
| await reconcileTarget(target, { | ||
| bustCaches, | ||
| enqueueConverge, | ||
| }); | ||
| } | ||
|
|
||
| await clearProcessedQueue( | ||
| queuedOrgIds, | ||
| targets.map((target) => target.organizationId) | ||
| ); |
There was a problem hiding this comment.
One reconcile failure aborts the entire tick and leaves queue cleanup skipped.
At Line 62, a thrown error from one org stops processing remaining targets and clearProcessedQueue() never runs. Isolate failures per target and only clear successfully processed org IDs.
Suggested pattern
- for (const target of targets) {
- await reconcileTarget(target, {
- bustCaches,
- enqueueConverge,
- });
- }
-
- await clearProcessedQueue(
- queuedOrgIds,
- targets.map((target) => target.organizationId)
- );
+ const processedOrgIds: string[] = [];
+ for (const target of targets) {
+ try {
+ await reconcileTarget(target, { bustCaches, enqueueConverge });
+ processedOrgIds.push(target.organizationId);
+ } catch (error) {
+ // log and continue
+ }
+ }
+
+ await clearProcessedQueue(queuedOrgIds, processedOrgIds);| const runtimeEnvironment = await this._prisma.runtimeEnvironment.findFirst({ | ||
| where: { id: environment.id }, | ||
| select: { | ||
| pauseSource: true, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Make pause-source enforcement atomic to avoid a race.
The check at Line 44 and the write at Line 71 are separate operations. A concurrent billing-limit pause can occur between them, allowing a manual resume to clear pauseSource incorrectly.
Suggested patch (atomic resume write)
- await this._prisma.runtimeEnvironment.update({
- where: {
- id: environment.id,
- },
- data: {
- paused: action === "paused",
- pauseSource: action === "resumed" ? null : undefined,
- },
- });
+ if (action === "resumed") {
+ const resumed = await this._prisma.runtimeEnvironment.updateMany({
+ where: {
+ id: environment.id,
+ NOT: { pauseSource: "BILLING_LIMIT" as any },
+ },
+ data: {
+ paused: false,
+ pauseSource: null,
+ },
+ });
+
+ if (resumed.count === 0) {
+ throw new Error(
+ "This environment is paused because your organization reached its billing limit. Resolve the limit on the billing limits settings page to resume."
+ );
+ }
+ } else {
+ await this._prisma.runtimeEnvironment.update({
+ where: { id: environment.id },
+ data: { paused: true },
+ });
+ }Also applies to: 71-78
Closes #
✅ Checklist
Testing
[Describe the steps you took to test this change]
Changelog
[Short description of what has changed]
Screenshots
[Screenshots]
💯