Add billing tables (#8772)

Beforehand, the name of the branch is not representative of the work
that has been done in this PR

**TLDR:**

Solves https://github.com/twentyhq/private-issues/issues/192
Add 3 tables BillingCustomer, BillingProduct and BillingPrice and
BillingMeter to core, inspired by the Stripe implementation. Separates
migration, between common and billing on order to not populate the db of
the self-hosting instances with unused tables.

**In order to test:**

Run the command:
npx nx typeorm -- migration:run -d
src/database/typeorm/core/core.datasource.ts


**Considerations:**

I only put the information we should use right now in the Billing
module, for instance columns like meter or agreggation formula where
omitted in the creation of the tables.
These columns and other ones who fall on the same spectrum will be added
as we need them.

If you want to add more information to the table, I'll leave some
utility links down bellow:

- BillingPrices: https://docs.stripe.com/api/prices/object
- BillingCustomer: https://docs.stripe.com/api/customers/object
- BillingProduct:  https://docs.stripe.com/api/products/object

**Next Steps**

Use the Stripe Webhook in order to update the tables accordingly

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Ana Sofia Marin Alexandre
2024-12-05 12:17:35 -03:00
committed by GitHub
parent c993f2de0b
commit 11d244194f
67 changed files with 824 additions and 102 deletions

View File

@ -3,7 +3,11 @@ 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 { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
@ -27,6 +31,10 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
[
BillingSubscription,
BillingSubscriptionItem,
BillingCustomer,
BillingProduct,
BillingPrice,
BillingMeter,
BillingEntitlement,
Workspace,
UserWorkspace,

View File

@ -0,0 +1,65 @@
import { ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'billingCustomer', schema: 'core' })
@ObjectType('billingCustomer')
@Unique('IndexOnWorkspaceIdAndStripeCustomerIdUnique', [
'workspaceId',
'stripeCustomerId',
])
export class BillingCustomer {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'timestamptz' })
deletedAt?: Date;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@ManyToOne(() => Workspace, (workspace) => workspace.billingCustomers, {
onDelete: 'CASCADE',
})
@JoinColumn()
workspace: Relation<Workspace>;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@Column({ nullable: false, unique: true })
stripeCustomerId: string;
@OneToMany(
() => BillingSubscription,
(billingSubscription) => billingSubscription.billingCustomer,
)
billingSubscriptions: Relation<BillingSubscription[]>;
@OneToMany(
() => BillingEntitlement,
(billingEntitlement) => billingEntitlement.billingCustomer,
)
billingEntitlements: Relation<BillingEntitlement[]>;
}

View File

@ -14,6 +14,7 @@ import {
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
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' })
@ -53,4 +54,17 @@ export class BillingEntitlement {
@Column({ nullable: true, type: 'timestamptz' })
deletedAt?: Date;
@ManyToOne(
() => BillingCustomer,
(billingCustomer) => billingCustomer.billingEntitlements,
{
onDelete: 'CASCADE',
createForeignKeyConstraints: false, // TODO: remove this once the customer table is populated
},
)
@JoinColumn({
referencedColumnName: 'stripeCustomerId',
name: 'stripeCustomerId',
})
billingCustomer: Relation<BillingCustomer>;
}

View File

@ -0,0 +1,61 @@
import Stripe from 'stripe';
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
@Entity({ name: 'billingMeter', schema: 'core' })
export class BillingMeter {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'timestamptz' })
deletedAt?: Date;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ nullable: false, unique: true })
stripeMeterId: string;
@Column({ nullable: false })
displayName: string;
@Column({ nullable: false })
eventName: string;
@Column({
nullable: false,
type: 'enum',
enum: Object.values(BillingMeterStatus),
})
status: BillingMeterStatus;
@Column({ nullable: false, type: 'jsonb' })
customerMapping: Stripe.Billing.Meter.CustomerMapping;
@Column({
nullable: true,
type: 'enum',
enum: Object.values(BillingMeterEventTimeWindow),
})
eventTimeWindow: BillingMeterEventTimeWindow | null;
@OneToMany(() => BillingPrice, (billingPrice) => billingPrice.billingMeter)
billingPrices: Relation<BillingPrice[]>;
@Column({ nullable: false, type: 'jsonb' })
valueSettings: Stripe.Billing.Meter.ValueSettings;
}

View File

