🚸 (billing) Make sure customer is not created before launching checkout page
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { parseSubscriptionItems } from '../helpers/parseSubscriptionItems'
|
import { parseSubscriptionItems } from '../helpers/parseSubscriptionItems'
|
||||||
|
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const createCheckoutSession = authenticatedProcedure
|
export const createCheckoutSession = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -64,14 +65,30 @@ export const createCheckoutSession = authenticatedProcedure
|
|||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
},
|
||||||
|
select: {
|
||||||
|
stripeId: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!workspace)
|
|
||||||
|
if (!workspace || isAdminWriteWorkspaceForbidden(workspace, user))
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
message: 'Workspace not found',
|
message: 'Workspace not found',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (workspace.stripeId)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Customer already exists, use updateSubscription endpoint.',
|
||||||
|
})
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||||
apiVersion: '2022-11-15',
|
apiVersion: '2022-11-15',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const createCustomCheckoutSession = authenticatedProcedure
|
export const createCustomCheckoutSession = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -38,15 +39,23 @@ export const createCustomCheckoutSession = authenticatedProcedure
|
|||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
|
||||||
},
|
},
|
||||||
include: {
|
select: {
|
||||||
|
stripeId: true,
|
||||||
claimableCustomPlan: true,
|
claimableCustomPlan: true,
|
||||||
|
name: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (
|
if (
|
||||||
!workspace?.claimableCustomPlan ||
|
!workspace?.claimableCustomPlan ||
|
||||||
workspace.claimableCustomPlan.claimedAt
|
workspace.claimableCustomPlan.claimedAt ||
|
||||||
|
isAdminWriteWorkspaceForbidden(workspace, user)
|
||||||
)
|
)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { WorkspaceRole } from '@typebot.io/prisma'
|
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const getBillingPortalUrl = authenticatedProcedure
|
export const getBillingPortalUrl = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -34,13 +34,18 @@ export const getBillingPortalUrl = authenticatedProcedure
|
|||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
stripeId: true,
|
stripeId: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!workspace?.stripeId)
|
if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
message: 'Workspace not found',
|
message: 'Workspace not found',
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { WorkspaceRole } from '@typebot.io/prisma'
|
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
|
import { subscriptionSchema } from '@typebot.io/schemas/features/billing/subscription'
|
||||||
import { priceIds } from '@typebot.io/lib/pricing'
|
import { priceIds } from '@typebot.io/lib/pricing'
|
||||||
|
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||||
|
|
||||||
export const getSubscription = authenticatedProcedure
|
export const getSubscription = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -36,9 +36,21 @@ export const getSubscription = authenticatedProcedure
|
|||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
},
|
||||||
|
select: {
|
||||||
|
stripeId: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if (!workspace || isReadWorkspaceFobidden(workspace, user))
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Workspace not found',
|
||||||
|
})
|
||||||
if (!workspace?.stripeId)
|
if (!workspace?.stripeId)
|
||||||
return {
|
return {
|
||||||
subscription: null,
|
subscription: null,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { WorkspaceRole } from '@typebot.io/prisma'
|
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
|
import { invoiceSchema } from '@typebot.io/schemas/features/billing/invoice'
|
||||||
|
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const listInvoices = authenticatedProcedure
|
export const listInvoices = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -36,10 +36,18 @@ export const listInvoices = authenticatedProcedure
|
|||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
},
|
||||||
|
select: {
|
||||||
|
stripeId: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!workspace?.stripeId)
|
if (!workspace?.stripeId || isAdminWriteWorkspaceForbidden(workspace, user))
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
message: 'Workspace not found',
|
message: 'Workspace not found',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEven
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
import { Plan } from '@typebot.io/prisma'
|
||||||
import { workspaceSchema } from '@typebot.io/schemas'
|
import { workspaceSchema } from '@typebot.io/schemas'
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@typebot.io/lib/pricing'
|
} from '@typebot.io/lib/pricing'
|
||||||
import { chatPriceIds, storagePriceIds } from './getSubscription'
|
import { chatPriceIds, storagePriceIds } from './getSubscription'
|
||||||
import { createCheckoutSessionUrl } from './createCheckoutSession'
|
import { createCheckoutSessionUrl } from './createCheckoutSession'
|
||||||
|
import { isAdminWriteWorkspaceForbidden } from '@/features/workspace/helpers/isAdminWriteWorkspaceForbidden'
|
||||||
|
|
||||||
export const updateSubscription = authenticatedProcedure
|
export const updateSubscription = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -63,10 +64,21 @@ export const updateSubscription = authenticatedProcedure
|
|||||||
const workspace = await prisma.workspace.findFirst({
|
const workspace = await prisma.workspace.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
},
|
||||||
|
select: {
|
||||||
|
stripeId: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!workspace?.stripeId)
|
if (
|
||||||
|
!workspace?.stripeId ||
|
||||||
|
isAdminWriteWorkspaceForbidden(workspace, user)
|
||||||
|
)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
message: 'Workspace not found',
|
message: 'Workspace not found',
|
||||||
|
|||||||
@@ -197,7 +197,9 @@ test('plan changes should work', async ({ page }) => {
|
|||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.click('text="Billing portal"'),
|
page.click('text="Billing portal"'),
|
||||||
])
|
])
|
||||||
await expect(page.getByText('$247.00 per month')).toBeVisible()
|
await expect(page.getByText('$247.00 per month')).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
await expect(page.getByText('(×25000)')).toBeVisible()
|
await expect(page.getByText('(×25000)')).toBeVisible()
|
||||||
await expect(page.getByText('(×15)')).toBeVisible()
|
await expect(page.getByText('(×15)')).toBeVisible()
|
||||||
await expect(page.locator('text="Add payment method"')).toBeVisible()
|
await expect(page.locator('text="Add payment method"')).toBeVisible()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { UsageProgressBars } from './UsageProgressBars'
|
|||||||
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
|
import { CurrentSubscriptionSummary } from './CurrentSubscriptionSummary'
|
||||||
|
|
||||||
export const BillingSettingsLayout = () => {
|
export const BillingSettingsLayout = () => {
|
||||||
const { workspace, refreshWorkspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
|
|
||||||
if (!workspace) return null
|
if (!workspace) return null
|
||||||
return (
|
return (
|
||||||
@@ -20,10 +20,7 @@ export const BillingSettingsLayout = () => {
|
|||||||
workspace.plan !== Plan.LIFETIME &&
|
workspace.plan !== Plan.LIFETIME &&
|
||||||
workspace.plan !== Plan.UNLIMITED &&
|
workspace.plan !== Plan.UNLIMITED &&
|
||||||
workspace.plan !== Plan.OFFERED && (
|
workspace.plan !== Plan.OFFERED && (
|
||||||
<ChangePlanForm
|
<ChangePlanForm workspace={workspace} />
|
||||||
workspace={workspace}
|
|
||||||
onUpgradeSuccess={refreshWorkspace}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ import { StripeClimateLogo } from './StripeClimateLogo'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
onUpgradeSuccess: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
export const ChangePlanForm = ({ workspace }: Props) => {
|
||||||
const scopedT = useScopedI18n('billing')
|
const scopedT = useScopedI18n('billing')
|
||||||
|
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
@@ -28,7 +27,9 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
|||||||
useState<PreCheckoutModalProps['selectedSubscription']>()
|
useState<PreCheckoutModalProps['selectedSubscription']>()
|
||||||
const [isYearly, setIsYearly] = useState(true)
|
const [isYearly, setIsYearly] = useState(true)
|
||||||
|
|
||||||
const { data } = trpc.billing.getSubscription.useQuery(
|
const trpcContext = trpc.useContext()
|
||||||
|
|
||||||
|
const { data, refetch } = trpc.billing.getSubscription.useQuery(
|
||||||
{
|
{
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
},
|
},
|
||||||
@@ -52,7 +53,8 @@ export const ChangePlanForm = ({ workspace, onUpgradeSuccess }: Props) => {
|
|||||||
window.location.href = checkoutUrl
|
window.location.href = checkoutUrl
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onUpgradeSuccess()
|
refetch()
|
||||||
|
trpcContext.workspace.getWorkspace.invalidate()
|
||||||
showToast({
|
showToast({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
description: scopedT('updateSuccessToast.description', {
|
description: scopedT('updateSuccessToast.description', {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const ChangePlanModal = ({
|
|||||||
type,
|
type,
|
||||||
}: ChangePlanModalProps) => {
|
}: ChangePlanModalProps) => {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const { workspace, refreshWorkspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
|
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
@@ -36,12 +36,7 @@ export const ChangePlanModal = ({
|
|||||||
{t('billing.upgradeLimitLabel', { type: type })}
|
{t('billing.upgradeLimitLabel', { type: type })}
|
||||||
</AlertInfo>
|
</AlertInfo>
|
||||||
)}
|
)}
|
||||||
{workspace && (
|
{workspace && <ChangePlanForm workspace={workspace} />}
|
||||||
<ChangePlanForm
|
|
||||||
workspace={workspace}
|
|
||||||
onUpgradeSuccess={refreshWorkspace}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const workspaceContext = createContext<{
|
|||||||
createWorkspace: (name?: string) => Promise<void>
|
createWorkspace: (name?: string) => Promise<void>
|
||||||
updateWorkspace: (updates: { icon?: string; name?: string }) => void
|
updateWorkspace: (updates: { icon?: string; name?: string }) => void
|
||||||
deleteCurrentWorkspace: () => Promise<void>
|
deleteCurrentWorkspace: () => Promise<void>
|
||||||
refreshWorkspace: () => void
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
@@ -166,11 +165,6 @@ export const WorkspaceProvider = ({
|
|||||||
await deleteWorkspaceMutation.mutateAsync({ workspaceId })
|
await deleteWorkspaceMutation.mutateAsync({ workspaceId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshWorkspace = () => {
|
|
||||||
trpcContext.workspace.getWorkspace.invalidate()
|
|
||||||
trpcContext.billing.getSubscription.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<workspaceContext.Provider
|
<workspaceContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -181,7 +175,6 @@ export const WorkspaceProvider = ({
|
|||||||
createWorkspace,
|
createWorkspace,
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
deleteCurrentWorkspace,
|
deleteCurrentWorkspace,
|
||||||
refreshWorkspace,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MemberInWorkspace, User } from '@typebot.io/prisma'
|
|||||||
|
|
||||||
export const isAdminWriteWorkspaceForbidden = (
|
export const isAdminWriteWorkspaceForbidden = (
|
||||||
workspace: {
|
workspace: {
|
||||||
members: MemberInWorkspace[]
|
members: Pick<MemberInWorkspace, 'role' | 'userId'>[]
|
||||||
},
|
},
|
||||||
user: Pick<User, 'email' | 'id'>
|
user: Pick<User, 'email' | 'id'>
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -133,6 +133,19 @@ const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
}
|
}
|
||||||
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({
|
||||||
|
customer: subscription.customer as string,
|
||||||
|
limit: 1,
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
const existingSubscription = data[0] as
|
||||||
|
| Stripe.Subscription
|
||||||
|
| undefined
|
||||||
|
if (existingSubscription)
|
||||||
|
return res.send({
|
||||||
|
message:
|
||||||
|
'An active subscription still exists. Skipping downgrade.',
|
||||||
|
})
|
||||||
const workspace = await prisma.workspace.update({
|
const workspace = await prisma.workspace.update({
|
||||||
where: {
|
where: {
|
||||||
stripeId: subscription.customer as string,
|
stripeId: subscription.customer as string,
|
||||||
|
|||||||
Reference in New Issue
Block a user