af3857a209
* feat(gatekeeper): add gatekeeper module feature flag * feat(gatekeeper): add workspace pricing table domain * feat(gatekeeper): add checkout session creation * feat(gatekeeper): verify stripe signature * wip(gatekeeper): checkout callbacks * feat(gatekeeper): add unlimited and academia plan types * refactor(envHelper): getStringFromEnv helper * chore(gatekeeper): add future todos * feat(gatekeeper): add productId to the subscription domain * feat(gatekeeper): add in memory repositories * feat(gatekeeper): add more errors * feat(gatekeeper): complete checkout session service * feat(gatekeeper): add stripe client implementation * feat(gatekeeper): add checkout session completion webhook callback path * feat(gendo): fix not needing env vars if gendo module is not enabled * feat(gatekeeper): require a license for billing * chore(gatekeeper): cleanup before testing * feat(gatekeeper): subscriptionData parsing model * ci: add billing integration and gatekeeper modules to test config * test(gatekeeper): add checkout service tests * feat(gatekeeper): make completeCheckout callback idempotent properly * feat(gatekeeper): move to knex based repositories * test(gatekeeper): billing repository tests * feat(gatekeeper): add yearly billing cycle toggle * feat(ci): add stripe integration context to test job * feat(billingPage): conditionally render the checkout CTAs * fix(gatekeeper): remove flaky test condition * feat(helm): add billing integration feature flag * WIP billing gql api * feat(gatekeeper): cancel checkout session api * feat(gatekeeper): handle existing checkout sessions, when trying to create a new one * feat(gatekeeper): add workspace plans gql api * feat(gatekeeper): handle cancelation and subscription updates * fix(gatekeeper): scope initialization * fix(gatekeeper): eliminate stripe client import sideeffect * fix(gatekeeper): eliminate stripe client import sideeffect 2 * fix(mainConstants): fitler gatekeeper scopes with feature flag
263 lines
9.8 KiB
TypeScript
263 lines
9.8 KiB
TypeScript
import {
|
|
SubscriptionData,
|
|
WorkspacePlan,
|
|
WorkspaceSubscription
|
|
} from '@/modules/gatekeeper/domain/billing'
|
|
import {
|
|
WorkspacePlanMismatchError,
|
|
WorkspacePlanNotFoundError,
|
|
WorkspaceSubscriptionNotFoundError
|
|
} from '@/modules/gatekeeper/errors/billing'
|
|
import { handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions'
|
|
import { expectToThrow } from '@/test/assertionHelper'
|
|
import { expect } from 'chai'
|
|
import cryptoRandomString from 'crypto-random-string'
|
|
import { merge } from 'lodash'
|
|
|
|
const createTestSubscriptionData = (
|
|
overrides: Partial<SubscriptionData> = {}
|
|
): SubscriptionData => {
|
|
const defaultValues: SubscriptionData = {
|
|
cancelAt: null,
|
|
customerId: cryptoRandomString({ length: 10 }),
|
|
products: [
|
|
{
|
|
priceId: cryptoRandomString({ length: 10 }),
|
|
productId: cryptoRandomString({ length: 10 }),
|
|
quantity: 3,
|
|
subscriptionItemId: cryptoRandomString({ length: 10 })
|
|
}
|
|
],
|
|
status: 'active',
|
|
subscriptionId: cryptoRandomString({ length: 10 })
|
|
}
|
|
return merge(defaultValues, overrides)
|
|
}
|
|
|
|
describe('subscriptions @gatekeeper', () => {
|
|
describe('handleSubscriptionUpdateFactory creates a function, that', () => {
|
|
it('throws if subscription is not found', async () => {
|
|
const subscriptionData = createTestSubscriptionData()
|
|
const err = await expectToThrow(async () => {
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => null,
|
|
getWorkspacePlan: async () => {
|
|
expect.fail()
|
|
},
|
|
upsertWorkspaceSubscription: async () => {
|
|
expect.fail()
|
|
},
|
|
upsertPaidWorkspacePlan: async () => {
|
|
expect.fail()
|
|
}
|
|
})({ subscriptionData })
|
|
})
|
|
expect(err.message).to.equal(new WorkspaceSubscriptionNotFoundError().message)
|
|
})
|
|
it('throws if workspacePlan is not found', async () => {
|
|
const subscriptionData = createTestSubscriptionData()
|
|
const err = await expectToThrow(async () => {
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => ({
|
|
subscriptionData,
|
|
billingInterval: 'monthly',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId: cryptoRandomString({ length: 10 })
|
|
}),
|
|
getWorkspacePlan: async () => null,
|
|
upsertWorkspaceSubscription: async () => {
|
|
expect.fail()
|
|
},
|
|
upsertPaidWorkspacePlan: async () => {
|
|
expect.fail()
|
|
}
|
|
})({ subscriptionData })
|
|
})
|
|
expect(err.message).to.equal(new WorkspacePlanNotFoundError().message)
|
|
})
|
|
;(['unlimited', 'academia'] as const).forEach((name) =>
|
|
it(`throws for non paid workspace plan: ${name}`, async () => {
|
|
const subscriptionData = createTestSubscriptionData()
|
|
const workspaceId = cryptoRandomString({ length: 10 })
|
|
const err = await expectToThrow(async () => {
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => ({
|
|
subscriptionData,
|
|
billingInterval: 'monthly',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId
|
|
}),
|
|
getWorkspacePlan: async () => ({ name, workspaceId, status: 'valid' }),
|
|
upsertWorkspaceSubscription: async () => {
|
|
expect.fail()
|
|
},
|
|
upsertPaidWorkspacePlan: async () => {
|
|
expect.fail()
|
|
}
|
|
})({ subscriptionData })
|
|
})
|
|
expect(err.message).to.equal(new WorkspacePlanMismatchError().message)
|
|
})
|
|
)
|
|
it('sets the state to cancelationScheduled', async () => {
|
|
const subscriptionData = createTestSubscriptionData({
|
|
status: 'active',
|
|
cancelAt: new Date(2099, 12, 31)
|
|
})
|
|
const workspaceId = cryptoRandomString({ length: 10 })
|
|
const workspaceSubscription = {
|
|
subscriptionData,
|
|
billingInterval: 'monthly' as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId
|
|
}
|
|
|
|
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
|
let updatedPlan: WorkspacePlan | undefined = undefined
|
|
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
|
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
|
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
|
updatedSubscription = workspaceSubscription
|
|
},
|
|
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
|
updatedPlan = workspacePlan
|
|
}
|
|
})({ subscriptionData })
|
|
expect(updatedPlan!.status).to.be.equal('cancelationScheduled')
|
|
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
|
})
|
|
it('sets the state to valid', async () => {
|
|
const subscriptionData = createTestSubscriptionData({
|
|
status: 'active',
|
|
cancelAt: null
|
|
})
|
|
const workspaceId = cryptoRandomString({ length: 10 })
|
|
const workspaceSubscription = {
|
|
subscriptionData,
|
|
billingInterval: 'monthly' as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId
|
|
}
|
|
|
|
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
|
let updatedPlan: WorkspacePlan | undefined = undefined
|
|
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
|
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
|
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
|
updatedSubscription = workspaceSubscription
|
|
},
|
|
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
|
updatedPlan = workspacePlan
|
|
}
|
|
})({ subscriptionData })
|
|
expect(updatedPlan!.status).to.be.equal('valid')
|
|
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
|
})
|
|
it('sets the state to paymentFailed', async () => {
|
|
const subscriptionData = createTestSubscriptionData({
|
|
status: 'past_due'
|
|
})
|
|
const workspaceId = cryptoRandomString({ length: 10 })
|
|
const workspaceSubscription = {
|
|
subscriptionData,
|
|
billingInterval: 'monthly' as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId
|
|
}
|
|
|
|
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
|
let updatedPlan: WorkspacePlan | undefined = undefined
|
|
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
|
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
|
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
|
updatedSubscription = workspaceSubscription
|
|
},
|
|
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
|
updatedPlan = workspacePlan
|
|
}
|
|
})({ subscriptionData })
|
|
expect(updatedPlan!.status).to.be.equal('paymentFailed')
|
|
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
|
})
|
|
it('sets the state to canceled', async () => {
|
|
const subscriptionData = createTestSubscriptionData({
|
|
status: 'canceled'
|
|
})
|
|
const workspaceId = cryptoRandomString({ length: 10 })
|
|
const workspaceSubscription = {
|
|
subscriptionData,
|
|
billingInterval: 'monthly' as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId
|
|
}
|
|
|
|
let updatedSubscription: WorkspaceSubscription | undefined = undefined
|
|
let updatedPlan: WorkspacePlan | undefined = undefined
|
|
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
|
getWorkspacePlan: async () => ({ name: 'team', workspaceId, status: 'trial' }),
|
|
upsertWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
|
updatedSubscription = workspaceSubscription
|
|
},
|
|
upsertPaidWorkspacePlan: async ({ workspacePlan }) => {
|
|
updatedPlan = workspacePlan
|
|
}
|
|
})({ subscriptionData })
|
|
expect(updatedPlan!.status).to.be.equal('canceled')
|
|
expect(updatedSubscription).deep.equal(workspaceSubscription)
|
|
})
|
|
;(
|
|
['incomplete', 'incomplete_expired', 'trialing', 'unpaid', 'paused'] as const
|
|
).forEach((status) => {
|
|
it(`does not update the plan or the subscription in case of an unhandled status: ${status}`, async () => {
|
|
const subscriptionData = createTestSubscriptionData({
|
|
status
|
|
})
|
|
const workspaceId = cryptoRandomString({ length: 10 })
|
|
const workspaceSubscription = {
|
|
subscriptionData,
|
|
billingInterval: 'monthly' as const,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
currentBillingCycleEnd: new Date(),
|
|
workspaceId
|
|
}
|
|
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspaceSubscriptionBySubscriptionId: async () => workspaceSubscription,
|
|
getWorkspacePlan: async () => ({
|
|
name: 'team',
|
|
workspaceId,
|
|
status: 'trial'
|
|
}),
|
|
upsertWorkspaceSubscription: async () => {
|
|
expect.fail()
|
|
},
|
|
upsertPaidWorkspacePlan: async () => {
|
|
expect.fail()
|
|
}
|
|
})({ subscriptionData })
|
|
})
|
|
})
|
|
})
|
|
})
|