🧑‍💻 (emails) Add decent emails management

Use mjml-react to generate emails. Put all emails in a independent package.
This commit is contained in:
Baptiste Arnaud
2022-10-01 07:00:05 +02:00
parent e1f2d49342
commit 1654de3c1f
50 changed files with 4811 additions and 4048 deletions

View File

@@ -1,39 +0,0 @@
<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>

File diff suppressed because one or more lines are too long

View File

@@ -1,38 +0,0 @@
<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>

File diff suppressed because one or more lines are too long

View File

@@ -1,188 +0,0 @@
const emailRegex =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const parseResult = (val?: string) =>
val && emailRegex.test(val) ? `<a href="mailto:${val}">${val}</a>` : val ?? ''
export const newLeadEmailContent = (
resultUrl: string,
answers: { [key: string]: string }
) => `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
</style>
</head>
<body style="word-spacing:normal;background-color:#ffffff;">
<div style="background-color:#ffffff;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="border:1px solid #E2E8F0;direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:598px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;padding-bottom:20px;padding-top:20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;line-height:23px;text-align:left;color:#000000;">Your typebot has collected a <strong>new lead!</strong> 🥳</div>
</td>
</tr>
${Object.keys(answers)
.map(
(key) => `<tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;line-height:23px;text-align:left;color:#000000;">${key}: ${parseResult(
answers[key]
)}</div>
</td>
</tr>`
)
.join('')}
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:598px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;padding-bottom:20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align:top;padding:0;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
<tbody>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:0;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#0042da" role="presentation" style="border:none;border-radius:5px;cursor:auto;mso-padding-alt:10px 25px;background:#0042da;" valign="middle">
<a href="${resultUrl}" style="display:inline-block;background:#0042da;color:white;font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:23px;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:5px;" target="_blank"> Check results </a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`

View File

@@ -1,37 +0,0 @@
<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>

File diff suppressed because one or more lines are too long

View File

@@ -1,37 +0,0 @@
<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>

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { withSentryConfig } = require('@sentry/nextjs')
const path = require('path')
const withTM = require('next-transpile-modules')(['utils', 'models'])
const withTM = require('next-transpile-modules')(['utils', 'models', 'emails'])
/** @type {import('next').NextConfig} */
const nextConfig = {

View File

@@ -41,6 +41,7 @@
"@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.36.2",
"dotenv": "^16.0.1",
"emails": "workspace:*",
"eslint": "8.23.0",
"eslint-config-next": "12.3.0",
"eslint-plugin-react": "^7.31.8",
@@ -52,6 +53,7 @@
"next-transpile-modules": "^9.0.0",
"papaparse": "^5.3.2",
"typescript": "^4.8.3",
"uglify-js": "^3.17.2",
"utils": "workspace:*"
}
}

View File

