diff --git a/packages/server/graphql/mutations/helpers/upgradeToTeamTierOld.ts b/packages/server/graphql/mutations/helpers/upgradeToTeamTierOld.ts index 6aee6abf69d..c43cb027fb0 100644 --- a/packages/server/graphql/mutations/helpers/upgradeToTeamTierOld.ts +++ b/packages/server/graphql/mutations/helpers/upgradeToTeamTierOld.ts @@ -43,22 +43,38 @@ const upgradeToTeamTierOld = async ( } } + await r({ + updatedOrg: r + .table('Organization') + .get(orgId) + .update({ + ...subscriptionFields, + creditCard: await getCCFromCustomer(customer), + tier: 'team', + stripeId: customer.id, + tierLimitExceededAt: null, + scheduledLockAt: null, + lockedAt: null, + updatedAt: now + }) + }).run() + + // If subscription already exists and has open invoices, try to process them + if (stripeSubscriptionId) { + const invoices = (await manager.listSubscriptionOpenInvoices(stripeSubscriptionId)).data + + if (invoices.length) { + for (const invoice of invoices) { + const invoiceResult = await manager.payInvoice(invoice.id) + // Unlock teams only if all invoices are paid + if (invoiceResult.status !== 'paid') { + throw new Error('Unable to process payment') + } + } + } + } + await Promise.all([ - r({ - updatedOrg: r - .table('Organization') - .get(orgId) - .update({ - ...subscriptionFields, - creditCard: await getCCFromCustomer(customer), - tier: 'team', - stripeId: customer.id, - tierLimitExceededAt: null, - scheduledLockAt: null, - lockedAt: null, - updatedAt: now - }) - }).run(), updateTeamByOrgId( { isPaid: true, diff --git a/packages/server/graphql/mutations/moveTeamToOrg.ts b/packages/server/graphql/mutations/moveTeamToOrg.ts index 4810d04e983..0764c2582a1 100644 --- a/packages/server/graphql/mutations/moveTeamToOrg.ts +++ b/packages/server/graphql/mutations/moveTeamToOrg.ts @@ -12,6 +12,7 @@ import {getUserId, isSuperUser} from '../../utils/authorization' import standardError from '../../utils/standardError' import {DataLoaderWorker, GQLContext} from '../graphql' import isValid from '../isValid' +import getKysely from '../../postgres/getKysely' const moveToOrg = async ( teamId: string, @@ -20,12 +21,21 @@ const moveToOrg = async ( dataLoader: DataLoaderWorker ) => { const r = await getRethink() + const pg = getKysely() + // AUTH const su = isSuperUser(authToken) // VALIDATION - const [org, teams] = await Promise.all([ + const [org, teams, isPaidResult] = await Promise.all([ r.table('Organization').get(orgId).run(), - getTeamsByIds([teamId]) + getTeamsByIds([teamId]), + pg + .selectFrom('Team') + .select('isPaid') + .where('orgId', '=', orgId) + .where('isArchived', '!=', true) + .limit(1) + .executeTakeFirst() ]) const team = teams[0] if (!team) { @@ -70,7 +80,7 @@ const moveToOrg = async ( // RESOLUTION const updates = { orgId, - isPaid: Boolean(org.stripeSubscriptionId), + isPaid: !!isPaidResult?.isPaid, tier: org.tier, updatedAt: new Date() } diff --git a/packages/server/graphql/private/mutations/stripeFailPayment.ts b/packages/server/graphql/private/mutations/stripeFailPayment.ts index 2ce63ecc769..b7e1c0a9848 100644 --- a/packages/server/graphql/private/mutations/stripeFailPayment.ts +++ b/packages/server/graphql/private/mutations/stripeFailPayment.ts @@ -6,6 +6,7 @@ import {isSuperUser} from '../../../utils/authorization' import publish from '../../../utils/publish' import {getStripeManager} from '../../../utils/stripe' import {MutationResolvers} from '../resolverTypes' +import updateTeamByOrgId from '../../../postgres/queries/updateTeamByOrgId' export type StripeFailPaymentPayloadSource = | { @@ -56,10 +57,21 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async ( } const {creditCard, stripeSubscriptionId} = org - if (paid || stripeSubscriptionId !== subscription) return {orgId} + if (paid || stripeSubscriptionId !== subscription || stripeSubscriptionId === null) return {orgId} // RESOLUTION - await terminateSubscription(orgId) + const subscriptionObject = await manager.retrieveSubscription(stripeSubscriptionId) + + if (subscriptionObject.status === 'incomplete' || subscriptionObject.status === 'canceled') { + // Terminate subscription if the first payment fails or if it is already canceled + // After 23 hours subscription updates to incomplete_expired and the invoice becomes void. + // Not to handle this particular case in 23 hours, we do it now + await terminateSubscription(orgId) + } else { + // Keep subscription, but disable teams + await updateTeamByOrgId({isPaid: false}, orgId) + } + const billingLeaderUserIds = (await r .table('OrganizationUser') .getAll(orgId, {index: 'orgId'}) diff --git a/packages/server/utils/stripe/StripeManager.ts b/packages/server/utils/stripe/StripeManager.ts index 089869ad2ff..91fd1598a7b 100644 --- a/packages/server/utils/stripe/StripeManager.ts +++ b/packages/server/utils/stripe/StripeManager.ts @@ -104,6 +104,17 @@ export default class StripeManager { return allSubscriptionItems.data[0] } + async listSubscriptionOpenInvoices(subscriptionId: string) { + return this.stripe.invoices.list({ + subscription: subscriptionId, + status: 'open' + }) + } + + async payInvoice(invoiceId: string) { + return this.stripe.invoices.pay(invoiceId) + } + async listLineItems(invoiceId: string, options: Stripe.InvoiceLineItemListParams) { return this.stripe.invoices.listLineItems(invoiceId, options) }