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:
Hariom Balhara
2024-05-13 11:37:10 +00:00
committed by GitHub
parent fd64b0f468
commit 917c7b0764
5 changed files with 157 additions and 7 deletions
@@ -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))
);
}
+15 -4
View File
@@ -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,
});