2
0

🛂 (billing) Add isPastDue field in workspace (#1046)

Closes #1039

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
  - Workspaces now include additional status indicator: `isPastDue`.
- New pages for handling workspaces that are past due. Meaning, an
invoice is unpaid.

- **Bug Fixes**
- Fixed issues with workspace status checks and redirections for
suspended workspaces.

- **Refactor**
- Refactored workspace-related API functions to accommodate new status
fields.
- Improved permission checks for reading and writing typebots based on
workspace status.

- **Chores**
  - Database schema updated to include `isPastDue` field for workspaces.
- Implemented new webhook event handling for subscription and invoice
updates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-11-23 08:16:23 +01:00
committed by GitHub
parent 94886ca58e
commit ca79934ef5
27 changed files with 450 additions and 97 deletions

View File

@@ -56,7 +56,17 @@ export const getLinkedTypebots = authenticatedProcedure
variables: true, variables: true,
name: true, name: true,
createdAt: true, createdAt: true,
workspaceId: true, workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
type: true, type: true,
@@ -97,7 +107,17 @@ export const getLinkedTypebots = authenticatedProcedure
variables: true, variables: true,
name: true, name: true,
createdAt: true, createdAt: true,
workspaceId: true, workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
type: true, type: true,

View File

@@ -32,6 +32,17 @@ export const getCollaborators = authenticatedProcedure
}, },
include: { include: {
collaborators: true, collaborators: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
}, },
}) })
if ( if (

View File

@@ -36,8 +36,19 @@ export const deleteResults = authenticatedProcedure
id: typebotId, id: typebotId,
}, },
select: { select: {
workspaceId: true,
groups: true, groups: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
role: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,

View File

@@ -33,8 +33,18 @@ export const getResult = authenticatedProcedure
}, },
select: { select: {
id: true, id: true,
workspaceId: true,
groups: true, groups: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,

View File

@@ -28,8 +28,18 @@ export const getResultLogs = authenticatedProcedure
}, },
select: { select: {
id: true, id: true,
workspaceId: true,
groups: true, groups: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,

View File

@@ -44,7 +44,6 @@ export const getResults = authenticatedProcedure
}, },
select: { select: {
id: true, id: true,
workspaceId: true,
groups: true, groups: true,
collaborators: { collaborators: {
select: { select: {
@@ -52,6 +51,17 @@ export const getResults = authenticatedProcedure
type: true, type: true,
}, },
}, },
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
}, },
}) })
if (!typebot || (await isReadTypebotForbidden(typebot, user))) if (!typebot || (await isReadTypebotForbidden(typebot, user)))

View File

@@ -81,7 +81,7 @@ export const processTelemetryEvent = authenticatedProcedure
client.capture({ client.capture({
distinctId: event.userId, distinctId: event.userId,
event: event.name, event: event.name,
properties: event.data, properties: 'data' in event ? event.data : undefined,
groups, groups,
}) })
}) })

View File

@@ -33,8 +33,19 @@ export const deleteTypebot = authenticatedProcedure
}, },
select: { select: {
id: true, id: true,
workspaceId: true,
groups: true, groups: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
role: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,

View File

@@ -52,6 +52,17 @@ export const getPublishedTypebot = authenticatedProcedure
include: { include: {
collaborators: true, collaborators: true,
publishedTypebot: true, publishedTypebot: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
}, },
}) })
if ( if (

View File

@@ -41,6 +41,17 @@ export const getTypebot = authenticatedProcedure
}, },
include: { include: {
collaborators: true, collaborators: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
}, },
}) })
if ( if (

View File

@@ -46,6 +46,14 @@ export const publishTypebot = authenticatedProcedure
workspace: { workspace: {
select: { select: {
plan: true, plan: true,
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
role: true,
},
},
}, },
}, },
}, },

View File

@@ -32,6 +32,18 @@ export const unpublishTypebot = authenticatedProcedure
include: { include: {
collaborators: true, collaborators: true,
publishedTypebot: true, publishedTypebot: true,
workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
role: true,
},
},
},
},
}, },
}) })
if (!existingTypebot?.publishedTypebot) if (!existingTypebot?.publishedTypebot)

View File

