From f381dc3d9d2c6dabeb74d465d0ef1839a3e48d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= <57442769+gjedlicska@users.noreply.github.com> Date: Wed, 27 Nov 2024 09:51:32 +0100 Subject: [PATCH] gergo/workspaceDefaultPlan (#3561) * feat(gatekeeper): create workspaces with trial plan by default * feat(gatekeeper): default to starter trial plan * fix(eventBus): fix tests --- .../cli/commands/workspaces/set-plan.ts | 1 + .../modules/gatekeeper/domain/billing.ts | 1 + .../gatekeeper/events/eventListener.ts | 13 +++- packages/server/modules/gatekeeper/index.ts | 1 - .../20241126142602_workspace_plan_date.ts | 16 ++++ .../gatekeeper/repositories/billing.ts | 9 ++- .../modules/gatekeeper/services/checkout.ts | 1 + .../intergration/billingRepositories.spec.ts | 4 +- .../gatekeeper/tests/unit/checkout.spec.ts | 13 +++- .../tests/unit/subscriptions.spec.ts | 23 +++++- .../modules/shared/test/unit/eventBus.spec.ts | 75 ++++++------------- .../workspaces/tests/helpers/creation.ts | 1 + 12 files changed, 98 insertions(+), 60 deletions(-) create mode 100644 packages/server/modules/gatekeeper/migrations/20241126142602_workspace_plan_date.ts diff --git a/packages/server/modules/cli/commands/workspaces/set-plan.ts b/packages/server/modules/cli/commands/workspaces/set-plan.ts index 6d454d8ee..3f18f6e51 100644 --- a/packages/server/modules/cli/commands/workspaces/set-plan.ts +++ b/packages/server/modules/cli/commands/workspaces/set-plan.ts @@ -52,6 +52,7 @@ const command: CommandModule< await upsertPaidWorkspacePlanFactory({ db })({ workspacePlan: { + createdAt: new Date(), workspaceId: workspace.id, name: args.plan, status: args.status diff --git a/packages/server/modules/gatekeeper/domain/billing.ts b/packages/server/modules/gatekeeper/domain/billing.ts index 27ec5be83..d3be141cd 100644 --- a/packages/server/modules/gatekeeper/domain/billing.ts +++ b/packages/server/modules/gatekeeper/domain/billing.ts @@ -26,6 +26,7 @@ export type PlanStatuses = type BaseWorkspacePlan = { workspaceId: string + createdAt: Date } export type PaidWorkspacePlan = BaseWorkspacePlan & { diff --git a/packages/server/modules/gatekeeper/events/eventListener.ts b/packages/server/modules/gatekeeper/events/eventListener.ts index e2ae7fc97..c9b102904 100644 --- a/packages/server/modules/gatekeeper/events/eventListener.ts +++ b/packages/server/modules/gatekeeper/events/eventListener.ts @@ -1,7 +1,8 @@ import { reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe' import { getWorkspacePlanFactory, - getWorkspaceSubscriptionFactory + getWorkspaceSubscriptionFactory, + upsertTrialWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing' import { addWorkspaceSubscriptionSeatIfNeededFactory } from '@/modules/gatekeeper/services/subscriptions' import { @@ -33,6 +34,16 @@ export const initializeEventListenersFactory = }) await addWorkspaceSubscriptionSeatIfNeeded(payload) + }), + eventBus.listen(WorkspaceEvents.Created, async ({ payload }) => { + await upsertTrialWorkspacePlanFactory({ db })({ + workspacePlan: { + name: 'starter', + status: 'trial', + workspaceId: payload.id, + createdAt: new Date() + } + }) }) ] diff --git a/packages/server/modules/gatekeeper/index.ts b/packages/server/modules/gatekeeper/index.ts index 81515b86e..45e9032bf 100644 --- a/packages/server/modules/gatekeeper/index.ts +++ b/packages/server/modules/gatekeeper/index.ts @@ -62,7 +62,6 @@ const scheduleWorkspaceSubscriptionDownscale = () => { 'WorkspaceSubscriptionDownscale', async () => { await manageSubscriptionDownscale() - // await cleanOrphanedWebhookConfigs() } ) } diff --git a/packages/server/modules/gatekeeper/migrations/20241126142602_workspace_plan_date.ts b/packages/server/modules/gatekeeper/migrations/20241126142602_workspace_plan_date.ts new file mode 100644 index 000000000..13d075c7a --- /dev/null +++ b/packages/server/modules/gatekeeper/migrations/20241126142602_workspace_plan_date.ts @@ -0,0 +1,16 @@ +import { Knex } from 'knex' + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('workspace_plans', (table) => { + table + .timestamp('createdAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('workspace_plans', (table) => { + table.dropColumn('createdAt') + }) +} diff --git a/packages/server/modules/gatekeeper/repositories/billing.ts b/packages/server/modules/gatekeeper/repositories/billing.ts index 986805d30..5fb44ba03 100644 --- a/packages/server/modules/gatekeeper/repositories/billing.ts +++ b/packages/server/modules/gatekeeper/repositories/billing.ts @@ -13,7 +13,8 @@ import { GetWorkspaceCheckoutSession, GetWorkspaceSubscription, GetWorkspaceSubscriptionBySubscriptionId, - GetWorkspaceSubscriptions + GetWorkspaceSubscriptions, + UpsertTrialWorkspacePlan } from '@/modules/gatekeeper/domain/billing' import { Knex } from 'knex' @@ -54,6 +55,12 @@ export const upsertPaidWorkspacePlanFactory = ({ db: Knex }): UpsertPaidWorkspacePlan => upsertWorkspacePlanFactory({ db }) +export const upsertTrialWorkspacePlanFactory = ({ + db +}: { + db: Knex +}): UpsertTrialWorkspacePlan => upsertWorkspacePlanFactory({ db }) + export const saveCheckoutSessionFactory = ({ db }: { db: Knex }): SaveCheckoutSession => async ({ checkoutSession }) => { diff --git a/packages/server/modules/gatekeeper/services/checkout.ts b/packages/server/modules/gatekeeper/services/checkout.ts index 94642eb05..92b497e67 100644 --- a/packages/server/modules/gatekeeper/services/checkout.ts +++ b/packages/server/modules/gatekeeper/services/checkout.ts @@ -167,6 +167,7 @@ export const completeCheckoutSessionFactory = // a plan determines the workspace feature set await upsertPaidWorkspacePlan({ workspacePlan: { + createdAt: new Date(), workspaceId: checkoutSession.workspaceId, name: checkoutSession.workspacePlan, status: 'valid' diff --git a/packages/server/modules/gatekeeper/tests/intergration/billingRepositories.spec.ts b/packages/server/modules/gatekeeper/tests/intergration/billingRepositories.spec.ts index 4bdb178c4..03f12477b 100644 --- a/packages/server/modules/gatekeeper/tests/intergration/billingRepositories.spec.ts +++ b/packages/server/modules/gatekeeper/tests/intergration/billingRepositories.spec.ts @@ -52,7 +52,8 @@ describe('billing repositories @gatekeeper', () => { const workspacePlan = { name: 'business', status: 'paymentFailed', - workspaceId + workspaceId, + createdAt: new Date() } as const await upsertPaidWorkspacePlan({ workspacePlan @@ -67,6 +68,7 @@ describe('billing repositories @gatekeeper', () => { const workspacePlan = { name: 'business', status: 'paymentFailed', + createdAt: new Date(), workspaceId } as const await upsertPaidWorkspacePlan({ diff --git a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts index f411bcf96..e3ec323a0 100644 --- a/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/checkout.spec.ts @@ -20,6 +20,7 @@ import { PaidWorkspacePlans, WorkspacePlanBillingIntervals } from '@/modules/gatekeeper/domain/workspacePricing' +import { omit } from 'lodash' describe('checkout @gatekeeper', () => { describe('startCheckoutSessionFactory creates a function, that', () => { @@ -30,6 +31,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'plus', status: 'valid', + createdAt: new Date(), workspaceId }), getWorkspaceCheckoutSession: () => { @@ -63,6 +65,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'plus', status: 'paymentFailed', + createdAt: new Date(), workspaceId }), getWorkspaceCheckoutSession: () => { @@ -96,6 +99,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', status: 'trial', + createdAt: new Date(), workspaceId }), getWorkspaceCheckoutSession: async () => ({ @@ -139,6 +143,8 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', status: 'trial', + createdAt: new Date(), + workspaceId }), getWorkspaceCheckoutSession: async () => ({ @@ -265,6 +271,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ workspaceId, name: 'starter', + createdAt: new Date(), status: 'trial' }), getWorkspaceCheckoutSession: async () => null, @@ -315,6 +322,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ workspaceId, name: 'starter', + createdAt: new Date(), status: 'trial' }), getWorkspaceCheckoutSession: async () => existingCheckoutSession!, @@ -356,6 +364,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ workspaceId, name: 'starter', + createdAt: new Date(), status: 'trial' }), getWorkspaceCheckoutSession: async () => existingCheckoutSession!, @@ -396,6 +405,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ workspaceId, name: 'starter', + createdAt: new Date(), status: 'trial' }), getWorkspaceCheckoutSession: async () => existingCheckoutSession!, @@ -448,6 +458,7 @@ describe('checkout @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'plus', workspaceId, + createdAt: new Date(), status: 'canceled' }), getWorkspaceCheckoutSession: async () => existingCheckoutSession!, @@ -578,7 +589,7 @@ describe('checkout @gatekeeper', () => { })({ sessionId, subscriptionId }) expect(storedCheckoutSession.paymentStatus).to.equal('paid') - expect(storedWorkspacePlan).to.deep.equal({ + expect(omit(storedWorkspacePlan, 'createdAt')).to.deep.equal({ workspaceId, name: storedCheckoutSession.workspacePlan, status: 'valid' diff --git a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts index 31bf88d7e..66116c9d2 100644 --- a/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts +++ b/packages/server/modules/gatekeeper/tests/unit/subscriptions.spec.ts @@ -73,7 +73,12 @@ describe('subscriptions @gatekeeper', () => { subscriptionData, workspaceId }), - getWorkspacePlan: async () => ({ name, workspaceId, status: 'valid' }), + getWorkspacePlan: async () => ({ + name, + workspaceId, + createdAt: new Date(), + status: 'valid' + }), upsertWorkspaceSubscription: async () => { expect.fail() }, @@ -104,6 +109,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', workspaceId, + createdAt: new Date(), status: 'trial' }), upsertWorkspaceSubscription: async ({ workspaceSubscription }) => { @@ -144,6 +150,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', workspaceId, + createdAt: new Date(), status: 'trial' }), upsertWorkspaceSubscription: async ({ workspaceSubscription }) => { @@ -180,6 +187,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', workspaceId, + createdAt: new Date(), status: 'trial' }), upsertWorkspaceSubscription: async ({ workspaceSubscription }) => { @@ -219,6 +227,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', workspaceId, + createdAt: new Date(), status: 'trial' }), upsertWorkspaceSubscription: async ({ workspaceSubscription }) => { @@ -255,6 +264,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'starter', workspaceId, + createdAt: new Date(), status: 'trial' }), upsertWorkspaceSubscription: async () => { @@ -302,6 +312,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'unlimited', workspaceId, + createdAt: new Date(), status: 'valid' }), getWorkspaceSubscription: async () => null, @@ -335,6 +346,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'unlimited', workspaceId, + createdAt: new Date(), status: 'valid' }), getWorkspaceSubscription: async () => workspaceSubscription, @@ -371,6 +383,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'plus', workspaceId, + createdAt: new Date(), status: 'canceled' }), getWorkspaceSubscription: async () => workspaceSubscription, @@ -402,6 +415,7 @@ describe('subscriptions @gatekeeper', () => { const workspacePlan: WorkspacePlan = { name: 'starter', workspaceId, + createdAt: new Date(), status: 'valid' } const priceId = cryptoRandomString({ length: 10 }) @@ -463,6 +477,7 @@ describe('subscriptions @gatekeeper', () => { const workspacePlan: WorkspacePlan = { name: 'starter', workspaceId, + createdAt: new Date(), status: 'valid' } const priceId = cryptoRandomString({ length: 10 }) @@ -541,6 +556,7 @@ describe('subscriptions @gatekeeper', () => { const workspacePlan: WorkspacePlan = { name: 'starter', workspaceId, + createdAt: new Date(), status: 'valid' } const roleCount = 10 @@ -612,6 +628,7 @@ describe('subscriptions @gatekeeper', () => { const workspacePlan: WorkspacePlan = { name: 'starter', workspaceId, + createdAt: new Date(), status: 'valid' } const roleCount = 1 @@ -690,6 +707,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'unlimited', workspaceId, + createdAt: new Date(), status: 'valid' }), countWorkspaceRole: async () => { @@ -718,6 +736,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: 'plus', workspaceId, + createdAt: new Date(), status: 'canceled' }), countWorkspaceRole: async () => { @@ -753,6 +772,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: workspacePlanName, workspaceId, + createdAt: new Date(), status: 'valid' }), countWorkspaceRole: async ({ workspaceRole }) => { @@ -807,6 +827,7 @@ describe('subscriptions @gatekeeper', () => { getWorkspacePlan: async () => ({ name: workspacePlanName, workspaceId, + createdAt: new Date(), status: 'valid' }), countWorkspaceRole: async ({ workspaceRole }) => { diff --git a/packages/server/modules/shared/test/unit/eventBus.spec.ts b/packages/server/modules/shared/test/unit/eventBus.spec.ts index 6fb246498..fe6ade070 100644 --- a/packages/server/modules/shared/test/unit/eventBus.spec.ts +++ b/packages/server/modules/shared/test/unit/eventBus.spec.ts @@ -1,26 +1,7 @@ import { getEventBus, initializeEventBus } from '@/modules/shared/services/eventBus' -import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events' -import { Workspace } from '@/modules/workspacesCore/domain/types' -import { Roles } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -const createFakeWorkspace = (): Omit => { - return { - id: cryptoRandomString({ length: 10 }), - slug: cryptoRandomString({ length: 10 }), - description: cryptoRandomString({ length: 10 }), - logo: null, - defaultLogoIndex: 0, - name: cryptoRandomString({ length: 10 }), - updatedAt: new Date(), - createdAt: new Date(), - defaultProjectRole: Roles.Stream.Contributor, - domainBasedMembershipProtectionEnabled: false, - discoverabilityEnabled: false - } -} - describe('Event Bus', () => { describe('initializeEventBus creates an event bus instance, that', () => { it('calls back all the listeners', async () => { @@ -106,69 +87,55 @@ describe('Event Bus', () => { const bus1 = getEventBus() const bus2 = getEventBus() - const workspaces: Workspace[] = [] + const payloads: string[] = [] - bus1.listen(WorkspaceEvents.Created, ({ payload }) => { - workspaces.push(payload) + bus1.listen('test.string', ({ payload }) => { + payloads.push(payload) }) - bus2.listen(WorkspaceEvents.Created, ({ payload }) => { - workspaces.push(payload) + bus2.listen('test.string', ({ payload }) => { + payloads.push(payload) }) - const workspacePayload = { - ...createFakeWorkspace(), - createdByUserId: cryptoRandomString({ length: 10 }), - eventName: WorkspaceEvents.Created, - domains: [] - } + const payload = cryptoRandomString({ length: 1 }) await bus1.emit({ - eventName: WorkspaceEvents.Created, - payload: { ...workspacePayload } + eventName: 'test.string', + payload }) - expect(workspaces.length).to.equal(2) - expect(workspaces).to.deep.equal([workspacePayload, workspacePayload]) + expect(payloads.length).to.equal(2) + expect(payloads).to.deep.equal([payload, payload]) }) it('allows to subscribe to wildcard events', async () => { const eventBus = getEventBus() const events: string[] = [] - eventBus.listen('workspace.*', ({ payload, eventName }) => { + eventBus.listen('test.*', ({ payload, eventName }) => { switch (eventName) { - case 'workspace.created': - events.push(payload.id) + case 'test.string': + events.push(payload) break - case 'workspace.role-deleted': - events.push(payload.userId) + case 'test.number': + events.push(`${payload}`) break } }) - const workspace = createFakeWorkspace() + const stringPayload = cryptoRandomString({ length: 10 }) await eventBus.emit({ - eventName: WorkspaceEvents.Created, - payload: { - ...workspace, - createdByUserId: cryptoRandomString({ length: 10 }) - } + eventName: 'test.string', + payload: stringPayload }) - const workspaceAcl = { - userId: cryptoRandomString({ length: 10 }), - workspaceId: cryptoRandomString({ length: 10 }), - role: Roles.Workspace.Member - } - await eventBus.emit({ - eventName: WorkspaceEvents.RoleDeleted, - payload: workspaceAcl + eventName: 'test.number', + payload: 999 }) - expect([workspace.id, workspaceAcl.userId]).to.deep.equal(events) + expect([stringPayload, `${999}`]).to.deep.equal(events) }) }) }) diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index 130df99c7..a685b7d50 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -134,6 +134,7 @@ export const createTestWorkspace = async ( if (addPlan) { await upsertWorkspacePlan({ workspacePlan: { + createdAt: new Date(), workspaceId: newWorkspace.id, name: 'business', status: 'valid'