Onboarding - delete PENDING_CREATION workspace if billing is deactivated (#12704)

[More context
details](https://discord.com/channels/1130383047699738754/1384834012882927637/1384834143673778226)
This commit is contained in:
Etienne
2025-06-20 18:18:50 +02:00
committed by GitHub
parent e11bd7aa0f
commit 830e49c5b1
26 changed files with 105 additions and 44 deletions

View File

@ -15,6 +15,13 @@ import {
import { 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';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
import {
BillingException,
BillingExceptionCode,
@ -23,19 +30,12 @@ import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billi
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-alert.service';
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-customer.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-invoice.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller()
@UseFilters(BillingRestApiExceptionFilter)
export class BillingController {
protected readonly logger = new Logger(BillingController.name);
export class BillingWebhookController {
protected readonly logger = new Logger(BillingWebhookController.name);
constructor(
private readonly stripeWebhookService: StripeWebhookService,

View File

@ -0,0 +1,66 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
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';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
imports: [
FeatureFlagModule,
StripeModule,
DomainManagerModule,
MessageQueueModule,
PermissionsModule,
WorkspaceModule,
BillingModule,
TypeOrmModule.forFeature(
[
BillingSubscription,
BillingSubscriptionItem,
BillingCustomer,
BillingProduct,
BillingPrice,
BillingMeter,
BillingEntitlement,
Workspace,
UserWorkspace,
FeatureFlag,
],
'core',
),
],
controllers: [BillingWebhookController],
providers: [
BillingWebhookProductService,
BillingWebhookPriceService,
BillingWebhookAlertService,
BillingWebhookInvoiceService,
BillingWebhookCustomerService,
BillingWebhookSubscriptionService,
BillingWebhookEntitlementService,
],
})
export class BillingWebhookModule {}

View File

@ -6,13 +6,14 @@ import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
@Injectable()
export class BillingWebhookEntitlementService {
constructor(

View File

@ -6,6 +6,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-price-event-to-database-price.util';
import {
BillingException,
BillingExceptionCode,
@ -15,7 +16,6 @@ import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-p
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
@Injectable()
export class BillingWebhookPriceService {

View File

@ -6,9 +6,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-product-event-to-database-product.util';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
@Injectable()
export class BillingWebhookProductService {
protected readonly logger = new Logger(BillingWebhookProductService.name);

View File

@ -8,6 +8,10 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing-webhook/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util';
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-customer.util';
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ -15,14 +19,10 @@ import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billin
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 { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
import { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util';
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
CleanWorkspaceDeletionWarningUserVarsJob,
@ -30,6 +30,7 @@ import {
} from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
export class BillingWebhookSubscriptionService {
protected readonly logger = new Logger(
BillingWebhookSubscriptionService.name,
@ -47,8 +48,7 @@ export class BillingWebhookSubscriptionService {
@InjectRepository(BillingCustomer, 'core')
private readonly billingCustomerRepository: Repository<BillingCustomer>,
private readonly billingSubscriptionService: BillingSubscriptionService,
@InjectRepository(FeatureFlag, 'core')
private readonly featureFlagRepository: Repository<FeatureFlag>,
private readonly workspaceService: WorkspaceService,
) {}
async processStripeEvent(
@ -115,6 +115,13 @@ export class BillingWebhookSubscriptionService {
});
}
if (
this.shouldSuspendWorkspace(data) &&
workspace.activationStatus === WorkspaceActivationStatus.PENDING_CREATION
) {
await this.workspaceService.deleteWorkspace(workspace.id);
}
if (
!this.shouldSuspendWorkspace(data) &&
workspace.activationStatus == WorkspaceActivationStatus.SUSPENDED

View File

@ -2,8 +2,8 @@ import {
mockStripeSubscriptionUpdatedEventWithDeletedItem,
mockStripeSubscriptionUpdatedEventWithoutUpdatedItem,
mockStripeSubscriptionUpdatedEventWithUpdatedItemOnly,
} from 'src/engine/core-modules/billing/webhooks/__mocks__/stripe-subscription-updated-events';
import { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util';
} from 'src/engine/core-modules/billing-webhook/__mocks__/stripe-subscription-updated-events';
import { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing-webhook/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util';
describe('getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent', () => {
it('should return an empty array if subscription items are not updated', () => {

View File

@ -2,8 +2,8 @@
import Stripe from 'stripe';
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => {
it('should return the SSO key with true value', () => {

View File

@ -1,12 +1,12 @@
/* @license Enterprise */
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-price-event-to-database-price.util';
import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum';
import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum';
import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum';
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
describe('transformStripePriceEventToDatabasePrice', () => {
const createMockPriceData = (overrides = {}) => ({

View File

@ -2,7 +2,7 @@
import Stripe from 'stripe';
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-product-event-to-database-product.util';
describe('transformStripeProductEventToDatabaseProduct', () => {
it('should return the correct data', () => {

View File

@ -1,6 +1,7 @@
/* @license Enterprise */
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-customer.util';
describe('transformStripeSubscriptionEventToDatabaseCustomer', () => {
const mockWorkspaceId = 'workspace_123';
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC

View File

@ -1,8 +1,8 @@
/* @license Enterprise */
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
describe('transformStripeSubscriptionEventToDatabaseSubscription', () => {
const mockWorkspaceId = 'workspace-123';

View File

@ -3,7 +3,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BillingController } from 'src/engine/core-modules/billing/billing.controller';
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
import { BillingAddWorkflowSubscriptionItemCommand } from 'src/engine/core-modules/billing/commands/billing-add-workflow-subscription-item.command';
import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.command';
@ -27,13 +26,6 @@ import { BillingSubscriptionService } from 'src/engine/core-modules/billing/serv
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-alert.service';
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-customer.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-invoice.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@ -65,12 +57,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
'core',
),
],
controllers: [BillingController],
providers: [
BillingSubscriptionService,
BillingSubscriptionItemService,
BillingWebhookSubscriptionService,
BillingWebhookEntitlementService,
BillingPortalWorkspaceService,
BillingProductService,
BillingResolver,
@ -78,11 +67,6 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
BillingWorkspaceMemberListener,
BillingFeatureUsedListener,
BillingService,
BillingWebhookProductService,
BillingWebhookPriceService,
BillingWebhookAlertService,
BillingWebhookInvoiceService,
BillingWebhookCustomerService,
BillingRestApiExceptionFilter,
BillingSyncCustomerDataCommand,
BillingUpdateSubscriptionPriceCommand,

View File

@ -10,6 +10,7 @@ import Stripe from 'stripe';
import { isDefined } from 'twenty-shared/utils';
import { Not, Repository } from 'typeorm';
import { getSubscriptionStatus } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
import {
BillingException,
BillingExceptionCode,
@ -19,6 +20,7 @@ import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-p
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
@ -28,10 +30,8 @@ import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/se
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { getPlanKeyFromSubscription } from 'src/engine/core-modules/billing/utils/get-plan-key-from-subscription.util';
import { getSubscriptionStatus } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
@Injectable()
export class BillingSubscriptionService {

View File

@ -10,6 +10,7 @@ import { aiModuleFactory } from 'src/engine/core-modules/ai/ai.module-factory';
import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module';
import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingWebhookModule } from 'src/engine/core-modules/billing-webhook/billing-webhook.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module';
import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module';
@ -58,6 +59,7 @@ import { FileModule } from './file/file.module';
AuditModule,
AuthModule,
BillingModule,
BillingWebhookModule,
ClientConfigModule,
FeatureFlagModule,
FileModule,