From dc6824f8399385231c04c211076ec285951af1e7 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:46:49 +0100 Subject: [PATCH] chore(server/migrations): disable workspace plan migrations --- packages/server/modules/gatekeeper/index.ts | 21 +- .../gatekeeper/services/planMigration.ts | 276 ------------------ 2 files changed, 1 insertion(+), 296 deletions(-) delete mode 100644 packages/server/modules/gatekeeper/services/planMigration.ts diff --git a/packages/server/modules/gatekeeper/index.ts b/packages/server/modules/gatekeeper/index.ts index f7b6e763d..eaea92255 100644 --- a/packages/server/modules/gatekeeper/index.ts +++ b/packages/server/modules/gatekeeper/index.ts @@ -53,7 +53,6 @@ import { manageSubscriptionDownscaleFactoryOld } from '@/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale' import { countSeatsByTypeInWorkspaceFactory } from '@/modules/gatekeeper/repositories/workspaceSeat' -import { migrateOldWorkspacePlans } from '@/modules/gatekeeper/services/planMigration' const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags() @@ -111,23 +110,6 @@ const scheduleWorkspaceSubscriptionDownscale = ({ ) } -const scheduleWorkspacePlanMigrations = (scheduleExecution: ScheduleExecution) => { - let isMigrationComplete = false - let isMigrationRunning = false - const cronExpression = '*/5 * * * * *' // every 5 seconds - return scheduleExecution( - cronExpression, - 'WorkspaceNewPlanMigration', - async (_scheduledTime, { logger }) => { - if (isMigrationComplete || isMigrationRunning) return - isMigrationRunning = true - await migrateOldWorkspacePlans({ db, stripe: getStripeClient(), logger })() - isMigrationRunning = false - isMigrationComplete = true - } - ) -} - const scheduleWorkspaceTrialEmails = ({ scheduleExecution }: { @@ -249,8 +231,7 @@ const gatekeeperModule: SpeckleModule = { scheduledTasks = [ scheduleWorkspaceSubscriptionDownscale({ scheduleExecution }), scheduleWorkspaceTrialEmails({ scheduleExecution }), - scheduleWorkspaceTrialExpiry({ scheduleExecution, emit: eventBus.emit }), - scheduleWorkspacePlanMigrations(scheduleExecution) + scheduleWorkspaceTrialExpiry({ scheduleExecution, emit: eventBus.emit }) ] quitListeners = initializeEventListenersFactory({ diff --git a/packages/server/modules/gatekeeper/services/planMigration.ts b/packages/server/modules/gatekeeper/services/planMigration.ts deleted file mode 100644 index 7016e4c61..000000000 --- a/packages/server/modules/gatekeeper/services/planMigration.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { - getWorkspacePlanFactory, - getWorkspaceSubscriptionFactory, - upsertWorkspacePlanFactory -} from '@/modules/gatekeeper/repositories/billing' -import { - throwUncoveredError, - WorkspacePlan, - WorkspacePlans, - WorkspacePlanStatuses -} from '@speckle/shared' -import { getWorkspaceRolesFactory } from '@/modules/workspaces/repositories/workspaces' -import { - SubscriptionDataInput, - WorkspaceSeat -} from '@/modules/gatekeeper/domain/billing' -import { Knex } from 'knex' -import { - getWorkspacePlanPriceId, - getWorkspacePlanProductId -} from '@/modules/gatekeeper/stripe' -import { reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe' -import Stripe from 'stripe' -import { cloneDeep } from 'lodash' -import { Logger } from '@/observability/logging' -import { withTransaction } from '@/modules/shared/helpers/dbHelper' - -export const migrateOldWorkspacePlans = - ({ db, stripe, logger }: { db: Knex; stripe: Stripe; logger: Logger }) => - async () => { - const oldPlanWorkspaces = await db( - 'workspace_plans' - ) - .select('*') - .whereIn('name', [ - 'business', - 'businessInvoiced', - 'plus', - 'plusInvoiced', - 'starter', - 'starterInvoiced', - 'academia', - 'unlimited' - ]) - - if (oldPlanWorkspaces.length === 0) { - logger.info('No old workspace plans to migrate') - return - } - - for (const oldPlan of oldPlanWorkspaces) { - try { - await withTransaction( - async ({ db }) => { - await migrateWorkspacePlan({ db, stripe, logger })({ - workspaceId: oldPlan.workspaceId - }) - }, - { db } - ) - } catch (err) { - logger.error( - { err, workspaceId: oldPlan.workspaceId, oldPlan }, - 'Failed to migrate workspace plan' - ) - } - } - } - -export const migrateWorkspacePlan = - ({ db, stripe, logger }: { db: Knex; stripe: Stripe; logger: Logger }) => - async ({ workspaceId }: { workspaceId: string }) => { - let log = logger.child({ workspaceId }) - log.info('Starting workspace plan migration for {workspaceId}') - const workspacePlan = await getWorkspacePlanFactory({ db })({ workspaceId }) - if (!workspacePlan) - throw new Error(`Workspace ${workspaceId} has no workspace plan`) - - log = log.child({ workspacePlan }) - - let newTargetPlan: WorkspacePlans | null = null - let newPlanStatus: WorkspacePlanStatuses | null = null - let isStripeMigrationNeeded = false - switch (workspacePlan.name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': - case 'teamUnlimitedInvoiced': - case 'proUnlimitedInvoiced': - // these are new plans already, no upgrades - break - case 'starter': - switch (workspacePlan.status) { - case 'trial': - case 'expired': - newPlanStatus = 'valid' - newTargetPlan = 'free' - break - case 'paymentFailed': - throw new Error( - `Cant migrate workspace ${workspaceId}, its currently an old 'starter' plan that has failed in payment` - ) - case 'canceled': - // just switch the plan, no need to change stripe - newTargetPlan = 'teamUnlimited' - newPlanStatus = workspacePlan.status - break - case 'cancelationScheduled': - case 'valid': - newTargetPlan = 'teamUnlimited' - newPlanStatus = workspacePlan.status - isStripeMigrationNeeded = true - break - default: - throwUncoveredError(workspacePlan) - } - break - case 'plus': - case 'business': - switch (workspacePlan.status) { - case 'paymentFailed': - throw new Error( - `Cant migrate workspace ${workspaceId}, its currently an old 'business' plan that has failed in payment` - ) - case 'canceled': - newTargetPlan = 'proUnlimited' - isStripeMigrationNeeded = false - newPlanStatus = workspacePlan.status - break - case 'cancelationScheduled': - case 'valid': - newTargetPlan = 'proUnlimited' - isStripeMigrationNeeded = true - newPlanStatus = workspacePlan.status - break - default: - throwUncoveredError(workspacePlan) - } - break - case 'starterInvoiced': - newTargetPlan = 'teamUnlimitedInvoiced' - newPlanStatus = workspacePlan.status - break - case 'plusInvoiced': - case 'businessInvoiced': - newTargetPlan = 'proUnlimitedInvoiced' - newPlanStatus = workspacePlan.status - break - case 'unlimited': - case 'academia': - newTargetPlan = workspacePlan.name - newPlanStatus = workspacePlan.status - break - case 'free': - break - - default: - throwUncoveredError(workspacePlan) - } - - if (!newTargetPlan) { - log.info('No migration needed for {workspaceId} from old plan {workspacePlan}') - return - } - - log.info( - { newTargetPlan, newPlanStatus, isStripeMigrationNeeded }, - 'Migrating {workspaceId} from old plan {workspacePlan} to new plan {newTargetPlan}' - ) - - // add editor seats to everyone - - const workspaceMembers = await getWorkspaceRolesFactory({ db })({ - workspaceId - }) - const seats = workspaceMembers.map((m) => ({ - workspaceId, - userId: m.userId, - type: 'editor' as const, - createdAt: new Date(), - updatedAt: new Date() - })) - log.debug( - { migratedSeats: seats, migratedSeatsCount: seats.length }, - 'Inserting {migratedSeatsCount} new seats for the workspace {workspaceId}' - ) - - await db('workspace_seats') - .insert(seats) - .onConflict(['workspaceId', 'userId']) - .merge() - - log.debug( - { migratedSeatsCount: seats.length }, - 'Workspace {workspaceId} has added {migratedSeatsCount} seats' - ) - await upsertWorkspacePlanFactory({ db })({ - //@ts-expect-error the switch above makes sure things are ok - workspacePlan: { - workspaceId, - name: newTargetPlan, - status: newPlanStatus ?? workspacePlan.status, - createdAt: workspacePlan.createdAt - } - }) - log.debug( - 'workspace {workspaceId} has had plan {workspacePlan} changed to the new plan {newTargetPlan}' - ) - if (isStripeMigrationNeeded) { - log.info('Migrating stripe subscription data for workspace {workspaceId}') - switch (newTargetPlan) { - case 'academia': - case 'free': - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': - case 'unlimited': - // this is just double checking that everything is right - // the switch above sets things up properly - throw new Error( - `Cannot upgrade stripe for a non paid plan for workspace ${workspaceId}` - ) - } - // if stripe paid plan, convert the stripe sub to use all editor seats - const workspaceSubscription = await getWorkspaceSubscriptionFactory({ db })({ - workspaceId - }) - if (!workspaceSubscription) - throw new Error( - `Subscription data not found for workspace ${workspaceId}, cannot do stripe migration` - ) - - let memberAndGuestSeatCount = workspaceSubscription.subscriptionData.products - .map((p) => p.quantity) - // we're just summing all the seats - .reduce((acc, curr) => acc + curr, 0) - - const workspaceTeamCount = workspaceMembers.length - if (memberAndGuestSeatCount < workspaceTeamCount) { - log.warn( - { memberAndGuestSeatCount, workspaceTeamCount }, - 'Workspace {workspaceId} has less paid member and guest seats, than people in the workspace. Reconciling' - ) - memberAndGuestSeatCount = workspaceTeamCount - } - const productId = getWorkspacePlanProductId({ workspacePlan: newTargetPlan }) - const priceId = getWorkspacePlanPriceId({ - workspacePlan: newTargetPlan, - billingInterval: workspaceSubscription.billingInterval, - currency: workspaceSubscription.currency - }) - - const subscriptionData: SubscriptionDataInput = cloneDeep( - workspaceSubscription.subscriptionData - ) - subscriptionData.products = [] - - subscriptionData.products.push({ - productId, - priceId, - quantity: memberAndGuestSeatCount - }) - - await reconcileWorkspaceSubscriptionFactory({ stripe })({ - subscriptionData, - prorationBehavior: 'create_prorations' - }) - } - log.info('🥳 Workspace plan migration completed for workspace {workspaceId}') - - // add and editor seat to all workspace members - // convert current plan to the new plan - // if plan in cancelled, still convert to the new plan - // if cancellation scheduled, skip migration, we'll deal with that manually - // - }