@@ -79,7 +79,6 @@ export const updateTypebot = authenticatedProcedure
id: true, id: true,
customDomain: true, customDomain: true,
publicId: true, publicId: true,
workspaceId: true,
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,
@@ -88,7 +87,16 @@ export const updateTypebot = authenticatedProcedure
}, },
workspace: { workspace: {
select: { select: {
id: true,
plan: true, plan: true,
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
role: true,
},
},
}, },
}, },
updatedAt: true, updatedAt: true,
@@ -160,7 +168,7 @@ export const updateTypebot = authenticatedProcedure
selectedThemeTemplateId: typebot.selectedThemeTemplateId, selectedThemeTemplateId: typebot.selectedThemeTemplateId,
events: typebot.events ?? undefined, events: typebot.events ?? undefined,
groups: typebot.groups groups: typebot.groups
? await sanitizeGroups(existingTypebot.workspaceId)(typebot.groups) ? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups)
: undefined, : undefined,
theme: typebot.theme ? typebot.theme : undefined, theme: typebot.theme ? typebot.theme : undefined,
settings: typebot.settings settings: typebot.settings

View File

@@ -1,26 +1,25 @@
import prisma from '@typebot.io/lib/prisma'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import { CollaboratorsOnTypebots, User } from '@typebot.io/prisma' import {
import { Typebot } from '@typebot.io/schemas' CollaboratorsOnTypebots,
User,
Workspace,
MemberInWorkspace,
} from '@typebot.io/prisma'
export const isReadTypebotForbidden = async ( export const isReadTypebotForbidden = async (
typebot: Pick<Typebot, 'workspaceId'> & { typebot: {
collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[] collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[]
} & {
workspace: Pick<Workspace, 'isQuarantined' | 'isPastDue'> & {
members: Pick<MemberInWorkspace, 'userId'>[]
}
}, },
user: Pick<User, 'email' | 'id'> user: Pick<User, 'email' | 'id'>
) => { ) =>
if ( typebot.workspace.isQuarantined ||
env.ADMIN_EMAIL === user.email || typebot.workspace.isPastDue ||
typebot.collaborators.find( (env.ADMIN_EMAIL !== user.email &&
!typebot.collaborators.some(
(collaborator) => collaborator.userId === user.id (collaborator) => collaborator.userId === user.id
) ) &&
) !typebot.workspace.members.some((member) => member.userId === user.id))
return false
const memberInWorkspace = await prisma.memberInWorkspace.findFirst({
where: {
workspaceId: typebot.workspaceId,
userId: user.id,
},
})
return memberInWorkspace === null
}

View File

@@ -1,29 +1,31 @@
import prisma from '@typebot.io/lib/prisma'
import { import {
CollaborationType, CollaborationType,
CollaboratorsOnTypebots, CollaboratorsOnTypebots,
MemberInWorkspace,
User, User,
Workspace,
} from '@typebot.io/prisma' } from '@typebot.io/prisma'
import { Typebot } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
export const isWriteTypebotForbidden = async ( export const isWriteTypebotForbidden = async (
typebot: Pick<Typebot, 'workspaceId'> & { typebot: {
collaborators: Pick<CollaboratorsOnTypebots, 'userId' | 'type'>[] collaborators: Pick<CollaboratorsOnTypebots, 'userId' | 'type'>[]
} & {
workspace: Pick<Workspace, 'isQuarantined' | 'isPastDue'> & {
members: Pick<MemberInWorkspace, 'userId' | 'role'>[]
}
}, },
user: Pick<User, 'id'> user: Pick<User, 'id'>
) => { ) => {
if ( return (
typebot.collaborators.find( typebot.workspace.isQuarantined ||
(collaborator) => collaborator.userId === user.id typebot.workspace.isPastDue ||
)?.type === CollaborationType.WRITE !(
typebot.collaborators.find(
(collaborator) => collaborator.userId === user.id
)?.type === CollaborationType.WRITE &&
typebot.workspace.members.some(
(m) => m.userId === user.id && m.role !== 'GUEST'
)
)
) )
return false
const memberInWorkspace = await prisma.memberInWorkspace.findFirst({
where: {
workspaceId: typebot.workspaceId,
userId: user.id,
},
})
return isNotDefined(memberInWorkspace) || memberInWorkspace.role === 'GUEST'
} }

View File

