fix: Allow less than 30 seats for orgs (#14995)
* fix: Allow less than 30 seats for orgs * Add test * Stub env variables
This commit is contained in:
@@ -107,7 +107,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||
teamId: _team.id,
|
||||
seats: _team.members.length,
|
||||
seatsUsed: _team.members.length,
|
||||
userId,
|
||||
pricePerSeat: null,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it, vi, beforeAll, afterAll } from "vitest";
|
||||
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
|
||||
import { purchaseTeamOrOrgSubscription } from "./payments";
|
||||
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("STRIPE_ORG_MONTHLY_PRICE_ID", "STRIPE_ORG_MONTHLY_PRICE_ID");
|
||||
vi.stubEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "STRIPE_TEAM_MONTHLY_PRICE_ID");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
vi.mock("@calcom/app-store/stripepayment/lib/customer", () => {
|
||||
return {
|
||||
getStripeCustomerIdFromUserId: function () {
|
||||
return "CUSTOMER_ID";
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/app-store/stripepayment/lib/server", () => {
|
||||
return {
|
||||
default: {
|
||||
checkout: {
|
||||
sessions: {
|
||||
create: vi.fn(),
|
||||
retrieve: vi.fn(),
|
||||
},
|
||||
},
|
||||
prices: {
|
||||
retrieve: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("purchaseTeamOrOrgSubscription", () => {
|
||||
it("should use `seatsToChargeFor` to create price", async () => {
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
mockStripeCheckoutPricesRetrieve({
|
||||
id: "PRICE_ID",
|
||||
product: {
|
||||
id: "PRODUCT_ID",
|
||||
},
|
||||
});
|
||||
|
||||
mockStripePricesCreate({
|
||||
id: "PRICE_ID",
|
||||
});
|
||||
|
||||
const team = await prismock.team.create({
|
||||
data: {
|
||||
name: "test",
|
||||
},
|
||||
});
|
||||
|
||||
const seatsToChargeFor = 1000;
|
||||
expect(
|
||||
await purchaseTeamOrOrgSubscription({
|
||||
teamId: team.id,
|
||||
seatsUsed: 10,
|
||||
seatsToChargeFor,
|
||||
userId: user.id,
|
||||
isOrg: true,
|
||||
pricePerSeat: 100,
|
||||
})
|
||||
).toEqual({ url: "SESSION_URL" });
|
||||
|
||||
expect(checkoutSessionsCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
line_items: [
|
||||
{
|
||||
price: "PRICE_ID",
|
||||
quantity: seatsToChargeFor,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function mockStripePricesCreate(data) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
return vi.mocked(stripe.prices.create).mockImplementation(() => new Promise((resolve) => resolve(data)));
|
||||
}
|
||||
|
||||
function mockStripeCheckoutPricesRetrieve(data) {
|
||||
return vi.mocked(stripe.prices.retrieve).mockImplementation(
|
||||
async () =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
new Promise((resolve) => {
|
||||
resolve(data);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function mockStripeCheckoutSessionRetrieve(data) {
|
||||
return vi.mocked(stripe.checkout.sessions.retrieve).mockImplementation(
|
||||
async () =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
new Promise((resolve) => resolve(data))
|
||||
);
|
||||
}
|
||||
|
||||
function mockStripeCheckoutSessionsCreate(data) {
|
||||
return vi.mocked(stripe.checkout.sessions.create).mockImplementation(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
async () => new Promise((resolve) => resolve(data))
|
||||
);
|
||||
}
|
||||
@@ -81,17 +81,28 @@ export const generateTeamCheckoutSession = async ({
|
||||
*/
|
||||
export const purchaseTeamOrOrgSubscription = async (input: {
|
||||
teamId: number;
|
||||
seats: number;
|
||||
/**
|
||||
* The actual number of seats in the team.
|
||||
* The seats that we would charge for could be more than this depending on the MINIMUM_NUMBER_OF_ORG_SEATS in case of an organization
|
||||
* For a team it would be the same as this value
|
||||
*/
|
||||
seatsUsed: number;
|
||||
/**
|
||||
* If provided, this is the exact number we would charge for.
|
||||
*/
|
||||
seatsToChargeFor?: number | null;
|
||||
userId: number;
|
||||
isOrg?: boolean;
|
||||
pricePerSeat: number | null;
|
||||
}) => {
|
||||
const { teamId, seats, userId, isOrg, pricePerSeat } = input;
|
||||
const { teamId, seatsToChargeFor, seatsUsed, userId, isOrg, pricePerSeat } = input;
|
||||
const { url } = await checkIfTeamPaymentRequired({ teamId });
|
||||
if (url) return { url };
|
||||
|
||||
// For orgs, enforce minimum of 30 seats
|
||||
const quantity = isOrg ? Math.max(seats, MINIMUM_NUMBER_OF_ORG_SEATS) : seats;
|
||||
// For orgs, enforce minimum of MINIMUM_NUMBER_OF_ORG_SEATS seats if `seatsToChargeFor` not set
|
||||
const seats = isOrg ? Math.max(seatsUsed, MINIMUM_NUMBER_OF_ORG_SEATS) : seatsUsed;
|
||||
const quantity = seatsToChargeFor ? seatsToChargeFor : seats;
|
||||
|
||||
const customer = await getStripeCustomerIdFromUserId(userId);
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
|
||||
@@ -39,7 +39,10 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||
teamId: prevTeam.id,
|
||||
seats: Math.max(prevTeam.members.length, metadata.data?.orgSeats ?? 0),
|
||||
seatsUsed: prevTeam.members.length,
|
||||
seatsToChargeFor: metadata.data?.orgSeats
|
||||
? Math.max(prevTeam.members.length, metadata.data?.orgSeats ?? 0)
|
||||
: null,
|
||||
userId: ctx.user.id,
|
||||
isOrg: true,
|
||||
pricePerSeat: metadata.data?.orgPricePerSeat ?? null,
|
||||
|
||||
@@ -45,7 +45,7 @@ const generateCheckoutSession = async ({
|
||||
|
||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||
teamId,
|
||||
seats,
|
||||
seatsUsed: seats,
|
||||
userId,
|
||||
pricePerSeat: null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user