From ca79934ef5c86feaa60cde4d4bc42171149a1f15 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 23 Nov 2023 08:16:23 +0100 Subject: [PATCH] :passport_control: (billing) Add isPastDue field in workspace (#1046) Closes #1039 ## 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. --- .../typebotLink/api/getLinkedTypebots.ts | 24 +++- .../collaboration/api/getCollaborators.ts | 11 ++ .../src/features/results/api/deleteResults.ts | 13 +- .../src/features/results/api/getResult.ts | 12 +- .../src/features/results/api/getResultLogs.ts | 12 +- .../src/features/results/api/getResults.ts | 12 +- .../telemetry/api/processTelemetryEvent.ts | 2 +- .../src/features/typebot/api/deleteTypebot.ts | 13 +- .../typebot/api/getPublishedTypebot.ts | 11 ++ .../src/features/typebot/api/getTypebot.ts | 11 ++ .../features/typebot/api/publishTypebot.ts | 8 ++ .../features/typebot/api/unpublishTypebot.ts | 12 ++ .../src/features/typebot/api/updateTypebot.ts | 12 +- .../typebot/helpers/isReadTypebotForbidden.ts | 37 +++--- .../helpers/isWriteTypebotForbidden.ts | 34 ++--- .../features/upload/api/generateUploadUrl.ts | 14 +- .../features/whatsapp/startWhatsAppPreview.ts | 12 +- .../features/workspace/WorkspaceProvider.tsx | 20 +-- apps/builder/src/pages/api/stripe/webhook.ts | 120 ++++++++++++++---- apps/builder/src/pages/past-due.tsx | 36 ++++++ apps/builder/src/pages/suspended.tsx | 32 +++-- apps/docs/openapi/builder/_spec_.json | 70 +++++++++- packages/prisma/mysql/schema.prisma | 1 + .../migration.sql | 2 + packages/prisma/postgresql/schema.prisma | 1 + packages/schemas/features/telemetry.ts | 14 ++ packages/schemas/features/workspace.ts | 1 + 27 files changed, 450 insertions(+), 97 deletions(-) create mode 100644 apps/builder/src/pages/past-due.tsx create mode 100644 packages/prisma/postgresql/migrations/20231121154057_add_is_past_due_field/migration.sql diff --git a/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts b/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts index 6e618b119..f3bc8b3f4 100644 --- a/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts +++ b/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts @@ -56,7 +56,17 @@ export const getLinkedTypebots = authenticatedProcedure variables: true, name: true, createdAt: true, - workspaceId: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, collaborators: { select: { type: true, @@ -97,7 +107,17 @@ export const getLinkedTypebots = authenticatedProcedure variables: true, name: true, createdAt: true, - workspaceId: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, collaborators: { select: { type: true, diff --git a/apps/builder/src/features/collaboration/api/getCollaborators.ts b/apps/builder/src/features/collaboration/api/getCollaborators.ts index 2a7954431..46de8bcc3 100644 --- a/apps/builder/src/features/collaboration/api/getCollaborators.ts +++ b/apps/builder/src/features/collaboration/api/getCollaborators.ts @@ -32,6 +32,17 @@ export const getCollaborators = authenticatedProcedure }, include: { collaborators: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, }, }) if ( diff --git a/apps/builder/src/features/results/api/deleteResults.ts b/apps/builder/src/features/results/api/deleteResults.ts index f90a36a4b..a56ac7bf6 100644 --- a/apps/builder/src/features/results/api/deleteResults.ts +++ b/apps/builder/src/features/results/api/deleteResults.ts @@ -36,8 +36,19 @@ export const deleteResults = authenticatedProcedure id: typebotId, }, select: { - workspaceId: true, groups: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + role: true, + }, + }, + }, + }, collaborators: { select: { userId: true, diff --git a/apps/builder/src/features/results/api/getResult.ts b/apps/builder/src/features/results/api/getResult.ts index 3e91810e2..484ce66cf 100644 --- a/apps/builder/src/features/results/api/getResult.ts +++ b/apps/builder/src/features/results/api/getResult.ts @@ -33,8 +33,18 @@ export const getResult = authenticatedProcedure }, select: { id: true, - workspaceId: true, groups: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, collaborators: { select: { userId: true, diff --git a/apps/builder/src/features/results/api/getResultLogs.ts b/apps/builder/src/features/results/api/getResultLogs.ts index 2e8a6b6de..72a8c340e 100644 --- a/apps/builder/src/features/results/api/getResultLogs.ts +++ b/apps/builder/src/features/results/api/getResultLogs.ts @@ -28,8 +28,18 @@ export const getResultLogs = authenticatedProcedure }, select: { id: true, - workspaceId: true, groups: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, collaborators: { select: { userId: true, diff --git a/apps/builder/src/features/results/api/getResults.ts b/apps/builder/src/features/results/api/getResults.ts index 7e6c4bee9..10d6b0571 100644 --- a/apps/builder/src/features/results/api/getResults.ts +++ b/apps/builder/src/features/results/api/getResults.ts @@ -44,7 +44,6 @@ export const getResults = authenticatedProcedure }, select: { id: true, - workspaceId: true, groups: true, collaborators: { select: { @@ -52,6 +51,17 @@ export const getResults = authenticatedProcedure type: true, }, }, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, }, }) if (!typebot || (await isReadTypebotForbidden(typebot, user))) diff --git a/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts b/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts index f88f770f2..f427fa65d 100644 --- a/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts +++ b/apps/builder/src/features/telemetry/api/processTelemetryEvent.ts @@ -81,7 +81,7 @@ export const processTelemetryEvent = authenticatedProcedure client.capture({ distinctId: event.userId, event: event.name, - properties: event.data, + properties: 'data' in event ? event.data : undefined, groups, }) }) diff --git a/apps/builder/src/features/typebot/api/deleteTypebot.ts b/apps/builder/src/features/typebot/api/deleteTypebot.ts index 8393ba424..8ad8e16c4 100644 --- a/apps/builder/src/features/typebot/api/deleteTypebot.ts +++ b/apps/builder/src/features/typebot/api/deleteTypebot.ts @@ -33,8 +33,19 @@ export const deleteTypebot = authenticatedProcedure }, select: { id: true, - workspaceId: true, groups: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + role: true, + }, + }, + }, + }, collaborators: { select: { userId: true, diff --git a/apps/builder/src/features/typebot/api/getPublishedTypebot.ts b/apps/builder/src/features/typebot/api/getPublishedTypebot.ts index 2aa9b76c7..8e7e8e216 100644 --- a/apps/builder/src/features/typebot/api/getPublishedTypebot.ts +++ b/apps/builder/src/features/typebot/api/getPublishedTypebot.ts @@ -52,6 +52,17 @@ export const getPublishedTypebot = authenticatedProcedure include: { collaborators: true, publishedTypebot: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, }, }) if ( diff --git a/apps/builder/src/features/typebot/api/getTypebot.ts b/apps/builder/src/features/typebot/api/getTypebot.ts index 65cc47cf6..73de37213 100644 --- a/apps/builder/src/features/typebot/api/getTypebot.ts +++ b/apps/builder/src/features/typebot/api/getTypebot.ts @@ -41,6 +41,17 @@ export const getTypebot = authenticatedProcedure }, include: { collaborators: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, }, }) if ( diff --git a/apps/builder/src/features/typebot/api/publishTypebot.ts b/apps/builder/src/features/typebot/api/publishTypebot.ts index e3d3e60ce..d034b51bf 100644 --- a/apps/builder/src/features/typebot/api/publishTypebot.ts +++ b/apps/builder/src/features/typebot/api/publishTypebot.ts @@ -46,6 +46,14 @@ export const publishTypebot = authenticatedProcedure workspace: { select: { plan: true, + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + role: true, + }, + }, }, }, }, diff --git a/apps/builder/src/features/typebot/api/unpublishTypebot.ts b/apps/builder/src/features/typebot/api/unpublishTypebot.ts index d3c91ae7b..215cff6d8 100644 --- a/apps/builder/src/features/typebot/api/unpublishTypebot.ts +++ b/apps/builder/src/features/typebot/api/unpublishTypebot.ts @@ -32,6 +32,18 @@ export const unpublishTypebot = authenticatedProcedure include: { collaborators: true, publishedTypebot: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + role: true, + }, + }, + }, + }, }, }) if (!existingTypebot?.publishedTypebot) diff --git a/apps/builder/src/features/typebot/api/updateTypebot.ts b/apps/builder/src/features/typebot/api/updateTypebot.ts index 490cc2c27..9d4c21f47 100644 --- a/apps/builder/src/features/typebot/api/updateTypebot.ts +++ b/apps/builder/src/features/typebot/api/updateTypebot.ts @@ -79,7 +79,6 @@ export const updateTypebot = authenticatedProcedure id: true, customDomain: true, publicId: true, - workspaceId: true, collaborators: { select: { userId: true, @@ -88,7 +87,16 @@ export const updateTypebot = authenticatedProcedure }, workspace: { select: { + id: true, plan: true, + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + role: true, + }, + }, }, }, updatedAt: true, @@ -160,7 +168,7 @@ export const updateTypebot = authenticatedProcedure selectedThemeTemplateId: typebot.selectedThemeTemplateId, events: typebot.events ?? undefined, groups: typebot.groups - ? await sanitizeGroups(existingTypebot.workspaceId)(typebot.groups) + ? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups) : undefined, theme: typebot.theme ? typebot.theme : undefined, settings: typebot.settings diff --git a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts index 0a4062854..b3f134987 100644 --- a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts +++ b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts @@ -1,26 +1,25 @@ -import prisma from '@typebot.io/lib/prisma' import { env } from '@typebot.io/env' -import { CollaboratorsOnTypebots, User } from '@typebot.io/prisma' -import { Typebot } from '@typebot.io/schemas' +import { + CollaboratorsOnTypebots, + User, + Workspace, + MemberInWorkspace, +} from '@typebot.io/prisma' export const isReadTypebotForbidden = async ( - typebot: Pick & { + typebot: { collaborators: Pick[] + } & { + workspace: Pick & { + members: Pick[] + } }, user: Pick -) => { - if ( - env.ADMIN_EMAIL === user.email || - typebot.collaborators.find( +) => + typebot.workspace.isQuarantined || + typebot.workspace.isPastDue || + (env.ADMIN_EMAIL !== user.email && + !typebot.collaborators.some( (collaborator) => collaborator.userId === user.id - ) - ) - return false - const memberInWorkspace = await prisma.memberInWorkspace.findFirst({ - where: { - workspaceId: typebot.workspaceId, - userId: user.id, - }, - }) - return memberInWorkspace === null -} + ) && + !typebot.workspace.members.some((member) => member.userId === user.id)) diff --git a/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts b/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts index e6a4b72ea..bcdd05f86 100644 --- a/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts +++ b/apps/builder/src/features/typebot/helpers/isWriteTypebotForbidden.ts @@ -1,29 +1,31 @@ -import prisma from '@typebot.io/lib/prisma' import { CollaborationType, CollaboratorsOnTypebots, + MemberInWorkspace, User, + Workspace, } from '@typebot.io/prisma' -import { Typebot } from '@typebot.io/schemas' -import { isNotDefined } from '@typebot.io/lib' export const isWriteTypebotForbidden = async ( - typebot: Pick & { + typebot: { collaborators: Pick[] + } & { + workspace: Pick & { + members: Pick[] + } }, user: Pick ) => { - if ( - typebot.collaborators.find( - (collaborator) => collaborator.userId === user.id - )?.type === CollaborationType.WRITE + return ( + typebot.workspace.isQuarantined || + typebot.workspace.isPastDue || + !( + 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' } diff --git a/apps/builder/src/features/upload/api/generateUploadUrl.ts b/apps/builder/src/features/upload/api/generateUploadUrl.ts index f9c98633b..2af3afb67 100644 --- a/apps/builder/src/features/upload/api/generateUploadUrl.ts +++ b/apps/builder/src/features/upload/api/generateUploadUrl.ts @@ -147,7 +147,19 @@ const parseFilePath = async ({ id: input.typebotId, }, select: { - workspaceId: true, + workspace: { + select: { + plan: true, + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + role: true, + }, + }, + }, + }, collaborators: { select: { userId: true, diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index 2f6af8b1c..f978fbecd 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -57,7 +57,17 @@ export const startWhatsAppPreview = authenticatedProcedure }, select: { id: true, - workspaceId: true, + workspace: { + select: { + isQuarantined: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, collaborators: { select: { userId: true, diff --git a/apps/builder/src/features/workspace/WorkspaceProvider.tsx b/apps/builder/src/features/workspace/WorkspaceProvider.tsx index 04926f654..db3ba2cdd 100644 --- a/apps/builder/src/features/workspace/WorkspaceProvider.tsx +++ b/apps/builder/src/features/workspace/WorkspaceProvider.tsx @@ -6,7 +6,7 @@ import { useMemo, useState, } from 'react' -import { byId, isNotDefined } from '@typebot.io/lib' +import { byId } from '@typebot.io/lib' import { WorkspaceRole } from '@typebot.io/prisma' import { useRouter } from 'next/router' import { trpc } from '@/lib/trpc' @@ -136,16 +136,20 @@ export const WorkspaceProvider = ({ ]) useEffect(() => { - if (isNotDefined(workspace?.isSuspended)) return - if (workspace?.isSuspended && pathname !== '/suspended') push('/suspended') - }, [pathname, push, workspace?.isSuspended]) + if (workspace?.isSuspended) { + if (pathname === '/suspended') return + 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) => { setWorkspaceIdInLocalStorage(workspaceId) - if (pathname === '/suspended') { - window.location.href = '/typebots' - return - } setWorkspaceId(workspaceId) } diff --git a/apps/builder/src/pages/api/stripe/webhook.ts b/apps/builder/src/pages/api/stripe/webhook.ts index a91a09fe1..e74ea71ce 100644 --- a/apps/builder/src/pages/api/stripe/webhook.ts +++ b/apps/builder/src/pages/api/stripe/webhook.ts @@ -66,7 +66,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { }, include: { members: { - select: { user: { select: { id: true } } }, + select: { userId: true }, where: { role: WorkspaceRole.ADMIN, }, @@ -74,19 +74,16 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { }, }) - for (const user of workspace.members.map((member) => member.user)) { - if (!user?.id) continue - await sendTelemetryEvents([ - { - name: 'Subscription updated', - workspaceId, - userId: user.id, - data: { - plan, - }, + await sendTelemetryEvents( + workspace.members.map((m) => ({ + name: 'Subscription updated', + workspaceId, + userId: m.userId, + data: { + plan, }, - ]) - } + })) + ) } else { const { claimableCustomPlanId, userId } = metadata if (!claimableCustomPlanId) @@ -124,6 +121,80 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { 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': { const subscription = event.data.object as Stripe.Subscription const { data } = await stripe.subscriptions.list({ @@ -151,7 +222,7 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { }, include: { members: { - select: { user: { select: { id: true } } }, + select: { userId: true }, where: { role: WorkspaceRole.ADMIN, }, @@ -159,19 +230,16 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { }, }) - for (const user of workspace.members.map((member) => member.user)) { - if (!user?.id) continue - await sendTelemetryEvents([ - { - name: 'Subscription updated', - workspaceId: workspace.id, - userId: user.id, - data: { - plan: Plan.FREE, - }, + await sendTelemetryEvents( + workspace.members.map((m) => ({ + name: 'Subscription updated', + workspaceId: workspace.id, + userId: m.userId, + data: { + plan: Plan.FREE, }, - ]) - } + })) + ) const typebots = await prisma.typebot.findMany({ where: { diff --git a/apps/builder/src/pages/past-due.tsx b/apps/builder/src/pages/past-due.tsx new file mode 100644 index 000000000..abadd849f --- /dev/null +++ b/apps/builder/src/pages/past-due.tsx @@ -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 ( + <> + + + + Your workspace has unpaid invoice(s). + Head over to the billing portal to pay it. + {workspace?.id && ( + + )} + + + ) +} diff --git a/apps/builder/src/pages/suspended.tsx b/apps/builder/src/pages/suspended.tsx index 7819197ed..de8657fe2 100644 --- a/apps/builder/src/pages/suspended.tsx +++ b/apps/builder/src/pages/suspended.tsx @@ -1,13 +1,28 @@ import { TextLink } from '@/components/TextLink' import { DashboardHeader } from '@/features/dashboard/components/DashboardHeader' -import { WorkspaceProvider } from '@/features/workspace/WorkspaceProvider' -import { Heading, Link, Text, VStack } from '@chakra-ui/react' +import { useWorkspace } from '@/features/workspace/WorkspaceProvider' +import { Heading, Text, VStack } 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.isSuspended) return + replace('/typebots') + }, [replace, workspace]) + return ( - + <> - + Your workspace has been suspended. We detected that one of your typebots does not comply with our{' '} @@ -18,14 +33,7 @@ export default function Page() { terms of service - - If you think it's a mistake, feel free to{' '} - - reach out - - . - - + ) } diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index 6eed33b09..14d01dbba 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -415,6 +415,52 @@ "data" ], "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": { "type": "boolean" + }, + "isPastDue": { + "type": "boolean" } }, "required": [ @@ -7507,7 +7556,8 @@ "customStorageLimit", "customSeatsLimit", "isQuarantined", - "isSuspended" + "isSuspended", + "isPastDue" ], "additionalProperties": false } @@ -7636,6 +7686,9 @@ }, "isSuspended": { "type": "boolean" + }, + "isPastDue": { + "type": "boolean" } }, "required": [ @@ -7656,7 +7709,8 @@ "customStorageLimit", "customSeatsLimit", "isQuarantined", - "isSuspended" + "isSuspended", + "isPastDue" ], "additionalProperties": false } @@ -7802,6 +7856,9 @@ }, "isSuspended": { "type": "boolean" + }, + "isPastDue": { + "type": "boolean" } }, "required": [ @@ -7822,7 +7879,8 @@ "customStorageLimit", "customSeatsLimit", "isQuarantined", - "isSuspended" + "isSuspended", + "isPastDue" ], "additionalProperties": false } @@ -54515,6 +54573,9 @@ }, "isSuspended": { "type": "boolean" + }, + "isPastDue": { + "type": "boolean" } }, "required": [ @@ -54535,7 +54596,8 @@ "customStorageLimit", "customSeatsLimit", "isQuarantined", - "isSuspended" + "isSuspended", + "isPastDue" ], "additionalProperties": false, "nullable": true diff --git a/packages/prisma/mysql/schema.prisma b/packages/prisma/mysql/schema.prisma index d7b7bed64..441577273 100644 --- a/packages/prisma/mysql/schema.prisma +++ b/packages/prisma/mysql/schema.prisma @@ -97,6 +97,7 @@ model Workspace { customSeatsLimit Int? isQuarantined Boolean @default(false) isSuspended Boolean @default(false) + isPastDue Boolean @default(false) themeTemplates ThemeTemplate[] } diff --git a/packages/prisma/postgresql/migrations/20231121154057_add_is_past_due_field/migration.sql b/packages/prisma/postgresql/migrations/20231121154057_add_is_past_due_field/migration.sql new file mode 100644 index 000000000..7485d75b3 --- /dev/null +++ b/packages/prisma/postgresql/migrations/20231121154057_add_is_past_due_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Workspace" ADD COLUMN "isPastDue" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/postgresql/schema.prisma b/packages/prisma/postgresql/schema.prisma index ceca8bbcd..a213454ad 100644 --- a/packages/prisma/postgresql/schema.prisma +++ b/packages/prisma/postgresql/schema.prisma @@ -91,6 +91,7 @@ model Workspace { customSeatsLimit Int? isQuarantined Boolean @default(false) isSuspended Boolean @default(false) + isPastDue Boolean @default(false) themeTemplates ThemeTemplate[] } diff --git a/packages/schemas/features/telemetry.ts b/packages/schemas/features/telemetry.ts index c30ecda9e..4394d9246 100644 --- a/packages/schemas/features/telemetry.ts +++ b/packages/schemas/features/telemetry.ts @@ -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', [ workspaceCreatedEventSchema, userCreatedEventSchema, @@ -115,6 +127,8 @@ export const eventSchema = z.discriminatedUnion('name', [ workspaceLimitReachedEventSchema, workspaceAutoQuarantinedEventSchema, subscriptionAutoUpdatedEventSchema, + workspacePastDueEventSchema, + workspaceNotPastDueEventSchema, ]) export type TelemetryEvent = z.infer diff --git a/packages/schemas/features/workspace.ts b/packages/schemas/features/workspace.ts index 525f42e83..90e33786b 100644 --- a/packages/schemas/features/workspace.ts +++ b/packages/schemas/features/workspace.ts @@ -50,6 +50,7 @@ export const workspaceSchema = z.object({ customSeatsLimit: z.number().nullable(), isQuarantined: z.boolean(), isSuspended: z.boolean(), + isPastDue: z.boolean(), }) satisfies z.ZodType export type Workspace = z.infer