fix: prevent billingPortal creation if no active subscription (#9701)

Billing portal is created in settings/billing page even if subscription
is canceled, causing server internal error. -> Skip back end request

Bonus : display settings/billing page with disabled button even if
subscription is canceled

---------

Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2025-01-21 15:01:18 +01:00
committed by GitHub
parent 47c2c774e3
commit d8815d7ebf
12 changed files with 123 additions and 270 deletions

View File

@ -79,18 +79,16 @@ export class BillingPortalWorkspaceService {
workspace: Workspace,
returnUrlPath?: string,
) {
const currentSubscription =
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
{
workspaceId: workspace.id,
},
);
const lastSubscription = await this.billingSubscriptionRepository.findOne({
where: { workspaceId: workspace.id },
order: { createdAt: 'DESC' },
});
if (!currentSubscription) {
if (!lastSubscription) {
throw new Error('Error: missing subscription');
}
const stripeCustomerId = currentSubscription.stripeCustomerId;
const stripeCustomerId = lastSubscription.stripeCustomerId;
if (!stripeCustomerId) {
throw new Error('Error: missing stripeCustomerId');

View File

@ -1,6 +1,6 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField, UnPagedRelation } from '@ptc-org/nestjs-query-graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { WorkspaceActivationStatus } from 'twenty-shared';
import {
Column,
@ -15,9 +15,6 @@ import {
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
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 { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
@ -30,15 +27,6 @@ registerEnumType(WorkspaceActivationStatus, {
@Entity({ name: 'workspace', schema: 'core' })
@ObjectType('Workspace')
@UnPagedRelation('billingSubscriptions', () => BillingSubscription, {
nullable: true,
})
@UnPagedRelation('billingEntitlements', () => BillingEntitlement, {
nullable: true,
})
@UnPagedRelation('billingCustomers', () => BillingCustomer, {
nullable: true,
})
export class Workspace {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')

View File

@ -1,10 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@ -17,8 +21,6 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity';
@ -28,6 +30,7 @@ import { WorkspaceService } from './services/workspace.service';
@Module({
imports: [
TypeORMModule,
TypeOrmModule.forFeature([BillingSubscription], 'core'),
NestjsQueryGraphQLModule.forFeature({
imports: [
DomainManagerModule,

View File

@ -7,8 +7,10 @@ import {
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Repository } from 'typeorm';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
@ -63,6 +65,8 @@ export class WorkspaceResolver {
private readonly fileService: FileService,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
) {}
@Query(() => Workspace)
@ -159,6 +163,19 @@ export class WorkspaceResolver {
return this.workspaceService.deleteWorkspace(id);
}
@ResolveField(() => [BillingSubscription])
async billingSubscriptions(
@Parent() workspace: Workspace,
): Promise<BillingSubscription[] | undefined> {
try {
return this.billingSubscriptionRepository.find({
where: { workspaceId: workspace.id },
});
} catch (error) {
workspaceGraphqlApiExceptionHandler(error);
}
}
@ResolveField(() => BillingSubscription, { nullable: true })
async currentBillingSubscription(
@Parent() workspace: Workspace,