2
0

Add usage-based new pricing plans

This commit is contained in:
Baptiste Arnaud
2022-09-17 16:37:33 +02:00
committed by Baptiste Arnaud
parent 6a1eaea700
commit 898367a33b
144 changed files with 4631 additions and 1624 deletions

View File

@@ -65,7 +65,8 @@ export const FileUploadForm = ({
],
})
setIsUploading(false)
if (urls.length) return onSubmit({ label: `File uploaded`, value: urls[0] })
if (urls.length)
return onSubmit({ label: `File uploaded`, value: urls[0] ?? '' })
setErrorMessage('An error occured while uploading the file')
}
const startFilesUpload = async (files: File[]) => {

View File

@@ -0,0 +1,18 @@
BEGIN;
UPDATE "Workspace" SET "plan" = 'PRO' WHERE "plan" = 'TEAM';
CREATE TYPE "Plan_new" AS ENUM ('FREE', 'STARTER', 'PRO', 'LIFETIME', 'OFFERED');
ALTER TABLE "Workspace" ALTER COLUMN "plan" DROP DEFAULT;
ALTER TABLE "Workspace" ALTER COLUMN "plan" TYPE "Plan_new" USING ("plan"::text::"Plan_new");
ALTER TYPE "Plan" RENAME TO "Plan_old";
ALTER TYPE "Plan_new" RENAME TO "Plan";
DROP TYPE "Plan_old";
ALTER TABLE "Workspace" ALTER COLUMN "plan" SET DEFAULT 'FREE';
UPDATE "Workspace" SET "plan" = 'STARTER' WHERE "plan" = 'PRO';
COMMIT;
ALTER TABLE "Workspace" ADD COLUMN "additionalChatsIndex" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "additionalStorageIndex" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "chatsLimitFirstEmailSentAt" TIMESTAMP(3),
ADD COLUMN "chatsLimitSecondEmailSentAt" TIMESTAMP(3),
ADD COLUMN "storageLimitFirstEmailSentAt" TIMESTAMP(3),
ADD COLUMN "storageLimitSecondEmailSentAt" TIMESTAMP(3);

View File

@@ -78,6 +78,12 @@ model Workspace {
members MemberInWorkspace[]
typebots Typebot[]
invitations WorkspaceInvitation[]
additionalChatsIndex Int @default(0)
additionalStorageIndex Int @default(0)
chatsLimitFirstEmailSentAt DateTime?
storageLimitFirstEmailSentAt DateTime?
chatsLimitSecondEmailSentAt DateTime?
storageLimitSecondEmailSentAt DateTime?
}
model MemberInWorkspace {
@@ -267,8 +273,8 @@ enum GraphNavigation {
enum Plan {
FREE
STARTER
PRO
TEAM
LIFETIME
OFFERED
}

View File

@@ -14,18 +14,23 @@
"@rollup/plugin-commonjs": "22.0.2",
"@rollup/plugin-node-resolve": "^14.0.1",
"@rollup/plugin-typescript": "8.5.0",
"@types/nodemailer": "6.4.5",
"aws-sdk": "2.1213.0",
"db": "workspace:*",
"models": "workspace:*",
"next": "12.3.0",
"nodemailer": "^6.7.8",
"rollup": "2.79.0",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-peer-deps-external": "^2.2.4",
"tslib": "^2.4.0",
"typescript": "^4.8.3",
"aws-sdk": "2.1213.0",
"models": "workspace:*",
"next": "12.3.0"
"typescript": "^4.8.3"
},
"peerDependencies": {
"aws-sdk": "^2.1152.0",
"db": "workspace:*",
"models": "workspace:*",
"next": "^12.0.0"
"next": "^12.0.0",
"nodemailer": "^6.7.8"
}
}

View File

@@ -1,2 +1,3 @@
export * from './utils'
export * from './storage'
export * from './sendEmailNotification'

View File

@@ -0,0 +1,18 @@
import { createTransport, SendMailOptions } from 'nodemailer'
import { env } from '../utils'
export const sendEmailNotification = (props: Omit<SendMailOptions, 'from'>) => {
const transporter = createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
},
})
return transporter.sendMail({
from: env('SMTP_FROM'),
...props,
})
}

View File

@@ -2,3 +2,5 @@ export * from './utils'
export * from './api'
export * from './encryption'
export * from './results'
export * from './pricing'
export * from './playwright'

View File

