2022-12-13 12:43:37 +00:00
|
|
|
import { z } from "zod";
|
|
|
|
|
2022-11-10 20:23:56 +00:00
|
|
|
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
|
|
|
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
|
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
|
|
import prisma from "@calcom/prisma";
|
|
|
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
|
|
|
|
2022-12-13 12:43:37 +00:00
|
|
|
const teamPaymentMetadataSchema = z.object({
|
|
|
|
paymentId: z.string(),
|
|
|
|
subscriptionId: z.string(),
|
|
|
|
subscriptionItemId: z.string(),
|
|
|
|
});
|
|
|
|
|
2022-11-16 21:07:20 +00:00
|
|
|
/** Used to prevent double charges for the same team */
|
|
|
|
export const checkIfTeamPaymentRequired = async ({ teamId = -1 }) => {
|
|
|
|
const team = await prisma.team.findUniqueOrThrow({
|
|
|
|
where: { id: teamId },
|
|
|
|
select: { metadata: true },
|
|
|
|
});
|
|
|
|
const metadata = teamMetadataSchema.parse(team.metadata);
|
|
|
|
/** If there's no paymentId, we need to pay this team */
|
|
|
|
if (!metadata?.paymentId) return { url: null };
|
|
|
|
const checkoutSession = await stripe.checkout.sessions.retrieve(metadata.paymentId);
|
|
|
|
/** If there's a pending session but it isn't paid, we need to pay this team */
|
2023-03-28 18:45:13 +00:00
|
|
|
if (checkoutSession.payment_status !== "paid") return { url: null };
|
2022-11-16 21:07:20 +00:00
|
|
|
/** If the session is already paid we return the upgrade URL so team is updated. */
|
|
|
|
return { url: `${WEBAPP_URL}/api/teams/${teamId}/upgrade?session_id=${metadata.paymentId}` };
|
|
|
|
};
|
|
|
|
|
2022-11-10 20:23:56 +00:00
|
|
|
export const purchaseTeamSubscription = async (input: { teamId: number; seats: number; userId: number }) => {
|
|
|
|
const { teamId, seats, userId } = input;
|
2022-11-16 21:07:20 +00:00
|
|
|
const { url } = await checkIfTeamPaymentRequired({ teamId });
|
|
|
|
if (url) return { url };
|
2022-11-10 20:23:56 +00:00
|
|
|
const customer = await getStripeCustomerIdFromUserId(userId);
|
2022-11-16 21:07:20 +00:00
|
|
|
const session = await stripe.checkout.sessions.create({
|
2022-11-10 20:23:56 +00:00
|
|
|
customer,
|
|
|
|
mode: "subscription",
|
2022-12-01 00:36:04 +00:00
|
|
|
allow_promotion_codes: true,
|
2022-11-10 20:23:56 +00:00
|
|
|
success_url: `${WEBAPP_URL}/api/teams/${teamId}/upgrade?session_id={CHECKOUT_SESSION_ID}`,
|
2023-02-24 18:39:09 +00:00
|
|
|
cancel_url: `${WEBAPP_URL}/settings/my-account/profile`,
|
2022-11-10 20:23:56 +00:00
|
|
|
line_items: [
|
|
|
|
{
|
|
|
|
/** We only need to set the base price and we can upsell it directly on Stripe's checkout */
|
|
|
|
price: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID,
|
|
|
|
quantity: seats,
|
|
|
|
},
|
|
|
|
],
|
2022-12-05 20:02:59 +00:00
|
|
|
customer_update: {
|
|
|
|
address: "auto",
|
|
|
|
},
|
|
|
|
automatic_tax: {
|
|
|
|
enabled: true,
|
|
|
|
},
|
2022-11-10 20:23:56 +00:00
|
|
|
metadata: {
|
|
|
|
teamId,
|
|
|
|
},
|
|
|
|
subscription_data: {
|
|
|
|
metadata: {
|
|
|
|
teamId,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
2022-11-16 21:07:20 +00:00
|
|
|
return { url: session.url };
|
2022-11-10 20:23:56 +00:00
|
|
|
};
|
|
|
|
|
2022-12-13 12:43:37 +00:00
|
|
|
const getTeamWithPaymentMetadata = async (teamId: number) => {
|
|
|
|
const team = await prisma.team.findUniqueOrThrow({
|
|
|
|
where: { id: teamId },
|
|
|
|
select: { metadata: true, members: true },
|
|
|
|
});
|
|
|
|
const metadata = teamPaymentMetadataSchema.parse(team.metadata);
|
|
|
|
return { ...team, metadata };
|
|
|
|
};
|
|
|
|
|
2022-11-10 20:23:56 +00:00
|
|
|
export const cancelTeamSubscriptionFromStripe = async (teamId: number) => {
|
|
|
|
try {
|
2022-12-13 12:43:37 +00:00
|
|
|
const team = await getTeamWithPaymentMetadata(teamId);
|
|
|
|
const { subscriptionId } = team.metadata;
|
|
|
|
return await stripe.subscriptions.cancel(subscriptionId);
|
2022-11-10 20:23:56 +00:00
|
|
|
} catch (error) {
|
|
|
|
let message = "Unknown error on cancelTeamSubscriptionFromStripe";
|
|
|
|
if (error instanceof Error) message = error.message;
|
|
|
|
console.error(message);
|
|
|
|
}
|
|
|
|
};
|
2022-12-13 12:43:37 +00:00
|
|
|
|
|
|
|
export const updateQuantitySubscriptionFromStripe = async (teamId: number) => {
|
|
|
|
try {
|
|
|
|
const { url } = await checkIfTeamPaymentRequired({ teamId });
|
|
|
|
/**
|
|
|
|
* If there's no pending checkout URL it means that this team has not been paid.
|
|
|
|
* We cannot update the subscription yet, this will be handled on publish/checkout.
|
|
|
|
**/
|
|
|
|
if (!url) return;
|
|
|
|
const team = await getTeamWithPaymentMetadata(teamId);
|
|
|
|
const { subscriptionId, subscriptionItemId } = team.metadata;
|
|
|
|
await stripe.subscriptions.update(subscriptionId, {
|
|
|
|
items: [{ quantity: team.members.length, id: subscriptionItemId }],
|
|
|
|
});
|
2023-03-28 19:38:01 +00:00
|
|
|
console.info(
|
|
|
|
`Updated subscription ${subscriptionId} for team ${teamId} to ${team.members.length} seats.`
|
|
|
|
);
|
2022-12-13 12:43:37 +00:00
|
|
|
} catch (error) {
|
|
|
|
let message = "Unknown error on updateQuantitySubscriptionFromStripe";
|
|
|
|
if (error instanceof Error) message = error.message;
|
|
|
|
console.error(message);
|
|
|
|
}
|
|
|
|
};
|