clean code and normalize billing eventNames (#9928)

Solves :
https://github.com/twentyhq/private-issues/issues/251

**TLDR:**

Clean Billing Code using feedback of the previous PR (#9865). Normalized
the metadata and names of the products, prices, and meters in Stripe so
that they can be accessed in stripe's test mode and live mode.

**In order to test:**

1. Have the environment variable IS_BILLING_ENABLED set to true and add
the other required environment variables for Billing to work
2. Do a database reset (to ensure that the new feature flag is properly
added and that the billing tables are created)
3. Run the command: npx nx run twenty-server:command
billing:sync-plans-data (if you don't do that the products and prices
will not be present in the database)
4. Run the server , the frontend, the worker, and the stripe listen
command (stripe listen --forward-to
http://localhost:3000/billing/webhooks)
5. Buy a subscription for the Acme workspace
6. Create a workflow and run it
7. After the run has been finished check in sprite the quantity of
events in the CreditMeter, you should see that there is a new occurence
with value one.
This commit is contained in:
Ana Sofia Marin Alexandre
2025-01-30 13:39:02 -03:00
committed by GitHub
parent 625466f38d
commit d777f62651
8 changed files with 21 additions and 25 deletions

View File

@ -13,7 +13,7 @@ import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.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 { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter'; import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
import { BillingExecuteBilledFunctionListener } from 'src/engine/core-modules/billing/listeners/billing-execute-billed-function.listener'; import { BillingFeatureUsedListener } from 'src/engine/core-modules/billing/listeners/billing-feature-used.listener';
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener'; import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service'; import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
@ -65,7 +65,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
BillingResolver, BillingResolver,
BillingPlanService, BillingPlanService,
BillingWorkspaceMemberListener, BillingWorkspaceMemberListener,
BillingExecuteBilledFunctionListener, BillingFeatureUsedListener,
BillingService, BillingService,
BillingWebhookProductService, BillingWebhookProductService,
BillingWebhookPriceService, BillingWebhookPriceService,

View File

@ -1,2 +0,0 @@
export const BILLING_EXECUTE_BILLED_FUNCTION =
'billing_execute_billed_function';

View File

@ -0,0 +1 @@
export const BILLING_FEATURE_USED = 'BILLING_FEATURE_USED';

View File

@ -1,5 +1,3 @@
export enum BillingMeterEventName { export enum BillingMeterEventName {
WORKFLOW_NODE_RUN = 'creditexecutiontest1', WORKFLOW_NODE_RUN = 'WORKFLOW_NODE_RUN',
} }
//this is a test event name (no conventions) would you want camel case?, snake case, or all caps?
//Something like workflowNodeRunBillingMeterEvent ?

View File

@ -1,33 +1,32 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator'; import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
import { BILLING_EXECUTE_BILLED_FUNCTION } from 'src/engine/core-modules/billing/constants/billing-execute-billed-function.constant'; import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type'; import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
@Injectable() @Injectable()
export class BillingExecuteBilledFunctionListener { export class BillingFeatureUsedListener {
constructor( constructor(
private readonly billingUsageService: BillingUsageService, private readonly billingUsageService: BillingUsageService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
) {} ) {}
@OnCustomBatchEvent(BILLING_EXECUTE_BILLED_FUNCTION) @OnCustomBatchEvent(BILLING_FEATURE_USED)
async handleExecuteBilledFunctionEvent( async handleBillingFeatureUsedEvent(
payload: WorkspaceEventBatch<BillingUsageEvent>, payload: WorkspaceEventBatch<BillingUsageEvent>,
) { ) {
if (!this.environmentService.get('IS_BILLING_ENABLED')) { if (!this.environmentService.get('IS_BILLING_ENABLED')) {
return; return;
} }
const canExecuteBilledFunction = const canFeatureBeUsed = await this.billingUsageService.canFeatureBeUsed(
await this.billingUsageService.canExecuteBilledFunction( payload.workspaceId,
payload.workspaceId, );
);
if (!canExecuteBilledFunction) { if (!canFeatureBeUsed) {
return; return;
} }

View File

@ -27,7 +27,7 @@ export class BillingUsageService {
private readonly stripeBillingMeterEventService: StripeBillingMeterEventService, private readonly stripeBillingMeterEventService: StripeBillingMeterEventService,
) {} ) {}
async canExecuteBilledFunction(workspaceId: string): Promise<boolean> { async canFeatureBeUsed(workspaceId: string): Promise<boolean> {
const isBillingEnabled = this.environmentService.get('IS_BILLING_ENABLED'); const isBillingEnabled = this.environmentService.get('IS_BILLING_ENABLED');
const isBillingPlansEnabled = const isBillingPlansEnabled =
await this.featureFlagService.isFeatureEnabled( await this.featureFlagService.isFeatureEnabled(
@ -82,7 +82,7 @@ export class BillingUsageService {
}); });
} catch (error) { } catch (error) {
throw new BillingException( throw new BillingException(
'Failed to send billing meter events to Cache Service', `Failed to send billing meter events to Stripe: ${error}`,
BillingExceptionCode.BILLING_METER_EVENT_FAILED, BillingExceptionCode.BILLING_METER_EVENT_FAILED,
); );
} }

View File

@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { BILLING_EXECUTE_BILLED_FUNCTION } from 'src/engine/core-modules/billing/constants/billing-execute-billed-function.constant'; import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names'; import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type'; import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
@ -74,7 +74,7 @@ export class WorkflowExecutorWorkspaceService {
(result.result ? undefined : 'Execution result error, no data or error'); (result.result ? undefined : 'Execution result error, no data or error');
if (!error) { if (!error) {
this.sendUsageEvent(); this.sendWorkflowNodeRunEvent();
} }
const updatedStepOutput = { const updatedStepOutput = {
@ -136,12 +136,12 @@ export class WorkflowExecutorWorkspaceService {
return { ...updatedOutput, status: WorkflowRunStatus.FAILED }; return { ...updatedOutput, status: WorkflowRunStatus.FAILED };
} }
async sendUsageEvent() { private sendWorkflowNodeRunEvent() {
const workspaceId = const workspaceId =
this.scopedWorkspaceContextFactory.create().workspaceId ?? ''; this.scopedWorkspaceContextFactory.create().workspaceId ?? '';
this.workspaceEventEmitter.emitCustomBatchEvent<BillingUsageEvent>( this.workspaceEventEmitter.emitCustomBatchEvent<BillingUsageEvent>(
BILLING_EXECUTE_BILLED_FUNCTION, BILLING_FEATURE_USED,
[ [
{ {
eventName: BillingMeterEventName.WORKFLOW_NODE_RUN, eventName: BillingMeterEventName.WORKFLOW_NODE_RUN,

View File

@ -27,10 +27,10 @@ export class WorkflowRunnerWorkspaceService {
payload: object, payload: object,
source: ActorMetadata, source: ActorMetadata,
) { ) {
const canExecuteBilledFunction = const canFeatureBeUsed =
await this.billingUsageService.canExecuteBilledFunction(workspaceId); await this.billingUsageService.canFeatureBeUsed(workspaceId);
if (!canExecuteBilledFunction) { if (!canFeatureBeUsed) {
this.logger.log( this.logger.log(
'Cannot execute billed function, there is no subscription for this workspace', 'Cannot execute billed function, there is no subscription for this workspace',
); );