@ -0,0 +1,139 @@
import { Field } from '@nestjs/graphql';
import Stripe from 'stripe';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum';
import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum';
import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum';
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
@Entity({ name: 'billingPrice', schema: 'core' })
export class BillingPrice {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'timestamptz' })
deletedAt?: Date;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ nullable: false, unique: true })
stripePriceId: string;
@Column({ nullable: false })
active: boolean;
@Column({ nullable: false })
stripeProductId: string;
@Column({ nullable: false })
currency: string;
@Column({ nullable: true, type: 'text' })
nickname: string | null;
@Column({
nullable: false,
type: 'enum',
enum: Object.values(BillingPriceTaxBehavior),
})
taxBehavior: BillingPriceTaxBehavior;
@Column({
nullable: false,
type: 'enum',
enum: Object.values(BillingPriceType),
})
type: BillingPriceType;
@Column({
nullable: false,
type: 'enum',
enum: Object.values(BillingPriceBillingScheme),
})
billingScheme: BillingPriceBillingScheme;
@Column({ nullable: true, type: 'jsonb' })
currencyOptions: Stripe.Price.CurrencyOptions | null;
@Column({ nullable: true, type: 'jsonb' })
tiers: Stripe.Price.Tier[] | null;
@Column({ nullable: true, type: 'jsonb' })
recurring: Stripe.Price.Recurring | null;
@Column({ nullable: true, type: 'jsonb' })
transformQuantity: Stripe.Price.TransformQuantity | null;
@Column({
nullable: true,
type: 'enum',
enum: Object.values(BillingPriceTiersMode),
})
tiersMode: BillingPriceTiersMode | null;
@Column({ nullable: true, type: 'text' })
unitAmountDecimal: string | null;
@Column({ nullable: true, type: 'numeric' })
unitAmount: number | null;
@Column({ nullable: true, type: 'text' })
stripeMeterId: string | null;
@Field(() => BillingUsageType)
@Column({
type: 'enum',
enum: Object.values(BillingUsageType),
nullable: false,
})
usageType: BillingUsageType;
@Field(() => SubscriptionInterval, { nullable: true })
@Column({
type: 'enum',
enum: Object.values(SubscriptionInterval),
nullable: true,
})
interval: SubscriptionInterval | null;
@ManyToOne(
() => BillingProduct,
(billingProduct) => billingProduct.billingPrices,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({
referencedColumnName: 'stripeProductId',
name: 'stripeProductId',
})
billingProduct: Relation<BillingProduct>;
@ManyToOne(() => BillingMeter, (billingMeter) => billingMeter.billingPrices, {
nullable: true,
})
@JoinColumn({
referencedColumnName: 'stripeMeterId',
name: 'stripeMeterId',
})
billingMeter: Relation<BillingMeter>;
}

View File

@ -0,0 +1,67 @@
import { registerEnumType } from '@nestjs/graphql';
import Stripe from 'stripe';
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { BillingProductMetadata } from 'src/engine/core-modules/billing/types/billing-product-metadata.type';
registerEnumType(BillingUsageType, { name: 'BillingUsageType' });
@Entity({ name: 'billingProduct', schema: 'core' })
export class BillingProduct {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'timestamptz' })
deletedAt?: Date;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ nullable: false })
active: boolean;
@Column({ nullable: true, type: 'text' })
description: string | null;
@Column({ nullable: false })
name: string;
@Column({ nullable: true, type: 'text' })
taxCode: string | null;
@Column({ nullable: false, type: 'jsonb', default: [] })
images: string[];
@Column({ nullable: false, type: 'jsonb', default: [] })
marketingFeatures: Stripe.Product.MarketingFeature[];
@Column({ nullable: false, unique: true })
stripeProductId: string;
@Column({ nullable: true, type: 'text' })
defaultStripePriceId: string | null;
@Column({ nullable: false, type: 'jsonb', default: {} })
metadata: BillingProductMetadata;
@OneToMany(() => BillingPrice, (billingPrice) => billingPrice.billingProduct)
billingPrices: Relation<BillingPrice[]>;
@Column({ nullable: true, type: 'text' })
unitLabel: string | null;
@Column({ nullable: true, type: 'text' })
url: string | null;
}

View File

