Skip to content

Commit

Permalink
fix: do not cancel stripe subscription in case of failed payment (#8285)
Browse files Browse the repository at this point in the history
* fix: do not cancel stripe subscription in case of failed payment

* Handle canceled state too

* Add more comments

* Take isPaid from org team when doing moveToOrg
  • Loading branch information
igorlesnenko authored Jun 15, 2023
1 parent 418ab25 commit 74b8aa5
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 20 deletions.
46 changes: 31 additions & 15 deletions packages/server/graphql/mutations/helpers/upgradeToTeamTierOld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 13 additions & 3 deletions packages/server/graphql/mutations/moveTeamToOrg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -70,7 +80,7 @@ const moveToOrg = async (
// RESOLUTION
const updates = {
orgId,
isPaid: Boolean(org.stripeSubscriptionId),
isPaid: !!isPaidResult?.isPaid,
tier: org.tier,
updatedAt: new Date()
}
Expand Down
16 changes: 14 additions & 2 deletions packages/server/graphql/private/mutations/stripeFailPayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand Down Expand Up @@ -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'})
Expand Down
11 changes: 11 additions & 0 deletions packages/server/utils/stripe/StripeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 74b8aa5

Please sign in to comment.