🛂 (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:
@@ -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: {
|
||||
|
||||
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 { 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 (
|
||||
<WorkspaceProvider>
|
||||
<>
|
||||
<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>
|
||||
<Text>
|
||||
We detected that one of your typebots does not comply with our{' '}
|
||||
@@ -18,14 +33,7 @@ export default function Page() {
|
||||
terms of service
|
||||
</TextLink>
|
||||
</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>
|
||||
</WorkspaceProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user