From e701d1ac2ee9bbe425701c250aa2e043bd8beca8 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre Date: Wed, 20 Nov 2024 15:53:09 +0100 Subject: [PATCH 1/6] add entitlement table and sso stripe feature --- package.json | 2 +- ...732098580545-addBillingEntitlementTable.ts | 25 ++++++ .../src/database/typeorm/typeorm.service.ts | 3 +- .../billing/billing.controller.ts | 16 +++- .../core-modules/billing/billing.module.ts | 2 + .../core-modules/billing/billing.resolver.ts | 3 +- .../billing/dto/checkout-session.input.ts | 2 +- .../billing/dto/product-price.entity.ts | 3 +- .../core-modules/billing/dto/product.input.ts | 2 +- .../entities/billing-entitlement.entity.ts | 56 ++++++++++++ .../entities/billing-subscription.entity.ts | 20 +---- .../available-product.enum.ts} | 0 .../enums/feature-stripe-lookup-key.enum.ts | 3 + .../enums/subcription-interval.enum.ts | 6 ++ .../billing/enums/subcription-status.enum.ts | 10 +++ .../billing/enums/webhook-events.enum.ts | 7 ++ .../billing-portal.workspace-service.ts | 7 -- .../services/billing-subscription.service.ts | 88 ++++++------------- .../services/billing-webhook.service.ts | 42 +++++++-- .../billing/services/billing.service.ts | 21 ++++- .../billing/stripe/stripe.service.ts | 3 +- .../core-modules/sso/services/sso.service.ts | 22 +++-- .../src/engine/core-modules/sso/sso.module.ts | 2 + .../workspace/workspace.entity.ts | 12 ++- yarn.lock | 10 +-- 25 files changed, 249 insertions(+), 118 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1732098580545-addBillingEntitlementTable.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts rename packages/twenty-server/src/engine/core-modules/billing/{interfaces/available-product.interface.ts => enums/available-product.enum.ts} (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/enums/subcription-interval.enum.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/enums/subcription-status.enum.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts diff --git a/package.json b/package.json index c9c7f6f1ef34..c12755c1386d 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "semver": "^7.5.4", "sharp": "^0.32.1", "slash": "^5.1.0", - "stripe": "^14.17.0", + "stripe": "^17.3.1", "ts-key-enum": "^2.0.12", "tslib": "^2.3.0", "tsup": "^8.2.4", diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1732098580545-addBillingEntitlementTable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1732098580545-addBillingEntitlementTable.ts new file mode 100644 index 000000000000..8100f8cdf079 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1732098580545-addBillingEntitlementTable.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBillingEntitlementTable1732098580545 + implements MigrationInterface +{ + name = 'AddBillingEntitlementTable1732098580545'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."billingEntitlement" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" text NOT NULL, "workspaceId" uuid NOT NULL, "stripeCustomerId" character varying NOT NULL, "value" boolean NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "IndexOnFeatureKeyAndWorkspaceIdUnique" UNIQUE ("key", "workspaceId"), CONSTRAINT "PK_4e6ed788c3ca0bf6610d5022576" PRIMARY KEY ("id"))`, + ); + + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" ADD CONSTRAINT "FK_599121a93d8177b5d713b941982" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_599121a93d8177b5d713b941982"`, + ); + + await queryRunner.query(`DROP TABLE "core"."billingEntitlement"`); + } +} diff --git a/packages/twenty-server/src/database/typeorm/typeorm.service.ts b/packages/twenty-server/src/database/typeorm/typeorm.service.ts index 70138d055cc5..2f693060925e 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.service.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.service.ts @@ -3,6 +3,7 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; +import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @@ -14,7 +15,6 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; - @Injectable() export class TypeORMService implements OnModuleInit, OnModuleDestroy { private mainDataSource: DataSource; @@ -36,6 +36,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { FeatureFlagEntity, BillingSubscription, BillingSubscriptionItem, + BillingEntitlement, PostgresCredentials, WorkspaceSSOIdentityProvider, ], diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index d38dfd1d5795..2c8c85c1aa3d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -10,19 +10,18 @@ import { import { Response } from 'express'; -import { WebhookEvent } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; +import { WebhookEvent } from 'src/engine/core-modules/billing/enums/webhook-events.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; - @Controller('billing') export class BillingController { protected readonly logger = new Logger(BillingController.name); constructor( private readonly stripeService: StripeService, - private readonly billingSubscriptionService: BillingSubscriptionService, private readonly billingWehbookService: BillingWebhookService, + private readonly billingSubscriptionService: BillingSubscriptionService, ) {} @Post('/webhooks') @@ -63,6 +62,17 @@ export class BillingController { event.data, ); } + if (event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT) { + try { + await this.billingWehbookService.processCustomerActiveEntitlement( + event.data, + ); + } catch (error) { + res.status(500).end(); + + return; + } + } res.status(200).end(); } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index ff33a896ea2d..2d8acfff49f8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -3,6 +3,7 @@ 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 { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.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 { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener'; @@ -24,6 +25,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; [ BillingSubscription, BillingSubscriptionItem, + BillingEntitlement, Workspace, UserWorkspace, FeatureFlagEntity, diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 949615dd6679..1997606cbc2a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -1,14 +1,13 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; - import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input'; import { CheckoutSessionInput } from 'src/engine/core-modules/billing/dto/checkout-session.input'; import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity'; import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input'; import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity'; import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts index 48738b85f8dd..41f75e59caa2 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts @@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import Stripe from 'stripe'; -import { SubscriptionInterval } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; @ArgsType() export class CheckoutSessionInput { diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts index d8c2d6d434f3..3ea7a6c8354e 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts @@ -2,8 +2,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import Stripe from 'stripe'; -import { SubscriptionInterval } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; - +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; @ObjectType() export class ProductPriceEntity { @Field(() => SubscriptionInterval) diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts index f58f681e01a8..56d1cceaf8ce 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts @@ -2,7 +2,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty, IsString } from 'class-validator'; -import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; @ArgsType() export class ProductInput { diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts new file mode 100644 index 000000000000..8e5947daee69 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts @@ -0,0 +1,56 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +@Entity({ name: 'billingEntitlement', schema: 'core' }) +@ObjectType('billingEntitlement') +@Unique('IndexOnFeatureKeyAndWorkspaceIdUnique', ['key', 'workspaceId']) +export class BillingEntitlement { + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field(() => String) + @Column({ nullable: false, type: 'text' }) + key: FeatureStripeLookupKey; + + @Field() + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @ManyToOne(() => Workspace, (workspace) => workspace.billingEntitlements, { + onDelete: 'CASCADE', + }) + @JoinColumn() + workspace: Relation; + + @Column({ nullable: false }) + stripeCustomerId: string; + + @Field() + @Column({ nullable: false }) + value: boolean; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @Column({ nullable: true, type: 'timestamptz' }) + deletedAt?: Date; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts index 5199137d73d0..b22ef7a7b432 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts @@ -16,26 +16,10 @@ import { import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -export enum SubscriptionStatus { - Active = 'active', - Canceled = 'canceled', - Incomplete = 'incomplete', - IncompleteExpired = 'incomplete_expired', - PastDue = 'past_due', - Paused = 'paused', - Trialing = 'trialing', - Unpaid = 'unpaid', -} - -export enum SubscriptionInterval { - Day = 'day', - Month = 'month', - Week = 'week', - Year = 'year', -} - registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' }); registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' }); diff --git a/packages/twenty-server/src/engine/core-modules/billing/interfaces/available-product.interface.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/available-product.enum.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/billing/interfaces/available-product.interface.ts rename to packages/twenty-server/src/engine/core-modules/billing/enums/available-product.enum.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts new file mode 100644 index 000000000000..991676f802eb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts @@ -0,0 +1,3 @@ +export enum FeatureStripeLookupKey { + SSO = 'sso_feat', +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-interval.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-interval.enum.ts new file mode 100644 index 000000000000..fe81cf3f40b7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-interval.enum.ts @@ -0,0 +1,6 @@ +export enum SubscriptionInterval { + Day = 'day', + Month = 'month', + Week = 'week', + Year = 'year', +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-status.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-status.enum.ts new file mode 100644 index 000000000000..dc9621becb34 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-status.enum.ts @@ -0,0 +1,10 @@ +export enum SubscriptionStatus { + Active = 'active', + Canceled = 'canceled', + Incomplete = 'incomplete', + IncompleteExpired = 'incomplete_expired', + PastDue = 'past_due', + Paused = 'paused', + Trialing = 'trialing', + Unpaid = 'unpaid', +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts new file mode 100644 index 000000000000..467b7802fddc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts @@ -0,0 +1,7 @@ +export enum WebhookEvent { + CUSTOMER_SUBSCRIPTION_CREATED = 'customer.subscription.created', + CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated', + CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted', + SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded', + CUSTOMER_ACTIVE_ENTITLEMENT = 'entitlements.active_entitlement_summary.updated', +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index 6c031463f6e1..dbad2efc76ef 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -12,13 +12,6 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { assert } from 'src/utils/assert'; -export enum WebhookEvent { - CUSTOMER_SUBSCRIPTION_CREATED = 'customer.subscription.created', - CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated', - CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted', - SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded', -} - @Injectable() export class BillingPortalWorkspaceService { protected readonly logger = new Logger(BillingPortalWorkspaceService.name); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index 248d3fa6ebbd..2d27d67c3fc4 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -5,19 +5,15 @@ import assert from 'assert'; import { User } from '@sentry/node'; import Stripe from 'stripe'; -import { In, Not, Repository } from 'typeorm'; - -import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; - -import { - BillingSubscription, - SubscriptionInterval, - SubscriptionStatus, -} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { Not, Repository } from 'typeorm'; + +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 { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; +import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Injectable() @@ -26,59 +22,12 @@ export class BillingSubscriptionService { constructor( private readonly stripeService: StripeService, private readonly environmentService: EnvironmentService, + @InjectRepository(BillingEntitlement, 'core') + private readonly billingEntitlementRepository: Repository, @InjectRepository(BillingSubscription, 'core') private readonly billingSubscriptionRepository: Repository, - @InjectRepository(FeatureFlagEntity, 'core') - private readonly featureFlagRepository: Repository, - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, ) {} - /** - * @deprecated This is fully deprecated, it's only used in the migration script for 0.23 - */ - async getActiveSubscriptionWorkspaceIds() { - if (!this.environmentService.get('IS_BILLING_ENABLED')) { - return (await this.workspaceRepository.find({ select: ['id'] })).map( - (workspace) => workspace.id, - ); - } - - const activeSubscriptions = await this.billingSubscriptionRepository.find({ - where: { - status: In([ - SubscriptionStatus.Active, - SubscriptionStatus.Trialing, - SubscriptionStatus.PastDue, - ]), - }, - select: ['workspaceId'], - }); - - const freeAccessFeatureFlags = await this.featureFlagRepository.find({ - where: { - key: FeatureFlagKey.IsFreeAccessEnabled, - value: true, - }, - select: ['workspaceId'], - }); - - const activeWorkspaceIdsBasedOnSubscriptions = activeSubscriptions.map( - (subscription) => subscription.workspaceId, - ); - - const activeWorkspaceIdsBasedOnFeatureFlags = freeAccessFeatureFlags.map( - (featureFlag) => featureFlag.workspaceId, - ); - - return Array.from( - new Set([ - ...activeWorkspaceIdsBasedOnSubscriptions, - ...activeWorkspaceIdsBasedOnFeatureFlags, - ]), - ); - } - async getCurrentBillingSubscriptionOrThrow(criteria: { workspaceId?: string; stripeCustomerId?: string; @@ -148,6 +97,23 @@ export class BillingSubscriptionService { } } + async getWorkspaceEntitlementByKey( + workspaceId: string, + lookupKey: FeatureStripeLookupKey, + ) { + const entitlement = await this.billingEntitlementRepository.findOneBy({ + workspaceId, + key: lookupKey, + value: true, + }); + + if (!entitlement?.value) { + return false; + } + + return true; + } + async applyBillingSubscription(user: User) { const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( { workspaceId: user.defaultWorkspaceId }, diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts index 7d1b5df5a1b9..4d58dacbcadf 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts @@ -4,11 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import Stripe from 'stripe'; import { Repository } from 'typeorm'; +import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; -import { - BillingSubscription, - SubscriptionStatus, -} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; import { Workspace, WorkspaceActivationStatus, @@ -20,6 +20,8 @@ export class BillingWebhookService { constructor( @InjectRepository(BillingSubscription, 'core') private readonly billingSubscriptionRepository: Repository, + @InjectRepository(BillingEntitlement, 'core') + private readonly billingEntitlementRepository: Repository, @InjectRepository(BillingSubscriptionItem, 'core') private readonly billingSubscriptionItemRepository: Repository, @InjectRepository(Workspace, 'core') @@ -43,7 +45,7 @@ export class BillingWebhookService { await this.billingSubscriptionRepository.upsert( { - workspaceId: workspaceId, + workspaceId, stripeCustomerId: data.object.customer as string, stripeSubscriptionId: data.object.id, status: data.object.status as SubscriptionStatus, @@ -96,4 +98,34 @@ export class BillingWebhookService { }); } } + + async processCustomerActiveEntitlement( + data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data, + ) { + const billingSubscription = + await this.billingSubscriptionRepository.findOneOrFail({ + where: { stripeCustomerId: data.object.customer }, + }); + const workspaceId = billingSubscription.workspaceId; + const stripeCustomerId = data.object.customer; + + const currentEntitlements = data.object.entitlements.data.map( + (item) => item.lookup_key, + ); + + await this.billingEntitlementRepository.upsert( + Object.values(FeatureStripeLookupKey).map((key) => { + return { + workspaceId, + key, + value: currentEntitlements.includes(key), + stripeCustomerId, + }; + }), + { + conflictPaths: ['workspaceId', 'key'], + skipUpdateIfNoValuesChanged: true, + }, + ); + } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts index 8004d46e5c70..79bce812d2aa 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts @@ -2,11 +2,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { isDefined } from 'class-validator'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Injectable() export class BillingService { @@ -52,4 +53,20 @@ export class BillingService { ].includes(currentBillingSubscription.status) ); } + + async verifyWorkspaceEntitlement( + workspaceId: string, + entitlementKey: FeatureStripeLookupKey, + ) { + const isBillingEnabled = this.isBillingEnabled(); + + if (!isBillingEnabled) { + return true; + } + + return this.billingSubscriptionService.getWorkspaceEntitlementByKey( + workspaceId, + entitlementKey, + ); + } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index a6b0d179936d..4d7695e9b80d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -2,10 +2,9 @@ import { Injectable, Logger } from '@nestjs/common'; import Stripe from 'stripe'; -import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; - import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { User } from 'src/engine/core-modules/user/user.entity'; diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 7b2148d23000..12b69ec16ce4 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -6,9 +6,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Issuer } from 'openid-client'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -30,8 +29,8 @@ import { import { User } from 'src/engine/core-modules/user/user.entity'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class SSOService { + private readonly featureLookUpKey = 'sso_feat' as FeatureStripeLookupKey; constructor( @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, @@ -40,8 +39,7 @@ export class SSOService { @InjectRepository(User, 'core') private readonly userRepository: Repository, private readonly environmentService: EnvironmentService, - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - private readonly cacheStorageService: CacheStorageService, + private readonly billingService: BillingService, ) {} private async isSSOEnabled(workspaceId: string) { @@ -57,6 +55,18 @@ export class SSOService { SSOExceptionCode.SSO_DISABLE, ); } + const isSSOBillingEnabled = + await this.billingService.verifyWorkspaceEntitlement( + workspaceId, + this.featureLookUpKey, + ); + + if (!isSSOBillingEnabled) { + throw new SSOException( + `${FeatureFlagKey.IsSSOEnabled} feature is enabled but no entitlement for this workspace`, + SSOExceptionCode.SSO_DISABLE, + ); + } } async createOIDCIdentityProvider( diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts index fc7fe9979987..d51a566d3a89 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts @@ -5,6 +5,7 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; +import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { SSOResolver } from 'src/engine/core-modules/sso/sso.resolver'; @@ -17,6 +18,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; [WorkspaceSSOIdentityProvider, User, AppToken, FeatureFlagEntity], 'core', ), + BillingModule, ], exports: [SSOService], providers: [SSOService, SSOResolver], diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index ac5c38287104..ec75e081f4d5 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -13,13 +13,14 @@ 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 { 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'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; export enum WorkspaceActivationStatus { ONGOING_CREATION = 'ONGOING_CREATION', @@ -38,6 +39,9 @@ registerEnumType(WorkspaceActivationStatus, { @UnPagedRelation('billingSubscriptions', () => BillingSubscription, { nullable: true, }) +@UnPagedRelation('billingEntitlements', () => BillingEntitlement, { + nullable: true, +}) export class Workspace { @IDField(() => UUIDScalarType) @PrimaryGeneratedColumn('uuid') @@ -117,6 +121,12 @@ export class Workspace { ) billingSubscriptions: Relation; + @OneToMany( + () => BillingEntitlement, + (billingEntitlement) => billingEntitlement.workspace, + ) + billingEntitlements: Relation; + @OneToMany( () => PostgresCredentials, (postgresCredentials) => postgresCredentials.workspace, diff --git a/yarn.lock b/yarn.lock index 289dd0c56623..94ebc7496965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42868,13 +42868,13 @@ __metadata: languageName: node linkType: hard -"stripe@npm:^14.17.0": - version: 14.25.0 - resolution: "stripe@npm:14.25.0" +"stripe@npm:^17.3.1": + version: 17.3.1 + resolution: "stripe@npm:17.3.1" dependencies: "@types/node": "npm:>=8.1.0" qs: "npm:^6.11.0" - checksum: 10c0/3f98230d537bdcb9e31775576743e9f2e2137d45021b3a59afe5af17dc54397e8f27bab7abce6fbb81545f69dc73f4c1325c987d2e0c88c2149e135c783d14ff + checksum: 10c0/96c9595428775d3bb5d619f770dd4775357ec778be4033915d629ef6033d29a70ec4d916311cc2e1e5e5e45646d9b53fcef6215e60e1ea183d89554bd61e7055 languageName: node linkType: hard @@ -44629,7 +44629,7 @@ __metadata: storybook-addon-cookie: "npm:^3.2.0" storybook-addon-pseudo-states: "npm:^2.1.2" storybook-dark-mode: "npm:^3.0.3" - stripe: "npm:^14.17.0" + stripe: "npm:^17.3.1" supertest: "npm:^6.1.3" ts-jest: "npm:^29.1.1" ts-key-enum: "npm:^2.0.12" From 73bcb18e1837a53d79c783ffbcc46ea1d3b1c659 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre Date: Thu, 21 Nov 2024 13:50:31 +0100 Subject: [PATCH 2/6] add pr comment changes --- .../billing/billing.controller.ts | 19 ++++++++++---- .../core-modules/billing/billing.exception.ts | 14 ++++++++++ .../core-modules/billing/billing.resolver.ts | 2 +- .../billing/dto/checkout-session.input.ts | 2 +- .../billing/dto/product-price.entity.ts | 2 +- .../core-modules/billing/dto/product.input.ts | 2 +- .../entities/billing-entitlement.entity.ts | 4 +-- .../entities/billing-subscription.entity.ts | 4 +-- ...m.ts => billing-available-product.enum.ts} | 0 .../enums/billing-entitlement-key.enum.ts | 3 +++ ... => billing-subscription-interval.enum.ts} | 0 ...ts => billing-subscription-status.enum.ts} | 0 ...enum.ts => billing-webhook-events.enum.ts} | 2 +- .../enums/feature-stripe-lookup-key.enum.ts | 3 --- .../services/billing-subscription.service.ts | 16 ++++++------ .../services/billing-webhook.service.ts | 26 ++++++++++++++----- .../billing/services/billing.service.ts | 6 ++--- .../billing/stripe/stripe.service.ts | 2 +- .../core-modules/sso/services/sso.service.ts | 4 +-- 19 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts rename packages/twenty-server/src/engine/core-modules/billing/enums/{available-product.enum.ts => billing-available-product.enum.ts} (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts rename packages/twenty-server/src/engine/core-modules/billing/enums/{subcription-interval.enum.ts => billing-subscription-interval.enum.ts} (100%) rename packages/twenty-server/src/engine/core-modules/billing/enums/{subcription-status.enum.ts => billing-subscription-status.enum.ts} (100%) rename packages/twenty-server/src/engine/core-modules/billing/enums/{webhook-events.enum.ts => billing-webhook-events.enum.ts} (74%) delete mode 100644 packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index 2c8c85c1aa3d..9cac8ec106ce 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -10,7 +10,11 @@ import { import { Response } from 'express'; -import { WebhookEvent } from 'src/engine/core-modules/billing/enums/webhook-events.enum'; +import { + BillingException, + BillingExceptionCode, +} from 'src/engine/core-modules/billing/billing.exception'; +import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; @@ -62,15 +66,20 @@ export class BillingController { event.data, ); } - if (event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT) { + if ( + event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED + ) { try { await this.billingWehbookService.processCustomerActiveEntitlement( event.data, ); } catch (error) { - res.status(500).end(); - - return; + if ( + error instanceof BillingException && + error.code === BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND + ) { + res.status(404).end(); + } } } res.status(200).end(); diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts new file mode 100644 index 000000000000..10e34c755edf --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -0,0 +1,14 @@ +/* @license Enterprise */ + +import { CustomException } from 'src/utils/custom-exception'; + +export class BillingException extends CustomException { + code: BillingExceptionCode; + constructor(message: string, code: BillingExceptionCode) { + super(message, code); + } +} + +export enum BillingExceptionCode { + BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 1997606cbc2a..c7076bdc2d63 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -7,7 +7,7 @@ import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input'; import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity'; import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity'; -import { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts index 41f75e59caa2..9371eee8f247 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts @@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import Stripe from 'stripe'; -import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; @ArgsType() export class CheckoutSessionInput { diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts index 3ea7a6c8354e..011d880b2af9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts @@ -2,7 +2,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import Stripe from 'stripe'; -import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; @ObjectType() export class ProductPriceEntity { @Field(() => SubscriptionInterval) diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts index 56d1cceaf8ce..126e1351d0ec 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts @@ -2,7 +2,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty, IsString } from 'class-validator'; -import { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; @ArgsType() export class ProductInput { diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts index 8e5947daee69..4bb831fdcba5 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts @@ -14,7 +14,7 @@ import { } from 'typeorm'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; -import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Entity({ name: 'billingEntitlement', schema: 'core' }) @ObjectType('billingEntitlement') @@ -26,7 +26,7 @@ export class BillingEntitlement { @Field(() => String) @Column({ nullable: false, type: 'text' }) - key: FeatureStripeLookupKey; + key: BillingEntitlementKey; @Field() @Column({ nullable: false, type: 'uuid' }) diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts index b22ef7a7b432..711c3f527589 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts @@ -16,8 +16,8 @@ import { import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; -import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' }); diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/available-product.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-available-product.enum.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/billing/enums/available-product.enum.ts rename to packages/twenty-server/src/engine/core-modules/billing/enums/billing-available-product.enum.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts new file mode 100644 index 000000000000..1547e5c529c1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts @@ -0,0 +1,3 @@ +export enum BillingEntitlementKey { + SSO = 'sso_feat', +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-interval.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-subscription-interval.enum.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/billing/enums/subcription-interval.enum.ts rename to packages/twenty-server/src/engine/core-modules/billing/enums/billing-subscription-interval.enum.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/subcription-status.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-subscription-status.enum.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/billing/enums/subcription-status.enum.ts rename to packages/twenty-server/src/engine/core-modules/billing/enums/billing-subscription-status.enum.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts similarity index 74% rename from packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts rename to packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts index 467b7802fddc..efb1e5f571ba 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/webhook-events.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts @@ -3,5 +3,5 @@ export enum WebhookEvent { CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated', CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted', SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded', - CUSTOMER_ACTIVE_ENTITLEMENT = 'entitlements.active_entitlement_summary.updated', + CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED = 'entitlements.active_entitlement_summary.updated', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts deleted file mode 100644 index 991676f802eb..000000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum FeatureStripeLookupKey { - SSO = 'sso_feat', -} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index 2d27d67c3fc4..f2ebf2d07a85 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -9,10 +9,10 @@ import { Not, Repository } from 'typeorm'; 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 { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; -import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; -import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/subcription-interval.enum'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-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'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @@ -99,19 +99,19 @@ export class BillingSubscriptionService { async getWorkspaceEntitlementByKey( workspaceId: string, - lookupKey: FeatureStripeLookupKey, + key: BillingEntitlementKey, ) { const entitlement = await this.billingEntitlementRepository.findOneBy({ workspaceId, - key: lookupKey, + key, value: true, }); - if (!entitlement?.value) { + if (!entitlement) { return false; } - return true; + return entitlement.value; } async applyBillingSubscription(user: User) { diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts index 4d58dacbcadf..fc6c7d2b4c37 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts @@ -4,11 +4,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import Stripe from 'stripe'; import { Repository } from 'typeorm'; +import { + BillingException, + BillingExceptionCode, +} from 'src/engine/core-modules/billing/billing.exception'; import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.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 { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { Workspace, WorkspaceActivationStatus, @@ -103,22 +107,30 @@ export class BillingWebhookService { data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data, ) { const billingSubscription = - await this.billingSubscriptionRepository.findOneOrFail({ + await this.billingSubscriptionRepository.findOne({ where: { stripeCustomerId: data.object.customer }, }); + + if (!billingSubscription) { + throw new BillingException( + 'Billing customer not found', + BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND, + ); + } + const workspaceId = billingSubscription.workspaceId; const stripeCustomerId = data.object.customer; - const currentEntitlements = data.object.entitlements.data.map( - (item) => item.lookup_key, + const activeEntitlementsKeys = data.object.entitlements.data.map( + (entitlement) => entitlement.lookup_key, ); await this.billingEntitlementRepository.upsert( - Object.values(FeatureStripeLookupKey).map((key) => { + Object.values(BillingEntitlementKey).map((key) => { return { workspaceId, key, - value: currentEntitlements.includes(key), + value: activeEntitlementsKeys.includes(key), stripeCustomerId, }; }), diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts index 79bce812d2aa..27cc61bb4a04 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts @@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { isDefined } from 'class-validator'; -import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/subcription-status.enum'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -56,7 +56,7 @@ export class BillingService { async verifyWorkspaceEntitlement( workspaceId: string, - entitlementKey: FeatureStripeLookupKey, + entitlementKey: BillingEntitlementKey, ) { const isBillingEnabled = this.isBillingEnabled(); diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index 4d7695e9b80d..cff2572444df 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -4,7 +4,7 @@ import Stripe from 'stripe'; import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; -import { AvailableProduct } from 'src/engine/core-modules/billing/enums/available-product.enum'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { User } from 'src/engine/core-modules/user/user.entity'; diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 12b69ec16ce4..0d5d90a0be8f 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -6,7 +6,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Issuer } from 'openid-client'; import { Repository } from 'typeorm'; -import { FeatureStripeLookupKey } from 'src/engine/core-modules/billing/enums/feature-stripe-lookup-key.enum'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -30,7 +30,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; @Injectable() export class SSOService { - private readonly featureLookUpKey = 'sso_feat' as FeatureStripeLookupKey; + private readonly featureLookUpKey = 'sso_feat' as BillingEntitlementKey; constructor( @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, From bf12f7d0d96c36a7632c4edf3c3796feb8fce3dd Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre Date: Thu, 21 Nov 2024 14:13:48 +0100 Subject: [PATCH 3/6] correct naming entitlement keys --- .../core-modules/billing/enums/billing-entitlement-key.enum.ts | 2 +- .../src/engine/core-modules/sso/services/sso.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts index 1547e5c529c1..f4301c63d9f6 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts @@ -1,3 +1,3 @@ export enum BillingEntitlementKey { - SSO = 'sso_feat', + SSO = 'SSO', } diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 0d5d90a0be8f..aad05cb2aa31 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -30,7 +30,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; @Injectable() export class SSOService { - private readonly featureLookUpKey = 'sso_feat' as BillingEntitlementKey; + private readonly featureLookUpKey = 'SSO' as BillingEntitlementKey; constructor( @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, From e4d9f006e3672d63ede1bc4427b7ec7882bda867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Thu, 21 Nov 2024 22:53:00 +0100 Subject: [PATCH 4/6] Small fix --- .../src/engine/core-modules/sso/services/sso.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index aad05cb2aa31..e6d073316bf1 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -30,7 +30,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; @Injectable() export class SSOService { - private readonly featureLookUpKey = 'SSO' as BillingEntitlementKey; + private readonly featureLookUpKey = BillingEntitlementKey.SSO; constructor( @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, From 28782dec3e4577bb771fe7c18248d5995ab2fbf5 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre Date: Fri, 22 Nov 2024 10:37:19 +0100 Subject: [PATCH 5/6] fix stripe ci error --- .../src/engine/core-modules/billing/stripe/stripe.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index cff2572444df..4eff7178f9ea 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -15,7 +15,7 @@ export class StripeService { constructor(private readonly environmentService: EnvironmentService) { this.stripe = new Stripe( - this.environmentService.get('BILLING_STRIPE_API_KEY'), + this.environmentService.get('BILLING_STRIPE_API_KEY') ?? '', {}, ); } From 9ca0f5f683d07bf50df87dca979ee63624754278 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre Date: Fri, 22 Nov 2024 11:27:31 +0100 Subject: [PATCH 6/6] fix worker ci --- .../engine/core-modules/billing/stripe/stripe.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index 4eff7178f9ea..2761415d0bca 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -14,8 +14,12 @@ export class StripeService { private readonly stripe: Stripe; constructor(private readonly environmentService: EnvironmentService) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = new Stripe( - this.environmentService.get('BILLING_STRIPE_API_KEY') ?? '', + this.environmentService.get('BILLING_STRIPE_API_KEY'), {}, ); }