Files
speckle-server/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts
T
Daniel Gak Anagrov 2c122a138d feat(workspaces): apply prepared transactions to workspaces (#5383)
* feat(multiregion): replace user replication

* chore(multiregion): optimise replication

* maybe it's this

* postgres is fun

* once more

* chore(multiregion): only replicate test user creation during multiregion tests

* feat: improved replicate_query logic

* fix: minor

* fix: starting issue

* feat: included user create and delete specs to multiregion

* feat: removed console logs

* fix: user defaults

* fix: multiregion test helper

* fix: update scenarios for users

* refactor(multiregion): swap replicateQuery concept to asMultiregionOperation (#5301)

feat(multiregion): introduced asMultregionOperator, refactor test to user builder classes

* chore: renamings

* fix: remove comments

* feat: remove user replication

* refactor: simplified spec usages

* chore: comments

* chore: branches and favs

* chore: more tests

* chore: more tests

* fix linting

* fix tests

* feat: dropping replication

* refactor: moved project delete to service

* fix: comment

* feat: updateStreamFactory and updateProjectFacotry

* deleteProjectFactory + replicateFactory

* deleteWorkspaceFactory

* fix: selector

* fix: tests

* fix tests, finished createStreamFactory

* feat: simplify changes

* fix: remove comment

* fix: minor strucutres

* fix: moveProjectToRegion

* fix: moved branch creation outside of multiregion scope

* fix: branch creation

* fix: tests

* fix: ci tests

* fix: removed log form test

* fix: on specs, no random regionKeys

* review fixes

* feat: workspace replciation

* fix: mr comments

* feat: removed test

* fix: worksapce test creation

* fix: mr issues

* updated mutations

* feat: drop workspace random defaults

---------

Co-authored-by: Charles Driesler <chuck@speckle.systems>
2025-09-11 10:08:26 +02:00

500 lines
19 KiB
TypeScript

import db from '@/db/knex'
import {
deleteCheckoutSessionFactory,
getCheckoutSessionFactory,
getWorkspaceCheckoutSessionFactory,
getWorkspacePlanFactory,
saveCheckoutSessionFactory,
upsertWorkspaceSubscriptionFactory,
updateCheckoutSessionStatusFactory,
upsertPaidWorkspacePlanFactory,
getWorkspaceSubscriptionFactory,
getWorkspaceSubscriptionBySubscriptionIdFactory,
getWorkspacesByPlanAgeFactory,
upsertWorkspacePlanFactory,
getWorkspaceSubscriptionsPastBillingCycleEndFactory
} from '@/modules/gatekeeper/repositories/billing'
import {
createTestSubscriptionData,
createTestWorkspaceSubscription
} from '@/modules/gatekeeper/tests/helpers'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command'
import type { UpsertWorkspace } from '@/modules/workspaces/domain/operations'
import { upsertWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
import { logger } from '@/observability/logging'
import { truncateTables } from '@/test/hooks'
import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces'
import { PaidWorkspacePlans, WorkspaceFeatureFlags } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
const upsertWorkspace: UpsertWorkspace = async (...args) =>
asMultiregionalOperation(
({ allDbs }) => replicateFactory(allDbs, upsertWorkspaceFactory)(...args),
{
logger,
name: 'delete workspace spec',
dbs: await getAllRegisteredDbs()
}
)
const createAndStoreTestWorkspace = createAndStoreTestWorkspaceFactory({
upsertWorkspace
})
const getWorkspacePlan = getWorkspacePlanFactory({ db })
const upsertPaidWorkspacePlan = upsertPaidWorkspacePlanFactory({ db })
const saveCheckoutSession = saveCheckoutSessionFactory({ db })
const deleteCheckoutSession = deleteCheckoutSessionFactory({ db })
const getCheckoutSession = getCheckoutSessionFactory({ db })
const getWorkspaceCheckoutSession = getWorkspaceCheckoutSessionFactory({ db })
const updateCheckoutSessionStatus = updateCheckoutSessionStatusFactory({ db })
const upsertWorkspaceSubscription = upsertWorkspaceSubscriptionFactory({ db })
const getWorkspaceSubscription = getWorkspaceSubscriptionFactory({ db })
const getWorkspaceSubscriptionBySubscriptionId =
getWorkspaceSubscriptionBySubscriptionIdFactory({ db })
const getWorkspacesByPlanAge = getWorkspacesByPlanAgeFactory({ db })
const getWorkspaceSubscriptionsPastBillingCycleEnd =
getWorkspaceSubscriptionsPastBillingCycleEndFactory({ db })
describe('billing repositories @gatekeeper', () => {
describe('workspacePlans', () => {
beforeEach(async () => {
await truncateTables(['workspace_plans'])
})
describe('upsertPaidWorkspacePlanFactory creates a function, that', () => {
it('creates a workspacePlan if it does not exist', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
let storedWorkspacePlan = await getWorkspacePlan({ workspaceId })
expect(storedWorkspacePlan).to.be.null
const workspacePlan = {
name: PaidWorkspacePlans.Team,
status: 'paymentFailed',
workspaceId,
createdAt: new Date(),
updatedAt: new Date(),
featureFlags: WorkspaceFeatureFlags.none
} as const
await upsertPaidWorkspacePlan({
workspacePlan
})
storedWorkspacePlan = await getWorkspacePlan({ workspaceId })
expect(storedWorkspacePlan).deep.equal(workspacePlan)
})
it('updates a workspacePlan name and status if a plan exists', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const workspacePlan = {
name: PaidWorkspacePlans.Team,
status: 'paymentFailed',
createdAt: new Date(),
updatedAt: new Date(),
featureFlags: WorkspaceFeatureFlags.none,
workspaceId
} as const
await upsertPaidWorkspacePlan({
workspacePlan
})
let storedWorkspacePlan = await getWorkspacePlan({ workspaceId })
expect(storedWorkspacePlan).deep.equal(workspacePlan)
const planUpdate = { ...workspacePlan, status: 'valid' } as const
await upsertPaidWorkspacePlan({ workspacePlan: planUpdate })
storedWorkspacePlan = await getWorkspacePlan({ workspaceId })
expect(storedWorkspacePlan).deep.equal(planUpdate)
})
})
describe('getWorkspaceByPlanAgeFactory returns a function, that', () => {
it('gets workspace where days to expire matches expected', async () => {
const workspace1 = await createAndStoreTestWorkspace()
const createdAt1 = new Date()
createdAt1.setHours(createdAt1.getHours() - 22)
const workspace1Plan = {
name: PaidWorkspacePlans.Team,
status: 'paymentFailed',
createdAt: createdAt1,
updatedAt: createdAt1,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace1.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspace1Plan
})
const workspace2 = await createAndStoreTestWorkspace()
const createdAt2 = new Date()
createdAt2.setHours(createdAt2.getHours() - 2)
const workspacePlan = {
name: PaidWorkspacePlans.Team,
status: 'paymentFailed',
createdAt: createdAt2,
updatedAt: createdAt2,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace2.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan
})
const workspacesByPlanByDaysTillExpire = await getWorkspacesByPlanAge({
planValidFor: 2,
daysTillExpiry: 2,
status: workspacePlan.status,
plan: workspacePlan.name
})
expect(workspacesByPlanByDaysTillExpire).to.deep.equalInAnyOrder([
workspace1,
workspace2
])
})
it('ignores workspaces where plans do not match', async () => {
const workspace1 = await createAndStoreTestWorkspace()
const createdAt1 = new Date()
createdAt1.setHours(createdAt1.getHours() - 22)
const workspace1Plan = {
name: PaidWorkspacePlans.Pro,
status: 'paymentFailed',
createdAt: createdAt1,
updatedAt: createdAt1,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace1.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspace1Plan
})
const workspace2 = await createAndStoreTestWorkspace()
const createdAt2 = new Date()
createdAt2.setHours(createdAt2.getHours() - 2)
const workspace2Plan = {
name: PaidWorkspacePlans.Team,
status: 'paymentFailed',
createdAt: createdAt2,
updatedAt: createdAt2,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace2.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspace2Plan
})
const workspacesByPlanByDaysTillExpire = await getWorkspacesByPlanAge({
planValidFor: 2,
daysTillExpiry: 2,
status: workspace2Plan.status,
plan: workspace2Plan.name
})
expect(workspacesByPlanByDaysTillExpire).to.deep.equalInAnyOrder([workspace2])
})
it('ignores workspaces where plan statuses do not match', async () => {
const workspace1 = await createAndStoreTestWorkspace()
const createdAt1 = new Date()
createdAt1.setHours(createdAt1.getHours() - 22)
const workspace1Plan = {
name: PaidWorkspacePlans.Team,
status: 'paymentFailed',
createdAt: createdAt1,
updatedAt: createdAt1,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace1.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspace1Plan
})
const workspace2 = await createAndStoreTestWorkspace()
const createdAt2 = new Date()
createdAt2.setHours(createdAt2.getHours() - 2)
const workspace2Plan = {
name: PaidWorkspacePlans.Team,
status: 'valid',
createdAt: createdAt2,
updatedAt: createdAt2,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace2.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspace2Plan
})
const workspacesByPlanByDaysTillExpire = await getWorkspacesByPlanAge({
planValidFor: 2,
daysTillExpiry: 2,
status: workspace2Plan.status,
plan: workspace2Plan.name
})
expect(workspacesByPlanByDaysTillExpire).to.deep.equalInAnyOrder([workspace2])
})
it('ignores workspaces where plan days till expiry do not match', async () => {
const workspace1 = await createAndStoreTestWorkspace()
const createdAt1 = new Date()
createdAt1.setHours(createdAt1.getHours() - 25)
const workspace1Plan = {
name: PaidWorkspacePlans.Team,
status: 'valid',
createdAt: createdAt1,
updatedAt: createdAt1,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace1.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspace1Plan
})
const workspace2 = await createAndStoreTestWorkspace()
const createdAt2 = new Date()
createdAt2.setHours(createdAt2.getHours() - 2)
const workspacePlan2 = {
name: PaidWorkspacePlans.Team,
status: 'valid',
createdAt: createdAt2,
updatedAt: createdAt2,
featureFlags: WorkspaceFeatureFlags.none,
workspaceId: workspace2.id
} as const
await upsertPaidWorkspacePlan({
workspacePlan: workspacePlan2
})
const workspacesByPlanByDaysTillExpire = await getWorkspacesByPlanAge({
planValidFor: 2,
daysTillExpiry: 2,
status: workspacePlan2.status,
plan: workspacePlan2.name
})
expect(workspacesByPlanByDaysTillExpire).to.deep.equalInAnyOrder([workspace2])
})
})
})
describe('checkoutSessions', () => {
describe('saveCheckoutSessionFactory creates a function that,', () => {
it('stores a checkout session', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
let storedSession = await getWorkspaceCheckoutSession({ workspaceId })
expect(storedSession).to.be.null
const checkoutSession = {
id: cryptoRandomString({ length: 10 }),
userId: cryptoRandomString({ length: 10 }),
billingInterval: 'monthly',
createdAt: new Date(),
paymentStatus: 'unpaid',
updatedAt: new Date(),
url: 'https://example.com',
workspaceId,
currency: 'usd',
workspacePlan: PaidWorkspacePlans.Team
} as const
await saveCheckoutSession({
checkoutSession
})
storedSession = await getCheckoutSession({ sessionId: checkoutSession.id })
expect(storedSession).deep.equal(checkoutSession)
})
})
describe('deleteCheckoutSessionFactory creates a function, that', () => {
it('deletes a checkout session', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const checkoutSession = {
id: cryptoRandomString({ length: 10 }),
userId: cryptoRandomString({ length: 10 }),
billingInterval: 'monthly',
createdAt: new Date(),
paymentStatus: 'unpaid',
updatedAt: new Date(),
url: 'https://example.com',
workspaceId,
currency: 'usd',
workspacePlan: PaidWorkspacePlans.Team
} as const
await saveCheckoutSession({
checkoutSession
})
let storedSession = await getCheckoutSession({ sessionId: checkoutSession.id })
expect(storedSession).deep.equal(checkoutSession)
await deleteCheckoutSession({ checkoutSessionId: checkoutSession.id })
storedSession = await getCheckoutSession({ sessionId: checkoutSession.id })
expect(storedSession).to.be.null
})
it('does not fail if the checkout session is not found', async () => {
await deleteCheckoutSession({
checkoutSessionId: cryptoRandomString({ length: 10 })
})
})
})
describe('updateCheckoutSessionFactory creates a function, that', () => {
it('updates the session paymentStatus and updatedAt', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const checkoutSession = {
id: cryptoRandomString({ length: 10 }),
userId: cryptoRandomString({ length: 10 }),
billingInterval: 'monthly',
createdAt: new Date(),
paymentStatus: 'unpaid',
updatedAt: new Date(),
url: 'https://example.com',
workspaceId,
currency: 'usd',
workspacePlan: PaidWorkspacePlans.Team
} as const
await saveCheckoutSession({
checkoutSession
})
let storedSession = await getCheckoutSession({
sessionId: checkoutSession.id
})
expect(storedSession).deep.equal(checkoutSession)
await updateCheckoutSessionStatus({
sessionId: checkoutSession.id,
paymentStatus: 'paid'
})
storedSession = await getCheckoutSession({
sessionId: checkoutSession.id
})
expect(storedSession?.paymentStatus).to.equal('paid')
})
})
describe('getWorkspaceCheckoutSessionFactory creates a function, that', () => {
it('returns null for workspaces without checkoutSessions', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const storedSession = await getWorkspaceCheckoutSession({ workspaceId })
expect(storedSession).to.be.null
})
it('gets the checkout session for the workspace', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const checkoutSession = {
id: cryptoRandomString({ length: 10 }),
userId: cryptoRandomString({ length: 10 }),
billingInterval: 'monthly',
createdAt: new Date(),
paymentStatus: 'unpaid',
updatedAt: new Date(),
url: 'https://example.com',
workspaceId,
currency: 'usd',
workspacePlan: PaidWorkspacePlans.Team
} as const
await saveCheckoutSession({
checkoutSession
})
const storedSession = await getWorkspaceCheckoutSession({ workspaceId })
expect(storedSession).deep.equal(checkoutSession)
})
})
})
describe('workspaceSubscriptions', () => {
describe('upsertWorkspaceSubscription creates a function, that', () => {
it('saves and updates the subscription', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const subscriptionData = createTestSubscriptionData({
products: [
{
priceId: cryptoRandomString({ length: 10 }),
quantity: 10,
productId: cryptoRandomString({ length: 10 }),
subscriptionItemId: cryptoRandomString({ length: 10 })
}
]
})
const workspaceSubscription = createTestWorkspaceSubscription({
workspaceId,
billingInterval: 'monthly',
subscriptionData
})
await upsertWorkspaceSubscription({ workspaceSubscription })
let storedSubscription = await getWorkspaceSubscription({ workspaceId })
expect(storedSubscription).deep.equal(workspaceSubscription)
workspaceSubscription.billingInterval = 'yearly'
workspaceSubscription.subscriptionData.products[0].quantity = 3
await upsertWorkspaceSubscription({ workspaceSubscription })
storedSubscription = await getWorkspaceSubscription({ workspaceId })
expect(storedSubscription).deep.equal(workspaceSubscription)
})
})
describe('getWorkspaceSubscriptionFactory creates a function, that', () => {
it('returns null if the subscription is not found', async () => {
const sub = await getWorkspaceSubscription({
workspaceId: cryptoRandomString({ length: 10 })
})
expect(sub).to.be.null
})
})
describe('getWorkspaceSubscriptionBySubscriptionIdFactory creates a function, that', () => {
it('returns null if the subscription is not found', async () => {
const sub = await getWorkspaceSubscriptionBySubscriptionId({
subscriptionId: cryptoRandomString({ length: 10 })
})
expect(sub).to.be.null
})
it('returns the sub', async () => {
const workspace = await createAndStoreTestWorkspace()
const workspaceId = workspace.id
const workspaceSubscription = createTestWorkspaceSubscription({ workspaceId })
await upsertWorkspaceSubscription({ workspaceSubscription })
const storedSubscription = await getWorkspaceSubscriptionBySubscriptionId({
subscriptionId: workspaceSubscription.subscriptionData.subscriptionId
})
expect(storedSubscription).deep.equal(workspaceSubscription)
})
})
describe('getWorkspaceSubscriptionsPastBillingCycleEndFactory', () => {
before(async () => {
await truncateTables(['workspace_subscriptions'])
})
it('returns subs, that are about to end their billing cycle', async () => {
const workspace1 = await createAndStoreTestWorkspace()
const workspace1Id = workspace1.id
const workspace1Subscription = createTestWorkspaceSubscription({
workspaceId: workspace1Id,
currentBillingCycleEnd: new Date(2099, 0, 1)
})
await upsertWorkspaceSubscription({
workspaceSubscription: workspace1Subscription
})
const workspace2 = await createAndStoreTestWorkspace()
const workspace2Id = workspace2.id
const currentBillingCycleEnd = new Date()
currentBillingCycleEnd.setMinutes(currentBillingCycleEnd.getMinutes() + 4)
const workspace2Subscription = createTestWorkspaceSubscription({
workspaceId: workspace2Id
})
await upsertWorkspacePlanFactory({ db })({
workspacePlan: {
workspaceId: workspace2Subscription.workspaceId,
name: PaidWorkspacePlans.Team,
status: 'valid',
createdAt: new Date(),
updatedAt: new Date(),
featureFlags: WorkspaceFeatureFlags.none
}
})
await upsertWorkspaceSubscription({
workspaceSubscription: workspace2Subscription
})
const subscriptions = await getWorkspaceSubscriptionsPastBillingCycleEnd()
expect(subscriptions).deep.equalInAnyOrder([workspace2Subscription])
})
})
})
})