Free flow is working
This commit is contained in:
@@ -25,7 +25,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
}
|
||||
|
||||
return redirectWithErrorMessage(
|
||||
`${v3PlansPath({ slug: org.slug })}`,
|
||||
v3PlansPath({ slug: org.slug }),
|
||||
request,
|
||||
"You didn't complete your details on Stripe. Please try again."
|
||||
);
|
||||
|
||||
+19
-5
@@ -1,8 +1,9 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { v3SubscribedPath } from "~/utils/pathBuilder";
|
||||
import { newProjectPath, v3PlansPath } from "~/utils/pathBuilder";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
organizationId: z.string(),
|
||||
@@ -14,6 +15,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const org = await prisma.organization.findUnique({
|
||||
select: {
|
||||
slug: true,
|
||||
_count: {
|
||||
select: {
|
||||
projects: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: organizationId,
|
||||
@@ -24,9 +30,17 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
throw new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
`${v3SubscribedPath({ slug: org.slug })}`,
|
||||
request,
|
||||
"You are now subscribed to Trigger.dev"
|
||||
const hasProject = org._count.projects > 0;
|
||||
|
||||
if (hasProject) {
|
||||
return redirectWithSuccessMessage(
|
||||
v3PlansPath({ slug: org.slug }),
|
||||
request,
|
||||
"Your subscription has been successfully activated."
|
||||
);
|
||||
}
|
||||
|
||||
return redirect(
|
||||
newProjectPath({ slug: org.slug }, "Your subscription has been successfully activated.")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,5 +30,5 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
|
||||
let errorMessage = reason ? decodeURIComponent(reason) : "Subscribing failed to complete";
|
||||
|
||||
return redirectWithErrorMessage(`${v3PlansPath({ slug: org.slug })}`, request, errorMessage);
|
||||
return redirectWithErrorMessage(v3PlansPath({ slug: org.slug }), request, errorMessage);
|
||||
};
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { newProjectPath, selectPlanPath, v3PlansPath } from "~/utils/pathBuilder";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
organizationId: z.string(),
|
||||
});
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { organizationId } = ParamsSchema.parse(params);
|
||||
|
||||
const org = await prisma.organization.findUnique({
|
||||
select: {
|
||||
slug: true,
|
||||
_count: {
|
||||
select: {
|
||||
projects: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
throw new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const hasProject = org._count.projects > 0;
|
||||
|
||||
if (hasProject) {
|
||||
return redirectWithSuccessMessage(
|
||||
v3PlansPath({ slug: org.slug }),
|
||||
request,
|
||||
"Free tier unlocked successfully."
|
||||
);
|
||||
}
|
||||
|
||||
return redirect(newProjectPath({ slug: org.slug }, "You're on the Free plan."));
|
||||
};
|
||||
@@ -16,12 +16,10 @@ import { FormTitle } from "~/components/primitives/FormTitle";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { prisma } from "~/db.server";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { createProject } from "~/models/project.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
@@ -31,8 +29,6 @@ import {
|
||||
projectPath,
|
||||
selectPlanPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { RequestV3Access } from "../resources.orgs.$organizationSlug.v3-access";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -69,6 +65,9 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
const message = url.searchParams.get("message");
|
||||
|
||||
return typedjson({
|
||||
organization: {
|
||||
id: organization.id,
|
||||
@@ -80,6 +79,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
hasRequestedV3: organization.hasRequestedV3,
|
||||
},
|
||||
defaultVersion: url.searchParams.get("version") ?? "v2",
|
||||
message: message ? decodeURIComponent(message) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { organization } = useTypedLoaderData<typeof loader>();
|
||||
const { organization, message } = useTypedLoaderData<typeof loader>();
|
||||
const lastSubmission = useActionData();
|
||||
const { v3Enabled, isManagedCloud } = useFeatures();
|
||||
|
||||
@@ -144,9 +144,9 @@ export default function Page() {
|
||||
description={`This will create a new project in your "${organization.title}" organization.`}
|
||||
/>
|
||||
<Form method="post" {...form.props}>
|
||||
{organization.projectsCount === 0 && (
|
||||
<Callout variant="info" className="mb-4">
|
||||
Organizations require at least one project, please create one to continue.
|
||||
{message && (
|
||||
<Callout variant="success" className="mb-4">
|
||||
{message}
|
||||
</Callout>
|
||||
)}
|
||||
<Fieldset>
|
||||
@@ -184,15 +184,9 @@ export default function Page() {
|
||||
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
|
||||
</InputGroup>
|
||||
) : canCreateV3Projects ? (
|
||||
<>
|
||||
<Callout variant="info">This will be a v3 project</Callout>
|
||||
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v3"} />
|
||||
</>
|
||||
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v3"} />
|
||||
) : (
|
||||
<>
|
||||
<Callout variant="info">This will be a v2 project</Callout>
|
||||
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v2"} />
|
||||
</>
|
||||
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v2"} />
|
||||
)}
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { PricingPlans } from "~/components/billing/v3/PricingPlans";
|
||||
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { BillingService } from "~/services/billing.v3.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder";
|
||||
import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan";
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
await requireUserId(request);
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
const { isManagedCloud } = featuresForRequest(request);
|
||||
@@ -17,27 +19,34 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
const billingPresenter = new BillingService(isManagedCloud);
|
||||
const result = await billingPresenter.getPlans();
|
||||
|
||||
if (!result) {
|
||||
const plans = await billingPresenter.getPlans();
|
||||
if (!plans) {
|
||||
throw new Response(null, { status: 404, statusText: "Plans not found" });
|
||||
}
|
||||
|
||||
return typedjson(result);
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { slug: organizationSlug },
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Response(null, { status: 404, statusText: "Organization not found" });
|
||||
}
|
||||
|
||||
const currentPlan = await billingPresenter.currentPlan(organization.id);
|
||||
|
||||
return typedjson({ ...plans, ...currentPlan, organizationSlug });
|
||||
}
|
||||
|
||||
export default function ChoosePlanPage() {
|
||||
const { plans } = useTypedLoaderData<typeof loader>();
|
||||
const { plans, v3Subscription, organizationSlug } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full w-full max-w-[80rem] flex-col items-center justify-center gap-12 overflow-y-auto px-12">
|
||||
<Header1>Subscribe for full access</Header1>
|
||||
<PricingPlans
|
||||
plans={plans}
|
||||
// organizationSlug={organizationSlug}
|
||||
// plans={plans}
|
||||
// showActionText={false}
|
||||
// freeButtonPath={projectPath({ slug: organizationSlug }, { slug: projectSlug })}
|
||||
subscription={v3Subscription}
|
||||
organizationSlug={organizationSlug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+172
-36
@@ -1,11 +1,91 @@
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { FreePlanDefinition, Limits, PaidPlanDefinition, Plans } from "@trigger.dev/billing/v3";
|
||||
import { CheckIcon, ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import {
|
||||
FreePlanDefinition,
|
||||
FreeTierStatus,
|
||||
Limits,
|
||||
PaidPlanDefinition,
|
||||
Plans,
|
||||
SetPlanBody,
|
||||
SubscriptionResult,
|
||||
} from "@trigger.dev/billing/v3";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { DefinitionTip } from "~/components/DefinitionTooltip";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { prisma } from "~/db.server";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { redirectWithErrorMessage } from "~/models/message.server";
|
||||
import { BillingService } from "~/services/billing.v3.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const Params = z.object({
|
||||
organizationSlug: z.string(),
|
||||
});
|
||||
|
||||
const schema = z.object({
|
||||
type: z.enum(["free", "paid"]),
|
||||
planCode: z.string().optional(),
|
||||
callerPath: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
if (request.method.toLowerCase() !== "post") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const { organizationSlug } = Params.parse(params);
|
||||
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = Object.fromEntries(await request.formData());
|
||||
const form = schema.parse(formData);
|
||||
|
||||
const { isManagedCloud } = featuresForRequest(request);
|
||||
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: { slug: organizationSlug },
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw redirectWithErrorMessage(form.callerPath, request, "Organization not found");
|
||||
}
|
||||
|
||||
let payload: SetPlanBody;
|
||||
|
||||
switch (form.type) {
|
||||
case "free": {
|
||||
payload = {
|
||||
type: "free" as const,
|
||||
userId,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "paid": {
|
||||
if (form.planCode === undefined) {
|
||||
throw redirectWithErrorMessage(form.callerPath, request, "Not a valid plan");
|
||||
}
|
||||
payload = {
|
||||
type: "paid" as const,
|
||||
planCode: form.planCode,
|
||||
userId,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const billingService = new BillingService(isManagedCloud);
|
||||
return billingService.setPlan(organization, request, form.callerPath, payload);
|
||||
}
|
||||
|
||||
type PricingPlansProps = {
|
||||
plans: Plans;
|
||||
subscription?: SubscriptionResult;
|
||||
organizationSlug: string;
|
||||
};
|
||||
|
||||
const pricingDefinitions = {
|
||||
@@ -45,13 +125,17 @@ const pricingDefinitions = {
|
||||
},
|
||||
};
|
||||
|
||||
export function PricingPlans({ plans }: PricingPlansProps) {
|
||||
export function PricingPlans({ plans, subscription, organizationSlug }: PricingPlansProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<TierFree plan={plans.free} />
|
||||
<TierHobby plan={plans.hobby} />
|
||||
<TierPro plan={plans.pro} />
|
||||
<TierFree
|
||||
plan={plans.free}
|
||||
status={subscription?.freeTierStatus ?? "requires_connect"}
|
||||
organizationSlug={organizationSlug}
|
||||
/>
|
||||
<TierHobby plan={plans.hobby} organizationSlug={organizationSlug} />
|
||||
<TierPro plan={plans.pro} organizationSlug={organizationSlug} />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<TierEnterprise />
|
||||
@@ -60,42 +144,88 @@ export function PricingPlans({ plans }: PricingPlansProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TierFree({ plan }: { plan: FreePlanDefinition | PaidPlanDefinition }) {
|
||||
export function TierFree({
|
||||
plan,
|
||||
status,
|
||||
organizationSlug,
|
||||
}: {
|
||||
plan: FreePlanDefinition;
|
||||
status: FreeTierStatus;
|
||||
organizationSlug: string;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const navigation = useNavigation();
|
||||
const formAction = `/resources/orgs/${organizationSlug}/select-plan`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
|
||||
return (
|
||||
<TierContainer>
|
||||
<PricingHeader title={plan.title} cost={0} />
|
||||
<TierLimit href="https://trigger.dev/pricing#computePricing">
|
||||
${plan.limits.includedUsage} free usage
|
||||
</TierLimit>
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<div className="py-6">
|
||||
<Button variant="tertiary/large" fullWidth className="text-md font-medium">
|
||||
Unlock free plan
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<ConcurrentRuns limits={plan.limits} />
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.tasks.title}
|
||||
content={pricingDefinitions.tasks.content}
|
||||
>
|
||||
tasks
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<TeamMembers limits={plan.limits} />
|
||||
<Environments limits={plan.limits} />
|
||||
<Schedules limits={plan.limits} />
|
||||
<LogRetention limits={plan.limits} />
|
||||
<SupportLevel limits={plan.limits} />
|
||||
<Alerts limits={plan.limits} />
|
||||
</ul>
|
||||
{status === "rejected" ? (
|
||||
<div>
|
||||
<hr className="my-6 border-grid-bright" />
|
||||
<div className="flex flex-col gap-2 rounded-sm border border-warning p-4">
|
||||
<ExclamationTriangleIcon className="h-6 w-6 text-warning" />
|
||||
<Paragraph variant="small/bright">
|
||||
Your Trigger.dev account failed to be verified for the free plan because your GitHub
|
||||
account is too new. We require verification to prevent scammers and malicious use of
|
||||
our platform.
|
||||
</Paragraph>
|
||||
<Paragraph variant="small/bright">
|
||||
You can still select a paid plan to continue or if you think this is a mistake, get in
|
||||
touch.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Form action={formAction} method="post">
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<TierLimit href="https://trigger.dev/pricing#computePricing">
|
||||
${plan.limits.includedUsage} free usage
|
||||
</TierLimit>
|
||||
<div className="py-6">
|
||||
<Button
|
||||
variant="tertiary/large"
|
||||
fullWidth
|
||||
className="text-md font-medium"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? Spinner : undefined}
|
||||
>
|
||||
{status === "requires_connect" ? "Unlock free plan" : "Select plan"}
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<ConcurrentRuns limits={plan.limits} />
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.tasks.title}
|
||||
content={pricingDefinitions.tasks.content}
|
||||
>
|
||||
tasks
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<TeamMembers limits={plan.limits} />
|
||||
<Environments limits={plan.limits} />
|
||||
<Schedules limits={plan.limits} />
|
||||
<LogRetention limits={plan.limits} />
|
||||
<SupportLevel limits={plan.limits} />
|
||||
<Alerts limits={plan.limits} />
|
||||
</ul>
|
||||
</Form>
|
||||
)}
|
||||
</TierContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function TierHobby({ plan }: { plan: PaidPlanDefinition }) {
|
||||
export function TierHobby({
|
||||
plan,
|
||||
organizationSlug,
|
||||
}: {
|
||||
plan: PaidPlanDefinition;
|
||||
organizationSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<TierContainer isHighlighted>
|
||||
<PricingHeader title={plan.title} isHighlighted cost={plan.tierPrice} />
|
||||
@@ -128,7 +258,13 @@ export function TierHobby({ plan }: { plan: PaidPlanDefinition }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TierPro({ plan }: { plan: PaidPlanDefinition }) {
|
||||
export function TierPro({
|
||||
plan,
|
||||
organizationSlug,
|
||||
}: {
|
||||
plan: PaidPlanDefinition;
|
||||
organizationSlug: string;
|
||||
}) {
|
||||
return (
|
||||
<TierContainer>
|
||||
<PricingHeader title={plan.title} isHighlighted cost={plan.tierPrice} />
|
||||
@@ -1,8 +1,10 @@
|
||||
import { BillingClient, SetPlanBody } from "@trigger.dev/billing/v3";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { organizationBillingPath } from "~/utils/pathBuilder";
|
||||
import { newProjectPath, organizationBillingPath } from "~/utils/pathBuilder";
|
||||
|
||||
export class BillingService {
|
||||
#billingClient: BillingClient | undefined;
|
||||
@@ -27,56 +29,55 @@ export class BillingService {
|
||||
}
|
||||
}
|
||||
|
||||
//todo
|
||||
// async currentPlan(orgId: string) {
|
||||
// if (!this.#billingClient) return undefined;
|
||||
// try {
|
||||
// const result = await this.#billingClient.currentPlan(orgId);
|
||||
async currentPlan(orgId: string) {
|
||||
if (!this.#billingClient) return undefined;
|
||||
try {
|
||||
const result = await this.#billingClient.currentPlan(orgId);
|
||||
|
||||
// const firstDayOfMonth = new Date();
|
||||
// firstDayOfMonth.setDate(1);
|
||||
// firstDayOfMonth.setHours(0, 0, 0, 0);
|
||||
const firstDayOfMonth = new Date();
|
||||
firstDayOfMonth.setDate(1);
|
||||
firstDayOfMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
// const firstDayOfNextMonth = new Date();
|
||||
// firstDayOfNextMonth.setDate(1);
|
||||
// firstDayOfNextMonth.setMonth(firstDayOfNextMonth.getMonth() + 1);
|
||||
// firstDayOfNextMonth.setHours(0, 0, 0, 0);
|
||||
const firstDayOfNextMonth = new Date();
|
||||
firstDayOfNextMonth.setDate(1);
|
||||
firstDayOfNextMonth.setMonth(firstDayOfNextMonth.getMonth() + 1);
|
||||
firstDayOfNextMonth.setHours(0, 0, 0, 0);
|
||||
|
||||
// const currentRunCount = await this.#replica.jobRun.count({
|
||||
// where: {
|
||||
// organizationId: orgId,
|
||||
// createdAt: {
|
||||
// gte: firstDayOfMonth,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
const currentRunCount = await this.#replica.jobRun.count({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
createdAt: {
|
||||
gte: firstDayOfMonth,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// if (!result.success) {
|
||||
// logger.error("Error getting current plan", { orgId, error: result.error });
|
||||
// return undefined;
|
||||
// }
|
||||
if (!result.success) {
|
||||
logger.error("Error getting current plan", { orgId, error: result.error });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// const periodStart = firstDayOfMonth;
|
||||
// const periodEnd = firstDayOfNextMonth;
|
||||
// const periodRemainingDuration = periodEnd.getTime() - new Date().getTime();
|
||||
const periodStart = firstDayOfMonth;
|
||||
const periodEnd = firstDayOfNextMonth;
|
||||
const periodRemainingDuration = periodEnd.getTime() - new Date().getTime();
|
||||
|
||||
// const usage = {
|
||||
// currentRunCount,
|
||||
// runCountCap: result.subscription?.plan.runs?.freeAllowance,
|
||||
// exceededRunCount: result.subscription?.plan.runs?.freeAllowance
|
||||
// ? currentRunCount > result.subscription?.plan.runs?.freeAllowance
|
||||
// : false,
|
||||
// periodStart,
|
||||
// periodEnd,
|
||||
// periodRemainingDuration,
|
||||
// };
|
||||
const usage = {
|
||||
currentRunCount,
|
||||
runCountCap: result.subscription?.plan.runs?.freeAllowance,
|
||||
exceededRunCount: result.subscription?.plan.runs?.freeAllowance
|
||||
? currentRunCount > result.subscription?.plan.runs?.freeAllowance
|
||||
: false,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
periodRemainingDuration,
|
||||
};
|
||||
|
||||
// return { ...result, usage };
|
||||
// } catch (e) {
|
||||
// logger.error("Error getting current plan", { orgId, error: e });
|
||||
// return undefined;
|
||||
// }
|
||||
// }
|
||||
return { ...result, usage };
|
||||
} catch (e) {
|
||||
logger.error("Error getting current plan", { orgId, error: e });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async customerPortalUrl(orgId: string, orgSlug: string) {
|
||||
if (!this.#billingClient) return undefined;
|
||||
@@ -105,14 +106,62 @@ export class BillingService {
|
||||
}
|
||||
}
|
||||
|
||||
async setPlan(orgId: string, plan: SetPlanBody) {
|
||||
if (!this.#billingClient) return undefined;
|
||||
async setPlan(
|
||||
organization: { id: string; slug: string },
|
||||
request: Request,
|
||||
callerPath: string,
|
||||
plan: SetPlanBody
|
||||
) {
|
||||
if (!this.#billingClient) {
|
||||
throw redirectWithErrorMessage(callerPath, request, "Error setting plan");
|
||||
}
|
||||
try {
|
||||
const result = await this.#billingClient.setPlan(orgId, plan);
|
||||
return result;
|
||||
const result = await this.#billingClient.setPlan(organization.id, plan);
|
||||
|
||||
if (!result) {
|
||||
throw redirectWithErrorMessage(callerPath, request, "Error setting plan");
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw redirectWithErrorMessage(callerPath, request, result.error);
|
||||
}
|
||||
|
||||
switch (result.action) {
|
||||
case "free_connect_required": {
|
||||
return redirect(result.connectUrl);
|
||||
}
|
||||
case "free_connected": {
|
||||
if (result.accepted) {
|
||||
return redirect(newProjectPath(organization, "You're on the Free plan."));
|
||||
} else {
|
||||
return redirectWithErrorMessage(
|
||||
callerPath,
|
||||
request,
|
||||
"Free tier unlock failed, your GitHub account is too new."
|
||||
);
|
||||
}
|
||||
}
|
||||
case "create_subscription_flow_start": {
|
||||
return redirect(result.checkoutUrl);
|
||||
}
|
||||
case "updated_subscription": {
|
||||
return redirectWithSuccessMessage(
|
||||
callerPath,
|
||||
request,
|
||||
"Subscription updated successfully."
|
||||
);
|
||||
}
|
||||
case "canceled_subscription": {
|
||||
return redirectWithSuccessMessage(callerPath, request, "Subscription canceled.");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error setting plan", { orgId, error: e });
|
||||
return undefined;
|
||||
logger.error("Error setting plan", { organizationId: organization.id, error: e });
|
||||
throw redirectWithErrorMessage(
|
||||
callerPath,
|
||||
request,
|
||||
e instanceof Error ? e.message : "Error setting plan"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +296,10 @@ export function endpointStreamingPath(environment: { id: string }) {
|
||||
return `/resources/environments/${environment.id}/endpoint/stream`;
|
||||
}
|
||||
|
||||
export function newProjectPath(organization: OrgForPath) {
|
||||
return `${organizationPath(organization)}/projects/new`;
|
||||
export function newProjectPath(organization: OrgForPath, message?: string) {
|
||||
return `${organizationPath(organization)}/projects/new${
|
||||
message ? `?message=${encodeURIComponent(message)}` : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
function projectParam(project: ProjectForPath) {
|
||||
@@ -445,10 +447,6 @@ export function v3PlansPath(organization: OrgForPath) {
|
||||
return `${organizationPath(organization)}/v3/billing/plans`;
|
||||
}
|
||||
|
||||
export function v3SubscribedPath(organization: OrgForPath) {
|
||||
return `${organizationPath(organization)}/v3/subscribed`;
|
||||
}
|
||||
|
||||
// Integration
|
||||
export function integrationClientPath(organization: OrgForPath, client: IntegrationForPath) {
|
||||
return `${organizationIntegrationsPath(organization)}/${clientParam(client)}`;
|
||||
|
||||
Reference in New Issue
Block a user