fix(billing) - fix orphaned stripe subs (#20814)
Fix sentry issues https://twenty-v7.sentry.io/issues/7203797925/?environment=prod&project=4507072499810304&query=is%3Aunresolved%20assigned%3Ame&referrer=issue-stream An orphaned sub is a not "canceled" stripe sub with no matching workspace - Clean all orphaned sub (script not included in this PR) - Ensure to soft delete > cancel stripe sub > check for not active sub > hard delete in every workspace deletion flow Bonus : - Remove dead code - Update doc on RLS (to improve AI chat knowledge)
This commit is contained in:
-7
@@ -15,7 +15,6 @@ import {
|
||||
import { type Response } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-alert.service';
|
||||
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
|
||||
@@ -46,7 +45,6 @@ export class BillingWebhookController {
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly billingWebhookProductService: BillingWebhookProductService,
|
||||
private readonly billingWebhookPriceService: BillingWebhookPriceService,
|
||||
private readonly billingWebhookAlertService: BillingWebhookAlertService,
|
||||
private readonly billingWebhookInvoiceService: BillingWebhookInvoiceService,
|
||||
private readonly billingWebhookCustomerService: BillingWebhookCustomerService,
|
||||
private readonly billingWebhookSubscriptionScheduleService: BillingWebhookSubscriptionScheduleService,
|
||||
@@ -118,11 +116,6 @@ export class BillingWebhookController {
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.ALERT_TRIGGERED:
|
||||
return await this.billingWebhookAlertService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.INVOICE_FINALIZED:
|
||||
case BillingWebhookEvent.INVOICE_PAID:
|
||||
return await this.billingWebhookInvoiceService.processStripeEvent(
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { BillingWebhookController } from 'src/engine/core-modules/billing-webhook/billing-webhook.controller';
|
||||
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-alert.service';
|
||||
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
|
||||
@@ -58,7 +57,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
providers: [
|
||||
BillingWebhookProductService,
|
||||
BillingWebhookPriceService,
|
||||
BillingWebhookAlertService,
|
||||
BillingWebhookInvoiceService,
|
||||
BillingWebhookCustomerService,
|
||||
BillingWebhookSubscriptionService,
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { BillingProductEntity } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingSubscriptionItemEntity } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookAlertService {
|
||||
protected readonly logger = new Logger(BillingWebhookAlertService.name);
|
||||
constructor(
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
@InjectRepository(BillingProductEntity)
|
||||
private readonly billingProductRepository: Repository<BillingProductEntity>,
|
||||
@InjectRepository(BillingSubscriptionItemEntity)
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItemEntity>,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(data: Stripe.BillingAlertTriggeredEvent.Data) {
|
||||
const { customer: stripeCustomerId, alert } = data.object;
|
||||
|
||||
const stripeMeterId = alert.usage_threshold?.meter;
|
||||
|
||||
assertIsDefinedOrThrow(stripeMeterId);
|
||||
|
||||
const subscription =
|
||||
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
|
||||
{ stripeCustomerId },
|
||||
);
|
||||
|
||||
const product = await this.billingProductRepository.findOne({
|
||||
where: {
|
||||
billingPrices: {
|
||||
stripeMeterId:
|
||||
typeof stripeMeterId === 'string'
|
||||
? stripeMeterId
|
||||
: stripeMeterId.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new BillingException(
|
||||
`Product associated to meter ${stripeMeterId} not found`,
|
||||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.billingSubscriptionItemRepository.update(
|
||||
{
|
||||
billingSubscriptionId: subscription.id,
|
||||
stripeProductId: product.stripeProductId,
|
||||
},
|
||||
{ hasReachedCurrentPeriodCap: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -23,9 +23,7 @@ import { BillingSubscriptionItemEntity } from 'src/engine/core-modules/billing/e
|
||||
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
|
||||
import { StripeBillingAlertService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-alert.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
@@ -57,10 +55,8 @@ export class BillingWebhookSubscriptionService {
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(BillingCustomerEntity)
|
||||
private readonly billingCustomerRepository: Repository<BillingCustomerEntity>,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly stripeSubscriptionScheduleService: StripeSubscriptionScheduleService,
|
||||
private readonly stripeBillingAlertService: StripeBillingAlertService,
|
||||
private readonly billingUsageService: BillingUsageService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
@@ -153,10 +149,7 @@ export class BillingWebhookSubscriptionService {
|
||||
|
||||
if (shouldSuspend) {
|
||||
if (workspace.activationStatus === WorkspaceActivationStatus.ACTIVE) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
activationStatus: WorkspaceActivationStatus.SUSPENDED,
|
||||
suspendedAt: new Date(),
|
||||
});
|
||||
await this.workspaceService.suspendWorkspace(workspaceId);
|
||||
} else if (
|
||||
workspace.activationStatus ===
|
||||
WorkspaceActivationStatus.PENDING_CREATION
|
||||
|
||||
Reference in New Issue
Block a user