Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not cancel stripe subscription in case of failed payment #8285

Merged
merged 6 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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