@@ -147,7 +147,19 @@ const parseFilePath = async ({
id: input.typebotId, id: input.typebotId,
}, },
select: { select: {
workspaceId: true, workspace: {
select: {
plan: true,
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
role: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,

View File

@@ -57,7 +57,17 @@ export const startWhatsAppPreview = authenticatedProcedure
}, },
select: { select: {
id: true, id: true,
workspaceId: true, workspace: {
select: {
isQuarantined: true,
isPastDue: true,
members: {
select: {
userId: true,
},
},
},
},
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,

View File

@@ -6,7 +6,7 @@ import {
useMemo, useMemo,
useState, useState,
} from 'react' } from 'react'
import { byId, isNotDefined } from '@typebot.io/lib' import { byId } from '@typebot.io/lib'
import { WorkspaceRole } from '@typebot.io/prisma' import { WorkspaceRole } from '@typebot.io/prisma'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { trpc } from '@/lib/trpc' import { trpc } from '@/lib/trpc'
@@ -136,16 +136,20 @@ export const WorkspaceProvider = ({
]) ])
useEffect(() => { useEffect(() => {
if (isNotDefined(workspace?.isSuspended)) return if (workspace?.isSuspended) {
if (workspace?.isSuspended && pathname !== '/suspended') push('/suspended') if (pathname === '/suspended') return
}, [pathname, push, workspace?.isSuspended]) push('/suspended')
return
}
if (workspace?.isPastDue) {
if (pathname === '/past-due') return
push('/past-due')
return
}
}, [pathname, push, workspace?.isPastDue, workspace?.isSuspended])
const switchWorkspace = (workspaceId: string) => { const switchWorkspace = (workspaceId: string) => {
setWorkspaceIdInLocalStorage(workspaceId) setWorkspaceIdInLocalStorage(workspaceId)
if (pathname === '/suspended') {
window.location.href = '/typebots'
return
}
setWorkspaceId(workspaceId) setWorkspaceId(workspaceId)
} }

View File

@@ -66,7 +66,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
}, },
include: { include: {
members: { members: {
select: { user: { select: { id: true } } }, select: { userId: true },
where: { where: {
role: WorkspaceRole.ADMIN, role: WorkspaceRole.ADMIN,
}, },
@@ -74,19 +74,16 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
}, },
}) })
for (const user of workspace.members.map((member) => member.user)) { await sendTelemetryEvents(
if (!user?.id) continue workspace.members.map((m) => ({
await sendTelemetryEvents([ name: 'Subscription updated',
{ workspaceId,
name: 'Subscription updated', userId: m.userId,
workspaceId, data: {
userId: user.id, plan,
data: {
plan,
},
}, },
]) }))
} )
} else { } else {
const { claimableCustomPlanId, userId } = metadata const { claimableCustomPlanId, userId } = metadata
if (!claimableCustomPlanId) if (!claimableCustomPlanId)
@@ -124,6 +121,80 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(200).send({ message: 'workspace upgraded in DB' }) return res.status(200).send({ message: 'workspace upgraded in DB' })
} }
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription
if (subscription.status !== 'past_due')
return res.send({ message: 'Not past_due, skipping.' })
const workspace = await prisma.workspace.update({
where: {
stripeId: subscription.customer as string,
},
data: {
isPastDue: true,
},
select: {
id: true,
members: {
select: { userId: true, role: true },
where: { role: WorkspaceRole.ADMIN },
},
},
})
await sendTelemetryEvents(
workspace.members.map((m) => ({
name: 'Workspace past due',
workspaceId: workspace.id,
userId: m.userId,
}))
)
return res.send({ message: 'Workspace set to past due.' })
}
case 'invoices.paid': {
const invoice = event.data.object as Stripe.Invoice
const workspace = await prisma.workspace.findFirst({
where: {
stripeId: invoice.customer as string,
},
select: {
isPastDue: true,
},
})
if (!workspace?.isPastDue)
return res.send({ message: 'Workspace not past_due, skipping.' })
const outstandingInvoices = await stripe.invoices.list({
customer: invoice.customer as string,
status: 'open',
})
if (outstandingInvoices.data.length > 0)
return res.send({
message: 'Workspace has outstanding invoices, skipping.',
})
const updatedWorkspace = await prisma.workspace.update({
where: {
stripeId: invoice.customer as string,
},
data: {
isPastDue: false,
},
select: {
id: true,
members: {
select: { userId: true },
where: {
role: WorkspaceRole.ADMIN,
},
},
},
})
await sendTelemetryEvents(
updatedWorkspace.members.map((m) => ({
name: 'Workspace past due status removed',
workspaceId: updatedWorkspace.id,
userId: m.userId,
}))
)
return res.send({ message: 'Workspace was regulated' })
}
case 'customer.subscription.deleted': { case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription const subscription = event.data.object as Stripe.Subscription
const { data } = await stripe.subscriptions.list({ const { data } = await stripe.subscriptions.list({
@@ -151,7 +222,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
}, },
include: { include: {
members: { members: {
select: { user: { select: { id: true } } }, select: { userId: true },
where: { where: {
role: WorkspaceRole.ADMIN, role: WorkspaceRole.ADMIN,
}, },
@@ -159,19 +230,16 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
}, },
}) })
for (const user of workspace.members.map((member) => member.user)) { await sendTelemetryEvents(
if (!user?.id) continue workspace.members.map((m) => ({
await sendTelemetryEvents([ name: 'Subscription updated',
{ workspaceId: workspace.id,
name: 'Subscription updated', userId: m.userId,
workspaceId: workspace.id, data: {
userId: user.id, plan: Plan.FREE,
data: {
plan: Plan.FREE,
},
}, },
]) }))
} )
const typebots = await prisma.typebot.findMany({ const typebots = await prisma.typebot.findMany({
where: { where: {

View File

@@ -0,0 +1,36 @@
import { AlertIcon } from '@/components/icons'
import { BillingPortalButton } from '@/features/billing/components/BillingPortalButton'
import { DashboardHeader } from '@/features/dashboard/components/DashboardHeader'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { Heading, VStack, Text } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function Page() {
const { replace } = useRouter()
const { workspace } = useWorkspace()
useEffect(() => {
if (!workspace || workspace.isPastDue) return
replace('/typebots')
}, [replace, workspace])
return (
<>
<DashboardHeader />
<VStack
w="full"
h="calc(100vh - 64px)"
justifyContent="center"
spacing={4}
>
<AlertIcon fontSize="4xl" />
<Heading fontSize="2xl">Your workspace has unpaid invoice(s).</Heading>
<Text>Head over to the billing portal to pay it.</Text>
{workspace?.id && (
<BillingPortalButton workspaceId={workspace?.id} colorScheme="blue" />
)}
</VStack>
</>
)
}

View File

@@ -1,13 +1,28 @@
import { TextLink } from '@/components/TextLink' import { TextLink } from '@/components/TextLink'
import { DashboardHeader } from '@/features/dashboard/components/DashboardHeader' import { DashboardHeader } from '@/features/dashboard/components/DashboardHeader'
import { WorkspaceProvider } from '@/features/workspace/WorkspaceProvider' import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { Heading, Link, Text, VStack } from '@chakra-ui/react' import { Heading, Text, VStack } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function Page() { export default function Page() {
const { replace } = useRouter()
const { workspace } = useWorkspace()
useEffect(() => {
if (!workspace || workspace.isSuspended) return
replace('/typebots')
}, [replace, workspace])
return ( return (
<WorkspaceProvider> <>
<DashboardHeader /> <DashboardHeader />
<VStack w="full" h="calc(100vh - 64px)" justifyContent="center"> <VStack
w="full"
h="calc(100vh - 64px)"
justifyContent="center"
spacing={4}
>
<Heading>Your workspace has been suspended.</Heading> <Heading>Your workspace has been suspended.</Heading>
<Text> <Text>
We detected that one of your typebots does not comply with our{' '} We detected that one of your typebots does not comply with our{' '}
@@ -18,14 +33,7 @@ export default function Page() {
terms of service terms of service
</TextLink> </TextLink>
</Text> </Text>
<Text>
If you think it&apos;s a mistake, feel free to{' '}
<Link href="mailto:support@typebot.io" textDecor="underline">
reach out
</Link>
.
</Text>
</VStack> </VStack>
</WorkspaceProvider> </>
) )
} }

View File

@@ -415,6 +415,52 @@
"data" "data"
], ],
"additionalProperties": false "additionalProperties": false
},
{
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"name": {
"type": "string",
"enum": [
"Workspace past due"
]
}
},
"required": [
"userId",
"workspaceId",
"name"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"name": {
"type": "string",
"enum": [
"Workspace past due status removed"
]
}
},
"required": [
"userId",
"workspaceId",
"name"
],
"additionalProperties": false
} }
] ]
} }
@@ -7487,6 +7533,9 @@
}, },
"isSuspended": { "isSuspended": {
"type": "boolean" "type": "boolean"
},
"isPastDue": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@@ -7507,7 +7556,8 @@
"customStorageLimit", "customStorageLimit",
"customSeatsLimit", "customSeatsLimit",
"isQuarantined", "isQuarantined",
"isSuspended" "isSuspended",
"isPastDue"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -7636,6 +7686,9 @@
}, },
"isSuspended": { "isSuspended": {
"type": "boolean" "type": "boolean"
},
"isPastDue": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@@ -7656,7 +7709,8 @@
"customStorageLimit", "customStorageLimit",
"customSeatsLimit", "customSeatsLimit",
"isQuarantined", "isQuarantined",
"isSuspended" "isSuspended",
"isPastDue"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -7802,6 +7856,9 @@
}, },
"isSuspended": { "isSuspended": {
"type": "boolean" "type": "boolean"
},
"isPastDue": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@@ -7822,7 +7879,8 @@
"customStorageLimit", "customStorageLimit",
"customSeatsLimit", "customSeatsLimit",
"isQuarantined", "isQuarantined",
"isSuspended" "isSuspended",
"isPastDue"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -54515,6 +54573,9 @@
}, },
"isSuspended": { "isSuspended": {
"type": "boolean" "type": "boolean"
},
"isPastDue": {
"type": "boolean"
} }
}, },
"required": [ "required": [
@@ -54535,7 +54596,8 @@
"customStorageLimit", "customStorageLimit",
"customSeatsLimit", "customSeatsLimit",
"isQuarantined", "isQuarantined",
"isSuspended" "isSuspended",
"isPastDue"
], ],
"additionalProperties": false, "additionalProperties": false,
"nullable": true "nullable": true

