fix billing issues (#11160)

- first commit : fix quantity update on Enterprise plan
- second commit : fix user with soft deleted workspace trying to
recreate another workspace

closes https://github.com/twentyhq/core-team-issues/issues/634
This commit is contained in:
Etienne
2025-03-25 19:09:36 +01:00
committed by GitHub
parent 20f2a251b6
commit 934abf1fb3
3 changed files with 32 additions and 9 deletions

View File

@ -17,7 +17,6 @@ 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 { 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 { 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 { 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 { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.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'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service'; import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
@ -63,26 +62,29 @@ export class BillingSubscriptionService {
{ workspaceId }, { workspaceId },
); );
const getStripeProductId = ( const planKey = getPlanKeyFromSubscription(billingSubscription);
await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO)
)?.stripeProductId;
if (!getStripeProductId) { const baseProduct =
await this.billingPlanService.getPlanBaseProduct(planKey);
if (!baseProduct) {
throw new BillingException( throw new BillingException(
'Base product not found', 'Base product not found',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND, BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
); );
} }
const stripeProductId = baseProduct.stripeProductId;
const billingSubscriptionItem = const billingSubscriptionItem =
billingSubscription.billingSubscriptionItems.filter( billingSubscription.billingSubscriptionItems.filter(
(billingSubscriptionItem) => (billingSubscriptionItem) =>
billingSubscriptionItem.stripeProductId === getStripeProductId, billingSubscriptionItem.stripeProductId === stripeProductId,
)?.[0]; )?.[0];
if (!billingSubscriptionItem) { if (!billingSubscriptionItem) {
throw new Error( throw new Error(
`Cannot find billingSubscriptionItem for product ${getStripeProductId} for workspace ${workspaceId}`, `Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`,
); );
} }

View File

@ -34,6 +34,7 @@ describe('WorkspaceService', () => {
let workspaceCacheStorageService: WorkspaceCacheStorageService; let workspaceCacheStorageService: WorkspaceCacheStorageService;
let messageQueueService: MessageQueueService; let messageQueueService: MessageQueueService;
let customDomainService: CustomDomainService; let customDomainService: CustomDomainService;
let billingSubscriptionService: BillingSubscriptionService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -61,6 +62,18 @@ describe('WorkspaceService', () => {
softDelete: jest.fn(), softDelete: jest.fn(),
}, },
}, },
{
provide: BillingService,
useValue: {
isBillingEnabled: jest.fn().mockReturnValue(true),
},
},
{
provide: BillingSubscriptionService,
useValue: {
deleteSubscriptions: jest.fn(),
},
},
...[ ...[
WorkspaceManagerService, WorkspaceManagerService,
WorkspaceManagerService, WorkspaceManagerService,
@ -68,8 +81,6 @@ describe('WorkspaceService', () => {
UserService, UserService,
DomainManagerService, DomainManagerService,
CustomDomainService, CustomDomainService,
BillingSubscriptionService,
BillingService,
EnvironmentService, EnvironmentService,
EmailService, EmailService,
OnboardingService, OnboardingService,
@ -115,6 +126,9 @@ describe('WorkspaceService', () => {
); );
customDomainService = module.get<CustomDomainService>(CustomDomainService); customDomainService = module.get<CustomDomainService>(CustomDomainService);
customDomainService.deleteCustomHostnameByHostnameSilently = jest.fn(); customDomainService.deleteCustomHostnameByHostnameSilently = jest.fn();
billingSubscriptionService = module.get<BillingSubscriptionService>(
BillingSubscriptionService,
);
}); });
afterEach(() => { afterEach(() => {
@ -220,8 +234,11 @@ describe('WorkspaceService', () => {
.spyOn(workspaceRepository, 'findOne') .spyOn(workspaceRepository, 'findOne')
.mockResolvedValue(mockWorkspace); .mockResolvedValue(mockWorkspace);
jest.spyOn(userWorkspaceRepository, 'find').mockResolvedValue([]); jest.spyOn(userWorkspaceRepository, 'find').mockResolvedValue([]);
await service.deleteWorkspace(mockWorkspace.id, true); await service.deleteWorkspace(mockWorkspace.id, true);
expect(billingSubscriptionService.deleteSubscriptions).toHaveBeenCalled;
expect(workspaceRepository.softDelete).toHaveBeenCalledWith({ expect(workspaceRepository.softDelete).toHaveBeenCalledWith({
id: mockWorkspace.id, id: mockWorkspace.id,
}); });

View File

@ -334,6 +334,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
); );
if (softDelete) { if (softDelete) {
if (this.billingService.isBillingEnabled()) {
await this.billingSubscriptionService.deleteSubscriptions(workspace.id);
}
await this.workspaceRepository.softDelete({ id }); await this.workspaceRepository.softDelete({ id });
this.logger.log(`workspace ${id} soft deleted`); this.logger.log(`workspace ${id} soft deleted`);