diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts index e3d9f904c6..76a669adf8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts @@ -147,7 +147,7 @@ export class BillingWebhookSubscriptionService { await this.billingUsageService.flushAvailableCreditsFromCache(workspace.id); await this.workspaceCacheService.invalidateAndRecompute(workspace.id, [ - 'billingSubscription', + 'currentBillingSubscription', ]); const shouldSuspend = this.shouldSuspendWorkspace(data); diff --git a/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.service.ts index 354bdfb201..9ec8f50fa5 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/app-billing/app-billing.service.ts @@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { type ChargeDto } from 'src/engine/core-modules/billing/app-billing/dtos/charge.dto'; +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { USAGE_RECORDED } from 'src/engine/core-modules/usage/constants/usage-recorded.constant'; import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum'; @@ -52,13 +53,15 @@ export class AppBillingService { let periodStart: Date | undefined; if (this.billingService.isBillingEnabled()) { - const { - billingSubscription: { currentPeriodStart }, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', - ]); + const { currentBillingSubscription } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'currentBillingSubscription', + ]); - periodStart = currentPeriodStart; + periodStart = + currentBillingSubscription === NO_BILLING_SUBSCRIPTION + ? undefined + : currentBillingSubscription.currentPeriodStart; } this.workspaceEventEmitter.emitCustomBatchEvent( 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 2f8a963044..2b0f4584f3 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 @@ -33,7 +33,7 @@ import { BillingUsageCapService } from 'src/engine/core-modules/billing/services import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { ResourceCreditService } from 'src/engine/core-modules/billing/services/resource-credit.service'; -import { WorkspaceBillingSubscriptionCacheService } from 'src/engine/core-modules/billing/services/workspace-billing-subscription-cache.service'; +import { WorkspaceCurrentBillingSubscriptionCacheService } from 'src/engine/core-modules/billing/services/workspace-current-billing-subscription-cache.service'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { EnterpriseModule } from 'src/engine/core-modules/enterprise/enterprise.module'; @@ -94,7 +94,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache BillingCreditRolloverService, ResourceCreditService, BillingGaugeService, - WorkspaceBillingSubscriptionCacheService, + WorkspaceCurrentBillingSubscriptionCacheService, provideWorkspaceScopedRepository(BillingEntitlementEntity), provideWorkspaceScopedRepository(BillingCustomerEntity), provideWorkspaceScopedRepository(BillingSubscriptionEntity), diff --git a/packages/twenty-server/src/engine/core-modules/billing/constants/no-billing-subscription.constant.ts b/packages/twenty-server/src/engine/core-modules/billing/constants/no-billing-subscription.constant.ts new file mode 100644 index 0000000000..27cae64a28 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/constants/no-billing-subscription.constant.ts @@ -0,0 +1,3 @@ +/* @license Enterprise */ + +export const NO_BILLING_SUBSCRIPTION = 'NO_BILLING_SUBSCRIPTION'; diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-usage.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-usage.service.ts index 86bfa501fe..cbd60c5dec 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-usage.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-usage.service.ts @@ -13,6 +13,7 @@ import { BillingException, BillingExceptionCode, } from 'src/engine/core-modules/billing/billing.exception'; +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; import { type BillingResourceCreditUsageDTO } from 'src/engine/core-modules/billing/dtos/billing-resource-credit-usage.dto'; import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; @@ -59,14 +60,14 @@ export class BillingUsageService { return true; } - const { billingSubscription } = + const { currentBillingSubscription } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', + 'currentBillingSubscription', ]); return ( - isDefined(billingSubscription) && - billingSubscription.status !== SubscriptionStatus.Canceled + currentBillingSubscription !== NO_BILLING_SUBSCRIPTION && + currentBillingSubscription.status !== SubscriptionStatus.Canceled ); } @@ -288,11 +289,16 @@ export class BillingUsageService { workspaceId: string; usedCredits: number; }): Promise { - const { - billingSubscription: { currentPeriodStart, currentPeriodEnd }, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', - ]); + const { currentBillingSubscription } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'currentBillingSubscription', + ]); + + if (currentBillingSubscription === NO_BILLING_SUBSCRIPTION) { + return 0; + } + + const { currentPeriodStart, currentPeriodEnd } = currentBillingSubscription; const cachedAvailableCredits = await this.getAvailableCreditsFromCache( workspaceId, @@ -363,10 +369,17 @@ export class BillingUsageService { return false; } - const { billingSubscription: subscription } = + const { currentBillingSubscription } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', + 'currentBillingSubscription', ]); + + if (currentBillingSubscription === NO_BILLING_SUBSCRIPTION) { + return false; + } + + const subscription = currentBillingSubscription; + const cached = await this.getAvailableCreditsFromCache( subscription.workspaceId, subscription.currentPeriodStart, diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/workspace-billing-subscription-cache.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/workspace-current-billing-subscription-cache.service.ts similarity index 67% rename from packages/twenty-server/src/engine/core-modules/billing/services/workspace-billing-subscription-cache.service.ts rename to packages/twenty-server/src/engine/core-modules/billing/services/workspace-current-billing-subscription-cache.service.ts index 9f9703b8be..851a979ae9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/workspace-billing-subscription-cache.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/workspace-current-billing-subscription-cache.service.ts @@ -1,28 +1,36 @@ /* @license Enterprise */ import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'twenty-shared/utils'; + import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { type FlatBillingSubscription } from 'src/engine/core-modules/billing/types/flat-billing-subscription.type'; +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; +import { type CurrentBillingSubscription } from 'src/engine/core-modules/billing/types/flat-billing-subscription.type'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; @Injectable() -@WorkspaceCache('billingSubscription') -export class WorkspaceBillingSubscriptionCacheService extends WorkspaceCacheProvider { +@WorkspaceCache('currentBillingSubscription') +export class WorkspaceCurrentBillingSubscriptionCacheService extends WorkspaceCacheProvider { constructor( private readonly billingSubscriptionService: BillingSubscriptionService, ) { super(); } - async computeForCache(workspaceId: string): Promise { + async computeForCache( + workspaceId: string, + ): Promise { const subscription = - await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow( - { - workspaceId, - }, - ); + await this.billingSubscriptionService.getCurrentBillingSubscription({ + workspaceId, + }); + + if (!isDefined(subscription)) { + return NO_BILLING_SUBSCRIPTION; + } return { id: subscription.id, diff --git a/packages/twenty-server/src/engine/core-modules/billing/types/flat-billing-subscription.type.ts b/packages/twenty-server/src/engine/core-modules/billing/types/flat-billing-subscription.type.ts index 940d7602bb..a130a971d9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/types/flat-billing-subscription.type.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/types/flat-billing-subscription.type.ts @@ -1,5 +1,6 @@ /* @license Enterprise */ +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; import { type BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum'; import { type SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { type SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; @@ -22,3 +23,7 @@ export type FlatBillingSubscription = { trialEnd: Date | null; collectionMethod: BillingSubscriptionCollectionMethod; }; + +export type CurrentBillingSubscription = + | FlatBillingSubscription + | typeof NO_BILLING_SUBSCRIPTION; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts index 7afd3a4dcd..624f4f5bd5 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service.ts @@ -25,6 +25,7 @@ import { FlatApplication } from 'src/engine/core-modules/application/types/flat- import { EventLogEmitterService } from 'src/engine/core-modules/event-logs/emit/event-log-emitter.service'; import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/logic-function/logic-function-executed'; import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @@ -462,18 +463,19 @@ export class LogicFunctionExecutorService { let periodStart: Date | undefined; if (this.billingService.isBillingEnabled()) { - const { - billingSubscription: { currentPeriodStart }, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', - ]); + const { currentBillingSubscription } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'currentBillingSubscription', + ]); - periodStart = currentPeriodStart; + if (currentBillingSubscription !== NO_BILLING_SUBSCRIPTION) { + periodStart = currentBillingSubscription.currentPeriodStart; - await this.billingUsageService.decrementAvailableCreditsInCache({ - workspaceId, - usedCredits: 100, - }); + await this.billingUsageService.decrementAvailableCreditsInCache({ + workspaceId, + usedCredits: 100, + }); + } } this.workspaceEventEmitter.emitCustomBatchEvent( diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts index aa0d7e6d1d..0d47a1b92e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/__tests__/ai-billing.service.spec.ts @@ -94,7 +94,7 @@ describe('AiBillingService', () => { provide: WorkspaceCacheService, useValue: { getOrRecompute: jest.fn().mockResolvedValue({ - billingSubscription: { + currentBillingSubscription: { currentPeriodStart: new Date('2026-04-01T00:00:00Z'), }, }), diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts index ea961d7651..fef33acdc7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { type LanguageModelUsage } from 'ai'; +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; @@ -139,18 +140,19 @@ export class AiBillingService { let periodStart: Date | undefined; if (this.billingService.isBillingEnabled()) { - const { - billingSubscription: { currentPeriodStart }, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', - ]); + const { currentBillingSubscription } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'currentBillingSubscription', + ]); - periodStart = currentPeriodStart; + if (currentBillingSubscription !== NO_BILLING_SUBSCRIPTION) { + periodStart = currentBillingSubscription.currentPeriodStart; - await this.billingUsageService.decrementAvailableCreditsInCache({ - workspaceId, - usedCredits: creditsUsedMicro, - }); + await this.billingUsageService.decrementAvailableCreditsInCache({ + workspaceId, + usedCredits: creditsUsedMicro, + }); + } } this.workspaceEventEmitter.emitCustomBatchEvent( @@ -182,13 +184,15 @@ export class AiBillingService { let periodStart: Date | undefined; if (this.billingService.isBillingEnabled()) { - const { - billingSubscription: { currentPeriodStart }, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', - ]); + const { currentBillingSubscription } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'currentBillingSubscription', + ]); - periodStart = currentPeriodStart; + periodStart = + currentBillingSubscription === NO_BILLING_SUBSCRIPTION + ? undefined + : currentBillingSubscription.currentPeriodStart; } this.workspaceEventEmitter.emitCustomBatchEvent( diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index 7171865187..ba4b5b8a73 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -8,7 +8,7 @@ import { type ResolverNameMapEntry } from 'src/engine/api/graphql/direct-executi import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type'; import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type'; import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; -import { type FlatBillingSubscription } from 'src/engine/core-modules/billing/types/flat-billing-subscription.type'; +import { type CurrentBillingSubscription } from 'src/engine/core-modules/billing/types/flat-billing-subscription.type'; import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type'; import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; @@ -60,7 +60,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = { apiKeyMap: 'cache:api-key-map', applicationVariableMaps: 'cache:application-variable', graphQLResolverNameMap: 'direct-execution:graphql-resolver-name-map', - billingSubscription: 'billing:subscription', + currentBillingSubscription: 'billing:subscription', } as const satisfies Record; export type AdditionalCacheDataMaps = { @@ -77,7 +77,7 @@ export type AdditionalCacheDataMaps = { flatWorkspaceMemberMaps: FlatWorkspaceMemberMaps; applicationVariableMaps: ApplicationVariableCacheMaps; graphQLResolverNameMap: Record; - billingSubscription: FlatBillingSubscription; + currentBillingSubscription: CurrentBillingSubscription; }; export type WorkspaceCacheDataMap = AllFlatEntityMaps & diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts index 3855f35112..2413e0e00b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts @@ -126,7 +126,7 @@ describe('WorkflowExecutorWorkspaceService', () => { provide: WorkspaceCacheService, useValue: { getOrRecompute: jest.fn().mockResolvedValue({ - billingSubscription: { + currentBillingSubscription: { currentPeriodStart: new Date('2026-04-01T00:00:00Z'), }, }), diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index cc3fa7b152..bf09898df9 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -8,6 +8,7 @@ import { WorkflowRunStepInfos, } from 'twenty-shared/workflow'; +import { NO_BILLING_SUBSCRIPTION } from 'src/engine/core-modules/billing/constants/no-billing-subscription.constant'; import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; @@ -327,18 +328,19 @@ export class WorkflowExecutorWorkspaceService { ) { let periodStart: Date | undefined; if (this.billingService.isBillingEnabled()) { - const { - billingSubscription: { currentPeriodStart }, - } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'billingSubscription', - ]); + const { currentBillingSubscription } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'currentBillingSubscription', + ]); - periodStart = currentPeriodStart; + if (currentBillingSubscription !== NO_BILLING_SUBSCRIPTION) { + periodStart = currentBillingSubscription.currentPeriodStart; - await this.billingUsageService.decrementAvailableCreditsInCache({ - workspaceId, - usedCredits: 100, - }); + await this.billingUsageService.decrementAvailableCreditsInCache({ + workspaceId, + usedCredits: 100, + }); + } } this.workspaceEventEmitter.emitCustomBatchEvent(