View File

@@ -97,6 +97,7 @@ model Workspace {
customSeatsLimit Int? customSeatsLimit Int?
isQuarantined Boolean @default(false) isQuarantined Boolean @default(false)
isSuspended Boolean @default(false) isSuspended Boolean @default(false)
isPastDue Boolean @default(false)
themeTemplates ThemeTemplate[] themeTemplates ThemeTemplate[]
} }

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Workspace" ADD COLUMN "isPastDue" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -91,6 +91,7 @@ model Workspace {
customSeatsLimit Int? customSeatsLimit Int?
isQuarantined Boolean @default(false) isQuarantined Boolean @default(false)
isSuspended Boolean @default(false) isSuspended Boolean @default(false)
isPastDue Boolean @default(false)
themeTemplates ThemeTemplate[] themeTemplates ThemeTemplate[]
} }

View File

@@ -105,6 +105,18 @@ const workspaceAutoQuarantinedEventSchema = workspaceEvent.merge(
}) })
) )
export const workspacePastDueEventSchema = workspaceEvent.merge(
z.object({
name: z.literal('Workspace past due'),
})
)
export const workspaceNotPastDueEventSchema = workspaceEvent.merge(
z.object({
name: z.literal('Workspace past due status removed'),
})
)
export const eventSchema = z.discriminatedUnion('name', [ export const eventSchema = z.discriminatedUnion('name', [
workspaceCreatedEventSchema, workspaceCreatedEventSchema,
userCreatedEventSchema, userCreatedEventSchema,
@@ -115,6 +127,8 @@ export const eventSchema = z.discriminatedUnion('name', [
workspaceLimitReachedEventSchema, workspaceLimitReachedEventSchema,
workspaceAutoQuarantinedEventSchema, workspaceAutoQuarantinedEventSchema,
subscriptionAutoUpdatedEventSchema, subscriptionAutoUpdatedEventSchema,
workspacePastDueEventSchema,
workspaceNotPastDueEventSchema,
]) ])
export type TelemetryEvent = z.infer<typeof eventSchema> export type TelemetryEvent = z.infer<typeof eventSchema>

View File

@@ -50,6 +50,7 @@ export const workspaceSchema = z.object({
customSeatsLimit: z.number().nullable(), customSeatsLimit: z.number().nullable(),
isQuarantined: z.boolean(), isQuarantined: z.boolean(),
isSuspended: z.boolean(), isSuspended: z.boolean(),
isPastDue: z.boolean(),
}) satisfies z.ZodType<WorkspacePrisma> }) satisfies z.ZodType<WorkspacePrisma>
export type Workspace = z.infer<typeof workspaceSchema> export type Workspace = z.infer<typeof workspaceSchema>