Skip to content

Commit 18bce2f

Browse files
fix(webapp): use EntitlementResult for production entitlement validation
Replace the invalid @trigger.dev/platform/v3 import so billing limit typecheck passes in CI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent cb90d64 commit 18bce2f

24 files changed

Lines changed: 277 additions & 75 deletions

apps/webapp/app/components/billing/BillingAlertsSection.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ export const billingAlertsSchema = z.object({
5353
const values = typeof i === "string" ? [i] : Array.isArray(i) ? i : [];
5454
return values
5555
.filter((v) => v !== "")
56-
.map((v) => Number(v))
57-
.filter((n) => Number.isFinite(n));
56+
.map((v) => Number(v));
5857
}, z.number().array().refine(thresholdValuesAreUnique, "Each alert must be unique")),
5958
});
6059

apps/webapp/app/components/billing/OrgBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { v3BillingLimitsPath, v3BillingPath, v3QueuesPath } from "~/utils/pathBu
1717

1818
function getUpgradeResetDate(): Date {
1919
const nextMonth = new Date();
20-
nextMonth.setUTCMonth(nextMonth.getMonth() + 1);
20+
nextMonth.setUTCMonth(nextMonth.getUTCMonth() + 1);
2121
nextMonth.setUTCDate(1);
2222
nextMonth.setUTCHours(0, 0, 0, 0);
2323
return nextMonth;

apps/webapp/app/components/billing/billingAlertsFormat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type BillingLimitMode = "plan" | "custom" | "none";
1818

1919
export function getBillingLimitMode(billingLimit: BillingLimitResult): BillingLimitMode {
2020
if (!billingLimit.isConfigured) {
21-
return "plan";
21+
return "none";
2222
}
2323
return billingLimit.mode;
2424
}

apps/webapp/app/components/billing/selectOrgBanner.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ export function selectOrgBanner(input: {
3131
if (status === "grace") {
3232
return OrgBannerKind.LimitGrace;
3333
}
34-
} else if (billingLimit && !billingLimit.isConfigured && showSelfServe) {
35-
return OrgBannerKind.NoLimitConfigured;
3634
}
3735

3836
if (hasExceededFreeTier) {
3937
return OrgBannerKind.Upgrade;
4038
}
4139

40+
if (billingLimit && !billingLimit.isConfigured && showSelfServe) {
41+
return OrgBannerKind.NoLimitConfigured;
42+
}
43+
4244
if (showEnvironmentWarning) {
4345
return OrgBannerKind.EnvironmentWarning;
4446
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,24 +183,22 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
183183
}
184184

185185
switch (action) {
186-
case "environment-pause":
186+
case "environment-pause": {
187187
const pauseService = new PauseEnvironmentService();
188-
{
189-
const result = await pauseService.call(environment, "paused");
190-
if (!result.success) {
191-
return redirectWithErrorMessage(redirectPath, request, result.error);
192-
}
188+
const result = await pauseService.call(environment, "paused");
189+
if (!result.success) {
190+
return redirectWithErrorMessage(redirectPath, request, result.error);
193191
}
194192
return redirectWithSuccessMessage(redirectPath, request, "Environment paused");
195-
case "environment-resume":
193+
}
194+
case "environment-resume": {
196195
const resumeService = new PauseEnvironmentService();
197-
{
198-
const result = await resumeService.call(environment, "resumed");
199-
if (!result.success) {
200-
return redirectWithErrorMessage(redirectPath, request, result.error);
201-
}
196+
const result = await resumeService.call(environment, "resumed");
197+
if (!result.success) {
198+
return redirectWithErrorMessage(redirectPath, request, result.error);
202199
}
203200
return redirectWithSuccessMessage(redirectPath, request, "Environment resumed");
201+
}
204202
case "queue-pause":
205203
case "queue-resume": {
206204
const friendlyId = formData.get("friendlyId");

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import type { MetaFunction } from "@remix-run/react";
33
import {
44
json,
55
redirect,
6-
type ActionFunction,
7-
type LoaderFunctionArgs,
86
} from "@remix-run/server-runtime";
97
import { tryCatch } from "@trigger.dev/core";
108
import { typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -43,6 +41,7 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
4341
import { prisma } from "~/db.server";
4442
import { featuresForRequest } from "~/features.server";
4543
import { useScrollContainerToTop } from "~/hooks/useScrollContainerToTop";
44+
import { resolveOrgIdFromSlug } from "~/models/organization.server";
4645
import {
4746
commitSession,
4847
getSession,
@@ -59,6 +58,7 @@ import {
5958
setBillingAlert,
6059
setBillingLimit,
6160
} from "~/services/platform.v3.server";
61+
import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
6262
import type { BillingLimitResult } from "~/services/billingLimit.schemas";
6363
import {
6464
getAlertsResetRequested,
@@ -77,15 +77,31 @@ import {
7777
v3BillingLimitsPath,
7878
v3BillingPath,
7979
} from "~/utils/pathBuilder";
80-
import { requireUserId } from "~/services/session.server";
80+
81+
const billingLimitsAuthorization = {
82+
action: "manage" as const,
83+
resource: { type: "billing" as const },
84+
};
8185

8286
export const meta: MetaFunction = () => {
8387
return [{ title: `Billing limits | Trigger.dev` }];
8488
};
8589

86-
export async function loader({ params, request }: LoaderFunctionArgs) {
87-
const userId = await requireUserId(request);
88-
const { organizationSlug } = OrganizationParamsSchema.parse(params);
90+
export const loader = dashboardLoader(
91+
{
92+
params: OrganizationParamsSchema,
93+
context: async (params) => {
94+
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
95+
return organizationId ? { organizationId } : {};
96+
},
97+
authorization: {
98+
...billingLimitsAuthorization,
99+
message: "With your current role, you can't manage billing limits.",
100+
},
101+
},
102+
async ({ params, request, user }) => {
103+
const userId = user.id;
104+
const { organizationSlug } = params;
89105

90106
const { isManagedCloud } = featuresForRequest(request);
91107
if (!isManagedCloud) {
@@ -131,9 +147,9 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
131147
firstDayOfMonth.setUTCHours(0, 0, 0, 0);
132148

133149
const firstDayOfNextMonth = new Date();
134-
firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);
135150
firstDayOfNextMonth.setUTCDate(1);
136151
firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);
152+
firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);
137153

138154
const [usage, queuedRunCount, billingLimitPauseEnvCount] = await Promise.all([
139155
getCachedUsage(organization.id, { from: firstDayOfMonth, to: firstDayOfNextMonth }),
@@ -166,7 +182,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
166182
submittedResumeMode,
167183
suggestedNewLimitDollars,
168184
});
169-
}
185+
}
186+
);
170187

171188
type LoaderData = {
172189
billingLimit: BillingLimitResult;
@@ -182,9 +199,18 @@ type LoaderData = {
182199
suggestedNewLimitDollars: number;
183200
};
184201

185-
export const action: ActionFunction = async ({ request, params }) => {
186-
const userId = await requireUserId(request);
187-
const { organizationSlug } = OrganizationParamsSchema.parse(params);
202+
export const action = dashboardAction(
203+
{
204+
params: OrganizationParamsSchema,
205+
context: async (params) => {
206+
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
207+
return organizationId ? { organizationId } : {};
208+
},
209+
authorization: billingLimitsAuthorization,
210+
},
211+
async ({ request, params, user }) => {
212+
const userId = user.id;
213+
const { organizationSlug } = params;
188214

189215
const organization = await prisma.organization.findFirst({
190216
where: { slug: organizationSlug, members: { some: { userId } } },
@@ -476,7 +502,8 @@ export const action: ActionFunction = async ({ request, params }) => {
476502
}
477503

478504
return json({ error: "Unknown form intent" }, { status: 400 });
479-
};
505+
}
506+
);
480507

481508
export default function Page() {
482509
const {

apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.billing-limit.hit.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { prisma } from "~/db.server";
4-
import { BillingLimitHitWebhookBodySchema } from "~/services/billingLimit.schemas";
4+
import { BillingLimitHitWebhookBodySchema, type BillingLimitHitWebhookBody } from "~/services/billingLimit.schemas";
5+
import { logger } from "~/services/logger.server";
56
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
67
import { bustBillingLimitCaches } from "~/services/platform.v3.server";
78
import {
@@ -24,7 +25,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
2425
}
2526

2627
const { organizationId } = ParamsSchema.parse(params);
27-
const body = BillingLimitHitWebhookBodySchema.parse(await request.json());
28+
29+
let body: BillingLimitHitWebhookBody;
30+
try {
31+
body = BillingLimitHitWebhookBodySchema.parse(await request.json());
32+
} catch (error) {
33+
logger.error("Invalid billing limit hit webhook payload", {
34+
error,
35+
organizationId,
36+
});
37+
return json({ error: "Invalid request body" }, { status: 400 });
38+
}
2839

2940
const organization = await prisma.organization.findFirst({
3041
where: { id: organizationId },

apps/webapp/app/runEngine/validators/validateProductionEntitlement.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { ReportUsageResult } from "@trigger.dev/platform/v3";
1+
import type { EntitlementResult } from "~/services/billingLimit.schemas";
22
import { OutOfEntitlementError } from "~/v3/outOfEntitlementError.server";
33
import type { EntitlementValidationParams, EntitlementValidationResult } from "../types";
44

55
export type GetEntitlementFn = (
66
organizationId: string
7-
) => Promise<ReportUsageResult | undefined>;
7+
) => Promise<EntitlementResult | undefined>;
88

99
export async function validateProductionEntitlement(
1010
params: EntitlementValidationParams,

apps/webapp/app/services/billingLimit.schemas.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ export const BillingLimitStateSchema = z.discriminatedUnion("status", [
1515
}),
1616
z.object({
1717
status: z.literal("grace"),
18-
hitAt: z.string(),
19-
graceEndsAt: z.string(),
18+
hitAt: z.string().datetime({ offset: true }),
19+
graceEndsAt: z.string().datetime({ offset: true }),
2020
}),
2121
z.object({
2222
status: z.literal("rejected"),
23-
hitAt: z.string(),
24-
graceEndsAt: z.string(),
23+
hitAt: z.string().datetime({ offset: true }),
24+
graceEndsAt: z.string().datetime({ offset: true }),
2525
}),
2626
]);
2727

@@ -132,7 +132,7 @@ export type BillingLimitsActiveResult = z.infer<typeof BillingLimitsActiveResult
132132
export const BillingLimitPendingResolveOrgSchema = z.object({
133133
organizationId: z.string(),
134134
resumeMode: z.enum(["queue", "new_only"]),
135-
resolvedAt: z.string(),
135+
resolvedAt: z.string().datetime({ offset: true }),
136136
});
137137

138138
export const BillingLimitsPendingResolvesResultSchema = z.object({
@@ -144,7 +144,7 @@ export type BillingLimitsPendingResolvesResult = z.infer<
144144
>;
145145

146146
export const BillingLimitHitWebhookBodySchema = z.object({
147-
hitAt: z.string(),
147+
hitAt: z.string().datetime({ offset: true }),
148148
cancelInProgressRuns: z.boolean(),
149149
limitState: z.literal("grace"),
150150
});

apps/webapp/app/services/upsertBranch.server.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,7 @@ export class UpsertBranchService {
154154
});
155155

156156
const alreadyExisted = branch.createdAt < now;
157-
158-
if (!alreadyExisted) {
159-
await applyBillingLimitPauseAfterEnvCreate(branch);
160-
}
157+
await applyBillingLimitPauseAfterEnvCreate(branch);
161158

162159
return {
163160
success: true as const,

0 commit comments

Comments
 (0)