@@ -0,0 +1,60 @@
import { PrismaClient } from 'db'
type CreateFakeResultsProps = {
typebotId: string
count: number
idPrefix?: string
isChronological?: boolean
fakeStorage?: number
}
export const createFakeResults =
(prisma: PrismaClient) =>
async ({
count,
idPrefix = '',
typebotId,
isChronological = true,
fakeStorage,
}: CreateFakeResultsProps) => {
await prisma.result.createMany({
data: [
...Array.from(Array(count)).map((_, idx) => {
const today = new Date()
const rand = Math.random()
return {
id: `${idPrefix}-result${idx}`,
typebotId,
createdAt: isChronological
? new Date(
today.setTime(today.getTime() + 1000 * 60 * 60 * 24 * idx)
)
: new Date(),
isCompleted: rand > 0.5,
hasStarted: true,
}
}),
],
})
return createAnswers(prisma)({ idPrefix, fakeStorage, count })
}
const createAnswers =
(prisma: PrismaClient) =>
({
count,
idPrefix,
fakeStorage,
}: Pick<CreateFakeResultsProps, 'fakeStorage' | 'idPrefix' | 'count'>) => {
return prisma.answer.createMany({
data: [
...Array.from(Array(count)).map((_, idx) => ({
resultId: `${idPrefix}-result${idx}`,
content: `content${idx}`,
blockId: 'block1',
groupId: 'block1',
storageUsed: fakeStorage ? Math.round(fakeStorage / count) : null,
})),
],
})
}

View File

@@ -0,0 +1,85 @@
import { Plan, Workspace } from 'db'
const infinity = -1
export const prices = {
[Plan.STARTER]: 39,
[Plan.PRO]: 89,
} as const
export const chatsLimit = {
[Plan.FREE]: { totalIncluded: 300 },
[Plan.STARTER]: {
totalIncluded: 2000,
increaseStep: {
amount: 500,
price: 10,
},
},
[Plan.PRO]: {
totalIncluded: 10000,
increaseStep: {
amount: 1000,
price: 10,
},
},
[Plan.OFFERED]: { totalIncluded: infinity },
[Plan.LIFETIME]: { totalIncluded: infinity },
} as const
export const storageLimit = {
[Plan.FREE]: { totalIncluded: 0 },
[Plan.STARTER]: {
totalIncluded: 2,
increaseStep: {
amount: 1,
price: 2,
},
},
[Plan.PRO]: {
totalIncluded: 10,
increaseStep: {
amount: 1,
price: 2,
},
},
[Plan.OFFERED]: { totalIncluded: 2 },
[Plan.LIFETIME]: { totalIncluded: 10 },
} as const
export const seatsLimit = {
[Plan.FREE]: { totalIncluded: 0 },
[Plan.STARTER]: {
totalIncluded: 2,
},
[Plan.PRO]: {
totalIncluded: 5,
},
[Plan.OFFERED]: { totalIncluded: 2 },
[Plan.LIFETIME]: { totalIncluded: 8 },
} as const
export const getChatsLimit = ({
plan,
additionalChatsIndex,
}: Pick<Workspace, 'additionalChatsIndex' | 'plan'>) => {
const { totalIncluded } = chatsLimit[plan]
const increaseStep =
plan === Plan.STARTER || plan === Plan.PRO
? chatsLimit[plan].increaseStep
: { amount: 0 }
if (totalIncluded === infinity) return infinity
return totalIncluded + increaseStep.amount * additionalChatsIndex
}
export const getStorageLimit = ({
plan,
additionalStorageIndex,
}: Pick<Workspace, 'additionalStorageIndex' | 'plan'>) => {
const { totalIncluded } = storageLimit[plan]
const increaseStep =
plan === Plan.STARTER || plan === Plan.PRO
? storageLimit[plan].increaseStep
: { amount: 0 }
return totalIncluded + increaseStep.amount * additionalStorageIndex
}

View File

@@ -200,7 +200,7 @@ type UploadFileProps = {
}[]
onUploadProgress?: (percent: number) => void
}
type UrlList = string[]
type UrlList = (string | null)[]
export const uploadFiles = async ({
basePath = '/api',
@@ -214,6 +214,7 @@ export const uploadFiles = async ({
i += 1
const { data } = await sendRequest<{
presignedUrl: { url: string; fields: any }
hasReachedStorageLimit: boolean
}>(
`${basePath}/storage/upload-url?filePath=${encodeURIComponent(
path
@@ -223,18 +224,21 @@ export const uploadFiles = async ({
if (!data?.presignedUrl) continue
const { url, fields } = data.presignedUrl
const formData = new FormData()
Object.entries({ ...fields, file }).forEach(([key, value]) => {
formData.append(key, value as string | Blob)
})
const upload = await fetch(url, {
method: 'POST',
body: formData,
})
if (data.hasReachedStorageLimit) urls.push(null)
else {
const formData = new FormData()
Object.entries({ ...fields, file }).forEach(([key, value]) => {
formData.append(key, value as string | Blob)
})
const upload = await fetch(url, {
method: 'POST',
body: formData,
})
if (!upload.ok) continue
if (!upload.ok) continue
urls.push(`${url.split('?')[0]}/${path}`)
urls.push(`${url.split('?')[0]}/${path}`)
}
}
return urls
}
@@ -276,3 +280,6 @@ export const getViewerUrl = (props?: {
: process.env.NEXT_PUBLIC_VERCEL_URL)
)
}
export const parseNumberWithCommas = (num: number) =>
num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')