Skip to content

Commit 05a3644

Browse files
committed
feat(webapp): treat schedules as a purchasable add-on
1 parent e0681d2 commit 05a3644

4 files changed

Lines changed: 146 additions & 4 deletions

File tree

apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type RuntimeEnvironmentType, type ScheduleType } from "@trigger.dev/dat
22
import { type ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
33
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
44
import { getTaskIdentifiers } from "~/models/task.server";
5-
import { getLimit } from "~/services/platform.v3.server";
5+
import { getCurrentPlan, getLimit, getPlans } from "~/services/platform.v3.server";
66
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
77
import { ServiceValidationError } from "~/v3/services/baseService.server";
88
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
@@ -103,7 +103,27 @@ export class ScheduleListPresenter extends BasePresenter {
103103
projectId,
104104
});
105105

106-
const limit = await getLimit(project.organizationId, "schedules", 100_000_000);
106+
const baseLimit = await getLimit(project.organizationId, "schedules", 100_000_000);
107+
const [currentPlan, plans] = await Promise.all([
108+
getCurrentPlan(project.organizationId),
109+
getPlans(),
110+
]);
111+
112+
const extraSchedules = currentPlan?.v3Subscription?.addOns?.schedules?.purchased ?? 0;
113+
const limit = baseLimit + extraSchedules;
114+
const canPurchaseSchedules =
115+
currentPlan?.v3Subscription?.plan?.limits.schedules.canExceed === true;
116+
const maxScheduleQuota = currentPlan?.v3Subscription?.addOns?.schedules?.quota ?? 0;
117+
const planScheduleLimit = currentPlan?.v3Subscription?.plan?.limits.schedules.number ?? 0;
118+
const schedulePricing = plans?.addOnPricing.schedules ?? null;
119+
120+
const purchaseInfo = {
121+
canPurchaseSchedules,
122+
extraSchedules,
123+
maxScheduleQuota,
124+
planScheduleLimit,
125+
schedulePricing,
126+
};
107127

108128
//get the latest BackgroundWorker
109129
const latestWorker = await findCurrentWorkerFromEnvironment(environment, this._replica);
@@ -119,6 +139,7 @@ export class ScheduleListPresenter extends BasePresenter {
119139
used: schedulesCount,
120140
limit,
121141
},
142+
...purchaseInfo,
122143
filters: {
123144
tasks,
124145
search,
@@ -314,6 +335,7 @@ export class ScheduleListPresenter extends BasePresenter {
314335
used: schedulesCount,
315336
limit,
316337
},
338+
...purchaseInfo,
317339
filters: {
318340
tasks,
319341
search,

apps/webapp/app/services/platform.v3.server.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,22 @@ export async function setBranchesAddOn(organizationId: string, amount: number) {
478478
}
479479
}
480480

481+
export async function setSchedulesAddOn(organizationId: string, amount: number) {
482+
if (!client) return undefined;
483+
484+
try {
485+
const result = await client.setAddOn(organizationId, { type: "schedules", amount });
486+
if (!result.success) {
487+
recordPlatformFailure("setSchedulesAddOn", "no_success");
488+
return undefined;
489+
}
490+
return result;
491+
} catch (e) {
492+
recordPlatformFailure("setSchedulesAddOn", "caught");
493+
return undefined;
494+
}
495+
}
496+
481497
export async function getUsage(organizationId: string, { from, to }: { from: Date; to: Date }) {
482498
if (!client) return undefined;
483499

apps/webapp/app/v3/services/checkSchedule.server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ZodError } from "zod";
22
import { CronPattern } from "../schedules";
33
import { BaseService, ServiceValidationError } from "./baseService.server";
4-
import { getLimit } from "~/services/platform.v3.server";
4+
import { getCurrentPlan, getLimit } from "~/services/platform.v3.server";
55
import { getTimezones } from "~/utils/timezones.server";
66
import { env } from "~/env.server";
77
import { type PrismaClientOrTransaction, type RuntimeEnvironmentType } from "@trigger.dev/database";
@@ -89,7 +89,11 @@ export class CheckScheduleService extends BaseService {
8989

9090
//if creating a schedule, check they're under the limits
9191
if (!schedule.friendlyId) {
92-
const limit = await getLimit(project.organizationId, "schedules", 100_000_000);
92+
const baseLimit = await getLimit(project.organizationId, "schedules", 100_000_000);
93+
const currentPlan = await getCurrentPlan(project.organizationId);
94+
const purchasedSchedules =
95+
currentPlan?.v3Subscription?.addOns?.schedules?.purchased ?? 0;
96+
const limit = baseLimit + purchasedSchedules;
9397
const schedulesCount = await CheckScheduleService.getUsedSchedulesCount({
9498
prisma: this._prisma,
9599
projectId,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { BaseService } from "./baseService.server";
2+
import { tryCatch } from "@trigger.dev/core/utils";
3+
import { setSchedulesAddOn } from "~/services/platform.v3.server";
4+
import assertNever from "assert-never";
5+
import { sendToPlain } from "~/utils/plain.server";
6+
import { uiComponent } from "@team-plain/typescript-sdk";
7+
8+
type Input = {
9+
userId: string;
10+
organizationId: string;
11+
action: "purchase" | "quota-increase";
12+
amount: number;
13+
};
14+
15+
type Result =
16+
| {
17+
success: true;
18+
}
19+
| {
20+
success: false;
21+
error: string;
22+
};
23+
24+
export class SetSchedulesAddOnService extends BaseService {
25+
async call({ userId, organizationId, action, amount }: Input): Promise<Result> {
26+
switch (action) {
27+
case "purchase": {
28+
const result = await setSchedulesAddOn(organizationId, amount);
29+
if (!result) {
30+
return {
31+
success: false,
32+
error: "Failed to update schedules",
33+
};
34+
}
35+
36+
switch (result.result) {
37+
case "success": {
38+
return { success: true };
39+
}
40+
case "error": {
41+
return { success: false, error: result.error };
42+
}
43+
case "max_quota_reached": {
44+
return {
45+
success: false,
46+
error: `You can't purchase more than ${result.maxQuota} schedules without requesting an increase.`,
47+
};
48+
}
49+
default: {
50+
return {
51+
success: false,
52+
error: "Failed to update schedules, unknown result.",
53+
};
54+
}
55+
}
56+
}
57+
case "quota-increase": {
58+
const user = await this._replica.user.findFirst({
59+
where: { id: userId },
60+
});
61+
62+
if (!user) {
63+
return { success: false, error: "No matching user found." };
64+
}
65+
66+
const organization = await this._replica.organization.findFirst({
67+
select: { title: true },
68+
where: { id: organizationId },
69+
});
70+
71+
const [error] = await tryCatch(
72+
sendToPlain({
73+
userId,
74+
email: user.email,
75+
name: user.name ?? user.displayName ?? user.email,
76+
title: `Schedules quota request: ${amount}`,
77+
components: [
78+
uiComponent.text({
79+
text: `Org: ${organization?.title} (${organizationId})`,
80+
}),
81+
uiComponent.divider({ spacingSize: "M" }),
82+
uiComponent.text({
83+
text: `Total schedules requested: ${amount}`,
84+
}),
85+
],
86+
})
87+
);
88+
89+
if (error) {
90+
return { success: false, error: error.message };
91+
}
92+
93+
return { success: true };
94+
}
95+
default: {
96+
assertNever(action);
97+
}
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)