feat: enforce credit limits via Stripe alerts for all billing scenarios (#16801)
## Summary This PR ensures usage alerts are created for all billing scenarios. ## Background Per [Stripe documentation](https://docs.stripe.com/billing/subscriptions/usage-based/alerts), usage alerts are **one-time per customer** - they trigger once and only consider usage reported after the alert is created. This means we need to create a new alert whenever: 1. ✅ Subscription is created (trial) - Already implemented 2. ✅ Trial ends (user becomes Active subscriber) - **Added in this PR** 3. ✅ Credit tier changes (upgrade) - **Added in this PR** 4. ✅ **New billing cycle starts** - **Critical fix in this PR!** ## The Issue Previously, the invoice webhook reset `hasReachedCurrentPeriodCap = false` at cycle end, but didn't create a new alert. This meant after the first billing period, there was no alert to trigger and users could exceed their limit without being blocked. ## Changes ### 1. Invoice Webhook (`billing-webhook-invoice.service.ts`) When invoice is finalized for `subscription_cycle`: - Reset `hasReachedCurrentPeriodCap = false` ✅ (already done) - **NEW**: Create alert at the current tier cap ### 2. End Trial Period (`billing-subscription.service.ts`) In `endTrialPeriod()`: - **NEW**: Create alert at the paid tier cap (not trial cap) ### 3. Credit Tier Upgrades (`billing-subscription.service.ts`) In `changeMeteredPrice()`, when upgrading immediately (not scheduled for period end): - **NEW**: Reset `hasReachedCurrentPeriodCap = false` - **NEW**: Create alert at new tier cap ### 4. Alert Title Update (`stripe-billing-alert.service.ts`) Changed from "Trial usage cap" to "Usage cap" since alerts are now used for all scenarios. ## Stripe Alert Limits - Max 25 alerts per meter+customer combination - Alerts only evaluate usage reported after creation - One-time alerts trigger once per customer With monthly billing cycles + occasional tier changes, we should stay well under the 25 alert limit. ## Testing - TypeScript typechecking passes - Backend services properly inject new dependencies
This commit is contained in:
+15
-2
@@ -2,11 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { BillingSubscriptionItemEntity } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
|
||||
const SUBSCRIPTION_CYCLE_BILLING_REASON = 'subscription_cycle';
|
||||
|
||||
@@ -16,12 +17,18 @@ export class BillingWebhookInvoiceService {
|
||||
constructor(
|
||||
@InjectRepository(BillingSubscriptionItemEntity)
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItemEntity>,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(data: Stripe.InvoiceFinalizedEvent.Data) {
|
||||
const { billing_reason: billingReason, subscription } = data.object;
|
||||
const {
|
||||
billing_reason: billingReason,
|
||||
subscription,
|
||||
customer,
|
||||
} = data.object;
|
||||
|
||||
const stripeSubscriptionId = subscription as string | undefined;
|
||||
const stripeCustomerId = customer as string | undefined;
|
||||
|
||||
if (
|
||||
isDefined(stripeSubscriptionId) &&
|
||||
@@ -31,6 +38,12 @@ export class BillingWebhookInvoiceService {
|
||||
{ stripeSubscriptionId },
|
||||
{ hasReachedCurrentPeriodCap: false },
|
||||
);
|
||||
|
||||
if (isDefined(stripeCustomerId)) {
|
||||
await this.billingSubscriptionService.createBillingAlertForCustomer(
|
||||
stripeCustomerId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user