✨ Add usage-based new pricing plans
This commit is contained in:
committed by
Baptiste Arnaud
parent
6a1eaea700
commit
898367a33b
39
apps/viewer/assets/emails/almostReachedChatsLimitEmail.mjml
Normal file
39
apps/viewer/assets/emails/almostReachedChatsLimitEmail.mjml
Normal file
@@ -0,0 +1,39 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/yourBotIsFlyingEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>Your bots are chatting a lot. That's amazing. ❤️</mj-text>
|
||||
<mj-text>This means you've almost reached your monthly chats limit. You currently reached 80% of ${readableChatsLimit}.</mj-text>
|
||||
<mj-text>This limit will be reset on ${readableResetDate}.</mj-text>
|
||||
<mj-text>Your bots won't start the chat if you reach the limit before this date. ⚠️</mj-text>
|
||||
<mj-text>If you need more monthly responses, you will need to upgrade your plan.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
||||
33
apps/viewer/assets/emails/almostReachedChatsLimitEmail.ts
Normal file
33
apps/viewer/assets/emails/almostReachedChatsLimitEmail.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/yourBotIsFlyingEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>Your bots are working a lot. That's amazing. 🤖</mj-text>
|
||||
<mj-text>This means you've almost reached your storage limit. You currently reached 80% of your ${readableStorageLimit} limit.</mj-text>
|
||||
<mj-text>Your bots won't collect new files once you reach the limit. ⚠️</mj-text>
|
||||
<mj-text>To make sure it won't happen, you need to upgrade your plan or delete existing results to free up space.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
||||
31
apps/viewer/assets/emails/almostReachedStorageLimitEmail.ts
Normal file
31
apps/viewer/assets/emails/almostReachedStorageLimitEmail.ts
Normal file
File diff suppressed because one or more lines are too long
37
apps/viewer/assets/emails/reachedChatsLimitEmail.mjml
Normal file
37
apps/viewer/assets/emails/reachedChatsLimitEmail.mjml
Normal file
@@ -0,0 +1,37 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>It just happened, you've reached your monthly ${readableChatsLimit} chats limit 😮</mj-text>
|
||||
<mj-text>It means your bots are closed until ${readableResetDate}.</mj-text>
|
||||
<mj-text>If you'd like to continue chatting with your users this month, then you need to upgrade your plan. 🚀</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
||||
33
apps/viewer/assets/emails/reachedChatsLimitEmail.ts
Normal file
33
apps/viewer/assets/emails/reachedChatsLimitEmail.ts
Normal file
File diff suppressed because one or more lines are too long
37
apps/viewer/assets/emails/reachedStorageLimitEmail.mjml
Normal file
37
apps/viewer/assets/emails/reachedStorageLimitEmail.mjml
Normal file
@@ -0,0 +1,37 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>It just happened, you've reached your ${readableStorageLimit} storage limit 😮</mj-text>
|
||||
<mj-text>It means your bots won't collect new files from your users.</mj-text>
|
||||
<mj-text>If you'd like to continue collecting files, then you need to upgrade your plan or remove existing results to free up space. 🚀</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
||||
31
apps/viewer/assets/emails/reachedStorageLimitEmail.ts
Normal file
31
apps/viewer/assets/emails/reachedStorageLimitEmail.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -65,12 +65,14 @@ export const TypebotPage = ({
|
||||
const resultIdFromSession = getExistingResultFromSession()
|
||||
if (resultIdFromSession) setResultId(resultIdFromSession)
|
||||
else {
|
||||
const { error, data: result } = await createResult(typebot.typebotId)
|
||||
const { error, data } = await createResult(typebot.typebotId)
|
||||
if (error) return setError(error)
|
||||
if (result) {
|
||||
setResultId(result.id)
|
||||
if (data?.hasReachedLimit)
|
||||
return setError(new Error('This bot is now closed.'))
|
||||
if (data?.result) {
|
||||
setResultId(data.result.id)
|
||||
if (typebot.settings.general.isNewResultOnRefreshEnabled !== true)
|
||||
setResultInSession(result.id)
|
||||
setResultInSession(data.result.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,14 @@
|
||||
"@types/sanitize-html": "2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.36.2",
|
||||
"@typescript-eslint/parser": "5.36.2",
|
||||
"dotenv": "^16.0.1",
|
||||
"eslint": "8.23.0",
|
||||
"eslint-config-next": "12.3.0",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"google-auth-library": "^8.5.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"mjml": "^4.13.0",
|
||||
"models": "workspace:*",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"papaparse": "^5.3.2",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -8,51 +8,88 @@ import {
|
||||
Typebot,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { Plan, PrismaClient, WorkspaceRole } from 'db'
|
||||
import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db'
|
||||
import { readFileSync } from 'fs'
|
||||
import { encrypt } from 'utils'
|
||||
import { createFakeResults, encrypt } from 'utils'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const proWorkspaceId = 'proWorkspaceViewer'
|
||||
const userId = 'userId'
|
||||
export const freeWorkspaceId = 'freeWorkspace'
|
||||
export const starterWorkspaceId = 'starterWorkspace'
|
||||
|
||||
export const teardownDatabase = async () => {
|
||||
try {
|
||||
await prisma.workspace.deleteMany({
|
||||
where: { members: { some: { userId: { in: ['proUser'] } } } },
|
||||
})
|
||||
await prisma.user.deleteMany({
|
||||
where: { id: { in: ['proUser'] } },
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
return
|
||||
await prisma.workspace.deleteMany({
|
||||
where: {
|
||||
members: {
|
||||
some: { userId },
|
||||
},
|
||||
},
|
||||
})
|
||||
await prisma.user.deleteMany({
|
||||
where: { id: userId },
|
||||
})
|
||||
return prisma.webhook.deleteMany()
|
||||
}
|
||||
|
||||
export const setupDatabase = () => createUser()
|
||||
export const setupDatabase = async () => {
|
||||
await createWorkspaces()
|
||||
await createUser()
|
||||
}
|
||||
|
||||
export const createUser = () =>
|
||||
prisma.user.create({
|
||||
export const createWorkspaces = async () =>
|
||||
prisma.workspace.createMany({
|
||||
data: [
|
||||
{
|
||||
id: freeWorkspaceId,
|
||||
name: 'Free workspace',
|
||||
plan: Plan.FREE,
|
||||
},
|
||||
{
|
||||
id: starterWorkspaceId,
|
||||
name: 'Starter workspace',
|
||||
plan: Plan.STARTER,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const createUser = async () => {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: 'proUser',
|
||||
id: userId,
|
||||
email: 'user@email.com',
|
||||
name: 'User',
|
||||
apiTokens: { create: { token: 'userToken', name: 'default' } },
|
||||
workspaces: {
|
||||
create: {
|
||||
role: WorkspaceRole.ADMIN,
|
||||
workspace: {
|
||||
create: {
|
||||
id: proWorkspaceId,
|
||||
name: 'Pro workspace',
|
||||
plan: Plan.PRO,
|
||||
name: 'John Doe',
|
||||
graphNavigation: GraphNavigation.TRACKPAD,
|
||||
apiTokens: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
name: 'Token 1',
|
||||
token: 'jirowjgrwGREHEtoken1',
|
||||
createdAt: new Date(2022, 1, 1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Github',
|
||||
token: 'jirowjgrwGREHEgdrgithub',
|
||||
createdAt: new Date(2022, 1, 2),
|
||||
},
|
||||
{
|
||||
name: 'N8n',
|
||||
token: 'jirowjgrwGREHrgwhrwn8n',
|
||||
createdAt: new Date(2022, 1, 3),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
await prisma.memberInWorkspace.createMany({
|
||||
data: [
|
||||
{ role: WorkspaceRole.ADMIN, userId, workspaceId: freeWorkspaceId },
|
||||
{ role: WorkspaceRole.ADMIN, userId, workspaceId: starterWorkspaceId },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const createWebhook = (typebotId: string, webhook?: Partial<Webhook>) =>
|
||||
prisma.webhook.create({
|
||||
@@ -66,12 +103,12 @@ export const createWebhook = (typebotId: string, webhook?: Partial<Webhook>) =>
|
||||
|
||||
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
||||
await prisma.typebot.createMany({
|
||||
data: partialTypebots.map(parseTestTypebot) as any[],
|
||||
data: partialTypebots.map(parseTestTypebot),
|
||||
})
|
||||
return prisma.publicTypebot.createMany({
|
||||
data: partialTypebots.map((t) =>
|
||||
parseTypebotToPublicTypebot(t.id + '-published', parseTestTypebot(t))
|
||||
) as any[],
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,7 +144,7 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
||||
id: partialTypebot.id ?? 'typebot',
|
||||
folderId: null,
|
||||
name: 'My typebot',
|
||||
workspaceId: proWorkspaceId,
|
||||
workspaceId: freeWorkspaceId,
|
||||
icon: null,
|
||||
theme: defaultTheme,
|
||||
settings: defaultSettings,
|
||||
@@ -170,7 +207,7 @@ export const importTypebotInDatabase = async (
|
||||
const typebot: Typebot = {
|
||||
...JSON.parse(readFileSync(path).toString()),
|
||||
...updates,
|
||||
workspaceId: proWorkspaceId,
|
||||
workspaceId: starterWorkspaceId,
|
||||
}
|
||||
await prisma.typebot.create({
|
||||
data: typebot,
|
||||
@@ -183,39 +220,7 @@ export const importTypebotInDatabase = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const createResults = async ({ typebotId }: { typebotId: string }) => {
|
||||
await prisma.result.deleteMany()
|
||||
await prisma.result.createMany({
|
||||
data: [
|
||||
...Array.from(Array(200)).map((_, idx) => {
|
||||
const today = new Date()
|
||||
const rand = Math.random()
|
||||
return {
|
||||
id: `result${idx}`,
|
||||
typebotId,
|
||||
createdAt: new Date(
|
||||
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
|
||||
),
|
||||
isCompleted: rand > 0.5,
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
return createAnswers()
|
||||
}
|
||||
|
||||
const createAnswers = () => {
|
||||
return prisma.answer.createMany({
|
||||
data: [
|
||||
...Array.from(Array(200)).map((_, idx) => ({
|
||||
resultId: `result${idx}`,
|
||||
content: `content${idx}`,
|
||||
blockId: 'block1',
|
||||
groupId: 'group1',
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
export const createResults = createFakeResults(prisma)
|
||||
|
||||
export const createSmtpCredentials = (
|
||||
id: string,
|
||||
@@ -229,7 +234,7 @@ export const createSmtpCredentials = (
|
||||
iv,
|
||||
name: smtpData.from.email as string,
|
||||
type: CredentialsType.SMTP,
|
||||
workspaceId: proWorkspaceId,
|
||||
workspaceId: freeWorkspaceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ test.beforeAll(async () => {
|
||||
{ id: typebotId }
|
||||
)
|
||||
await createWebhook(typebotId)
|
||||
await createResults({ typebotId })
|
||||
await createResults({ typebotId, count: 20 })
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import cuid from 'cuid'
|
||||
import path from 'path'
|
||||
import { parse } from 'papaparse'
|
||||
import { typebotViewer } from '../services/selectorUtils'
|
||||
import { importTypebotInDatabase } from '../services/database'
|
||||
import { createResults, importTypebotInDatabase } from '../services/database'
|
||||
import { readFileSync } from 'fs'
|
||||
import { isDefined } from 'utils'
|
||||
import { mockSessionApiCalls } from 'playwright/services/browser'
|
||||
import { describe } from 'node:test'
|
||||
|
||||
test.beforeEach(({ page }) => mockSessionApiCalls(page))
|
||||
const THREE_GIGABYTES = 3 * 1024 * 1024 * 1024
|
||||
|
||||
test('should work as expected', async ({ page, browser }) => {
|
||||
const typebotId = cuid()
|
||||
@@ -85,3 +85,46 @@ test('should work as expected', async ({ page, browser }) => {
|
||||
page2.locator('span:has-text("The specified key does not exist.")')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
describe('Storage limit is reached', () => {
|
||||
const typebotId = cuid()
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../fixtures/typebots/fileUpload.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
}
|
||||
)
|
||||
await createResults({
|
||||
typebotId,
|
||||
count: 20,
|
||||
fakeStorage: THREE_GIGABYTES,
|
||||
})
|
||||
})
|
||||
|
||||
test("shouldn't upload anything if limit has been reached", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`/${typebotId}-public`)
|
||||
await typebotViewer(page)
|
||||
.locator(`input[type="file"]`)
|
||||
.setInputFiles([
|
||||
path.join(__dirname, '../fixtures/typebots/api.json'),
|
||||
path.join(__dirname, '../fixtures/typebots/fileUpload.json'),
|
||||
path.join(__dirname, '../fixtures/typebots/hugeGroup.json'),
|
||||
])
|
||||
await expect(typebotViewer(page).locator(`text="3"`)).toBeVisible()
|
||||
await typebotViewer(page).locator('text="Upload 3 files"').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text="3 files uploaded"`)
|
||||
).toBeVisible()
|
||||
await page.evaluate(() =>
|
||||
window.localStorage.setItem('workspaceId', 'starterWorkspace')
|
||||
)
|
||||
await page.goto(`${process.env.BUILDER_URL}/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text="150%"')).toBeVisible()
|
||||
await expect(page.locator('text="api.json"')).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
23
apps/viewer/playwright/tests/limits.spec.ts
Normal file
23
apps/viewer/playwright/tests/limits.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import {
|
||||
createResults,
|
||||
freeWorkspaceId,
|
||||
importTypebotInDatabase,
|
||||
} from '../services/database'
|
||||
import cuid from 'cuid'
|
||||
import path from 'path'
|
||||
|
||||
test('should not start if chat limit is reached', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../fixtures/typebots/fileUpload.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
publicId: `${typebotId}-public`,
|
||||
workspaceId: freeWorkspaceId,
|
||||
}
|
||||
)
|
||||
await createResults({ typebotId, count: 320 })
|
||||
await page.goto(`/${typebotId}-public`)
|
||||
await expect(page.locator('text="This bot is now closed."')).toBeVisible()
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { Result } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const createResult = async (typebotId: string) => {
|
||||
return sendRequest<Result>({
|
||||
return sendRequest<{ result: Result; hasReachedLimit: boolean }>({
|
||||
url: `/api/typebots/${typebotId}/results`,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user