✨ Add usage-based new pricing plans
This commit is contained in:
committed by
Baptiste Arnaud
parent
6a1eaea700
commit
898367a33b
@ -1,8 +1,22 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { almostReachedStorageLimitEmail } from 'assets/emails/almostReachedStorageLimitEmail'
|
||||
import { reachedStorageLimitEmail } from 'assets/emails/reachedStorageLimitEmail'
|
||||
import { WorkspaceRole } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { InputBlockType, PublicTypebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { badRequest, generatePresignedUrl, methodNotAllowed, byId } from 'utils'
|
||||
import {
|
||||
badRequest,
|
||||
generatePresignedUrl,
|
||||
methodNotAllowed,
|
||||
byId,
|
||||
getStorageLimit,
|
||||
sendEmailNotification,
|
||||
isDefined,
|
||||
env,
|
||||
} from 'utils'
|
||||
|
||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||
|
||||
const handler = async (
|
||||
req: NextApiRequest,
|
||||
@ -24,6 +38,7 @@ const handler = async (
|
||||
const typebotId = req.query.typebotId as string
|
||||
const blockId = req.query.blockId as string
|
||||
if (!filePath) return badRequest(res, 'Missing filePath or fileType')
|
||||
const hasReachedStorageLimit = await checkStorageLimit(typebotId)
|
||||
const typebot = (await prisma.publicTypebot.findFirst({
|
||||
where: { typebotId },
|
||||
})) as unknown as PublicTypebot
|
||||
@ -42,9 +57,118 @@ const handler = async (
|
||||
sizeLimit: sizeLimit * 1024 * 1024,
|
||||
})
|
||||
|
||||
return res.status(200).send({ presignedUrl })
|
||||
return res.status(200).send({ presignedUrl, hasReachedStorageLimit })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const checkStorageLimit = async (typebotId: string) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: { id: typebotId },
|
||||
include: {
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
additionalStorageIndex: true,
|
||||
plan: true,
|
||||
storageLimitFirstEmailSentAt: true,
|
||||
storageLimitSecondEmailSentAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot?.workspace) throw new Error('Workspace not found')
|
||||
const { workspace } = typebot
|
||||
const {
|
||||
_sum: { storageUsed: totalStorageUsed },
|
||||
} = await prisma.answer.aggregate({
|
||||
where: {
|
||||
storageUsed: { gt: 0 },
|
||||
result: {
|
||||
typebot: {
|
||||
workspace: {
|
||||
id: typebot?.workspaceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_sum: { storageUsed: true },
|
||||
})
|
||||
if (!totalStorageUsed) return false
|
||||
const hasSentFirstEmail = workspace.storageLimitFirstEmailSentAt !== null
|
||||
const hasSentSecondEmail = workspace.storageLimitSecondEmailSentAt !== null
|
||||
const storageLimit = getStorageLimit(typebot.workspace)
|
||||
if (
|
||||
totalStorageUsed >= storageLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
|
||||
!hasSentFirstEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
sendAlmostReachStorageLimitEmail({
|
||||
workspaceId: workspace.id,
|
||||
storageLimit,
|
||||
})
|
||||
if (
|
||||
totalStorageUsed >= storageLimit &&
|
||||
!hasSentSecondEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
sendReachStorageLimitEmail({
|
||||
workspaceId: workspace.id,
|
||||
storageLimit,
|
||||
})
|
||||
return (totalStorageUsed ?? 0) >= getStorageLimit(typebot?.workspace)
|
||||
}
|
||||
|
||||
const sendAlmostReachStorageLimitEmail = async ({
|
||||
workspaceId,
|
||||
storageLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
storageLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableStorageLimit = `${storageLimit}GB`
|
||||
await sendEmailNotification({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You're close to your storage limit",
|
||||
html: almostReachedStorageLimitEmail({
|
||||
readableStorageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
})
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { storageLimitFirstEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
const sendReachStorageLimitEmail = async ({
|
||||
workspaceId,
|
||||
storageLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
storageLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableStorageLimit = `${storageLimit}GB`
|
||||
await sendEmailNotification({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You've hit your storage limit",
|
||||
html: reachedStorageLimitEmail({
|
||||
readableStorageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
})
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { storageLimitSecondEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
saveSuccessLog,
|
||||
} from 'services/api/utils'
|
||||
import Mail from 'nodemailer/lib/mailer'
|
||||
import { newLeadEmailContent } from 'assets/newLeadEmailContent'
|
||||
import { newLeadEmailContent } from 'assets/emails/newLeadEmailContent'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
||||
|
@ -1,8 +1,20 @@
|
||||
import { almostReachedChatsLimitEmail } from 'assets/emails/almostReachedChatsLimitEmail'
|
||||
import { reachedSChatsLimitEmail } from 'assets/emails/reachedChatsLimitEmail'
|
||||
import { Workspace, WorkspaceRole } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { ResultWithAnswers } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authenticateUser } from 'services/api/utils'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
import {
|
||||
env,
|
||||
getChatsLimit,
|
||||
isDefined,
|
||||
methodNotAllowed,
|
||||
parseNumberWithCommas,
|
||||
sendEmailNotification,
|
||||
} from 'utils'
|
||||
|
||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'GET') {
|
||||
@ -30,10 +42,150 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
typebotId,
|
||||
isCompleted: false,
|
||||
},
|
||||
include: {
|
||||
typebot: {
|
||||
include: {
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
plan: true,
|
||||
additionalChatsIndex: true,
|
||||
chatsLimitFirstEmailSentAt: true,
|
||||
chatsLimitSecondEmailSentAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.send(result)
|
||||
const hasReachedLimit = await checkChatsUsage(result.typebot.workspace)
|
||||
res.send({ result, hasReachedLimit })
|
||||
return
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
const checkChatsUsage = async (
|
||||
workspace: Pick<
|
||||
Workspace,
|
||||
| 'id'
|
||||
| 'plan'
|
||||
| 'additionalChatsIndex'
|
||||
| 'chatsLimitFirstEmailSentAt'
|
||||
| 'chatsLimitSecondEmailSentAt'
|
||||
>
|
||||
) => {
|
||||
const chatLimit = getChatsLimit(workspace)
|
||||
if (chatLimit === -1) return
|
||||
const now = new Date()
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
||||
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||
const chatsCount = await prisma.result.count({
|
||||
where: {
|
||||
typebot: { workspaceId: workspace.id },
|
||||
hasStarted: true,
|
||||
createdAt: { gte: firstDayOfMonth, lte: lastDayOfMonth },
|
||||
},
|
||||
})
|
||||
const hasSentFirstEmail =
|
||||
workspace.chatsLimitFirstEmailSentAt !== null &&
|
||||
workspace.chatsLimitFirstEmailSentAt < firstDayOfNextMonth &&
|
||||
workspace.chatsLimitFirstEmailSentAt > firstDayOfMonth
|
||||
const hasSentSecondEmail =
|
||||
workspace.chatsLimitSecondEmailSentAt !== null &&
|
||||
workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth &&
|
||||
workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth
|
||||
if (
|
||||
chatsCount >= chatLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
|
||||
!hasSentFirstEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendAlmostReachChatsLimitEmail({
|
||||
workspaceId: workspace.id,
|
||||
chatLimit,
|
||||
firstDayOfNextMonth,
|
||||
})
|
||||
if (
|
||||
chatsCount >= chatLimit &&
|
||||
!hasSentSecondEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendReachedAlertEmail({
|
||||
workspaceId: workspace.id,
|
||||
chatLimit,
|
||||
firstDayOfNextMonth,
|
||||
})
|
||||
return chatsCount >= chatLimit
|
||||
}
|
||||
|
||||
const sendAlmostReachChatsLimitEmail = async ({
|
||||
workspaceId,
|
||||
chatLimit,
|
||||
firstDayOfNextMonth,
|
||||
}: {
|
||||
workspaceId: string
|
||||
chatLimit: number
|
||||
firstDayOfNextMonth: Date
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableChatsLimit = parseNumberWithCommas(chatLimit)
|
||||
const readableResetDate = firstDayOfNextMonth
|
||||
.toDateString()
|
||||
.split(' ')
|
||||
.slice(1, 4)
|
||||
.join(' ')
|
||||
|
||||
await sendEmailNotification({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: almostReachedChatsLimitEmail({
|
||||
readableChatsLimit,
|
||||
readableResetDate,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
})
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { chatsLimitFirstEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
const sendReachedAlertEmail = async ({
|
||||
workspaceId,
|
||||
chatLimit,
|
||||
firstDayOfNextMonth,
|
||||
}: {
|
||||
workspaceId: string
|
||||
chatLimit: number
|
||||
firstDayOfNextMonth: Date
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableChatsLimit = parseNumberWithCommas(chatLimit)
|
||||
const readableResetDate = firstDayOfNextMonth
|
||||
.toDateString()
|
||||
.split(' ')
|
||||
.slice(1, 4)
|
||||
.join(' ')
|
||||
await sendEmailNotification({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: reachedSChatsLimitEmail({
|
||||
readableChatsLimit,
|
||||
readableResetDate,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
})
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { chatsLimitSecondEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
export default handler
|
||||
|
@ -13,11 +13,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
let storageUsed = 0
|
||||
if (uploadedFiles && answer.content.includes('http')) {
|
||||
const fileUrls = answer.content.split(', ')
|
||||
for (const url of fileUrls) {
|
||||
const { headers } = await got(url)
|
||||
const size = headers['content-length']
|
||||
if (isNotDefined(size)) return
|
||||
storageUsed += parseInt(size, 10)
|
||||
const hasReachedStorageLimit = fileUrls[0] === null
|
||||
if (!hasReachedStorageLimit) {
|
||||
for (const url of fileUrls) {
|
||||
const { headers } = await got(url)
|
||||
const size = headers['content-length']
|
||||
if (isNotDefined(size)) return
|
||||
storageUsed += parseInt(size, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await prisma.answer.upsert({
|
||||
|
Reference in New Issue
Block a user