gergo/workspaceDefaultPlan (#3561)

* feat(gatekeeper): create workspaces with trial plan by default

* feat(gatekeeper): default to starter trial plan

* fix(eventBus): fix tests
This commit is contained in:
Gergő Jedlicska
2024-11-27 09:51:32 +01:00
committed by GitHub
parent c841bb45f2
commit f381dc3d9d
12 changed files with 98 additions and 60 deletions
@@ -52,6 +52,7 @@ const command: CommandModule<
await upsertPaidWorkspacePlanFactory({ db })({
workspacePlan: {
createdAt: new Date(),
workspaceId: workspace.id,
name: args.plan,
status: args.status
@@ -26,6 +26,7 @@ export type PlanStatuses =
type BaseWorkspacePlan = {
workspaceId: string
createdAt: Date
}
export type PaidWorkspacePlan = BaseWorkspacePlan & {
@@ -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()
}
})
})
]
@@ -62,7 +62,6 @@ const scheduleWorkspaceSubscriptionDownscale = () => {
'WorkspaceSubscriptionDownscale',
async () => {
await manageSubscriptionDownscale()
// await cleanOrphanedWebhookConfigs()
}
)
}
@@ -0,0 +1,16 @@
import { Knex } from 'knex'
export async function up(knex: Knex): Promise<void> {
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<void> {
await knex.schema.alterTable('workspace_plans', (table) => {
table.dropColumn('createdAt')
})
}
@@ -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 }) => {
@@ -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'
@@ -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({
@@ -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'
@@ -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 }) => {
@@ -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<Workspace, 'domains'> => {
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)
})
})
})
@@ -134,6 +134,7 @@ export const createTestWorkspace = async (
if (addPlan) {
await upsertWorkspacePlan({
workspacePlan: {
createdAt: new Date(),
workspaceId: newWorkspace.id,
name: 'business',
status: 'valid'