diff --git a/front/lib/auth.ts b/front/lib/auth.ts index 949eaca64424..03ccc5575ca0 100644 --- a/front/lib/auth.ts +++ b/front/lib/auth.ts @@ -736,6 +736,17 @@ export class Authenticator { return this._subscription; } + getNonNullableSubscriptionResource(): SubscriptionResource { + const subscriptionResource = this.subscriptionResource(); + + if (!subscriptionResource) { + throw new Error("Unexpected unauthenticated call to `getNonNullableSubscriptionResource`."); + } + + return subscriptionResource; + } + + plan(): PlanType | null { return this._subscription ? this._subscription.getPlan() : null; } diff --git a/front/lib/plans/stripe.ts b/front/lib/plans/stripe.ts index 423c20394792..1408031d1365 100644 --- a/front/lib/plans/stripe.ts +++ b/front/lib/plans/stripe.ts @@ -3,13 +3,13 @@ import type { LightWorkspaceType, Result, SubscriptionType, + UserType, WorkspaceType, } from "@dust-tt/types"; import { Err, isDevelopment, Ok } from "@dust-tt/types"; import { Stripe } from "stripe"; import config from "@app/lib/api/config"; -import type { Authenticator } from "@app/lib/auth"; import { Plan, Subscription } from "@app/lib/models/plan"; import { isOldFreePlan } from "@app/lib/plans/plan_codes"; import { countActiveSeatsInWorkspace } from "@app/lib/plans/usage/seats"; @@ -77,25 +77,18 @@ async function getDefautPriceFromMetadata( * to go through the checkout process. */ export const createProPlanCheckoutSession = async ({ - auth, + owner, + user, billingPeriod, planCode, }: { - auth: Authenticator; + owner: WorkspaceType; + user: UserType; billingPeriod: BillingPeriod; planCode: string; }): Promise => { const stripe = getStripeClient(); - const owner = auth.workspace(); - if (!owner) { - throw new Error("No workspace found"); - } - const user = auth.user(); - if (!user) { - throw new Error("No user found"); - } - const plan = await Plan.findOne({ where: { code: planCode } }); if (!plan) { throw new Error( diff --git a/front/lib/resources/subscription_resource.ts b/front/lib/resources/subscription_resource.ts index f82f9a5af150..ee90a3085f78 100644 --- a/front/lib/resources/subscription_resource.ts +++ b/front/lib/resources/subscription_resource.ts @@ -7,6 +7,7 @@ import type { Result, SubscriptionPerSeatPricing, SubscriptionType, + UserType, WorkspaceType, } from "@dust-tt/types"; import { Ok, sendUserOperationMessage } from "@dust-tt/types"; @@ -20,6 +21,7 @@ import type { import type Stripe from "stripe"; import { sendProactiveTrialCancelledEmail } from "@app/lib/api/email"; +import { getWorkspaceInfos } from "@app/lib/api/workspace"; import type { Authenticator } from "@app/lib/auth"; import { Subscription } from "@app/lib/models/plan"; import { Plan } from "@app/lib/models/plan"; @@ -145,7 +147,7 @@ export class SubscriptionResource extends BaseResource { subscriptionResourceByWorkspaceSid[sId] = new SubscriptionResource( Subscription, activeSubscription?.get() || - this.createFreeNoPlanSubscription(workspace.id), + this.createFreeNoPlanSubscription(workspace), renderPlanFromModel({ plan }) ); } @@ -156,10 +158,7 @@ export class SubscriptionResource extends BaseResource { static async fetchByAuthenticator( auth: Authenticator ): Promise { - const owner = auth.workspace(); - if (!owner) { - throw new Error("Cannot find workspace."); - } + const owner = auth.getNonNullableWorkspace(); const subscriptions = await Subscription.findAll({ where: { workspaceId: owner.id }, @@ -170,7 +169,7 @@ export class SubscriptionResource extends BaseResource { (s) => new SubscriptionResource( Subscription, - s, + s.get(), renderPlanFromModel({ plan: s.plan }) ) ); @@ -190,7 +189,7 @@ export class SubscriptionResource extends BaseResource { return new SubscriptionResource( Subscription, - res, + res.get(), renderPlanFromModel({ plan: res.plan }) ); } @@ -209,11 +208,11 @@ export class SubscriptionResource extends BaseResource { }): Promise { const workspace = await this.findWorkspaceOrThrow(workspaceId); - await this.endActiveSubscription(workspace.id); + await this.endActiveSubscription(workspace); return new SubscriptionResource( Subscription, - this.createFreeNoPlanSubscription(workspace.id), + this.createFreeNoPlanSubscription(workspace), renderPlanFromModel({ plan: FREE_NO_PLAN_DATA }) ); } @@ -313,7 +312,7 @@ export class SubscriptionResource extends BaseResource { return new SubscriptionResource( Subscription, - newSubscription, + newSubscription.get(), renderPlanFromModel({ plan: newPlan }) ); } @@ -322,10 +321,7 @@ export class SubscriptionResource extends BaseResource { auth: Authenticator, enterpriseDetails: EnterpriseUpgradeFormType ) { - const owner = auth.workspace(); - if (!owner) { - throw new Error("Cannot find workspace."); - } + const owner = auth.getNonNullableWorkspace(); if (!auth.isDustSuperUser()) { throw new Error("Cannot upgrade workspace to plan: not allowed."); @@ -355,10 +351,7 @@ export class SubscriptionResource extends BaseResource { auth: Authenticator, planCode: string ) { - const owner = auth.workspace(); - if (!owner) { - throw new Error("Cannot find workspace}"); - } + const owner = auth.getNonNullableWorkspace(); if (!auth.isDustSuperUser()) { throw new Error("Cannot upgrade workspace to plan: not allowed."); @@ -374,7 +367,7 @@ export class SubscriptionResource extends BaseResource { } // We search for an active subscription for this workspace - const activeSubscription = auth.subscription(); + const activeSubscription = auth.subscriptionResource(); if (activeSubscription && activeSubscription.plan.code === newPlan.code) { throw new Error( `Cannot subscribe to plan ${planCode}: already subscribed.` @@ -401,10 +394,8 @@ export class SubscriptionResource extends BaseResource { ); } - const isAlreadyOnProPlan = await this.isSubscriptionOnProPlan( - owner, - activeSubscription - ); + const isAlreadyOnProPlan = + await activeSubscription.isSubscriptionOnProPlan(owner); if (!isAlreadyOnProPlan) { throw new Error( @@ -429,63 +420,6 @@ export class SubscriptionResource extends BaseResource { }); } - static async getCheckoutUrlForUpgrade( - auth: Authenticator, - billingPeriod: BillingPeriod - ): Promise { - const owner = auth.workspace(); - - if (!owner) { - throw new Error( - "Unauthorized `auth` data: cannot process to subscription of new Plan." - ); - } - - const planCode = owner.metadata?.isBusiness - ? PRO_PLAN_SEAT_39_CODE - : PRO_PLAN_SEAT_29_CODE; - - const proPlan = await Plan.findOne({ - where: { code: PRO_PLAN_SEAT_29_CODE }, - }); - if (!proPlan) { - throw new Error(`Cannot subscribe to plan ${planCode}: not found.`); - } - - const existingSubscription = auth.subscription(); - - // We verify that the workspace is not already subscribed to the Pro plan product. - if (existingSubscription) { - const isAlreadyOnProPlan = await this.isSubscriptionOnProPlan( - owner, - existingSubscription - ); - if (isAlreadyOnProPlan) { - throw new Error( - `Cannot subscribe to plan ${planCode}: already subscribed to a Pro plan.` - ); - } - } - - // We enter Stripe Checkout flow. - const checkoutUrl = await createProPlanCheckoutSession({ - auth, - billingPeriod, - planCode, - }); - - if (!checkoutUrl) { - throw new Error( - `Cannot subscribe to plan ${planCode}: error while creating Stripe Checkout session (URL is null).` - ); - } - - return { - checkoutUrl, - plan: renderPlanFromModel({ plan: proPlan }), - }; - } - static async maybeCancelInactiveTrials( auth: Authenticator, eventStripeSubscription: Stripe.Subscription @@ -532,7 +466,7 @@ export class SubscriptionResource extends BaseResource { const firstAdmin = await getWorkspaceFirstAdmin(workspace); if (!firstAdmin) { logger.info( - { action: "cancelling-trial", workspaceId: workspace.sId }, + { action: "cancelling-trial", workspaceId: auth.workspace()?.sId }, "No first adming found -- skipping email." ); @@ -548,6 +482,50 @@ export class SubscriptionResource extends BaseResource { } } + async getCheckoutUrlForUpgrade( + owner: WorkspaceType, + user: UserType, + billingPeriod: BillingPeriod + ): Promise { + const planCode = owner.metadata?.isBusiness + ? PRO_PLAN_SEAT_39_CODE + : PRO_PLAN_SEAT_29_CODE; + + const proPlan = await Plan.findOne({ + where: { code: PRO_PLAN_SEAT_29_CODE }, + }); + if (!proPlan) { + throw new Error(`Cannot subscribe to plan ${planCode}: not found.`); + } + + // We verify that the workspace is not already subscribed to the Pro plan product. + const isAlreadyOnProPlan = await this.isSubscriptionOnProPlan(owner); + if (isAlreadyOnProPlan) { + throw new Error( + `Cannot subscribe to plan ${planCode}: already subscribed to a Pro plan.` + ); + } + + // We enter Stripe Checkout flow. + const checkoutUrl = await createProPlanCheckoutSession({ + owner, + user, + billingPeriod, + planCode, + }); + + if (!checkoutUrl) { + throw new Error( + `Cannot subscribe to plan ${planCode}: error while creating Stripe Checkout session (URL is null).` + ); + } + + return { + checkoutUrl, + plan: renderPlanFromModel({ plan: proPlan }), + }; + } + async delete( auth: Authenticator, { transaction }: { transaction?: Transaction } = {} @@ -630,14 +608,14 @@ export class SubscriptionResource extends BaseResource { } private static createFreeNoPlanSubscription( - workspaceId: number + workspace: LightWorkspaceType ): Attributes { const now = new Date(); return { id: FREE_NO_PLAN_SUBSCRIPTION_ID, sId: generateRandomModelSId(), status: "ended", - workspaceId: workspaceId, + workspaceId: workspace.id, createdAt: now, updatedAt: now, startDate: now, @@ -662,29 +640,10 @@ export class SubscriptionResource extends BaseResource { ); } - private static async isSubscriptionOnProPlan( - owner: WorkspaceType, - subscription: SubscriptionType - ): Promise { - if (!subscription.stripeSubscriptionId) { - return false; - } - const stripeSubscription = await getStripeSubscription( - subscription.stripeSubscriptionId - ); - if (!stripeSubscription) { - return false; - } - - return this.isStripeSubscriptionOnProPlan(owner, stripeSubscription); - } - private static async findWorkspaceOrThrow( workspaceId: string - ): Promise { - const workspace = await Workspace.findOne({ - where: { sId: workspaceId }, - }); + ): Promise { + const workspace = await getWorkspaceInfos(workspaceId); if (!workspace) { throw new Error(`Cannot find workspace ${workspaceId}`); @@ -699,13 +658,13 @@ export class SubscriptionResource extends BaseResource { * @returns The active subscription that was ended, or null if none existed */ private static async endActiveSubscription( - workspaceId: number + workspace: LightWorkspaceType ): Promise { const now = new Date(); // Find active subscription const activeSubscription = await Subscription.findOne({ - where: { workspaceId, status: "active" }, + where: { workspaceId: workspace.id, status: "active" }, }); if (activeSubscription) { @@ -734,4 +693,23 @@ export class SubscriptionResource extends BaseResource { return activeSubscription; } + + private async isSubscriptionOnProPlan( + owner: WorkspaceType + ): Promise { + if (!this.stripeSubscriptionId) { + return false; + } + const stripeSubscription = await getStripeSubscription( + this.stripeSubscriptionId + ); + if (!stripeSubscription) { + return false; + } + + return SubscriptionResource.isStripeSubscriptionOnProPlan( + owner, + stripeSubscription + ); + } } diff --git a/front/migrations/20240912_backfill_editedbyuser_id.ts b/front/migrations/20240912_backfill_editedbyuser_id.ts index 69e2d9b913a4..a1ff9fd220bc 100644 --- a/front/migrations/20240912_backfill_editedbyuser_id.ts +++ b/front/migrations/20240912_backfill_editedbyuser_id.ts @@ -3,7 +3,7 @@ import { Op } from "sequelize"; import { Workspace } from "@app/lib/models/workspace"; import { DataSourceModel } from "@app/lib/resources/storage/models/data_source"; -import { getWorkspaceFirstAdmin } from "@app/lib/workspace"; +import { getWorkspaceFirstAdmin, renderLightWorkspaceType } from "@app/lib/workspace"; import { makeScript } from "@app/scripts/helpers"; makeScript({}, async ({ execute }, logger) => { diff --git a/front/pages/api/w/[wId]/subscriptions/index.ts b/front/pages/api/w/[wId]/subscriptions/index.ts index cbdc46a15309..0ce273fee9b0 100644 --- a/front/pages/api/w/[wId]/subscriptions/index.ts +++ b/front/pages/api/w/[wId]/subscriptions/index.ts @@ -94,9 +94,11 @@ async function handler( } try { - const { checkoutUrl, plan: newPlan } = - await SubscriptionResource.getCheckoutUrlForUpgrade( - auth, + const { checkoutUrl, plan: newPlan } = await auth + .getNonNullableSubscriptionResource() + .getCheckoutUrlForUpgrade( + auth.getNonNullableWorkspace(), + auth.getNonNullableUser(), bodyValidation.right.billingPeriod ); return res.status(200).json({ checkoutUrl, plan: newPlan });