mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 11:43:34 +03:00
add entitlement table and sso stripe feature
This commit is contained in:
parent
a7a7d62502
commit
e701d1ac2e
@ -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",
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBillingEntitlementTable1732098580545
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddBillingEntitlementTable1732098580545';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_599121a93d8177b5d713b941982"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(`DROP TABLE "core"."billingEntitlement"`);
|
||||
}
|
||||
}
|
@ -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,
|
||||
],
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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<Workspace>;
|
||||
|
||||
@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;
|
||||
}
|
@ -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' });
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
export enum FeatureStripeLookupKey {
|
||||
SSO = 'sso_feat',
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export enum SubscriptionInterval {
|
||||
Day = 'day',
|
||||
Month = 'month',
|
||||
Week = 'week',
|
||||
Year = 'year',
|
||||
}
|
@ -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',
|
||||
}
|
@ -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',
|
||||
}
|
@ -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);
|
||||
|
@ -5,19 +5,15 @@ import assert from 'assert';
|
||||
|
||||
import { User } from '@sentry/node';
|
||||
import Stripe from 'stripe';
|
||||
import { In, Not, Repository } from 'typeorm';
|
||||
import { 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 { 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<BillingEntitlement>,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @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 },
|
||||
|
@ -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<BillingSubscription>,
|
||||
@InjectRepository(BillingEntitlement, 'core')
|
||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
||||
@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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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<FeatureFlagEntity>,
|
||||
@ -40,8 +39,7 @@ export class SSOService {
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
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(
|
||||
|
@ -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],
|
||||
|
@ -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<BillingSubscription[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => BillingEntitlement,
|
||||
(billingEntitlement) => billingEntitlement.workspace,
|
||||
)
|
||||
billingEntitlements: Relation<BillingEntitlement[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => PostgresCredentials,
|
||||
(postgresCredentials) => postgresCredentials.workspace,
|
||||
|
10
yarn.lock
10
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"
|
||||
|
Loading…
Reference in New Issue
Block a user