🛂 (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:
@@ -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,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
36
apps/builder/src/pages/past-due.tsx
Normal file
36
apps/builder/src/pages/past-due.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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's a mistake, feel free to{' '}
|
|
||||||
<Link href="mailto:support@typebot.io" textDecor="underline">
|
|
||||||
reach out
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</WorkspaceProvider>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Workspace" ADD COLUMN "isPastDue" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user