@@ -1,31 +1,16 @@
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document'
/* eslint-disable @next/next/no-sync-scripts */
import { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
const Document = () => (
<Html>
<Head>
<script src="/__env.js" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
render() {
return (
<Html>
<Head>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/__env.js" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
export default Document

View File

@@ -1,17 +1,14 @@
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,
sendEmailNotification,
} from 'utils/api'
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
import { byId, getStorageLimit, isDefined, env } from 'utils'
import {
sendAlmostReachedStorageLimitEmail,
sendReachedStorageLimitEmail,
} from 'emails'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
@@ -60,6 +57,7 @@ const handler = async (
return methodNotAllowed(res)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const checkStorageLimit = async (typebotId: string) => {
const typebot = await prisma.typebot.findFirst({
where: { id: typebotId },
@@ -102,7 +100,7 @@ const checkStorageLimit = async (typebotId: string) => {
!hasSentFirstEmail &&
env('E2E_TEST') !== 'true'
)
await sendAlmostReachStorageLimitEmail({
await sendAlmostReachStorageLimitNotification({
workspaceId: workspace.id,
storageLimit,
})
@@ -111,14 +109,14 @@ const checkStorageLimit = async (typebotId: string) => {
!hasSentSecondEmail &&
env('E2E_TEST') !== 'true'
)
await sendReachStorageLimitEmail({
await sendReachStorageLimitNotification({
workspaceId: workspace.id,
storageLimit,
})
return totalStorageUsed >= storageLimitBytes
}
const sendAlmostReachStorageLimitEmail = async ({
const sendAlmostReachStorageLimitNotification = async ({
workspaceId,
storageLimit,
}: {
@@ -129,22 +127,20 @@ const sendAlmostReachStorageLimitEmail = async ({
where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } },
})
const readableStorageLimit = `${storageLimit}GB`
await sendEmailNotification({
await sendAlmostReachedStorageLimitEmail({
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}`,
}),
storageLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
})
await prisma.workspace.update({
where: { id: workspaceId },
data: { storageLimitFirstEmailSentAt: new Date() },
})
}
const sendReachStorageLimitEmail = async ({
const sendReachStorageLimitNotification = async ({
workspaceId,
storageLimit,
}: {
@@ -155,15 +151,13 @@ const sendReachStorageLimitEmail = async ({
where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } },
})
const readableStorageLimit = `${storageLimit}GB`
await sendEmailNotification({
await sendReachedStorageLimitEmail({
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}`,
}),
storageLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
})
await prisma.workspace.update({
where: { id: workspaceId },
data: { storageLimitSecondEmailSentAt: new Date() },

View File

@@ -18,7 +18,7 @@ import {
saveSuccessLog,
} from 'services/api/utils'
import Mail from 'nodemailer/lib/mailer'
import { newLeadEmailContent } from 'assets/emails/newLeadEmailContent'
import { DefaultBotNotificationEmail, render } from 'emails'
const cors = initMiddleware(Cors())
@@ -84,6 +84,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
typebotId,
resultValues,
})
if (!emailBody) {
await saveErrorLog(resultId, 'Email not sent', {
transportConfig,
@@ -121,6 +122,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await saveErrorLog(resultId, 'Email not sent', {
transportConfig,
email,
error: err,
})
return res.status(500).send({
message: `Email not sent. Error: ${err}`,
@@ -177,10 +179,12 @@ const getEmailBody = async ({
],
})(resultValues)
return {
html: newLeadEmailContent(
`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`,
omit(answers, 'submittedAt')
),
html: render(
<DefaultBotNotificationEmail
resultsUrl={`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
answers={omit(answers, 'submittedAt')}
/>
).html,
}
}

View File

@@ -1,12 +1,14 @@
import { almostReachedChatsLimitEmail } from 'assets/emails/almostReachedChatsLimitEmail'
import { reachedSChatsLimitEmail } from 'assets/emails/reachedChatsLimitEmail'
import { Workspace, WorkspaceRole } from 'db'
import {
sendAlmostReachedChatsLimitEmail,
sendReachedChatsLimitEmail,
} from 'emails'
import prisma from 'libs/prisma'
import { ResultWithAnswers } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { authenticateUser } from 'services/api/utils'
import { env, getChatsLimit, isDefined, parseNumberWithCommas } from 'utils'
import { sendEmailNotification, methodNotAllowed } from 'utils/api'
import { env, getChatsLimit, isDefined } from 'utils'
import { methodNotAllowed } from 'utils/api'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
@@ -60,6 +62,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
methodNotAllowed(res)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const checkChatsUsage = async (
workspace: Pick<
Workspace,
@@ -96,87 +99,64 @@ const checkChatsUsage = async (
!hasSentFirstEmail &&
env('E2E_TEST') !== 'true'
)
await sendAlmostReachChatsLimitEmail({
await sendAlmostReachChatsLimitNotification({
workspaceId: workspace.id,
chatLimit: chatsLimit,
firstDayOfNextMonth,
chatsLimit,
})
if (
chatsCount >= chatsLimit &&
!hasSentSecondEmail &&
env('E2E_TEST') !== 'true'
)
await sendReachedAlertEmail({
await sendReachedAlertNotification({
workspaceId: workspace.id,
chatLimit: chatsLimit,
firstDayOfNextMonth,
chatsLimit,
})
return chatsCount >= chatsLimit
}
const sendAlmostReachChatsLimitEmail = async ({
const sendAlmostReachChatsLimitNotification = async ({
workspaceId,
chatLimit,
firstDayOfNextMonth,
chatsLimit,
}: {
workspaceId: string
chatLimit: number
firstDayOfNextMonth: Date
chatsLimit: number
}) => {
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({
await sendAlmostReachedChatsLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined),
subject: "You're close to your chats limit",
html: almostReachedChatsLimitEmail({
readableChatsLimit,
readableResetDate,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
}),
chatsLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
})
await prisma.workspace.update({
where: { id: workspaceId },
data: { chatsLimitFirstEmailSentAt: new Date() },
})
}
const sendReachedAlertEmail = async ({
const sendReachedAlertNotification = async ({
workspaceId,
chatLimit,
firstDayOfNextMonth,
chatsLimit,
}: {
workspaceId: string
chatLimit: number
firstDayOfNextMonth: Date
chatsLimit: number
}) => {
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({
await sendReachedChatsLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined),
subject: "You've hit your monthly chats limit",
html: reachedSChatsLimitEmail({
readableChatsLimit,
readableResetDate,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
}),
chatsLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
})
await prisma.workspace.update({
where: { id: workspaceId },
data: { chatsLimitSecondEmailSentAt: new Date() },