diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts index b7cac892f9..60dd15c0c0 100644 --- a/apps/web/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -182,21 +182,17 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [ export enum PROJECT_FEATURE_KEYS { FREE = "free", STARTUP = "startup", - SCALE = "scale", - ENTERPRISE = "enterprise", + CUSTOM = "custom", } export enum STRIPE_PROJECT_NAMES { STARTUP = "Formbricks Startup", - SCALE = "Formbricks Scale", - ENTERPRISE = "Formbricks Enterprise", + CUSTOM = "Formbricks Custom", } export enum STRIPE_PRICE_LOOKUP_KEYS { STARTUP_MAY25_MONTHLY = "STARTUP_MAY25_MONTHLY", STARTUP_MAY25_YEARLY = "STARTUP_MAY25_YEARLY", - SCALE_MONTHLY = "formbricks_scale_monthly", - SCALE_YEARLY = "formbricks_scale_yearly", } export const BILLING_LIMITS = { @@ -210,10 +206,10 @@ export const BILLING_LIMITS = { RESPONSES: 5000, MIU: 7500, }, - SCALE: { - PROJECTS: 5, - RESPONSES: 10000, - MIU: 30000, + CUSTOM: { + PROJECTS: null, + RESPONSES: null, + MIU: null, }, } as const; diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index df975d08e5..077878cd99 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -986,15 +986,12 @@ "manage_subscription": "Abonnement verwalten", "monthly": "Monatlich", "monthly_identified_users": "Monatlich identifizierte Nutzer", - "per_month": "pro Monat", - "per_year": "pro Jahr", "plan_upgraded_successfully": "Plan erfolgreich aktualisiert", "premium_support_with_slas": "Premium-Support mit SLAs", "remove_branding": "Branding entfernen", "startup": "Start-up", "startup_description": "Alles in 'Free' mit zusätzlichen Funktionen.", "switch_plan": "Plan wechseln", - "switch_plan_confirmation_text": "Bist du sicher, dass du zum {plan}-Plan wechseln möchtest? Dir werden {price} {period} berechnet.", "team_access_roles": "Rollen für Teammitglieder", "unable_to_upgrade_plan": "Plan kann nicht aktualisiert werden", "unlimited_miu": "Unbegrenzte MIU", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 78e9eb1119..3a6cf7bfe0 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -986,15 +986,12 @@ "manage_subscription": "Manage Subscription", "monthly": "Monthly", "monthly_identified_users": "Monthly Identified Users", - "per_month": "per month", - "per_year": "per year", "plan_upgraded_successfully": "Plan upgraded successfully", "premium_support_with_slas": "Premium support with SLAs", "remove_branding": "Remove Branding", "startup": "Startup", "startup_description": "Everything in Free with additional features.", "switch_plan": "Switch Plan", - "switch_plan_confirmation_text": "Are you sure you want to switch to the {plan} plan? You will be charged {price} {period}.", "team_access_roles": "Team Access Roles", "unable_to_upgrade_plan": "Unable to upgrade plan", "unlimited_miu": "Unlimited MIU", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index a83b241fd0..3f7c18ae0f 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -986,15 +986,12 @@ "manage_subscription": "Gérer l'abonnement", "monthly": "Mensuel", "monthly_identified_users": "Utilisateurs mensuels identifiés", - "per_month": "par mois", - "per_year": "par an", "plan_upgraded_successfully": "Plan mis à jour avec succès", "premium_support_with_slas": "Assistance premium avec accord de niveau de service", "remove_branding": "Suppression du logo", "startup": "Initial", "startup_description": "Tout est gratuit avec des fonctionnalités supplémentaires.", "switch_plan": "Changer de plan", - "switch_plan_confirmation_text": "Êtes-vous sûr de vouloir passer au plan {plan} ? Vous serez facturé {price} {period}.", "team_access_roles": "Gestion des accès", "unable_to_upgrade_plan": "Impossible de mettre à niveau le plan", "unlimited_miu": "MIU Illimité", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index d0691757f6..5f70820ab1 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -986,15 +986,12 @@ "manage_subscription": "サブスクリプションを管理", "monthly": "月間", "monthly_identified_users": "月間識別ユーザー数", - "per_month": "月", - "per_year": "年", "plan_upgraded_successfully": "プランを正常にアップグレードしました", "premium_support_with_slas": "SLA付きプレミアムサポート", "remove_branding": "ブランディングを削除", "startup": "スタートアップ", "startup_description": "無料プランのすべての機能に追加機能。", "switch_plan": "プランを切り替え", - "switch_plan_confirmation_text": "本当に {plan} プランに切り替えますか? {price} {period} が請求されます。", "team_access_roles": "チームアクセスロール", "unable_to_upgrade_plan": "プランをアップグレードできません", "unlimited_miu": "無制限のMIU", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 845fa1edb7..286064a997 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -986,15 +986,12 @@ "manage_subscription": "Gerenciar Assinatura", "monthly": "mensal", "monthly_identified_users": "Usuários Identificados Mensalmente", - "per_month": "por mês", - "per_year": "por ano", "plan_upgraded_successfully": "Plano atualizado com sucesso", "premium_support_with_slas": "Suporte premium com SLAs", "remove_branding": "Remover Marca", "startup": "startup", "startup_description": "Tudo no Grátis com recursos adicionais.", "switch_plan": "Mudar Plano", - "switch_plan_confirmation_text": "Tem certeza de que deseja mudar para o plano {plan}? Você será cobrado {price} {period}.", "team_access_roles": "Funções de Acesso da Equipe", "unable_to_upgrade_plan": "Não foi possível atualizar o plano", "unlimited_miu": "MIU Ilimitado", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 69ee8269a7..9b8d584a22 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -986,15 +986,12 @@ "manage_subscription": "Gerir Subscrição", "monthly": "Mensal", "monthly_identified_users": "Utilizadores Identificados Mensalmente", - "per_month": "por mês", - "per_year": "por ano", "plan_upgraded_successfully": "Plano atualizado com sucesso", "premium_support_with_slas": "Suporte premium com SLAs", "remove_branding": "Possibilidade de remover o logo", "startup": "Inicialização", "startup_description": "Tudo no plano Gratuito com funcionalidades adicionais.", "switch_plan": "Mudar Plano", - "switch_plan_confirmation_text": "Tem a certeza de que deseja mudar para o plano {plan}? Ser-lhe-á cobrado {price} {period}.", "team_access_roles": "Funções de Acesso da Equipa", "unable_to_upgrade_plan": "Não é possível atualizar o plano", "unlimited_miu": "MIU Ilimitado", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 762f30de08..b1dc2bf7a3 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -986,15 +986,12 @@ "manage_subscription": "Gestionați abonamentul", "monthly": "Lunar", "monthly_identified_users": "Utilizatori identificați lunar", - "per_month": "pe lună", - "per_year": "pe an", "plan_upgraded_successfully": "Planul a fost upgradat cu succes", "premium_support_with_slas": "Suport premium cu SLA-uri", "remove_branding": "Eliminare branding", "startup": "Pornire", "startup_description": "Totul din versiunea gratuită cu funcții suplimentare.", "switch_plan": "Schimbă planul", - "switch_plan_confirmation_text": "Sigur doriți să treceți la planul {plan}? Vi se va percepe {price} {period}.", "team_access_roles": "Roluri acces echipă", "unable_to_upgrade_plan": "Nu se poate upgrada planul", "unlimited_miu": "MIU Nelimitat", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index c9c21fc9ba..3690d05cad 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -986,15 +986,12 @@ "manage_subscription": "管理 订阅", "monthly": "每月", "monthly_identified_users": "每月 已识别的 用户", - "per_month": "每月", - "per_year": "每年", "plan_upgraded_successfully": "计划 升级 成功", "premium_support_with_slas": "优质支持与 SLAs", "remove_branding": "移除 品牌", "startup": "初创企业", "startup_description": "包含免费版的所有功能以及附加功能.", "switch_plan": "切换 计划", - "switch_plan_confirmation_text": "你确定要切换到 {plan} 计划吗?你将被收取 {price} {period} 。", "team_access_roles": "团队访问角色", "unable_to_upgrade_plan": "无法升级计划", "unlimited_miu": "无限 MIU", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 777c89d1a3..eae646e923 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -986,15 +986,12 @@ "manage_subscription": "管理訂閱", "monthly": "每月", "monthly_identified_users": "每月識別使用者", - "per_month": "每月", - "per_year": "每年", "plan_upgraded_successfully": "方案已成功升級", "premium_support_with_slas": "具有 SLA 的頂級支援", "remove_branding": "移除品牌", "startup": "啟動版", "startup_description": "免費方案中的所有功能以及其他功能。", "switch_plan": "切換方案", - "switch_plan_confirmation_text": "您確定要切換到 {plan} 計劃嗎?您將被收取 {price} {period}。", "team_access_roles": "團隊存取角色", "unable_to_upgrade_plan": "無法升級方案", "unlimited_miu": "無限 MIU", diff --git a/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts b/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts index f6085c6c05..aa4678ceb1 100644 --- a/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts +++ b/apps/web/modules/ee/billing/api/lib/checkout-session-completed.ts @@ -1,43 +1,67 @@ import Stripe from "stripe"; +import { logger } from "@formbricks/logger"; import { ResourceNotFoundError } from "@formbricks/types/errors"; -import { STRIPE_API_VERSION } from "@/lib/constants"; +import { BILLING_LIMITS, PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@/lib/constants"; import { env } from "@/lib/env"; -import { getOrganization } from "@/lib/organization/service"; +import { getOrganization, updateOrganization } from "@/lib/organization/service"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { - // https://github.com/stripe/stripe-node#configuration apiVersion: STRIPE_API_VERSION, }); export const handleCheckoutSessionCompleted = async (event: Stripe.Event) => { const checkoutSession = event.data.object as Stripe.Checkout.Session; - if (!checkoutSession.metadata || !checkoutSession.metadata.organizationId) + if (!checkoutSession.metadata?.organizationId) throw new ResourceNotFoundError("No organizationId found in checkout session", checkoutSession.id); - const stripeSubscriptionObject = await stripe.subscriptions.retrieve( - checkoutSession.subscription as string - ); - const { customer: stripeCustomer } = (await stripe.checkout.sessions.retrieve(checkoutSession.id, { - expand: ["customer"], - })) as { customer: Stripe.Customer }; - - const organization = await getOrganization(checkoutSession.metadata!.organizationId); + const organization = await getOrganization(checkoutSession.metadata.organizationId); if (!organization) throw new ResourceNotFoundError("Organization not found", checkoutSession.metadata.organizationId); - await stripe.subscriptions.update(stripeSubscriptionObject.id, { - metadata: { - organizationId: organization.id, - responses: checkoutSession.metadata.responses, - miu: checkoutSession.metadata.miu, + const subscription = await stripe.subscriptions.retrieve(checkoutSession.subscription as string, { + expand: ["items.data.price"], + }); + + let period: "monthly" | "yearly" = "monthly"; + + if (subscription.items?.data && subscription.items.data.length > 0) { + const firstItem = subscription.items.data[0]; + const interval = firstItem.price?.recurring?.interval; + period = interval === "year" ? "yearly" : "monthly"; + } + + await updateOrganization(checkoutSession.metadata.organizationId, { + billing: { + ...organization.billing, + stripeCustomerId: checkoutSession.customer as string, + plan: PROJECT_FEATURE_KEYS.STARTUP, + period, + limits: { + projects: BILLING_LIMITS.STARTUP.PROJECTS, + monthly: { + responses: BILLING_LIMITS.STARTUP.RESPONSES, + miu: BILLING_LIMITS.STARTUP.MIU, + }, + }, + periodStart: new Date(), }, }); - await stripe.customers.update(stripeCustomer.id, { - name: organization.name, - metadata: { organizationId: organization.id }, - invoice_settings: { - default_payment_method: stripeSubscriptionObject.default_payment_method as string, + logger.info( + { + organizationId: checkoutSession.metadata.organizationId, + plan: PROJECT_FEATURE_KEYS.STARTUP, + period, + checkoutSessionId: checkoutSession.id, }, - }); + "Subscription activated" + ); + + const stripeCustomer = await stripe.customers.retrieve(checkoutSession.customer as string); + if (stripeCustomer && !stripeCustomer.deleted) { + await stripe.customers.update(stripeCustomer.id, { + name: organization.name, + metadata: { organizationId: organization.id }, + }); + } }; diff --git a/apps/web/modules/ee/billing/api/lib/constants.ts b/apps/web/modules/ee/billing/api/lib/constants.ts index 4178d3c27a..44d32f08cb 100644 --- a/apps/web/modules/ee/billing/api/lib/constants.ts +++ b/apps/web/modules/ee/billing/api/lib/constants.ts @@ -58,7 +58,7 @@ export const getCloudPricingData = (t: TFunction): { plans: TPricingPlan[] } => }; const customPlan: TPricingPlan = { - id: "enterprise", + id: "custom", name: t("environments.settings.billing.custom"), featured: false, CTA: t("common.request_pricing"), diff --git a/apps/web/modules/ee/billing/api/lib/create-subscription.ts b/apps/web/modules/ee/billing/api/lib/create-subscription.ts index ea4819899f..0eebbde63f 100644 --- a/apps/web/modules/ee/billing/api/lib/create-subscription.ts +++ b/apps/web/modules/ee/billing/api/lib/create-subscription.ts @@ -16,22 +16,17 @@ export const createSubscription = async ( try { const organization = await getOrganization(organizationId); if (!organization) throw new Error("Organization not found."); - let isNewOrganization = - !organization.billing.stripeCustomerId || - !(await stripe.customers.retrieve(organization.billing.stripeCustomerId)); const priceObject = ( await stripe.prices.list({ lookup_keys: [priceLookupKey], - expand: ["data.product"], }) ).data[0]; if (!priceObject) throw new Error("Price not found"); - const responses = parseInt((priceObject.product as Stripe.Product).metadata.responses); - const miu = parseInt((priceObject.product as Stripe.Product).metadata.miu); - const checkoutSessionCreateParams: Stripe.Checkout.SessionCreateParams = { + // Always create a checkout session - let Stripe handle existing customers + const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [ { @@ -41,63 +36,20 @@ export const createSubscription = async ( ], success_url: `${WEBAPP_URL}/billing-confirmation?environmentId=${environmentId}`, cancel_url: `${WEBAPP_URL}/environments/${environmentId}/settings/billing`, + customer: organization.billing.stripeCustomerId ?? undefined, allow_promotion_codes: true, subscription_data: { metadata: { organizationId }, - trial_period_days: 30, + trial_period_days: 15, }, - metadata: { organizationId, responses, miu }, + metadata: { organizationId }, billing_address_collection: "required", automatic_tax: { enabled: true }, tax_id_collection: { enabled: true }, payment_method_data: { allow_redisplay: "always" }, - ...(!isNewOrganization && { - customer: organization.billing.stripeCustomerId ?? undefined, - customer_update: { - name: "auto", - }, - }), - }; - - // if the organization has never purchased a plan then we just create a new session and store their stripe customer id - if (isNewOrganization) { - const session = await stripe.checkout.sessions.create(checkoutSessionCreateParams); - - return { status: 200, data: "Your Plan has been upgraded!", newPlan: true, url: session.url }; - } - - const existingSubscription = await stripe.subscriptions.list({ - customer: organization.billing.stripeCustomerId as string, }); - if (existingSubscription.data?.length > 0) { - const existingSubscriptionItem = existingSubscription.data[0].items.data[0]; - - await stripe.subscriptions.update(existingSubscription.data[0].id, { - items: [ - { - id: existingSubscriptionItem.id, - deleted: true, - }, - { - price: priceObject.id, - }, - ], - cancel_at_period_end: false, - }); - } else { - // Create a new checkout again if there is no active subscription - const session = await stripe.checkout.sessions.create(checkoutSessionCreateParams); - - return { status: 200, data: "Your Plan has been upgraded!", newPlan: true, url: session.url }; - } - - return { - status: 200, - data: "Congrats! Added to your existing subscription!", - newPlan: false, - url: "", - }; + return { status: 200, data: "Your Plan has been upgraded!", newPlan: true, url: session.url }; } catch (err) { logger.error(err, "Error creating subscription"); return { diff --git a/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts b/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts index b2522af991..f01948b01f 100644 --- a/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts +++ b/apps/web/modules/ee/billing/api/lib/invoice-finalized.ts @@ -1,32 +1,68 @@ import Stripe from "stripe"; +import { logger } from "@formbricks/logger"; +import { ResourceNotFoundError } from "@formbricks/types/errors"; +import { STRIPE_API_VERSION } from "@/lib/constants"; +import { env } from "@/lib/env"; import { getOrganization, updateOrganization } from "@/lib/organization/service"; export const handleInvoiceFinalized = async (event: Stripe.Event) => { const invoice = event.data.object as Stripe.Invoice; - const stripeSubscriptionDetails = invoice.subscription_details; - const organizationId = stripeSubscriptionDetails?.metadata?.organizationId; - - if (!organizationId) { - throw new Error("No organizationId found in subscription"); + const subscriptionId = invoice.subscription as string; + if (!subscriptionId) { + logger.warn({ invoiceId: invoice.id }, "Invoice finalized without subscription ID"); + return { status: 400, message: "No subscription ID found in invoice" }; } - const organization = await getOrganization(organizationId); - if (!organization) { - throw new Error("Organization not found"); + try { + const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { + apiVersion: STRIPE_API_VERSION, + }); + + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + const organizationId = subscription.metadata?.organizationId; + + if (!organizationId) { + logger.warn( + { + subscriptionId, + invoiceId: invoice.id, + }, + "No organizationId found in subscription metadata" + ); + return { status: 400, message: "No organizationId found in subscription" }; + } + + const organization = await getOrganization(organizationId); + if (!organization) { + throw new ResourceNotFoundError("Organization not found", organizationId); + } + + const periodStartTimestamp = invoice.lines.data[0]?.period?.start; + const periodStart = periodStartTimestamp ? new Date(periodStartTimestamp * 1000) : new Date(); + + await updateOrganization(organizationId, { + billing: { + ...organization.billing, + periodStart, + }, + }); + + logger.info( + { + organizationId, + periodStart, + invoiceId: invoice.id, + }, + "Billing period updated successfully" + ); + + return { status: 200, message: "Billing period updated successfully" }; + } catch (error) { + logger.error(error, "Error updating billing period", { + invoiceId: invoice.id, + subscriptionId, + }); + return { status: 500, message: "Error updating billing period" }; } - - const periodStartTimestamp = invoice.lines.data[0].period.start; - const periodStart = periodStartTimestamp ? new Date(periodStartTimestamp * 1000) : new Date(); - - await updateOrganization(organizationId, { - ...organization, - billing: { - ...organization.billing, - stripeCustomerId: invoice.customer as string, - periodStart, - }, - }); - - return { status: 200, message: "success" }; }; diff --git a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts index ceee14203c..81acba2834 100644 --- a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts +++ b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts @@ -4,7 +4,6 @@ import { STRIPE_API_VERSION } from "@/lib/constants"; import { env } from "@/lib/env"; import { handleCheckoutSessionCompleted } from "@/modules/ee/billing/api/lib/checkout-session-completed"; import { handleInvoiceFinalized } from "@/modules/ee/billing/api/lib/invoice-finalized"; -import { handleSubscriptionCreatedOrUpdated } from "@/modules/ee/billing/api/lib/subscription-created-or-updated"; import { handleSubscriptionDeleted } from "@/modules/ee/billing/api/lib/subscription-deleted"; const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { @@ -20,7 +19,7 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin event = stripe.webhooks.constructEvent(requestBody, stripeSignature, webhookSecret); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error"; - if (err! instanceof Error) logger.error(err, "Error in Stripe webhook handler"); + if (err instanceof Error) logger.error(err, "Error in Stripe webhook handler"); return { status: 400, message: `Webhook Error: ${errorMessage}` }; } @@ -28,11 +27,6 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin await handleCheckoutSessionCompleted(event); } else if (event.type === "invoice.finalized") { await handleInvoiceFinalized(event); - } else if ( - event.type === "customer.subscription.created" || - event.type === "customer.subscription.updated" - ) { - await handleSubscriptionCreatedOrUpdated(event); } else if (event.type === "customer.subscription.deleted") { await handleSubscriptionDeleted(event); } diff --git a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts b/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts deleted file mode 100644 index d7c7d56b9f..0000000000 --- a/apps/web/modules/ee/billing/api/lib/subscription-created-or-updated.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Stripe from "stripe"; -import { logger } from "@formbricks/logger"; -import { ResourceNotFoundError } from "@formbricks/types/errors"; -import { - TOrganizationBillingPeriod, - TOrganizationBillingPlan, - ZOrganizationBillingPeriod, - ZOrganizationBillingPlan, -} from "@formbricks/types/organizations"; -import { PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@/lib/constants"; -import { env } from "@/lib/env"; -import { getOrganization, updateOrganization } from "@/lib/organization/service"; - -const stripe = new Stripe(env.STRIPE_SECRET_KEY!, { - // https://github.com/stripe/stripe-node#configuration - apiVersion: STRIPE_API_VERSION, -}); - -export const handleSubscriptionCreatedOrUpdated = async (event: Stripe.Event) => { - const stripeSubscriptionObject = event.data.object as Stripe.Subscription; - const organizationId = stripeSubscriptionObject.metadata.organizationId; - - if ( - !["active", "trialing"].includes(stripeSubscriptionObject.status) || - stripeSubscriptionObject.cancel_at_period_end - ) { - return; - } - - if (!organizationId) { - logger.error({ event, organizationId }, "No organizationId found in subscription"); - return { status: 400, message: "skipping, no organizationId found" }; - } - - const organization = await getOrganization(organizationId); - if (!organization) throw new ResourceNotFoundError("Organization not found", organizationId); - - const subscriptionPrice = stripeSubscriptionObject.items.data[0].price; - const product = await stripe.products.retrieve(subscriptionPrice.product as string); - - if (!product) - throw new ResourceNotFoundError( - "Product not found", - stripeSubscriptionObject.items.data[0].price.product.toString() - ); - - let period: TOrganizationBillingPeriod = "monthly"; - const periodParsed = ZOrganizationBillingPeriod.safeParse(subscriptionPrice.metadata.period); - if (periodParsed.success) { - period = periodParsed.data; - } - - let updatedBillingPlan: TOrganizationBillingPlan = organization.billing.plan; - - let responses: number | null = null; - let miu: number | null = null; - let projects: number | null = null; - - if (product.metadata.responses === "unlimited") { - responses = null; - } else if (parseInt(product.metadata.responses) > 0) { - responses = parseInt(product.metadata.responses); - } else { - logger.error({ responses: product.metadata.responses }, "Invalid responses metadata in product"); - throw new Error("Invalid responses metadata in product"); - } - - if (product.metadata.miu === "unlimited") { - miu = null; - } else if (parseInt(product.metadata.miu) > 0) { - miu = parseInt(product.metadata.miu); - } else { - logger.error({ miu: product.metadata.miu }, "Invalid miu metadata in product"); - throw new Error("Invalid miu metadata in product"); - } - - if (product.metadata.projects === "unlimited") { - projects = null; - } else if (parseInt(product.metadata.projects) > 0) { - projects = parseInt(product.metadata.projects); - } else { - logger.error({ projects: product.metadata.projects }, "Invalid projects metadata in product"); - throw new Error("Invalid projects metadata in product"); - } - - const plan = ZOrganizationBillingPlan.parse(product.metadata.plan); - - switch (plan) { - case PROJECT_FEATURE_KEYS.FREE: - updatedBillingPlan = PROJECT_FEATURE_KEYS.STARTUP; - break; - - case PROJECT_FEATURE_KEYS.STARTUP: - updatedBillingPlan = PROJECT_FEATURE_KEYS.STARTUP; - break; - - case PROJECT_FEATURE_KEYS.ENTERPRISE: - updatedBillingPlan = PROJECT_FEATURE_KEYS.ENTERPRISE; - break; - } - - await updateOrganization(organizationId, { - billing: { - ...organization.billing, - stripeCustomerId: stripeSubscriptionObject.customer as string, - plan: updatedBillingPlan, - period, - limits: { - projects, - monthly: { - responses, - miu, - }, - }, - }, - }); - - await stripe.customers.update(stripeSubscriptionObject.customer as string, { - name: organization.name, - metadata: { organizationId: organization.id }, - invoice_settings: { - default_payment_method: stripeSubscriptionObject.default_payment_method as string, - }, - }); -}; diff --git a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts index 4e96123546..5417d84e49 100644 --- a/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts +++ b/apps/web/modules/ee/billing/api/lib/subscription-deleted.ts @@ -30,4 +30,12 @@ export const handleSubscriptionDeleted = async (event: Stripe.Event) => { period: "monthly", }, }); + + logger.info( + { + organizationId, + subscriptionId: stripeSubscriptionObject.id, + }, + "Subscription cancelled - downgraded to FREE plan" + ); }; diff --git a/apps/web/modules/ee/billing/components/pricing-card.tsx b/apps/web/modules/ee/billing/components/pricing-card.tsx index 4a747433b6..43624a8a4e 100644 --- a/apps/web/modules/ee/billing/components/pricing-card.tsx +++ b/apps/web/modules/ee/billing/components/pricing-card.tsx @@ -19,7 +19,7 @@ interface PricingCardProps { projectFeatureKeys: { FREE: string; STARTUP: string; - ENTERPRISE: string; + CUSTOM: string; }; } @@ -33,17 +33,21 @@ export const PricingCard = ({ }: PricingCardProps) => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); + const [contactModalOpen, setContactModalOpen] = useState(false); + + const displayPrice = (() => { + if (plan.id === projectFeatureKeys.CUSTOM) { + return plan.price.monthly; + } + return planPeriod === "monthly" ? plan.price.monthly : plan.price.yearly; + })(); const isCurrentPlan = useMemo(() => { if (organization.billing.plan === projectFeatureKeys.FREE && plan.id === projectFeatureKeys.FREE) { return true; } - if ( - organization.billing.plan === projectFeatureKeys.ENTERPRISE && - plan.id === projectFeatureKeys.ENTERPRISE - ) { + if (organization.billing.plan === projectFeatureKeys.CUSTOM && plan.id === projectFeatureKeys.CUSTOM) { return true; } @@ -53,7 +57,7 @@ export const PricingCard = ({ organization.billing.plan, plan.id, planPeriod, - projectFeatureKeys.ENTERPRISE, + projectFeatureKeys.CUSTOM, projectFeatureKeys.FREE, ]); @@ -62,7 +66,7 @@ export const PricingCard = ({ return null; } - if (plan.id === projectFeatureKeys.ENTERPRISE) { + if (plan.id === projectFeatureKeys.CUSTOM) { return (
- {plan.id !== projectFeatureKeys.ENTERPRISE && ( + {plan.id !== projectFeatureKeys.CUSTOM && (/ {planPeriod === "monthly" ? "Month" : "Year"} @@ -203,28 +203,13 @@ export const PricingCard = ({