From 50057a78a1ff501fc331f5555077442a52697563 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 27 Jun 2024 17:26:23 -0700 Subject: [PATCH 1/2] fix: speed up team upgrade Signed-off-by: Matt Krick --- .../components/OrgBilling/BillingForm.tsx | 8 ++++++++ .../fragments/UpgradeToTeamTierFrag.ts | 13 ++++-------- .../client/subscriptions/TeamSubscription.ts | 3 --- .../mutations/helpers/oldUpgradeToTeamTier.ts | 2 +- .../private/mutations/upgradeToTeamTier.ts | 4 ---- .../mutations/createStripeSubscription.ts | 20 +++++++++++-------- packages/server/utils/defaultTier.ts | 2 +- packages/server/utils/stripe/StripeManager.ts | 13 +++++++++++- 8 files changed, 38 insertions(+), 27 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx b/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx index a70094f3c44..eb8404d8fdf 100644 --- a/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx +++ b/packages/client/modules/userDashboard/components/OrgBilling/BillingForm.tsx @@ -8,6 +8,7 @@ import { } from '@stripe/react-stripe-js' import {StripeElementChangeEvent} from '@stripe/stripe-js' import React, {useState} from 'react' +import {commitLocalUpdate} from 'relay-runtime' import {CreateStripeSubscriptionMutation$data} from '../../../../__generated__/CreateStripeSubscriptionMutation.graphql' import Ellipsis from '../../../../components/Ellipsis/Ellipsis' import PrimaryButton from '../../../../components/PrimaryButton' @@ -15,8 +16,10 @@ import StyledError from '../../../../components/StyledError' import useAtmosphere from '../../../../hooks/useAtmosphere' import useMutationProps from '../../../../hooks/useMutationProps' import CreateStripeSubscriptionMutation from '../../../../mutations/CreateStripeSubscriptionMutation' +import upgradeToTeamTierSuccessUpdater from '../../../../mutations/handlers/upgradeToTeamTierSuccessUpdater' import {PALETTE} from '../../../../styles/paletteV3' import SendClientSideEvent from '../../../../utils/SendClientSideEvent' +import createProxyRecord from '../../../../utils/relay/createProxyRecord' const ButtonBlock = styled('div')({ display: 'flex', @@ -131,6 +134,11 @@ const BillingForm = (props: Props) => { setIsLoading(false) return } + commitLocalUpdate(atmosphere, (store) => { + const payload = createProxyRecord(store, 'payload', {}) + payload.setLinkedRecord(store.get(orgId)!, 'organization') + upgradeToTeamTierSuccessUpdater(payload) + }) onCompleted() } diff --git a/packages/client/mutations/fragments/UpgradeToTeamTierFrag.ts b/packages/client/mutations/fragments/UpgradeToTeamTierFrag.ts index f1f5fcb6226..d3d93ce6e8c 100644 --- a/packages/client/mutations/fragments/UpgradeToTeamTierFrag.ts +++ b/packages/client/mutations/fragments/UpgradeToTeamTierFrag.ts @@ -16,18 +16,13 @@ graphql` periodStart updatedAt lockedAt + teams { + isPaid + tier + } } meetings { showConversionModal } } ` - -graphql` - fragment UpgradeToTeamTierFrag_team on UpgradeToTeamTierSuccess { - teams { - isPaid - tier - } - } -` diff --git a/packages/client/subscriptions/TeamSubscription.ts b/packages/client/subscriptions/TeamSubscription.ts index ec2ff7aa1e6..c5bc699bf2a 100644 --- a/packages/client/subscriptions/TeamSubscription.ts +++ b/packages/client/subscriptions/TeamSubscription.ts @@ -181,9 +181,6 @@ const subscription = graphql` OldUpgradeToTeamTierPayload { ...OldUpgradeToTeamTierMutation_team @relay(mask: false) } - UpgradeToTeamTierSuccess { - ...UpgradeToTeamTierFrag_team @relay(mask: false) - } UpdateIntegrationProviderSuccess { ...UpdateIntegrationProviderMutation_team @relay(mask: false) } diff --git a/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts b/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts index 53d4f9552e0..361416e52ab 100644 --- a/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts +++ b/packages/server/graphql/mutations/helpers/oldUpgradeToTeamTier.ts @@ -32,7 +32,7 @@ const oldUpgradeToTeamTier = async ( const manager = getStripeManager() const customer = stripeId ? await manager.updatePayment(stripeId, source) - : await manager.createCustomer(orgId, email, source) + : await manager.createCustomer(orgId, email, undefined, source) let subscriptionFields = {} if (!stripeSubscriptionId) { diff --git a/packages/server/graphql/private/mutations/upgradeToTeamTier.ts b/packages/server/graphql/private/mutations/upgradeToTeamTier.ts index 38efacb4caf..18a040da6c5 100644 --- a/packages/server/graphql/private/mutations/upgradeToTeamTier.ts +++ b/packages/server/graphql/private/mutations/upgradeToTeamTier.ts @@ -134,10 +134,6 @@ const upgradeToTeamTier: MutationResolvers['upgradeToTeamTier'] = async ( const data = {orgId, teamIds, meetingIds} publish(SubscriptionChannel.ORGANIZATION, orgId, 'UpgradeToTeamTierSuccess', data, subOptions) - teamIds.forEach((teamId) => { - const teamData = {orgId, teamIds: [teamId]} - publish(SubscriptionChannel.TEAM, teamId, 'UpgradeToTeamTierSuccess', teamData, subOptions) - }) return data } diff --git a/packages/server/graphql/public/mutations/createStripeSubscription.ts b/packages/server/graphql/public/mutations/createStripeSubscription.ts index 02ceb15c616..1dae15716f9 100644 --- a/packages/server/graphql/public/mutations/createStripeSubscription.ts +++ b/packages/server/graphql/public/mutations/createStripeSubscription.ts @@ -38,14 +38,18 @@ const createStripeSubscription: MutationResolvers['createStripeSubscription'] = return standardError(new Error('Organization already has a subscription'), {userId: viewerId}) } const {email} = viewer - const customer = stripeId - ? await manager.retrieveCustomer(stripeId) - : await manager.createCustomer(orgId, email) - const {id: customerId} = customer - const res = await manager.attachPaymentToCustomer(customerId, paymentMethodId) - if (res instanceof Error) return standardError(res, {userId: viewerId}) - // wait until the payment is attached to the customer before updating the default payment method - await manager.updateDefaultPaymentMethod(customerId, paymentMethodId) + let customer: Stripe.Response + if (stripeId) { + customer = await manager.retrieveCustomer(stripeId) + const {id: customerId} = customer + const res = await manager.attachPaymentToCustomer(customerId, paymentMethodId) + if (res instanceof Error) return standardError(res, {userId: viewerId}) + // wait until the payment is attached to the customer before updating the default payment method + await manager.updateDefaultPaymentMethod(customerId, paymentMethodId) + } else { + customer = await manager.createCustomer(orgId, email, paymentMethodId) + } + const subscription = await manager.createTeamSubscription(customer.id, orgId, orgUsersCount) const latestInvoice = subscription.latest_invoice as Stripe.Invoice diff --git a/packages/server/utils/defaultTier.ts b/packages/server/utils/defaultTier.ts index dcce379dd6a..793bf254dc1 100644 --- a/packages/server/utils/defaultTier.ts +++ b/packages/server/utils/defaultTier.ts @@ -1 +1 @@ -export const defaultTier = process.env.IS_ENTERPRISE ? 'enterprise' : 'starter' +export const defaultTier = process.env.IS_ENTERPRISE === 'true' ? 'enterprise' : 'starter' diff --git a/packages/server/utils/stripe/StripeManager.ts b/packages/server/utils/stripe/StripeManager.ts index 5a2c74e665d..346d5456776 100644 --- a/packages/server/utils/stripe/StripeManager.ts +++ b/packages/server/utils/stripe/StripeManager.ts @@ -90,10 +90,21 @@ export default class StripeManager { } } - async createCustomer(orgId: string, email: string, source?: string) { + async createCustomer( + orgId: string, + email: string, + paymentMethodId?: string | undefined, + source?: string + ) { return this.stripe.customers.create({ email, source, + payment_method: paymentMethodId, + invoice_settings: paymentMethodId + ? { + default_payment_method: paymentMethodId + } + : undefined, metadata: { orgId } From 56ec729db58a7aeeb4d572fe3b1ea906e4afe1af Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 1 Jul 2024 12:16:53 -0700 Subject: [PATCH 2/2] fix: don't update Org with subscription until payment succeeds Signed-off-by: Matt Krick --- .../mutations/stripeDeleteSubscription.ts | 4 ++- .../private/mutations/upgradeToTeamTier.ts | 29 +++++-------------- .../mutations/createStripeSubscription.ts | 21 +------------- 3 files changed, 12 insertions(+), 42 deletions(-) diff --git a/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts b/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts index 960bb1a01f2..b42338b2371 100644 --- a/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts +++ b/packages/server/graphql/private/mutations/stripeDeleteSubscription.ts @@ -31,8 +31,10 @@ const stripeDeleteSubscription: MutationResolvers['stripeDeleteSubscription'] = const org: Organization = await dataLoader.get('organizations').load(orgId) const {stripeSubscriptionId} = org + if (!stripeSubscriptionId) return false + if (stripeSubscriptionId !== subscriptionId) { - throw new Error('Subscription ID does not match') + throw new Error(`Subscription ID does not match: ${stripeSubscriptionId} vs ${subscriptionId}`) } await r diff --git a/packages/server/graphql/private/mutations/upgradeToTeamTier.ts b/packages/server/graphql/private/mutations/upgradeToTeamTier.ts index 18a040da6c5..e7fd466a0ad 100644 --- a/packages/server/graphql/private/mutations/upgradeToTeamTier.ts +++ b/packages/server/graphql/private/mutations/upgradeToTeamTier.ts @@ -28,8 +28,10 @@ const upgradeToTeamTier: MutationResolvers['upgradeToTeamTier'] = async ( const userId = getUserId(authToken) const manager = getStripeManager() const invoice = await manager.retrieveInvoice(invoiceId) - const customerId = invoice.customer as string - const customer = await manager.retrieveCustomer(customerId) + const stripeId = invoice.customer as string + const stripeSubscriptionId = invoice.subscription as string + const customer = await manager.retrieveCustomer(stripeId) + if (customer.deleted) { return standardError(new Error('Customer has been deleted'), {userId}) } @@ -51,24 +53,7 @@ const upgradeToTeamTier: MutationResolvers['upgradeToTeamTier'] = async ( dataLoader.get('users').loadNonNull(viewerId) ]) - const { - stripeId, - tier, - activeDomain, - name: orgName, - stripeSubscriptionId, - trialStartDate - } = organization - - if (!stripeId) { - return standardError(new Error('Organization does not have a stripe id'), { - userId: viewerId - }) - } - - if (!stripeSubscriptionId) { - return standardError(new Error('Organization does not have a subscription'), {userId: viewerId}) - } + const {tier, activeDomain, name: orgName, trialStartDate} = organization if (tier === 'enterprise') { return standardError(new Error("Can not change an org's plan from enterprise to team"), { @@ -93,7 +78,9 @@ const upgradeToTeamTier: MutationResolvers['upgradeToTeamTier'] = async ( scheduledLockAt: null, lockedAt: null, updatedAt: now, - trialStartDate: null + trialStartDate: null, + stripeId, + stripeSubscriptionId }) }).run(), pg diff --git a/packages/server/graphql/public/mutations/createStripeSubscription.ts b/packages/server/graphql/public/mutations/createStripeSubscription.ts index 1dae15716f9..671078946aa 100644 --- a/packages/server/graphql/public/mutations/createStripeSubscription.ts +++ b/packages/server/graphql/public/mutations/createStripeSubscription.ts @@ -1,7 +1,6 @@ import Stripe from 'stripe' import getRethink from '../../../database/rethinkDriver' import {getUserId} from '../../../utils/authorization' -import {fromEpochSeconds} from '../../../utils/epochTime' import standardError from '../../../utils/standardError' import {getStripeManager} from '../../../utils/stripe' import {MutationResolvers} from '../resolverTypes' @@ -12,7 +11,6 @@ const createStripeSubscription: MutationResolvers['createStripeSubscription'] = {authToken, dataLoader} ) => { const viewerId = getUserId(authToken) - const now = new Date() const r = await getRethink() const [viewer, organization, orgUsersCount, organizationUser] = await Promise.all([ @@ -44,7 +42,7 @@ const createStripeSubscription: MutationResolvers['createStripeSubscription'] = const {id: customerId} = customer const res = await manager.attachPaymentToCustomer(customerId, paymentMethodId) if (res instanceof Error) return standardError(res, {userId: viewerId}) - // wait until the payment is attached to the customer before updating the default payment method + // cannot updateDefaultPaymentMethod until it is attached to the customer await manager.updateDefaultPaymentMethod(customerId, paymentMethodId) } else { customer = await manager.createCustomer(orgId, email, paymentMethodId) @@ -56,23 +54,6 @@ const createStripeSubscription: MutationResolvers['createStripeSubscription'] = const paymentIntent = latestInvoice.payment_intent as Stripe.PaymentIntent const clientSecret = paymentIntent.client_secret - const subscriptionFields = { - periodEnd: fromEpochSeconds(subscription.current_period_end), - periodStart: fromEpochSeconds(subscription.current_period_start), - stripeSubscriptionId: subscription.id - } - - await r({ - updatedOrg: r - .table('Organization') - .get(orgId) - .update({ - ...subscriptionFields, - stripeId: customer.id, - updatedAt: now - }) - }).run() - const data = {stripeSubscriptionClientSecret: clientSecret} return data }