feat: add monthly annual billing org form (#15520)
* Add annuall billing options * added payment tests * Add conitional + tests * Fix types + use correct ID * Fix type error * fix type check * Assign default to monthly billing period * cleanup * chore: rename fn and params * Prevent ability to deselect toggle group * Calculate yearly price * Fix TS error * revese billingPeriod * reformat how we handle billingPeriod * Improve type --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
co-authored by
Peer Richelsen
Hariom
parent
f897c680a4
commit
7b69a6ab9c
@@ -13,7 +13,7 @@ import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
import { Alert, Button, Form, RadioGroup as RadioArea, TextField } from "@calcom/ui";
|
||||
import { Alert, Button, Form, Label, RadioGroup as RadioArea, TextField, ToggleGroup } from "@calcom/ui";
|
||||
|
||||
function extractDomainFromEmail(email: string) {
|
||||
let out = "";
|
||||
@@ -32,6 +32,11 @@ export const CreateANewOrganizationForm = () => {
|
||||
return <CreateANewOrganizationFormChild session={session} />;
|
||||
};
|
||||
|
||||
enum BillingPeriod {
|
||||
MONTHLY = "MONTHLY",
|
||||
ANNUALLY = "ANNUALLY",
|
||||
}
|
||||
|
||||
const CreateANewOrganizationFormChild = ({
|
||||
session,
|
||||
}: {
|
||||
@@ -47,11 +52,13 @@ const CreateANewOrganizationFormChild = ({
|
||||
const newOrganizationFormMethods = useForm<{
|
||||
name: string;
|
||||
seats: number;
|
||||
billingPeriod: BillingPeriod;
|
||||
pricePerSeat: number;
|
||||
slug: string;
|
||||
orgOwnerEmail: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
billingPeriod: BillingPeriod.MONTHLY,
|
||||
slug: !isAdmin ? deriveSlugFromEmail(defaultOrgOwnerEmail) : undefined,
|
||||
orgOwnerEmail: !isAdmin ? defaultOrgOwnerEmail : undefined,
|
||||
name: !isAdmin ? deriveOrgNameFromEmail(defaultOrgOwnerEmail) : undefined,
|
||||
@@ -107,6 +114,39 @@ const CreateANewOrganizationFormChild = ({
|
||||
<Alert severity="error" message={serverErrorMessage} />
|
||||
</div>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<div className="mb-5">
|
||||
<Controller
|
||||
name="billingPeriod"
|
||||
control={newOrganizationFormMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label htmlFor="billingPeriod">Billing Period</Label>
|
||||
<ToggleGroup
|
||||
isFullWidth
|
||||
id="billingPeriod"
|
||||
value={value}
|
||||
onValueChange={(e: BillingPeriod) => {
|
||||
if ([BillingPeriod.ANNUALLY, BillingPeriod.MONTHLY].includes(e)) {
|
||||
onChange(e);
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
value: "MONTHLY",
|
||||
label: "Monthly",
|
||||
},
|
||||
{
|
||||
value: "ANNUALLY",
|
||||
label: "Annually",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Controller
|
||||
name="orgOwnerEmail"
|
||||
control={newOrganizationFormMethods.control}
|
||||
|
||||
@@ -130,6 +130,206 @@ describe("purchaseTeamOrOrgSubscription", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
it("Should create a monthly subscription if billing period is set to monthly", async () => {
|
||||
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
name: "test",
|
||||
email: "test@email.com",
|
||||
},
|
||||
});
|
||||
|
||||
const checkoutSessionsCreate = mockStripeCheckoutSessionsCreate({
|
||||
url: "SESSION_URL",
|
||||
});
|
||||
|
||||
mockStripeCheckoutSessionRetrieve(
|
||||
{
|
||||
currency: "USD",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
},
|
||||
[FAKE_PAYMENT_ID]
|
||||
);
|
||||
|
||||
mockStripeCheckoutPricesRetrieve({
|
||||
id: "PRICE_ID",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const checkoutPricesCreate = mockStripePricesCreate({
|
||||
id: "PRICE_ID",
|
||||
});
|
||||
|
||||
const team = await prismock.team.create({
|
||||
data: {
|
||||
name: "test",
|
||||
metadata: {
|
||||
paymentId: FAKE_PAYMENT_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const seatsToChargeFor = 1000;
|
||||
expect(
|
||||
await purchaseTeamOrOrgSubscription({
|
||||
teamId: team.id,
|
||||
seatsUsed: 10,
|
||||
seatsToChargeFor,
|
||||
userId: user.id,
|
||||
isOrg: true,
|
||||
pricePerSeat: 100,
|
||||
billingPeriod: "MONTHLY",
|
||||
})
|
||||
).toEqual({ url: "SESSION_URL" });
|
||||
|
||||
expect(checkoutPricesCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ recurring: { interval: "month" } })
|
||||
);
|
||||
|
||||
expect(checkoutSessionsCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
line_items: [
|
||||
{
|
||||
price: "PRICE_ID",
|
||||
quantity: seatsToChargeFor,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
it("Should create a annual subscription if billing period is set to annual", async () => {
|
||||
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
name: "test",
|
||||
email: "test@email.com",
|
||||
},
|
||||
});
|
||||
|
||||
const checkoutSessionsCreate = mockStripeCheckoutSessionsCreate({
|
||||
url: "SESSION_URL",
|
||||
});
|
||||
|
||||
mockStripeCheckoutSessionRetrieve(
|
||||
{
|
||||
currency: "USD",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
},
|
||||
[FAKE_PAYMENT_ID]
|
||||
);
|
||||
|
||||
mockStripeCheckoutPricesRetrieve({
|
||||
id: "PRICE_ID",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const checkoutPricesCreate = mockStripePricesCreate({
|
||||
id: "PRICE_ID",
|
||||
});
|
||||
|
||||
const team = await prismock.team.create({
|
||||
data: {
|
||||
name: "test",
|
||||
metadata: {
|
||||
paymentId: FAKE_PAYMENT_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const seatsToChargeFor = 1000;
|
||||
expect(
|
||||
await purchaseTeamOrOrgSubscription({
|
||||
teamId: team.id,
|
||||
seatsUsed: 10,
|
||||
seatsToChargeFor,
|
||||
userId: user.id,
|
||||
isOrg: true,
|
||||
pricePerSeat: 100,
|
||||
billingPeriod: "ANNUALLY",
|
||||
})
|
||||
).toEqual({ url: "SESSION_URL" });
|
||||
|
||||
expect(checkoutPricesCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ recurring: { interval: "year" } })
|
||||
);
|
||||
|
||||
expect(checkoutSessionsCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
line_items: [
|
||||
{
|
||||
price: "PRICE_ID",
|
||||
quantity: seatsToChargeFor,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("It should not create a custom price if price_per_seat is not set", async () => {
|
||||
const FAKE_PAYMENT_ID = "FAKE_PAYMENT_ID";
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
name: "test",
|
||||
email: "test@email.com",
|
||||
},
|
||||
});
|
||||
|
||||
mockStripeCheckoutSessionsCreate({
|
||||
url: "SESSION_URL",
|
||||
});
|
||||
|
||||
mockStripeCheckoutSessionRetrieve(
|
||||
{
|
||||
currency: "USD",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
},
|
||||
[FAKE_PAYMENT_ID]
|
||||
);
|
||||
|
||||
mockStripeCheckoutPricesRetrieve({
|
||||
id: "PRICE_ID",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const checkoutPricesCreate = mockStripePricesCreate({
|
||||
id: "PRICE_ID",
|
||||
});
|
||||
|
||||
const team = await prismock.team.create({
|
||||
data: {
|
||||
name: "test",
|
||||
metadata: {
|
||||
paymentId: FAKE_PAYMENT_ID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const seatsToChargeFor = 1000;
|
||||
expect(
|
||||
await purchaseTeamOrOrgSubscription({
|
||||
teamId: team.id,
|
||||
seatsUsed: 10,
|
||||
seatsToChargeFor,
|
||||
userId: user.id,
|
||||
isOrg: true,
|
||||
billingPeriod: "ANNUALLY",
|
||||
})
|
||||
).toEqual({ url: "SESSION_URL" });
|
||||
|
||||
expect(checkoutPricesCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateQuantitySubscriptionFromStripe", () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { MINIMUM_NUMBER_OF_ORG_SEATS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { IS_PRODUCTION, MINIMUM_NUMBER_OF_ORG_SEATS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BillingPeriod } from "@calcom/prisma/zod-utils";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["teams/lib/payments"] });
|
||||
@@ -95,8 +96,17 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
userId: number;
|
||||
isOrg?: boolean;
|
||||
pricePerSeat: number | null;
|
||||
billingPeriod?: BillingPeriod;
|
||||
}) => {
|
||||
const { teamId, seatsToChargeFor, seatsUsed, userId, isOrg, pricePerSeat } = input;
|
||||
const {
|
||||
teamId,
|
||||
seatsToChargeFor,
|
||||
seatsUsed,
|
||||
userId,
|
||||
isOrg,
|
||||
pricePerSeat,
|
||||
billingPeriod = BillingPeriod.MONTHLY,
|
||||
} = input;
|
||||
const { url } = await checkIfTeamPaymentRequired({ teamId });
|
||||
if (url) return { url };
|
||||
|
||||
@@ -106,6 +116,24 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
|
||||
const customer = await getStripeCustomerIdFromUserId(userId);
|
||||
|
||||
const fixedPrice = await getFixedPrice();
|
||||
|
||||
let priceId: string | undefined;
|
||||
|
||||
if (pricePerSeat) {
|
||||
const customPriceObj = await getPriceObject(fixedPrice);
|
||||
priceId = await createPrice({
|
||||
isOrg: !!isOrg,
|
||||
teamId,
|
||||
pricePerSeat,
|
||||
billingPeriod,
|
||||
product: customPriceObj.product as string, // We don't expand the object from stripe so just use the product as ID
|
||||
currency: customPriceObj.currency,
|
||||
});
|
||||
} else {
|
||||
priceId = fixedPrice as string;
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
mode: "subscription",
|
||||
@@ -114,7 +142,7 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
cancel_url: `${WEBAPP_URL}/settings/my-account/profile`,
|
||||
line_items: [
|
||||
{
|
||||
price: await getPriceId(),
|
||||
price: priceId,
|
||||
quantity: quantity,
|
||||
},
|
||||
],
|
||||
@@ -136,37 +164,34 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
});
|
||||
return { url: session.url };
|
||||
|
||||
/**
|
||||
* Determines the priceId depending on if a custom price is required or not.
|
||||
* If the organization has a custom price per seat, it will create a new price in stripe and return its ID.
|
||||
*/
|
||||
async function getPriceId() {
|
||||
const fixedPriceId = isOrg
|
||||
? process.env.STRIPE_ORG_MONTHLY_PRICE_ID
|
||||
: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID;
|
||||
|
||||
if (!fixedPriceId) {
|
||||
throw new Error(
|
||||
"You need to have STRIPE_ORG_MONTHLY_PRICE_ID and STRIPE_TEAM_MONTHLY_PRICE_ID env variables set"
|
||||
);
|
||||
}
|
||||
|
||||
log.debug("Getting price ID", safeStringify({ fixedPriceId, isOrg, teamId, pricePerSeat }));
|
||||
|
||||
if (!pricePerSeat) {
|
||||
return fixedPriceId;
|
||||
}
|
||||
|
||||
const priceObj = await stripe.prices.retrieve(fixedPriceId);
|
||||
if (!priceObj) throw new Error(`No price found for ID ${fixedPriceId}`);
|
||||
async function createPrice({
|
||||
isOrg,
|
||||
teamId,
|
||||
pricePerSeat,
|
||||
billingPeriod,
|
||||
product,
|
||||
currency,
|
||||
}: {
|
||||
isOrg: boolean;
|
||||
teamId: number;
|
||||
pricePerSeat: number;
|
||||
billingPeriod: BillingPeriod;
|
||||
product: Stripe.Product | string;
|
||||
currency: string;
|
||||
}) {
|
||||
try {
|
||||
const pricePerSeatInCents = pricePerSeat * 100;
|
||||
// Price comes in monthly so we need to convert it to a monthly/yearly price
|
||||
const occurrence = billingPeriod === "MONTHLY" ? 1 : 12;
|
||||
const yearlyPrice = pricePerSeatInCents * occurrence;
|
||||
|
||||
const customPriceObj = await stripe.prices.create({
|
||||
nickname: `Custom price for ${isOrg ? "Organization" : "Team"} ID: ${teamId}`,
|
||||
unit_amount: pricePerSeat * 100, // Stripe expects the amount in cents
|
||||
unit_amount: yearlyPrice, // Stripe expects the amount in cents
|
||||
// Use the same currency as in the fixed price to avoid hardcoding it.
|
||||
currency: priceObj.currency,
|
||||
recurring: { interval: "month" }, // Define your subscription interval
|
||||
product: typeof priceObj.product === "string" ? priceObj.product : priceObj.product.id,
|
||||
currency: currency,
|
||||
recurring: { interval: billingPeriod === "MONTHLY" ? "month" : "year" }, // Define your subscription interval
|
||||
product: typeof product === "string" ? product : product.id,
|
||||
tax_behavior: "exclusive",
|
||||
});
|
||||
return customPriceObj.id;
|
||||
@@ -179,8 +204,35 @@ export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
throw new Error("Error in creation of custom price");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the priceId depending on if a custom price is required or not.
|
||||
* If the organization has a custom price per seat, it will create a new price in stripe and return its ID.
|
||||
*/
|
||||
async function getFixedPrice() {
|
||||
const fixedPriceId = isOrg
|
||||
? process.env.STRIPE_ORG_MONTHLY_PRICE_ID
|
||||
: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID;
|
||||
|
||||
if (!fixedPriceId) {
|
||||
throw new Error(
|
||||
"You need to have STRIPE_ORG_MONTHLY_PRICE_ID and STRIPE_TEAM_MONTHLY_PRICE_ID env variables set"
|
||||
);
|
||||
}
|
||||
|
||||
log.debug("Getting price ID", safeStringify({ fixedPriceId, isOrg, teamId, pricePerSeat }));
|
||||
|
||||
return fixedPriceId;
|
||||
}
|
||||
};
|
||||
|
||||
async function getPriceObject(priceId: string) {
|
||||
const priceObj = await stripe.prices.retrieve(priceId);
|
||||
if (!priceObj) throw new Error(`No price found for ID ${priceId}`);
|
||||
|
||||
return priceObj;
|
||||
}
|
||||
|
||||
export const getTeamWithPaymentMetadata = async (teamId: number) => {
|
||||
const team = await prisma.team.findUniqueOrThrow({
|
||||
where: { id: teamId },
|
||||
|
||||
@@ -14,6 +14,7 @@ const orgSelect = {
|
||||
slug: true,
|
||||
logoUrl: true,
|
||||
};
|
||||
|
||||
export class OrganizationRepository {
|
||||
static async createWithExistingUserAsOwner({
|
||||
orgData,
|
||||
@@ -28,6 +29,7 @@ export class OrganizationRepository {
|
||||
seats: number | null;
|
||||
pricePerSeat: number | null;
|
||||
isPlatform: boolean;
|
||||
billingPeriod?: "MONTHLY" | "ANNUALLY";
|
||||
};
|
||||
owner: {
|
||||
id: number;
|
||||
@@ -68,6 +70,7 @@ export class OrganizationRepository {
|
||||
isOrganizationAdminReviewed: boolean;
|
||||
autoAcceptEmail: string;
|
||||
seats: number | null;
|
||||
billingPeriod?: "MONTHLY" | "ANNUALLY";
|
||||
pricePerSeat: number | null;
|
||||
isPlatform: boolean;
|
||||
};
|
||||
@@ -109,6 +112,7 @@ export class OrganizationRepository {
|
||||
isOrganizationAdminReviewed: boolean;
|
||||
autoAcceptEmail: string;
|
||||
seats: number | null;
|
||||
billingPeriod?: "MONTHLY" | "ANNUALLY";
|
||||
pricePerSeat: number | null;
|
||||
isPlatform: boolean;
|
||||
}) {
|
||||
@@ -130,6 +134,7 @@ export class OrganizationRepository {
|
||||
orgSeats: orgData.seats,
|
||||
orgPricePerSeat: orgData.pricePerSeat,
|
||||
isPlatform: orgData.isPlatform,
|
||||
billingPeriod: orgData.billingPeriod,
|
||||
},
|
||||
isPlatform: orgData.isPlatform,
|
||||
},
|
||||
|
||||
@@ -364,6 +364,11 @@ export const orgSettingsSchema = z
|
||||
.nullable();
|
||||
export type userMetadataType = z.infer<typeof userMetadata>;
|
||||
|
||||
export enum BillingPeriod {
|
||||
MONTHLY = "MONTHLY",
|
||||
ANNUALLY = "ANNUALLY",
|
||||
}
|
||||
|
||||
export const teamMetadataSchema = z
|
||||
.object({
|
||||
requestedSlug: z.string().or(z.null()),
|
||||
@@ -380,6 +385,7 @@ export const teamMetadataSchema = z
|
||||
lastRevertTime: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
billingPeriod: z.nativeEnum(BillingPeriod).optional(),
|
||||
})
|
||||
.partial()
|
||||
.nullable();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import { BillingPeriod } from "./create.schema";
|
||||
import type { TCreateInputSchema } from "./create.schema";
|
||||
|
||||
type CreateOptions = {
|
||||
@@ -39,7 +40,16 @@ const getIPAddress = async (url: string): Promise<string> => {
|
||||
};
|
||||
|
||||
export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
const { slug, name, orgOwnerEmail, seats, pricePerSeat, isPlatform } = input;
|
||||
const {
|
||||
slug,
|
||||
name,
|
||||
orgOwnerEmail,
|
||||
seats,
|
||||
pricePerSeat,
|
||||
isPlatform,
|
||||
billingPeriod: billingPeriodRaw,
|
||||
} = input;
|
||||
|
||||
const loggedInUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: ctx.user.id,
|
||||
@@ -63,6 +73,9 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
|
||||
const IS_USER_ADMIN = loggedInUser.role === UserPermissionRole.ADMIN;
|
||||
|
||||
// We only allow creating an annual billing period if you are a system admin
|
||||
const billingPeriod = (IS_USER_ADMIN ? billingPeriodRaw : BillingPeriod.MONTHLY) ?? BillingPeriod.MONTHLY;
|
||||
|
||||
if (!ORG_SELF_SERVE_ENABLED && !IS_USER_ADMIN && !isPlatform) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Only admins can create organizations" });
|
||||
}
|
||||
@@ -140,6 +153,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
seats: seats ?? null,
|
||||
pricePerSeat: pricePerSeat ?? null,
|
||||
isPlatform,
|
||||
billingPeriod,
|
||||
};
|
||||
|
||||
// Create a new user and invite them as the owner of the organization
|
||||
|
||||
@@ -2,6 +2,11 @@ import { z } from "zod";
|
||||
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
export enum BillingPeriod {
|
||||
MONTHLY = "MONTHLY",
|
||||
ANNUALLY = "ANNUALLY",
|
||||
}
|
||||
|
||||
export const ZCreateInputSchema = z.object({
|
||||
name: z.string(),
|
||||
slug: z.string().transform((val) => slugify(val.trim())),
|
||||
@@ -10,6 +15,7 @@ export const ZCreateInputSchema = z.object({
|
||||
seats: z.number().optional(),
|
||||
pricePerSeat: z.number().optional(),
|
||||
isPlatform: z.boolean().default(false),
|
||||
billingPeriod: z.nativeEnum(BillingPeriod).default(BillingPeriod.MONTHLY).optional(),
|
||||
});
|
||||
|
||||
export type TCreateInputSchema = z.infer<typeof ZCreateInputSchema>;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
|
||||
userId: ctx.user.id,
|
||||
isOrg: true,
|
||||
pricePerSeat: metadata.data?.orgPricePerSeat ?? null,
|
||||
billingPeriod: metadata.data?.billingPeriod ?? undefined,
|
||||
});
|
||||
|
||||
if (!checkoutSession.url)
|
||||
|
||||
Reference in New Issue
Block a user