Skip to content

Commit b238d14

Browse files
committed
feat(webapp): add billing limit schema and platform wiring
Add the EnvironmentPauseSource enum and migration, plus the billing-limit platform client wrappers and schemas.
1 parent a6400f9 commit b238d14

6 files changed

Lines changed: 519 additions & 4 deletions

File tree

.server-changes/billing-limits.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Add billing limits. Customers set a spend cap; when usage crosses it, billable
7+
environments pause for a grace period, new triggers are rejected once it ends,
8+
and a recovery flow resumes or cancels the queued backlog. Reconciliation keeps
9+
the webapp converged to billing's state.
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { BillingClient } from "@trigger.dev/platform";
2+
import { z } from "zod";
3+
4+
/**
5+
* Billing limit API schemas for the billing platform service.
6+
*
7+
* These mirror the planned @trigger.dev/platform types and are used via
8+
* BillingClient.fetch until the platform package is published with native
9+
* BillingClient methods.
10+
*/
11+
12+
export const BillingLimitStateSchema = z.discriminatedUnion("status", [
13+
z.object({
14+
status: z.literal("ok"),
15+
}),
16+
z.object({
17+
status: z.literal("grace"),
18+
hitAt: z.string(),
19+
graceEndsAt: z.string(),
20+
}),
21+
z.object({
22+
status: z.literal("rejected"),
23+
hitAt: z.string(),
24+
graceEndsAt: z.string(),
25+
}),
26+
]);
27+
28+
export type BillingLimitState = z.infer<typeof BillingLimitStateSchema>;
29+
30+
export const BillingLimitConfigSchema = z.discriminatedUnion("mode", [
31+
z.object({
32+
mode: z.literal("none"),
33+
}),
34+
z.object({
35+
mode: z.literal("plan"),
36+
}),
37+
z.object({
38+
mode: z.literal("custom"),
39+
amountCents: z.number().int().positive(),
40+
}),
41+
]);
42+
43+
export type BillingLimitConfig = z.infer<typeof BillingLimitConfigSchema>;
44+
45+
export const BillingLimitUnconfiguredSchema = z.object({
46+
isConfigured: z.literal(false),
47+
gracePeriodMs: z.number().int().nonnegative(),
48+
});
49+
50+
const billingLimitConfiguredFields = {
51+
isConfigured: z.literal(true),
52+
cancelInProgressRuns: z.boolean(),
53+
limitState: BillingLimitStateSchema,
54+
effectiveAmountCents: z.number().int().nonnegative().nullable(),
55+
gracePeriodMs: z.number().int().nonnegative(),
56+
};
57+
58+
export const BillingLimitConfiguredNoneSchema = z.object({
59+
...billingLimitConfiguredFields,
60+
mode: z.literal("none"),
61+
});
62+
63+
export const BillingLimitConfiguredPlanSchema = z.object({
64+
...billingLimitConfiguredFields,
65+
mode: z.literal("plan"),
66+
});
67+
68+
export const BillingLimitConfiguredCustomSchema = z.object({
69+
...billingLimitConfiguredFields,
70+
mode: z.literal("custom"),
71+
amountCents: z.number().int().positive(),
72+
});
73+
74+
export const BillingLimitConfiguredSchema = z.discriminatedUnion("mode", [
75+
BillingLimitConfiguredNoneSchema,
76+
BillingLimitConfiguredPlanSchema,
77+
BillingLimitConfiguredCustomSchema,
78+
]);
79+
80+
export const BillingLimitResultSchema = z.union([
81+
BillingLimitUnconfiguredSchema,
82+
BillingLimitConfiguredNoneSchema,
83+
BillingLimitConfiguredPlanSchema,
84+
BillingLimitConfiguredCustomSchema,
85+
]);
86+
87+
export type BillingLimitResult = z.infer<typeof BillingLimitResultSchema>;
88+
89+
export const UpdateBillingLimitRequestSchema = z.discriminatedUnion("mode", [
90+
z.object({
91+
mode: z.literal("none"),
92+
cancelInProgressRuns: z.boolean(),
93+
}),
94+
z.object({
95+
mode: z.literal("plan"),
96+
cancelInProgressRuns: z.boolean(),
97+
}),
98+
z.object({
99+
mode: z.literal("custom"),
100+
amountCents: z.number().int().positive(),
101+
cancelInProgressRuns: z.boolean(),
102+
}),
103+
]);
104+
105+
export type UpdateBillingLimitRequest = z.infer<typeof UpdateBillingLimitRequestSchema>;
106+
107+
export const ResolveBillingLimitRequestSchema = z.discriminatedUnion("action", [
108+
z.object({
109+
action: z.literal("increase"),
110+
newAmountCents: z.number().int().positive(),
111+
resumeMode: z.enum(["queue", "new_only"]),
112+
}),
113+
z.object({
114+
action: z.literal("remove"),
115+
resumeMode: z.enum(["queue", "new_only"]),
116+
}),
117+
]);
118+
119+
export type ResolveBillingLimitRequest = z.infer<typeof ResolveBillingLimitRequestSchema>;
120+
121+
export const BillingLimitActiveOrgSchema = z.object({
122+
orgId: z.string(),
123+
limitState: z.enum(["grace", "rejected"]),
124+
});
125+
126+
export const BillingLimitsActiveResultSchema = z.object({
127+
orgs: z.array(BillingLimitActiveOrgSchema),
128+
});
129+
130+
export type BillingLimitsActiveResult = z.infer<typeof BillingLimitsActiveResultSchema>;
131+
132+
export const BillingLimitPendingResolveOrgSchema = z.object({
133+
organizationId: z.string(),
134+
resumeMode: z.enum(["queue", "new_only"]),
135+
resolvedAt: z.string(),
136+
});
137+
138+
export const BillingLimitsPendingResolvesResultSchema = z.object({
139+
orgs: z.array(BillingLimitPendingResolveOrgSchema),
140+
});
141+
142+
export type BillingLimitsPendingResolvesResult = z.infer<
143+
typeof BillingLimitsPendingResolvesResultSchema
144+
>;
145+
146+
export const BillingLimitHitWebhookBodySchema = z.object({
147+
hitAt: z.string(),
148+
cancelInProgressRuns: z.boolean(),
149+
limitState: z.literal("grace"),
150+
});
151+
152+
export type BillingLimitHitWebhookBody = z.infer<typeof BillingLimitHitWebhookBodySchema>;
153+
154+
/** Entitlement response — mirrors ReportUsageResult with billing limit fields until platform ships native types. */
155+
export const EntitlementResultSchema = z.object({
156+
hasAccess: z.boolean(),
157+
balance: z.number().optional(),
158+
usage: z.number().optional(),
159+
overage: z.number().optional(),
160+
plan: z
161+
.object({
162+
type: z.string(),
163+
code: z.string(),
164+
isPaying: z.boolean(),
165+
})
166+
.optional(),
167+
limitState: z.literal("grace").optional(),
168+
reason: z.enum(["free_tier_exceeded", "billing_limit"]).optional(),
169+
});
170+
171+
export type EntitlementResult = z.infer<typeof EntitlementResultSchema>;
172+
173+
export type BillingLimitPageData = BillingLimitResult & {
174+
queuedRunCount: number;
175+
currentSpendCents: number;
176+
};
177+
178+
/** Bridge webapp Zod schemas to BillingClient.fetch (separate Zod type instances). */
179+
export function asPlatformSchema(schema: z.ZodTypeAny) {
180+
return schema as unknown as Parameters<BillingClient["fetch"]>[1];
181+
}

0 commit comments

Comments
 (0)