✨ Add usage-based new pricing plans
This commit is contained in:
committed by
Baptiste Arnaud
parent
6a1eaea700
commit
898367a33b
@@ -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[]) => {
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './utils'
|
||||
export * from './storage'
|
||||
export * from './sendEmailNotification'
|
||||
|
||||
18
packages/utils/src/api/sendEmailNotification.ts
Normal file
18
packages/utils/src/api/sendEmailNotification.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -2,3 +2,5 @@ export * from './utils'
|
||||
export * from './api'
|
||||
export * from './encryption'
|
||||
export * from './results'
|
||||
export * from './pricing'
|
||||
export * from './playwright'
|
||||
|
||||
60
packages/utils/src/playwright.ts
Normal file
60
packages/utils/src/playwright.ts
Normal 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,
|
||||
})),
|
||||
],
|
||||
})
|
||||
}
|
||||
85
packages/utils/src/pricing.ts
Normal file
85
packages/utils/src/pricing.ts
Normal 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
|
||||
}
|
||||
@@ -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, ',')
|
||||
|
||||
Reference in New Issue
Block a user