chore(billing) - remove feature flag (#20531)

- remove feature flag
- remove old enforce cap usage logic
This commit is contained in:
Etienne
2026-05-13 18:52:29 +02:00
committed by GitHub
parent 695a374efd
commit fcd2d586ee
69 changed files with 729 additions and 4371 deletions
@@ -16,7 +16,6 @@ import { type Response } from 'express';
import Stripe from 'stripe';
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-alert.service';
import { BillingWebhookCreditGrantService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-credit-grant.service';
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service';
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
@@ -51,7 +50,6 @@ export class BillingWebhookController {
private readonly billingWebhookInvoiceService: BillingWebhookInvoiceService,
private readonly billingWebhookCustomerService: BillingWebhookCustomerService,
private readonly billingWebhookSubscriptionScheduleService: BillingWebhookSubscriptionScheduleService,
private readonly billingWebhookCreditGrantService: BillingWebhookCreditGrantService,
) {}
@Post(['webhooks/stripe'])
@@ -154,25 +152,6 @@ export class BillingWebhookController {
);
}
case BillingWebhookEvent.CREDIT_GRANT_CREATED:
case BillingWebhookEvent.CREDIT_GRANT_UPDATED: {
const customer = event.data.object.customer;
// customer can be string ID, Customer object, or DeletedCustomer object
const stripeCustomerId =
typeof customer === 'string' ? customer : customer?.id;
if (!stripeCustomerId) {
throw new BillingException(
'Customer ID is required for credit grant events',
BillingExceptionCode.BILLING_CUSTOMER_EVENT_WORKSPACE_NOT_FOUND,
);
}
return await this.billingWebhookCreditGrantService.processStripeEvent(
stripeCustomerId,
);
}
default:
return {};
}
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { BillingWebhookController } from 'src/engine/core-modules/billing-webhook/billing-webhook.controller';
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-alert.service';
import { BillingWebhookCreditGrantService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-credit-grant.service';
import { BillingWebhookCustomerService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-customer.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-entitlement.service';
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
@@ -65,7 +64,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
BillingWebhookSubscriptionService,
BillingWebhookSubscriptionScheduleService,
BillingWebhookEntitlementService,
BillingWebhookCreditGrantService,
],
})
export class BillingWebhookModule {}
@@ -1,52 +0,0 @@
/* @license Enterprise */
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { MeteredCreditService } from 'src/engine/core-modules/billing/services/metered-credit.service';
@Injectable()
export class BillingWebhookCreditGrantService {
constructor(
private readonly meteredCreditService: MeteredCreditService,
private readonly billingSubscriptionService: BillingSubscriptionService,
@InjectRepository(BillingCustomerEntity)
private readonly billingCustomerRepository: Repository<BillingCustomerEntity>,
) {}
async processStripeEvent(stripeCustomerId: string): Promise<void> {
const subscription =
await this.billingSubscriptionService.getCurrentBillingSubscription({
stripeCustomerId,
});
if (!isDefined(subscription)) {
return;
}
const meteredPricingInfo =
await this.meteredCreditService.getMeteredPricingInfo(subscription.id);
if (isDefined(meteredPricingInfo)) {
const creditBalanceMicro =
await this.meteredCreditService.getCreditBalance(
stripeCustomerId,
meteredPricingInfo.unitPriceCents,
);
await this.billingCustomerRepository.update(
{ stripeCustomerId },
{ creditBalanceMicro },
);
}
await this.meteredCreditService.recreateBillingAlertForSubscription(
subscription,
);
}
}
@@ -1,8 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { addMonths, addYears } from 'date-fns';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { type Repository } from 'typeorm';
@@ -19,13 +17,11 @@ import {
import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingSubscriptionItemEntity } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
import { BillingCreditRolloverService } from 'src/engine/core-modules/billing/services/billing-credit-rollover.service';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { MeteredCreditService } from 'src/engine/core-modules/billing/services/metered-credit.service';
import { ResourceCreditService } from 'src/engine/core-modules/billing/services/resource-credit.service';
import { StripeInvoiceService } from 'src/engine/core-modules/billing/stripe/services/stripe-invoice.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
const SUBSCRIPTION_CYCLE_BILLING_REASON = 'subscription_cycle';
@@ -43,9 +39,8 @@ export class BillingWebhookInvoiceService {
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly billingCreditRolloverService: BillingCreditRolloverService,
private readonly meteredCreditService: MeteredCreditService,
private readonly resourceCreditService: ResourceCreditService,
private readonly stripeInvoiceService: StripeInvoiceService,
private readonly featureFlagService: FeatureFlagService,
private readonly auditService: AuditService,
) {}
@@ -115,85 +110,28 @@ export class BillingWebhookInvoiceService {
Math.abs(periodStart - trialEnd) <= TRIAL_END_TOLERANCE_SECONDS;
if (periodStart && !isFirstPeriodAfterTrial) {
await this.processRollover(
subscription,
new Date(periodStart * 1000),
new Date(periodEnd * 1000),
);
}
const isV2 = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_BILLING_V2_ENABLED,
subscription.workspaceId,
);
if (!isV2) {
// Pass the new period start (which is the invoiced period's end) for alert threshold calculation
await this.meteredCreditService.recreateBillingAlertForSubscription(
subscription,
new Date(periodEnd * 1000),
);
await this.processRollover(subscription, new Date(periodStart * 1000));
}
}
private async processRollover(
subscription: BillingSubscriptionEntity,
invoicedPeriodStart: Date,
invoicedPeriodEnd: Date,
): Promise<void> {
const isV2 = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_BILLING_V2_ENABLED,
subscription.workspaceId,
);
if (isV2) {
const v2Params =
await this.meteredCreditService.getResourceCreditRolloverParameters(
subscription.id,
);
if (!isDefined(v2Params)) {
return;
}
await this.billingCreditRolloverService.processRolloverOnPeriodTransitionV2(
{
workspaceId: subscription.workspaceId,
stripeCustomerId: subscription.stripeCustomerId,
tierQuantity: v2Params.tierQuantity,
previousPeriodStart: invoicedPeriodStart,
},
);
return;
}
const rolloverParams =
await this.meteredCreditService.getMeteredRolloverParameters(
const params =
await this.resourceCreditService.getResourceCreditRolloverParameters(
subscription.id,
);
if (!isDefined(rolloverParams)) {
if (!isDefined(params)) {
return;
}
// The invoice covers the period that just ended (invoicedPeriodStart to invoicedPeriodEnd)
// We need to calculate unused credits from this period and roll them over
// Credits should expire at the end of the NEXT period
const nextPeriodEnd = this.calculateNextPeriodEnd(
invoicedPeriodEnd,
subscription.interval,
);
await this.billingCreditRolloverService.processRolloverOnPeriodTransition({
workspaceId: subscription.workspaceId,
stripeCustomerId: subscription.stripeCustomerId,
subscriptionId: subscription.id,
stripeMeterId: rolloverParams.stripeMeterId,
tierQuantity: params.tierQuantity,
previousPeriodStart: invoicedPeriodStart,
previousPeriodEnd: invoicedPeriodEnd,
newPeriodEnd: nextPeriodEnd,
tierQuantity: rolloverParams.tierQuantity,
unitPriceCents: rolloverParams.unitPriceCents,
});
}
@@ -284,15 +222,4 @@ export class BillingWebhookInvoiceService {
suspendedAt: new Date(),
});
}
private calculateNextPeriodEnd(
periodEnd: Date,
interval: SubscriptionInterval,
): Date {
if (interval === SubscriptionInterval.Year) {
return addYears(periodEnd, 1);
}
return addMonths(periodEnd, 1);
}
}