@ -1,3 +1,4 @@
import Stripe from 'stripe';
import {
Column,
CreateDateColumn,
@ -10,7 +11,6 @@ import {
} from 'typeorm';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@Entity({ name: 'billingSubscriptionItem', schema: 'core' })
@Unique('IndexOnBillingSubscriptionIdAndStripeProductIdUnique', [
'billingSubscriptionId',
@ -36,6 +36,15 @@ export class BillingSubscriptionItem {
@Column({ nullable: false })
billingSubscriptionId: string;
@Column({ nullable: true })
stripeSubscriptionId: string;
@Column({ nullable: false, type: 'jsonb', default: {} })
metadata: Stripe.Metadata;
@Column({ nullable: true, type: 'jsonb' })
billingThresholds: Stripe.SubscriptionItem.BillingThresholds;
@ManyToOne(
() => BillingSubscription,
(billingSubscription) => billingSubscription.billingSubscriptionItems,
@ -52,8 +61,8 @@ export class BillingSubscriptionItem {
stripePriceId: string;
@Column({ nullable: false })
stripeSubscriptionItemId: string;
stripeSubscriptionItemId: string; //TODO: add unique
@Column({ nullable: false })
quantity: number;
quantity: number; //TODO: add nullable and modify stripe service
}

View File

@ -15,7 +15,9 @@ import {
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.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';
@ -75,4 +77,73 @@ export class BillingSubscription {
(billingSubscriptionItem) => billingSubscriptionItem.billingSubscription,
)
billingSubscriptionItems: Relation<BillingSubscriptionItem[]>;
@ManyToOne(
() => BillingCustomer,
(billingCustomer) => billingCustomer.billingSubscriptions,
{
nullable: false,
createForeignKeyConstraints: false,
},
)
@JoinColumn({
referencedColumnName: 'stripeCustomerId',
name: 'stripeCustomerId',
})
billingCustomer: Relation<BillingCustomer>; //let's see if it works
@Column({ nullable: false, default: false })
cancelAtPeriodEnd: boolean;
@Column({ nullable: false, default: 'USD' })
currency: string;
@Column({
nullable: false,
type: 'timestamptz',
default: () => 'CURRENT_TIMESTAMP',
})
currentPeriodEnd: Date;
@Column({
nullable: false,
type: 'timestamptz',
default: () => 'CURRENT_TIMESTAMP',
})
currentPeriodStart: Date;
@Column({ nullable: false, type: 'jsonb', default: {} })
metadata: Stripe.Metadata;
@Column({ nullable: true, type: 'timestamptz' })
cancelAt: Date | null;
@Column({
nullable: true,
type: 'timestamptz',
})
canceledAt: Date | null;
@Column({ nullable: true, type: 'jsonb' })
automaticTax: Stripe.Subscription.AutomaticTax | null;
@Column({ nullable: true, type: 'jsonb' })
cancellationDetails: Stripe.Subscription.CancellationDetails | null;
@Column({
nullable: false,
type: 'enum',
enum: Object.values(BillingSubscriptionCollectionMethod),
default: BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY,
})
collectionMethod: BillingSubscriptionCollectionMethod;
@Column({ nullable: true, type: 'timestamptz' })
endedAt: Date | null;
@Column({ nullable: true, type: 'timestamptz' })
trialStart: Date | null;
@Column({ nullable: true, type: 'timestamptz' })
trialEnd: Date | null;
}

View File

@ -0,0 +1,4 @@
export enum BillingMeterEventTimeWindow {
DAY = 'DAY',
HOUR = 'HOUR',
}

View File

@ -0,0 +1,4 @@
export enum BillingMeterStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
}

View File

@ -0,0 +1,4 @@
export enum BillingPlanKey {
BASE_PLAN = 'BASE_PLAN',
PRO_PLAN = 'PRO_PLAN',
}

View File

@ -0,0 +1,4 @@
export enum BillingPriceBillingScheme {
PER_UNIT = 'PER_UNIT',
TIERED = 'TIERED',
}

View File

@ -0,0 +1,5 @@
export enum BillingPriceTaxBehavior {
EXCLUSIVE = 'EXCLUSIVE',
INCLUSIVE = 'INCLUSIVE',
UNSPECIFIED = 'UNSPECIFIED',
}

View File

@ -0,0 +1,4 @@
export enum BillingPriceTiersMode {
GRADUATED = 'GRADUATED',
VOLUME = 'VOLUME',
}

View File

@ -0,0 +1,4 @@
export enum BillingPriceType {
ONE_TIME = 'ONE_TIME',
RECURRING = 'RECURRING',
}

View File

@ -0,0 +1,4 @@
export enum BillingSubscriptionCollectionMethod {
CHARGE_AUTOMATICALLY = 'CHARGE_AUTOMATICALLY',
SEND_INVOICE = 'SEND_INVOICE',
}

View File

@ -0,0 +1,4 @@
export enum BillingUsageType {
METERED = 'METERED',
LICENSED = 'LICENSED',
}

View File

@ -2,10 +2,11 @@ import { Logger, Scope } from '@nestjs/common';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type UpdateSubscriptionJobData = { workspaceId: string };
@Processor({
@ -17,15 +18,18 @@ export class UpdateSubscriptionJob {
constructor(
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly stripeService: StripeService,
private readonly twentyORMManager: TwentyORMManager,
) {}
@Process(UpdateSubscriptionJob.name)
async handle(data: UpdateSubscriptionJobData): Promise<void> {
const workspaceMembersCount = await this.userWorkspaceService.getUserCount(
data.workspaceId,
);
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
);
const workspaceMembersCount = await workspaceMemberRepository.count();
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
return;

View File

@ -12,6 +12,7 @@ import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/bil
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import {
Workspace,
@ -54,6 +55,30 @@ export class BillingWebhookService {
stripeSubscriptionId: data.object.id,
status: data.object.status as SubscriptionStatus,
interval: data.object.items.data[0].plan.interval,
cancelAtPeriodEnd: data.object.cancel_at_period_end,
currency: data.object.currency.toUpperCase(),
currentPeriodEnd: new Date(data.object.current_period_end * 1000),
currentPeriodStart: new Date(data.object.current_period_start * 1000),
metadata: data.object.metadata,
collectionMethod:
data.object.collection_method.toUpperCase() as BillingSubscriptionCollectionMethod,
automaticTax: data.object.automatic_tax ?? undefined,
cancellationDetails: data.object.cancellation_details ?? undefined,
endedAt: data.object.ended_at
? new Date(data.object.ended_at * 1000)
: undefined,
trialStart: data.object.trial_start
? new Date(data.object.trial_start * 1000)
: undefined,
trialEnd: data.object.trial_end
? new Date(data.object.trial_end * 1000)
: undefined,
cancelAt: data.object.cancel_at
? new Date(data.object.cancel_at * 1000)
: undefined,
canceledAt: data.object.canceled_at
? new Date(data.object.canceled_at * 1000)
: undefined,
},
{
conflictPaths: ['stripeSubscriptionId'],
@ -70,10 +95,13 @@ export class BillingWebhookService {
data.object.items.data.map((item) => {
return {
billingSubscriptionId: billingSubscription.id,
stripeSubscriptionId: data.object.id,
stripeProductId: item.price.product as string,
stripePriceId: item.price.id,
stripeSubscriptionItemId: item.id,
quantity: item.quantity,
metadata: item.metadata,
billingThresholds: item.billing_thresholds ?? undefined,
};
}),
{

View File

@ -22,7 +22,10 @@ export class BillingService {
return this.environmentService.get('IS_BILLING_ENABLED');
}
async hasWorkspaceActiveSubscriptionOrFreeAccess(workspaceId: string) {
async hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
workspaceId: string,
entitlementKey?: BillingEntitlementKey,
) {
const isBillingEnabled = this.isBillingEnabled();
if (!isBillingEnabled) {
@ -39,6 +42,13 @@ export class BillingService {
return true;
}
if (entitlementKey) {
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
workspaceId,
entitlementKey,
);
}
const currentBillingSubscription =
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
{ workspaceId },
@ -53,20 +63,4 @@ export class BillingService {
].includes(currentBillingSubscription.status)
);
}
async verifyWorkspaceEntitlement(
workspaceId: string,
entitlementKey: BillingEntitlementKey,
) {
const isBillingEnabled = this.isBillingEnabled();
if (!isBillingEnabled) {
return true;
}
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
workspaceId,
entitlementKey,
);
}
}

View File

@ -0,0 +1,7 @@
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
export type BillingProductMetadata = {
planKey: BillingPlanKey;
priceUsageBased: BillingUsageType;
};

View File

@ -24,11 +24,11 @@ import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator';
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
import { CastToStringArray } from 'src/engine/core-modules/environment/decorators/cast-to-string-array.decorator';
import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator';
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator';
import { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator';

View File

@ -27,7 +27,7 @@ export class OnboardingService {
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
const hasSubscription =
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccess(
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
user.defaultWorkspaceId,
);

View File

@ -55,7 +55,7 @@ export class SSOService {
);
}
const isSSOBillingEnabled =
await this.billingService.verifyWorkspaceEntitlement(
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
workspaceId,
this.featureLookUpKey,
);

View File

@ -13,6 +13,7 @@ import {
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@ -42,6 +43,9 @@ registerEnumType(WorkspaceActivationStatus, {
@UnPagedRelation('billingEntitlements', () => BillingEntitlement, {
nullable: true,
})
@UnPagedRelation('billingCustomers', () => BillingCustomer, {
nullable: true,
})
export class Workspace {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
@ -121,6 +125,12 @@ export class Workspace {
)
billingSubscriptions: Relation<BillingSubscription[]>;
@OneToMany(
() => BillingCustomer,
(billingCustomer) => billingCustomer.workspace,
)
billingCustomers: Relation<BillingCustomer[]>;
@OneToMany(
() => BillingEntitlement,
(billingEntitlement) => billingEntitlement.workspace,

View File

@ -133,7 +133,11 @@ export class WorkspaceResolver {
@ResolveField(() => BillingSubscription, { nullable: true })
async currentBillingSubscription(
@Parent() workspace: Workspace,
): Promise<BillingSubscription | null> {
): Promise<BillingSubscription | undefined> {
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
return;
}
return this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
{ workspaceId: workspace.id },
);