Skip to content

Commit

Permalink
Update models & types to handle paid plans (#2300)
Browse files Browse the repository at this point in the history
* Update models & types to handle paid plans

* move stripeCustomerId to subscription

* Make subscriptionId nullable on PlanType

* Do not create processing subscriptions for paid plans

* Remove success boolean from subscription route
  • Loading branch information
PopDaph authored Oct 30, 2023
1 parent fc59d50 commit 43f4a8c
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 112 deletions.
28 changes: 13 additions & 15 deletions front/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,27 +440,20 @@ export async function planForWorkspace(
w: Workspace
): Promise<Promise<PlanType>> {
const activeSubscription = await Subscription.findOne({
attributes: ["id", "startDate", "endDate"],
attributes: [
"id",
"sId",
"stripeSubscriptionId",
"stripeCustomerId",
"startDate",
"endDate",
],
where: { workspaceId: w.id, status: "active" },
include: [
{
model: Plan,
as: "plan",
required: true,
attributes: [
"code",
"name",
"isSlackbotAllowed",
"maxMessages",
"isManagedSlackAllowed",
"isManagedNotionAllowed",
"isManagedGoogleDriveAllowed",
"isManagedGithubAllowed",
"maxDataSourcesCount",
"maxDataSourcesDocumentsCount",
"maxDataSourcesDocumentsSizeMb",
"maxUsersInWorkspace",
],
},
],
});
Expand Down Expand Up @@ -490,6 +483,11 @@ export async function planForWorkspace(
code: plan.code,
name: plan.name,
status: "active",
subscriptionId: activeSubscription?.sId || null,
stripeSubscriptionId: activeSubscription?.stripeSubscriptionId || null,
stripeCustomerId: activeSubscription?.stripeCustomerId || null,
stripeProductId: plan.stripeProductId,
billingType: plan.billingType,
startDate: startDate?.getTime() || null,
endDate: endDate?.getTime() || null,
limits: {
Expand Down
5 changes: 4 additions & 1 deletion front/lib/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export type APIErrorType =
| "message_not_found"
| "message_reaction_error"
| "test_plan_message_limit_reached"
| "global_agent_error";
| "global_agent_error"
| "subscription_error"
| "stripe_webhook_error"
| "stripe_api_error";

export type APIError = {
type: APIErrorType;
Expand Down
20 changes: 20 additions & 0 deletions front/lib/models/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class Plan extends Model<
declare code: string; // unique
declare name: string;
declare stripeProductId: string | null;
declare billingType: "fixed" | "monthly_active_users" | "free";

// workspace limitations
declare maxMessages: number;
Expand Down Expand Up @@ -65,6 +66,14 @@ Plan.init(
type: DataTypes.STRING,
allowNull: true,
},
billingType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "fixed",
validate: {
isIn: [["fixed", "monthly_active_users", "free"]],
},
},
maxMessages: {
type: DataTypes.INTEGER,
allowNull: false,
Expand Down Expand Up @@ -134,6 +143,9 @@ export class Subscription extends Model<

declare planId: ForeignKey<Plan["id"]>;
declare plan: NonAttribute<Plan>;

declare stripeCustomerId: string | null;
declare stripeSubscriptionId: string | null;
}
Subscription.init(
{
Expand Down Expand Up @@ -171,6 +183,14 @@ Subscription.init(
type: DataTypes.DATE,
allowNull: true,
},
stripeCustomerId: {
type: DataTypes.STRING,
allowNull: true,
},
stripeSubscriptionId: {
type: DataTypes.STRING,
allowNull: true,
},
},
{
modelName: "subscription",
Expand Down
1 change: 1 addition & 0 deletions front/lib/plans/enterprise_plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const ENT_PLAN_FAKE_DATA: PlanAttributes = {
code: ENT_PLAN_FAKE_CODE,
name: "Entreprise",
stripeProductId: null,
billingType: "fixed",
maxMessages: -1,
maxUsersInWorkspace: -1,
isSlackbotAllowed: true,
Expand Down
2 changes: 2 additions & 0 deletions front/lib/plans/free_plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const FREE_TEST_PLAN_DATA: PlanAttributes = {
code: FREE_TEST_PLAN_CODE,
name: "Test",
stripeProductId: null,
billingType: "free",
maxMessages: 100,
maxUsersInWorkspace: 1,
isSlackbotAllowed: false,
Expand All @@ -49,6 +50,7 @@ const FREE_PLANS_DATA: PlanAttributes[] = [
code: FREE_UPGRADED_PLAN_CODE,
name: "Free Trial",
stripeProductId: null,
billingType: "free",
maxMessages: -1,
maxUsersInWorkspace: -1,
isSlackbotAllowed: true,
Expand Down
2 changes: 2 additions & 0 deletions front/lib/plans/pro_plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const PRO_PLANS_DATA: PlanAttributes[] = [
code: "PRO_PLAN_MAU_29",
name: "Pro",
stripeProductId: "prod_OtB9SOIwFyiQnl",
billingType: "monthly_active_users",
maxMessages: -1,
maxUsersInWorkspace: 500,
isSlackbotAllowed: true,
Expand All @@ -45,6 +46,7 @@ const PRO_PLANS_DATA: PlanAttributes[] = [
code: "PRO_PLAN_FIXED_1000",
name: "Pro Fixed",
stripeProductId: "prod_OtBhelMswszehT",
billingType: "fixed",
maxMessages: -1,
maxUsersInWorkspace: 50,
isSlackbotAllowed: true,
Expand Down
141 changes: 67 additions & 74 deletions front/lib/plans/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FREE_UPGRADED_PLAN_CODE,
PlanAttributes,
} from "@app/lib/plans/free_plans";
//import { createCheckoutSession } from "@app/lib/plans/stripe"; PART OF NEXT PR
import { generateModelSId } from "@app/lib/utils";
import { PlanType } from "@app/types/user";

Expand Down Expand Up @@ -43,6 +44,11 @@ export const internalSubscribeWorkspaceToFreeTestPlan = async ({
code: freeTestPlan.code,
name: freeTestPlan.name,
status: "active",
subscriptionId: null,
stripeSubscriptionId: null,
stripeCustomerId: null,
stripeProductId: null,
billingType: freeTestPlan.billingType,
startDate: null,
endDate: null,
limits: {
Expand Down Expand Up @@ -123,6 +129,7 @@ export const internalSubscribeWorkspaceToFreeUpgradedPlan = async ({
planId: plan.id,
status: "active",
startDate: now,
stripeCustomerId: activeSubscription?.stripeCustomerId || null,
},
{ transaction: t }
);
Expand All @@ -131,6 +138,11 @@ export const internalSubscribeWorkspaceToFreeUpgradedPlan = async ({
code: plan.code,
name: plan.name,
status: "active",
subscriptionId: newSubscription.sId,
stripeSubscriptionId: newSubscription.stripeSubscriptionId,
stripeCustomerId: newSubscription.stripeCustomerId,
stripeProductId: null,
billingType: "free",
startDate: newSubscription.startDate.getTime(),
endDate: newSubscription.endDate?.getTime() || null,
limits: {
Expand Down Expand Up @@ -162,7 +174,7 @@ export const internalSubscribeWorkspaceToFreeUpgradedPlan = async ({
export const subscribeWorkspaceToPlan = async (
auth: Authenticator,
{ planCode }: { planCode: string }
): Promise<PlanType> => {
): Promise<string | void> => {
const user = auth.user();
const workspace = auth.workspace();
const activePlan = auth.plan();
Expand All @@ -182,87 +194,68 @@ export const subscribeWorkspaceToPlan = async (

// Case of a downgrade to the free default plan: we use the internal function
if (planCode === FREE_TEST_PLAN_CODE) {
return await internalSubscribeWorkspaceToFreeTestPlan({
await internalSubscribeWorkspaceToFreeTestPlan({
workspaceId: workspace.sId,
});
return;
}

const now = new Date();

return await front_sequelize.transaction(async (t) => {
// We get the plan to subscribe to
const newPlan = await Plan.findOne({
where: { code: planCode },
transaction: t,
});
if (!newPlan) {
throw new Error(`Cannot subscribe to plan ${planCode}: not found.`);
}

// We search for an active subscription for this workspace
const activeSubscription = await Subscription.findOne({
where: { workspaceId: workspace.id, status: "active" },
transaction: t,
});

// We check if the user is already subscribed to this plan
if (activeSubscription && activeSubscription.planId === newPlan.id) {
throw new Error(
`Cannot subscribe to plan ${planCode}: already subscribed.`
);
}
// We make sure the user is not trying to subscribe to a plan he already has
const newPlan = await Plan.findOne({
where: { code: planCode },
});
if (!newPlan) {
throw new Error(`Cannot subscribe to plan ${planCode}: not found.`);
}
const activeSubscription = await Subscription.findOne({
where: { workspaceId: workspace.id, status: "active" },
});
if (activeSubscription && activeSubscription.planId === newPlan.id) {
throw new Error(
`Cannot subscribe to plan ${planCode}: already subscribed.`
);
}

// We end the active subscription if any
if (activeSubscription) {
await activeSubscription.update(
if (newPlan.billingType === "free") {
// We can immediately subscribe to a free plan: end the current subscription if any and create a new active one.
const now = new Date();
await front_sequelize.transaction(async (t) => {
if (activeSubscription) {
await activeSubscription.update(
{
status: "ended",
endDate: now,
},
{ transaction: t }
);
}
await Subscription.create(
{
status: "ended",
endDate: now,
sId: generateModelSId(),
workspaceId: workspace.id,
planId: newPlan.id,
status: "active",
startDate: now,
stripeCustomerId: activeSubscription?.stripeCustomerId || null,
},
{ transaction: t }
);
}

// We create a new subscription
const newSubscription = await Subscription.create(
{
sId: generateModelSId(),
workspaceId: workspace.id,
planId: newPlan.id,
status: "active",
startDate: now,
},
{ transaction: t }
});
} else if (newPlan.stripeProductId) {
// We enter Stripe Checkout flow
// const checkoutUrl = await createCheckoutSession({ COMMENTED CAUSE PART OF THE NEXT PR
// owner: workspace,
// planCode: newPlan.code,
// productId: newPlan.stripeProductId,
// isFixedPriceBilling: true,
// stripeCustomerId: activeSubscription?.stripeCustomerId || null,
// });
// if (checkoutUrl) {
// return checkoutUrl;
// }
} else {
throw new Error(
`Plan with code ${planCode} is not a free plan and has no Stripe Product ID.`
);

return {
code: newPlan.code,
name: newPlan.name,
status: "active",
startDate: newSubscription.startDate.getTime(),
endDate: newSubscription.endDate?.getTime() || null,
limits: {
assistant: {
isSlackBotAllowed: newPlan.isSlackbotAllowed,
maxMessages: newPlan.maxMessages,
},
connections: {
isSlackAllowed: newPlan.isManagedSlackAllowed,
isNotionAllowed: newPlan.isManagedNotionAllowed,
isGoogleDriveAllowed: newPlan.isManagedGoogleDriveAllowed,
isGithubAllowed: newPlan.isManagedGithubAllowed,
},
dataSources: {
count: newPlan.maxDataSourcesCount,
documents: {
count: newPlan.maxDataSourcesDocumentsCount,
sizeMb: newPlan.maxDataSourcesDocumentsSizeMb,
},
},
users: {
maxUsers: newPlan.maxUsersInWorkspace,
},
},
};
});
}
};
Loading

0 comments on commit 43f4a8c

Please sign in to comment.