🛂 (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,
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,

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)))

View File

@@ -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,
})
})

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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

View File

@@ -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, 'workspaceId'> & {
typebot: {
collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[]
} & {
workspace: Pick<Workspace, 'isQuarantined' | 'isPastDue'> & {
members: Pick<MemberInWorkspace, 'userId'>[]
}
},
user: Pick<User, 'email' | 'id'>
) => {
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))

View File

@@ -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, 'workspaceId'> & {
typebot: {
collaborators: Pick<CollaboratorsOnTypebots, 'userId' | 'type'>[]
} & {
workspace: Pick<Workspace, 'isQuarantined' | 'isPastDue'> & {
members: Pick<MemberInWorkspace, 'userId' | 'role'>[]
}
},
user: Pick<User, 'id'>
) => {
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'
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)
}