diff --git a/front/lib/api/workspace.ts b/front/lib/api/workspace.ts index 9e0f66169441..ee4edf198aa8 100644 --- a/front/lib/api/workspace.ts +++ b/front/lib/api/workspace.ts @@ -1,6 +1,7 @@ import type { LightWorkspaceType, MembershipRoleType, + Result, RoleType, SubscriptionType, UserTypeWithWorkspaces, @@ -8,14 +9,18 @@ import type { WorkspaceSegmentationType, WorkspaceType, } from "@dust-tt/types"; -import { ACTIVE_ROLES } from "@dust-tt/types"; +import { ACTIVE_ROLES, Err, Ok } from "@dust-tt/types"; +import { Op } from "sequelize"; import type { PaginationParams } from "@app/lib/api/pagination"; import type { Authenticator } from "@app/lib/auth"; +import { Subscription } from "@app/lib/models/plan"; import { Workspace, WorkspaceHasDomain } from "@app/lib/models/workspace"; +import { getStripeSubscription } from "@app/lib/plans/stripe"; import { MembershipResource } from "@app/lib/resources/membership_resource"; import { UserResource } from "@app/lib/resources/user_resource"; import { renderLightWorkspaceType } from "@app/lib/workspace"; +import { launchDeleteWorkspaceWorkflow } from "@app/poke/temporal/client"; export async function getWorkspaceInfos( wId: string @@ -282,3 +287,56 @@ export async function unsafeGetWorkspacesByModelId( }) ).map((w) => renderLightWorkspaceType({ workspace: w })); } + +export async function areAllSubscriptionsCanceled( + workspace: LightWorkspaceType +): Promise { + const subscriptions = await Subscription.findAll({ + where: { + workspaceId: workspace.id, + stripeSubscriptionId: { + [Op.not]: null, + }, + }, + }); + + // If the workspace had a subscription, it must be canceled. + if (subscriptions.length > 0) { + for (const sub of subscriptions) { + if (!sub.stripeSubscriptionId) { + continue; + } + + const stripeSubscription = await getStripeSubscription( + sub.stripeSubscriptionId + ); + + if (!stripeSubscription) { + continue; + } + + if (stripeSubscription.status !== "canceled") { + return false; + } + } + } + + return true; +} + +export async function deleteWorkspace( + owner: LightWorkspaceType +): Promise> { + const allSubscriptionsCanceled = await areAllSubscriptionsCanceled(owner); + if (!allSubscriptionsCanceled) { + return new Err( + new Error( + "The workspace cannot be deleted because there are active subscriptions." + ) + ); + } + + await launchDeleteWorkspaceWorkflow({ workspaceId: owner.sId }); + + return new Ok(undefined); +} diff --git a/front/pages/api/poke/workspaces/[wId]/index.ts b/front/pages/api/poke/workspaces/[wId]/index.ts index f80840fceec4..7b2bc21f8cbe 100644 --- a/front/pages/api/poke/workspaces/[wId]/index.ts +++ b/front/pages/api/poke/workspaces/[wId]/index.ts @@ -4,11 +4,13 @@ import * as t from "io-ts"; import * as reporter from "io-ts-reporters"; import type { NextApiRequest, NextApiResponse } from "next"; -import { setInternalWorkspaceSegmentation } from "@app/lib/api/workspace"; +import { + deleteWorkspace, + setInternalWorkspaceSegmentation, +} from "@app/lib/api/workspace"; import { withSessionAuthentication } from "@app/lib/api/wrappers"; import { Authenticator, getSession } from "@app/lib/auth"; import { apiError } from "@app/logger/withlogging"; -import { launchDeleteWorkspaceWorkflow } from "@app/poke/temporal/client"; export const WorkspaceTypeSchema = t.type({ segmentation: t.union([t.literal("interesting"), t.null]), @@ -71,9 +73,21 @@ async function handler( return res.status(200).json({ workspace, }); - case "DELETE": - await launchDeleteWorkspaceWorkflow({ workspaceId: owner.sId }); + + case "DELETE": { + const deleteRes = await deleteWorkspace(owner); + if (deleteRes.isErr()) { + return apiError(req, res, { + status_code: 400, + api_error: { + type: "invalid_request_error", + message: deleteRes.error.message, + }, + }); + } + return res.status(200).json({ success: true }); + } default: return apiError(req, res, { diff --git a/front/poke/temporal/activities.ts b/front/poke/temporal/activities.ts index 10e83c6b9aac..7d75914e03eb 100644 --- a/front/poke/temporal/activities.ts +++ b/front/poke/temporal/activities.ts @@ -8,6 +8,7 @@ import { hardDeleteApp } from "@app/lib/api/apps"; import config from "@app/lib/api/config"; import { hardDeleteDataSource } from "@app/lib/api/data_sources"; import { hardDeleteVault } from "@app/lib/api/vaults"; +import { areAllSubscriptionsCanceled } from "@app/lib/api/workspace"; import { Authenticator } from "@app/lib/auth"; import { AgentBrowseAction } from "@app/lib/models/assistant/actions/browse"; import { AgentDataSourceConfiguration } from "@app/lib/models/assistant/actions/data_sources"; @@ -56,6 +57,7 @@ import { frontSequelize } from "@app/lib/resources/storage"; import { Provider } from "@app/lib/resources/storage/models/apps"; import { UserResource } from "@app/lib/resources/user_resource"; import { VaultResource } from "@app/lib/resources/vault_resource"; +import { renderLightWorkspaceType } from "@app/lib/workspace"; import logger from "@app/logger/logger"; export async function scrubDataSourceActivity({ @@ -158,28 +160,9 @@ export async function isWorkflowDeletableActivity({ workspaceId: string; }) { const auth = await Authenticator.internalAdminForWorkspace(workspaceId); - const workspace = auth.workspace(); - if (!workspace) { - return false; - } - - // Workspace must have no data sources. - if ( - (await DataSourceResource.listByWorkspace(auth, { limit: 1 })).length > 0 - ) { - return false; - } + const workspace = await auth.getNonNullableWorkspace(); - // For now we don't support deleting workspaces who had a paid subscription at some point. - const subscriptions = await Subscription.findAll({ - where: { - workspaceId: workspace.id, - stripeSubscriptionId: { - [Op.not]: null, - }, - }, - }); - return subscriptions.length === 0; + return areAllSubscriptionsCanceled(renderLightWorkspaceType({ workspace })); } export async function deleteConversationsActivity({