fix: list invoice with invalid status
fix: list invoice with invalid status
This commit is contained in:
parent
cc6931ecfe
commit
56c51ac7b5
|
|
@ -56,6 +56,10 @@ export class OrganizationService {
|
|||
return await queryRunner.manager.findOneBy(Organization, { name })
|
||||
}
|
||||
|
||||
public async readOrganizationBySubscriptionId(subscriptionId: typeof Organization.prototype.subscriptionId, queryRunner: QueryRunner) {
|
||||
return await queryRunner.manager.findOneBy(Organization, { subscriptionId })
|
||||
}
|
||||
|
||||
public async countOrganizations(queryRunner: QueryRunner) {
|
||||
return await queryRunner.manager.count(Organization)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,28 @@ import { Organization, OrganizationStatus } from '../database/entities/organizat
|
|||
import { OrganizationUser } from '../database/entities/organization-user.entity'
|
||||
import { Workspace, WorkspaceName } from '../database/entities/workspace.entity'
|
||||
import { WorkspaceUser } from '../database/entities/workspace-user.entity'
|
||||
import { OrganizationService } from './organization.service'
|
||||
import { OrganizationErrorMessage, OrganizationService } from './organization.service'
|
||||
import logger from '../../utils/logger'
|
||||
|
||||
enum SubscriptionStatus {
|
||||
INCOMPLETE = 'incomplete',
|
||||
INCOMPLETE_EXPIRED = 'incomplete_expired',
|
||||
TRIALING = 'trialing',
|
||||
ACTIVE = 'active',
|
||||
PAST_DUE = 'past_due',
|
||||
CANCELED = 'canceled',
|
||||
UNPAID = 'unpaid',
|
||||
PAUSED = 'paused'
|
||||
}
|
||||
|
||||
enum InvoiceStatus {
|
||||
DRAFT = 'draft',
|
||||
OPEN = 'open',
|
||||
PAID = 'paid',
|
||||
UNCOLLECTIBLE = 'uncollectible',
|
||||
VOID = 'void'
|
||||
}
|
||||
|
||||
// Note: Organization entity will have a 'status' field added later
|
||||
// This will support values like 'active', 'suspended', etc.
|
||||
|
||||
|
|
@ -27,7 +46,8 @@ export class StripeService {
|
|||
return this.stripe
|
||||
}
|
||||
|
||||
public async handleInvoicePaid(invoice: Stripe.Invoice, queryRunner: QueryRunner): Promise<void> {
|
||||
public async reactivateOrganizationIfEligible(invoice: Stripe.Invoice, queryRunner: QueryRunner): Promise<void> {
|
||||
try {
|
||||
await this.getStripe() // Initialize stripe if not already done
|
||||
|
||||
if (!invoice.subscription) {
|
||||
|
|
@ -37,73 +57,34 @@ export class StripeService {
|
|||
|
||||
const subscriptionId = typeof invoice.subscription === 'string' ? invoice.subscription : invoice.subscription.id
|
||||
|
||||
try {
|
||||
const organization = await queryRunner.manager.findOne(Organization, {
|
||||
where: { subscriptionId }
|
||||
})
|
||||
|
||||
const organizationService = new OrganizationService()
|
||||
const organization = await organizationService.readOrganizationBySubscriptionId(subscriptionId, queryRunner)
|
||||
if (!organization) {
|
||||
logger.warn(`No organization found for subscription ID: ${subscriptionId}`)
|
||||
logger.warn(`${OrganizationErrorMessage.ORGANIZATION_NOT_FOUND} for subscription ID: ${subscriptionId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get subscription details from Stripe
|
||||
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)
|
||||
|
||||
// Always ensure organization is active when invoice is paid
|
||||
// This handles both reactivation and plan upgrades
|
||||
const shouldUpdateStatus = (organization as any).status !== OrganizationStatus.ACTIVE
|
||||
|
||||
if (shouldUpdateStatus) {
|
||||
// Check if subscription is past_due - if so, don't reactivate yet
|
||||
if (subscription.status === 'past_due') {
|
||||
if (organization.status === OrganizationStatus.ACTIVE) {
|
||||
logger.info(`Organization ${organization.id} is already active`)
|
||||
return
|
||||
}
|
||||
|
||||
if (organization.status === OrganizationStatus.UNDER_REVIEW) {
|
||||
logger.info(`Organization ${organization.id} is under review`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for all uncollectible invoices and ensure they are all settled
|
||||
// Customer must pay/settle ALL uncollectible invoices before reactivation
|
||||
const uncollectibleInvoices = await this.stripe.invoices.list({
|
||||
subscription: subscriptionId,
|
||||
status: 'uncollectible',
|
||||
limit: 100 // Get all uncollectible invoices
|
||||
})
|
||||
|
||||
if (uncollectibleInvoices.data.length > 0) {
|
||||
// Check if all uncollectible invoices have been settled (paid)
|
||||
const unsettledUncollectible = uncollectibleInvoices.data.filter((invoice) => !invoice.paid)
|
||||
if (unsettledUncollectible.length > 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any unpaid invoices across all possible unpaid statuses
|
||||
// This ensures no outstanding debt remains before reactivation
|
||||
const unpaidStatuses = ['open', 'uncollectible', 'past_due']
|
||||
let hasUnpaidInvoices = false
|
||||
let unpaidInvoiceIds: string[] = []
|
||||
|
||||
for (const status of unpaidStatuses) {
|
||||
const invoices = await this.stripe.invoices.list({
|
||||
subscription: subscriptionId,
|
||||
status: status as any,
|
||||
status: InvoiceStatus.UNCOLLECTIBLE,
|
||||
limit: 100
|
||||
})
|
||||
|
||||
// Filter out invoices that are actually paid (for uncollectible status)
|
||||
const actuallyUnpaidInvoices = invoices.data.filter((inv) => !inv.paid)
|
||||
|
||||
if (actuallyUnpaidInvoices.length > 0) {
|
||||
hasUnpaidInvoices = true
|
||||
unpaidInvoiceIds.push(...actuallyUnpaidInvoices.map((inv) => inv.id))
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUnpaidInvoices) {
|
||||
logger.info(`Organization ${organization.id} still has unpaid invoices: ${unpaidInvoiceIds.join(', ')}`)
|
||||
if (uncollectibleInvoices.data.length > 0) {
|
||||
logger.info(`Organization ${organization.id} has uncollectible invoices`)
|
||||
return
|
||||
}
|
||||
|
||||
const organizationService = new OrganizationService()
|
||||
await organizationService.updateOrganization(
|
||||
{
|
||||
id: organization.id,
|
||||
|
|
@ -113,48 +94,6 @@ export class StripeService {
|
|||
queryRunner,
|
||||
true // fromStripe = true to allow status updates
|
||||
)
|
||||
}
|
||||
|
||||
// Check if subscription needs to be resumed after all debts are settled
|
||||
if (subscription.status === 'unpaid') {
|
||||
// Verify all debts are settled before resuming
|
||||
// Check for any unpaid invoices across all possible unpaid statuses
|
||||
// This ensures no outstanding debt remains before reactivation
|
||||
const allUnpaidStatuses = ['open', 'uncollectible', 'past_due']
|
||||
let hasAnyUnpaidInvoices = false
|
||||
let allUnpaidInvoiceIds: string[] = []
|
||||
|
||||
for (const status of allUnpaidStatuses) {
|
||||
const invoices = await this.stripe.invoices.list({
|
||||
subscription: subscriptionId,
|
||||
status: status as any,
|
||||
limit: 100
|
||||
})
|
||||
|
||||
// Filter out invoices that are actually paid (for uncollectible status)
|
||||
const actuallyUnpaidInvoices = invoices.data.filter((inv) => !inv.paid)
|
||||
|
||||
if (actuallyUnpaidInvoices.length > 0) {
|
||||
hasAnyUnpaidInvoices = true
|
||||
allUnpaidInvoiceIds.push(...actuallyUnpaidInvoices.map((inv) => inv.id))
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnyUnpaidInvoices) {
|
||||
// All debts settled - resume the subscription
|
||||
try {
|
||||
await this.stripe.subscriptions.update(subscriptionId, {
|
||||
pause_collection: null // This resumes the subscription
|
||||
})
|
||||
logger.info(`Successfully resumed subscription ${subscriptionId}`)
|
||||
} catch (resumeError) {
|
||||
logger.error(`Failed to resume subscription ${subscriptionId}: ${resumeError}`)
|
||||
// Don't throw here - we still want to provision access even if resume fails
|
||||
}
|
||||
} else {
|
||||
logger.info(`Cannot resume subscription ${subscriptionId} - unpaid invoices remain: ${allUnpaidInvoiceIds.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Always update cache with latest subscription data when invoice is paid
|
||||
// This ensures access is provisioned for plan upgrades even if org is already active
|
||||
|
|
@ -174,8 +113,7 @@ export class StripeService {
|
|||
|
||||
logger.info(`Successfully reactivated organization ${organization.id} and updated cache for subscription ${subscriptionId}`)
|
||||
} catch (error) {
|
||||
logger.error(`Error handling invoice paid: ${error}`)
|
||||
if (queryRunner && queryRunner.isTransactionActive) await queryRunner.rollbackTransaction()
|
||||
logger.error(`stripe.service.reactivateOrganizationIfEligible: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class StripeWebhooks {
|
|||
switch (event.type) {
|
||||
case 'invoice.paid': {
|
||||
const stripeService = new StripeService()
|
||||
await stripeService.handleInvoicePaid(event.data.object as Stripe.Invoice, queryRunner)
|
||||
await stripeService.reactivateOrganizationIfEligible(event.data.object as Stripe.Invoice, queryRunner)
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
|||
|
||||
// material-ui
|
||||
import { Button, Dialog, DialogContent, Typography, Stack, DialogActions, CircularProgress, Box } from '@mui/material'
|
||||
import { IconExternalLink, IconCreditCard, IconLogout, IconHelp, IconX } from '@tabler/icons-react'
|
||||
import { IconExternalLink, IconCreditCard, IconLogout, IconX } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import accountApi from '@/api/account.api'
|
||||
|
|
|
|||
Loading…
Reference in New Issue