From 6384a3adae6b9078fa782dcba7a787c955bdddd0 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 20 Sep 2022 19:15:47 +0200 Subject: [PATCH] :bug: (stripe) Fix plan update and management --- .../CurrentSubscriptionContent.tsx | 39 +++++++++----- .../shared/ChangePlanForm/ChangePlanForm.tsx | 6 ++- .../ChangePlanForm/queries/updatePlan.tsx | 6 +-- apps/builder/pages/_app.tsx | 25 ++++----- apps/builder/pages/api/stripe/subscription.ts | 53 ++++++++++++------- apps/builder/playwright/services/database.ts | 9 ++-- apps/viewer/playwright/services/database.ts | 4 +- packages/scripts/index.ts | 19 ++++--- packages/scripts/package.json | 6 +-- packages/scripts/prepareEmojis.ts | 33 ------------ packages/utils/playwright.ts | 2 +- pnpm-lock.yaml | 2 +- 12 files changed, 99 insertions(+), 105 deletions(-) delete mode 100644 packages/scripts/prepareEmojis.ts diff --git a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx index 854057ac71..ca755b44c8 100644 --- a/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx +++ b/apps/builder/components/dashboard/WorkspaceSettingsModal/BillingContent/CurrentSubscriptionContent.tsx @@ -7,6 +7,7 @@ import { Button, Heading, } from '@chakra-ui/react' +import { useToast } from 'components/shared/hooks/useToast' import { PlanTag } from 'components/shared/PlanTag' import { Plan } from 'db' import React, { useState } from 'react' @@ -26,38 +27,48 @@ export const CurrentSubscriptionContent = ({ const [isCancelling, setIsCancelling] = useState(false) const [isRedirectingToBillingPortal, setIsRedirectingToBillingPortal] = useState(false) + const { showToast } = useToast() const cancelSubscription = async () => { if (!stripeId) return setIsCancelling(true) - await cancelSubscriptionQuery(stripeId) + const { error } = await cancelSubscriptionQuery(stripeId) + if (error) { + showToast({ description: error.message }) + return + } onCancelSuccess() setIsCancelling(false) } const isSubscribed = (plan === Plan.STARTER || plan === Plan.PRO) && stripeId - if (isCancelling) return return ( Subscription Current workspace subscription: - - {isSubscribed && ( - - Cancel my subscription - + {isCancelling ? ( + + ) : ( + <> + + {isSubscribed && ( + + Cancel my subscription + + )} + )} - {isSubscribed && ( + {isSubscribed && !isCancelling && ( <> diff --git a/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx b/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx index bf0dc2243e..dc576c3f7d 100644 --- a/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx +++ b/apps/builder/components/shared/ChangePlanForm/ChangePlanForm.tsx @@ -35,7 +35,7 @@ export const ChangePlanForm = () => { selectedStorageLimitIndex === undefined ) return - await pay({ + const response = await pay({ stripeId: workspace.stripeId ?? undefined, user, plan, @@ -43,6 +43,10 @@ export const ChangePlanForm = () => { additionalChats: selectedChatsLimitIndex, additionalStorage: selectedStorageLimitIndex, }) + if (typeof response === 'object' && response?.error) { + showToast({ description: response.error.message }) + return + } refreshCurrentSubscriptionInfo({ additionalChatsIndex: selectedChatsLimitIndex, additionalStorageIndex: selectedStorageLimitIndex, diff --git a/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx b/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx index 439d535fed..91f225ba4a 100644 --- a/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx +++ b/apps/builder/components/shared/ChangePlanForm/queries/updatePlan.tsx @@ -20,7 +20,7 @@ type UpgradeProps = { export const pay = async ({ stripeId, ...props -}: UpgradeProps): Promise<{ newPlan: Plan } | undefined | void> => +}: UpgradeProps): Promise<{ newPlan?: Plan; error?: Error } | void> => isDefined(stripeId) ? updatePlan({ ...props, stripeId }) : redirectToCheckout(props) @@ -31,13 +31,13 @@ export const updatePlan = async ({ workspaceId, additionalChats, additionalStorage, -}: Omit): Promise<{ newPlan: Plan } | undefined> => { +}: Omit): Promise<{ newPlan?: Plan; error?: Error }> => { const { data, error } = await sendRequest<{ message: string }>({ method: 'PUT', url: '/api/stripe/subscription', body: { workspaceId, plan, stripeId, additionalChats, additionalStorage }, }) - if (error || !data) return + if (error || !data) return { error } return { newPlan: plan } } diff --git a/apps/builder/pages/_app.tsx b/apps/builder/pages/_app.tsx index 891bd0c20a..30c88eedba 100644 --- a/apps/builder/pages/_app.tsx +++ b/apps/builder/pages/_app.tsx @@ -18,6 +18,7 @@ import { SupportBubble } from 'components/shared/SupportBubble' import { WorkspaceContext } from 'contexts/WorkspaceContext' import { toTitleCase } from 'utils' import { Session } from 'next-auth' +import { Plan } from 'db' const { ToastContainer, toast } = createStandaloneToast(customTheme) @@ -35,7 +36,14 @@ const App = ({ }, [pathname]) useEffect(() => { - displayStripeCallbackMessage(query.stripe?.toString(), toast) + const newPlan = query.stripe?.toString() + if (newPlan === Plan.STARTER || newPlan === Plan.PRO) + toast({ + position: 'bottom-right', + status: 'success', + title: 'Upgrade success!', + description: `Workspace upgraded to ${toTitleCase(status)} 🎉`, + }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReady]) @@ -68,19 +76,4 @@ const App = ({ ) } -const displayStripeCallbackMessage = ( - status: string | undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toast: any -) => { - if (status && ['pro', 'team'].includes(status)) { - toast({ - position: 'bottom-right', - status: 'success', - title: 'Upgrade success!', - description: `Workspace upgraded to ${toTitleCase(status)} 🎉`, - }) - } -} - export default App diff --git a/apps/builder/pages/api/stripe/subscription.ts b/apps/builder/pages/api/stripe/subscription.ts index f15fd74489..2af21c4dd4 100644 --- a/apps/builder/pages/api/stripe/subscription.ts +++ b/apps/builder/pages/api/stripe/subscription.ts @@ -54,12 +54,12 @@ const getSubscriptionDetails = }) return { additionalChatsIndex: - subscriptions.data[0].items.data.find( + subscriptions.data[0]?.items.data.find( (item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID )?.quantity ?? 0, additionalStorageIndex: - subscriptions.data[0].items.data.find( + subscriptions.data[0]?.items.data.find( (item) => item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID )?.quantity ?? 0, @@ -100,33 +100,34 @@ const createCheckoutSession = (req: NextApiRequest) => { } const updateSubscription = async (req: NextApiRequest) => { - const { customerId, plan, workspaceId, additionalChats, additionalStorage } = - (typeof req.body === 'string' ? JSON.parse(req.body) : req.body) as { - customerId: string - workspaceId: string - additionalChats: number - additionalStorage: number - plan: 'STARTER' | 'PRO' - } + const { stripeId, plan, workspaceId, additionalChats, additionalStorage } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { + stripeId: string + workspaceId: string + additionalChats: number + additionalStorage: number + plan: 'STARTER' | 'PRO' + } if (!process.env.STRIPE_SECRET_KEY) throw Error('STRIPE_SECRET_KEY var is missing') const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2022-08-01', }) const { data } = await stripe.subscriptions.list({ - customer: customerId, + customer: stripeId, }) - const subscription = data[0] - const currentStarterPlanItemId = subscription.items.data.find( + const subscription = data[0] as Stripe.Subscription | undefined + const currentStarterPlanItemId = subscription?.items.data.find( (item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID )?.id - const currentProPlanItemId = subscription.items.data.find( + const currentProPlanItemId = subscription?.items.data.find( (item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID )?.id - const currentAdditionalChatsItemId = subscription.items.data.find( + const currentAdditionalChatsItemId = subscription?.items.data.find( (item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID )?.id - const currentAdditionalStorageItemId = subscription.items.data.find( + const currentAdditionalStorageItemId = subscription?.items.data.find( (item) => item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID )?.id const items = [ @@ -155,9 +156,18 @@ const updateSubscription = async (req: NextApiRequest) => { deleted: additionalStorage === 0, }, ].filter(isDefined) - await stripe.subscriptions.update(subscription.id, { - items, - }) + + if (subscription) { + await stripe.subscriptions.update(subscription.id, { + items, + }) + } else { + await stripe.subscriptions.create({ + customer: stripeId, + items, + }) + } + await prisma.workspace.update({ where: { id: workspaceId }, data: { @@ -187,7 +197,10 @@ const cancelSubscription = const existingSubscription = await stripe.subscriptions.list({ customer: workspace.stripeId, }) - await stripe.subscriptions.del(existingSubscription.data[0].id) + const currentSubscriptionId = existingSubscription.data[0]?.id + if (currentSubscriptionId) + await stripe.subscriptions.del(currentSubscriptionId) + await prisma.workspace.update({ where: { id: workspace.id }, data: { diff --git a/apps/builder/playwright/services/database.ts b/apps/builder/playwright/services/database.ts index 173b333195..ba65f8412a 100644 --- a/apps/builder/playwright/services/database.ts +++ b/apps/builder/playwright/services/database.ts @@ -18,7 +18,7 @@ import { Workspace, } from 'db' import { readFileSync } from 'fs' -import { createFakeResults } from 'utils' +import { injectFakeResults } from 'utils' import { encrypt } from 'utils/api' import Stripe from 'stripe' @@ -75,7 +75,10 @@ export const addSubscriptionToWorkspace = async ( customer: stripeId, items, default_payment_method: paymentId, - currency: 'usd', + currency: 'eur', + }) + await stripe.customers.update(stripeId, { + invoice_settings: { default_payment_method: paymentId }, }) await prisma.workspace.update({ where: { id: workspaceId }, @@ -264,7 +267,7 @@ export const updateUser = (data: Partial) => }, }) -export const createResults = createFakeResults(prisma) +export const createResults = injectFakeResults(prisma) export const createFolder = (workspaceId: string, name: string) => prisma.dashboardFolder.create({ diff --git a/apps/viewer/playwright/services/database.ts b/apps/viewer/playwright/services/database.ts index 4ab7f6f9d5..d3439f8e4a 100644 --- a/apps/viewer/playwright/services/database.ts +++ b/apps/viewer/playwright/services/database.ts @@ -10,7 +10,7 @@ import { } from 'models' import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db' import { readFileSync } from 'fs' -import { createFakeResults } from 'utils' +import { injectFakeResults } from 'utils' import { encrypt } from 'utils/api' const prisma = new PrismaClient() @@ -221,7 +221,7 @@ export const importTypebotInDatabase = async ( }) } -export const createResults = createFakeResults(prisma) +export const createResults = injectFakeResults(prisma) export const createSmtpCredentials = ( id: string, diff --git a/packages/scripts/index.ts b/packages/scripts/index.ts index ec5a2f047b..2b497177ac 100644 --- a/packages/scripts/index.ts +++ b/packages/scripts/index.ts @@ -1,18 +1,23 @@ import { PrismaClient } from 'db' import path from 'path' -import fs from 'fs' +import { injectFakeResults } from 'utils' require('dotenv').config({ path: path.join( __dirname, - process.env.NODE_ENV === 'production' - ? '.env.production' - : process.env.NODE_ENV === 'staging' - ? '.env.staging' - : '.env.local' + process.env.NODE_ENV === 'staging' ? '.env.staging' : '.env.local' ), }) -const main = async () => {} +const prisma = new PrismaClient() + +const main = async () => { + await injectFakeResults(prisma)({ + count: 150, + typebotId: 'cl89sq4vb030109laivd9ck97', + isChronological: false, + idPrefix: 'batch2', + }) +} main().then() diff --git a/packages/scripts/package.json b/packages/scripts/package.json index f2a5b7ac42..63776df8e8 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -7,9 +7,7 @@ "scripts": { "start:local": "ts-node index.ts", "start:staging": "NODE_ENV=staging ts-node index.ts", - "start:prod": "NODE_ENV=production ts-node index.ts", - "start:workspaces:migration": "ts-node workspaceMigration.ts", - "start:workspaces:migration:recover": "ts-node workspaceMigrationRecover.ts" + "start:prod": "NODE_ENV=production ts-node index.ts" }, "devDependencies": { "@types/node": "18.7.16", @@ -17,6 +15,6 @@ "models": "workspace:*", "ts-node": "^10.9.1", "typescript": "^4.8.3", - "utils": "*" + "utils": "workspace:*" } } diff --git a/packages/scripts/prepareEmojis.ts b/packages/scripts/prepareEmojis.ts deleted file mode 100644 index ac1b4dc929..0000000000 --- a/packages/scripts/prepareEmojis.ts +++ /dev/null @@ -1,33 +0,0 @@ -import fs from 'fs' - -export const prepareEmojis = () => { - const emojiData = JSON.parse(fs.readFileSync('./emojiData.json', 'utf8')) - const strippedEmojiData = { - 'Smileys & Emotion': emojiData['Smileys & Emotion'].map( - (emoji: { emoji: any }) => emoji.emoji - ), - 'People & Body': emojiData['People & Body'].map( - (emoji: { emoji: any }) => emoji.emoji - ), - 'Animals & Nature': emojiData['Animals & Nature'].map( - (emoji: { emoji: any }) => emoji.emoji - ), - 'Food & Drink': emojiData['Food & Drink'].map( - (emoji: { emoji: any }) => emoji.emoji - ), - 'Travel & Places': emojiData['Travel & Places'].map( - (emoji: { emoji: any }) => emoji.emoji - ), - Activities: emojiData['Activities'].map( - (emoji: { emoji: any }) => emoji.emoji - ), - Objects: emojiData['Objects'].map((emoji: { emoji: any }) => emoji.emoji), - Symbols: emojiData['Symbols'].map((emoji: { emoji: any }) => emoji.emoji), - Flags: emojiData['Flags'].map((emoji: { emoji: any }) => emoji.emoji), - } - fs.writeFileSync( - 'strippedEmojis.json', - JSON.stringify(strippedEmojiData), - 'utf8' - ) -} diff --git a/packages/utils/playwright.ts b/packages/utils/playwright.ts index 7b3cd2a20f..3772d00e85 100644 --- a/packages/utils/playwright.ts +++ b/packages/utils/playwright.ts @@ -8,7 +8,7 @@ type CreateFakeResultsProps = { fakeStorage?: number } -export const createFakeResults = +export const injectFakeResults = (prisma: PrismaClient) => async ({ count, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc9557ff2d..e9013eccdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -520,7 +520,7 @@ importers: models: workspace:* ts-node: ^10.9.1 typescript: ^4.8.3 - utils: '*' + utils: workspace:* devDependencies: '@types/node': 18.7.16 db: link:../db