add stripe alert listening and cap on subscriptionItems (#11330)
in this PR : - reverting https://github.com/twentyhq/twenty/pull/11319 > at trial period end, subscriptions switch to 'past_due' status if payment method not set up - adding cap on subscriptionItems and updating them when receiving alert event + refreshing them when beginning a new subscription cycle closes https://github.com/twentyhq/core-team-issues/issues/606
This commit is contained in:
@ -6,9 +6,9 @@ import { InformationBannerReconnectAccountInsufficientPermissions } from '@/info
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||
import styled from '@emotion/styled';
|
||||
import { SubscriptionStatus } from '~/generated-metadata/graphql';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { SubscriptionStatus } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledInformationBannerWrapper = styled.div`
|
||||
height: 40px;
|
||||
@ -40,7 +40,7 @@ export const InformationBannerWrapper = () => {
|
||||
<InformationBannerReconnectAccountInsufficientPermissions />
|
||||
<InformationBannerReconnectAccountEmailAliases />
|
||||
{displayBillingSubscriptionPausedBanner && (
|
||||
<InformationBannerBillingSubscriptionPaused />
|
||||
<InformationBannerBillingSubscriptionPaused /> // TODO: remove this once paused subscriptions are deprecated
|
||||
)}
|
||||
{displayBillingSubscriptionCanceledBanner && (
|
||||
<InformationBannerNoBillingSubscription />
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddHasReachedCurrentPeriodCapColumnInBillingSubscriptionItemTable1743577268972
|
||||
implements MigrationInterface
|
||||
{
|
||||
name =
|
||||
'AddHasReachedCurrentPeriodCapColumnInBillingSubscriptionItemTable1743577268972';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "hasReachedCurrentPeriodCap" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "hasReachedCurrentPeriodCap"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,9 @@ import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billi
|
||||
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
|
||||
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-alert.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-invoice.service';
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
|
||||
@ -38,6 +40,8 @@ export class BillingController {
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly billingWebhookProductService: BillingWebhookProductService,
|
||||
private readonly billingWebhookPriceService: BillingWebhookPriceService,
|
||||
private readonly billingWebhookAlertService: BillingWebhookAlertService,
|
||||
private readonly billingWebhookInvoiceService: BillingWebhookInvoiceService,
|
||||
) {}
|
||||
|
||||
@Post('/webhooks')
|
||||
@ -100,6 +104,16 @@ export class BillingController {
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.ALERT_TRIGGERED:
|
||||
return await this.billingWebhookAlertService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.INVOICE_FINALIZED:
|
||||
return await this.billingWebhookInvoiceService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED:
|
||||
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED:
|
||||
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED: {
|
||||
|
||||
@ -24,7 +24,9 @@ import { BillingSubscriptionService } from 'src/engine/core-modules/billing/serv
|
||||
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
||||
import { BillingWebhookAlertService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-alert.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-invoice.service';
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
|
||||
@ -73,6 +75,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
BillingService,
|
||||
BillingWebhookProductService,
|
||||
BillingWebhookPriceService,
|
||||
BillingWebhookAlertService,
|
||||
BillingWebhookInvoiceService,
|
||||
BillingRestApiExceptionFilter,
|
||||
BillingSyncCustomerDataCommand,
|
||||
BillingSyncPlansDataCommand,
|
||||
|
||||
@ -63,4 +63,7 @@ export class BillingSubscriptionItem {
|
||||
|
||||
@Column({ nullable: true, type: 'numeric' })
|
||||
quantity: number | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
hasReachedCurrentPeriodCap: boolean;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export enum SubscriptionStatus {
|
||||
Incomplete = 'incomplete',
|
||||
IncompleteExpired = 'incomplete_expired',
|
||||
PastDue = 'past_due',
|
||||
Paused = 'paused',
|
||||
Paused = 'paused', // TODO: remove this once paused subscriptions are deprecated
|
||||
Trialing = 'trialing',
|
||||
Unpaid = 'unpaid',
|
||||
}
|
||||
|
||||
@ -10,4 +10,6 @@ export enum BillingWebhookEvent {
|
||||
PRODUCT_UPDATED = 'product.updated',
|
||||
PRICE_CREATED = 'price.created',
|
||||
PRICE_UPDATED = 'price.updated',
|
||||
ALERT_TRIGGERED = 'billing.alert.triggered',
|
||||
INVOICE_FINALIZED = 'invoice.finalized',
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { JsonContains, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
BillingException,
|
||||
@ -35,11 +35,11 @@ export class BillingPlanService {
|
||||
}): Promise<BillingProduct[]> {
|
||||
const products = await this.billingProductRepository.find({
|
||||
where: {
|
||||
metadata: {
|
||||
planKey,
|
||||
metadata: JsonContains({
|
||||
priceUsageBased,
|
||||
planKey,
|
||||
isBaseProduct,
|
||||
},
|
||||
}),
|
||||
active: true,
|
||||
},
|
||||
relations: ['billingPrices'],
|
||||
|
||||
@ -65,7 +65,6 @@ export class BillingPortalWorkspaceService {
|
||||
const stripeSubscriptionLineItems = this.getStripeSubscriptionLineItems({
|
||||
quantity,
|
||||
billingPricesPerPlan,
|
||||
forTrialSubscription: !isDefined(subscription),
|
||||
});
|
||||
|
||||
const checkoutSession =
|
||||
@ -128,11 +127,9 @@ export class BillingPortalWorkspaceService {
|
||||
private getStripeSubscriptionLineItems({
|
||||
quantity,
|
||||
billingPricesPerPlan,
|
||||
forTrialSubscription,
|
||||
}: {
|
||||
quantity: number;
|
||||
billingPricesPerPlan?: BillingGetPricesPerPlanResult;
|
||||
forTrialSubscription: boolean;
|
||||
}): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
||||
if (billingPricesPerPlan) {
|
||||
return [
|
||||
@ -140,11 +137,9 @@ export class BillingPortalWorkspaceService {
|
||||
price: billingPricesPerPlan.baseProductPrice.stripePriceId,
|
||||
quantity,
|
||||
},
|
||||
...(forTrialSubscription
|
||||
? []
|
||||
: billingPricesPerPlan.meteredProductsPrices.map((price) => ({
|
||||
price: price.stripePriceId,
|
||||
}))),
|
||||
...billingPricesPerPlan.meteredProductsPrices.map((price) => ({
|
||||
price: price.stripePriceId,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.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 { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingProductService } from 'src/engine/core-modules/billing/services/billing-product.service';
|
||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
||||
@ -199,50 +198,4 @@ export class BillingSubscriptionService {
|
||||
|
||||
return subscriptionItemsToUpdate;
|
||||
}
|
||||
|
||||
async convertTrialSubscriptionToSubscriptionWithMeteredProducts(
|
||||
billingSubscription: BillingSubscription,
|
||||
) {
|
||||
const meteredProducts = (
|
||||
await this.billingProductRepository.find({
|
||||
where: {
|
||||
active: true,
|
||||
},
|
||||
relations: ['billingPrices'],
|
||||
})
|
||||
).filter(
|
||||
(product) =>
|
||||
product.metadata.priceUsageBased === BillingUsageType.METERED,
|
||||
);
|
||||
|
||||
// subscription update to enable metered product billing
|
||||
await this.stripeSubscriptionService.updateSubscription(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
{
|
||||
trial_settings: {
|
||||
end_behavior: {
|
||||
missing_payment_method: 'cancel',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
for (const meteredProduct of meteredProducts) {
|
||||
const meteredProductPrice = meteredProduct.billingPrices.find(
|
||||
(price) => price.active,
|
||||
);
|
||||
|
||||
if (!meteredProductPrice) {
|
||||
throw new BillingException(
|
||||
`Cannot find active price for product ${meteredProduct.id}`,
|
||||
BillingExceptionCode.BILLING_PRICE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.stripeSubscriptionItemService.createSubscriptionItem(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
meteredProductPrice.stripePriceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ export class StripeCheckoutService {
|
||||
),
|
||||
trial_settings: {
|
||||
end_behavior: {
|
||||
missing_payment_method: 'pause',
|
||||
missing_payment_method: 'create_invoice',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -27,16 +27,4 @@ export class StripeSubscriptionItemService {
|
||||
async updateSubscriptionItem(stripeItemId: string, quantity: number) {
|
||||
await this.stripe.subscriptionItems.update(stripeItemId, { quantity });
|
||||
}
|
||||
|
||||
async createSubscriptionItem(
|
||||
stripeSubscriptionId: string,
|
||||
stripePriceId: string,
|
||||
quantity?: number | undefined,
|
||||
) {
|
||||
await this.stripe.subscriptionItems.create({
|
||||
subscription: stripeSubscriptionId,
|
||||
price: stripePriceId,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,11 +76,4 @@ export class StripeSubscriptionService {
|
||||
items: stripeSubscriptionItemsToUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
async updateSubscription(
|
||||
stripeSubscriptionId: string,
|
||||
data: Stripe.SubscriptionUpdateParams,
|
||||
) {
|
||||
await this.stripe.subscriptions.update(stripeSubscriptionId, data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
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 { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
|
||||
const TRIAL_PERIOD_ALERT_TITLE = 'TRIAL_PERIOD_ALERT'; // to set in Stripe config
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookAlertService {
|
||||
protected readonly logger = new Logger(BillingWebhookAlertService.name);
|
||||
constructor(
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
@InjectRepository(BillingProduct, 'core')
|
||||
private readonly billingProductRepository: Repository<BillingProduct>,
|
||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(data: Stripe.BillingAlertTriggeredEvent.Data) {
|
||||
const { customer: stripeCustomerId, alert } = data.object;
|
||||
|
||||
const stripeMeterId = alert.usage_threshold?.meter as string | undefined;
|
||||
|
||||
if (alert.title === TRIAL_PERIOD_ALERT_TITLE && isDefined(stripeMeterId)) {
|
||||
const subscription = await this.billingSubscriptionRepository.findOne({
|
||||
where: { stripeCustomerId, status: SubscriptionStatus.Trialing },
|
||||
relations: ['billingSubscriptionItems'],
|
||||
});
|
||||
|
||||
if (!subscription) return;
|
||||
|
||||
const product = await this.billingProductRepository.findOne({
|
||||
where: {
|
||||
billingPrices: { stripeMeterId },
|
||||
},
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new BillingException(
|
||||
`Product associated to meter ${stripeMeterId} not found`,
|
||||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.billingSubscriptionItemRepository.update(
|
||||
{
|
||||
billingSubscriptionId: subscription.id,
|
||||
stripeProductId: product.stripeProductId,
|
||||
},
|
||||
{ hasReachedCurrentPeriodCap: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
|
||||
const SUBSCRIPTION_CYCLE_BILLING_REASON = 'subscription_cycle';
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookInvoiceService {
|
||||
protected readonly logger = new Logger(BillingWebhookInvoiceService.name);
|
||||
constructor(
|
||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(data: Stripe.InvoiceFinalizedEvent.Data) {
|
||||
const { billing_reason: billingReason, subscription } = data.object;
|
||||
|
||||
const stripeSubscriptionId = subscription as string | undefined;
|
||||
|
||||
if (
|
||||
isDefined(stripeSubscriptionId) &&
|
||||
billingReason === SUBSCRIPTION_CYCLE_BILLING_REASON
|
||||
) {
|
||||
await this.billingSubscriptionItemRepository.update(
|
||||
{ stripeSubscriptionId },
|
||||
{ hasReachedCurrentPeriodCap: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,10 @@ import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entitie
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
|
||||
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@ -29,19 +27,6 @@ import {
|
||||
CleanWorkspaceDeletionWarningUserVarsJobData,
|
||||
} from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
|
||||
|
||||
const BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS = {
|
||||
[WorkspaceActivationStatus.ACTIVE]: [
|
||||
SubscriptionStatus.Active,
|
||||
SubscriptionStatus.Trialing,
|
||||
SubscriptionStatus.PastDue,
|
||||
],
|
||||
[WorkspaceActivationStatus.SUSPENDED]: [
|
||||
SubscriptionStatus.Canceled,
|
||||
SubscriptionStatus.Unpaid,
|
||||
SubscriptionStatus.Paused,
|
||||
],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookSubscriptionService {
|
||||
protected readonly logger = new Logger(
|
||||
@ -61,7 +46,6 @@ export class BillingWebhookSubscriptionService {
|
||||
private readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||
@InjectRepository(FeatureFlag, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlag>,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(
|
||||
@ -114,13 +98,6 @@ export class BillingWebhookSubscriptionService {
|
||||
throw new Error('Billing subscription not found');
|
||||
}
|
||||
|
||||
const hasActiveWorkspaceCompatibleSubscription = billingSubscriptions.some(
|
||||
(subscription) =>
|
||||
BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[
|
||||
WorkspaceActivationStatus.ACTIVE
|
||||
].includes(subscription.status),
|
||||
);
|
||||
|
||||
await this.billingSubscriptionItemRepository.upsert(
|
||||
transformStripeSubscriptionEventToDatabaseSubscriptionItem(
|
||||
updatedBillingSubscription.id,
|
||||
@ -132,30 +109,9 @@ export class BillingWebhookSubscriptionService {
|
||||
},
|
||||
);
|
||||
|
||||
const wasTrialOrPausedSubscription = [
|
||||
SubscriptionStatus.Trialing,
|
||||
SubscriptionStatus.Paused,
|
||||
].includes(data.previous_attributes?.status as SubscriptionStatus);
|
||||
|
||||
const isMeteredProductBillingEnabled =
|
||||
await this.featureFlagRepository.findOneBy({
|
||||
key: FeatureFlagKey.IsMeteredProductBillingEnabled,
|
||||
workspaceId,
|
||||
value: true,
|
||||
});
|
||||
|
||||
if (wasTrialOrPausedSubscription && isMeteredProductBillingEnabled) {
|
||||
await this.billingSubscriptionService.convertTrialSubscriptionToSubscriptionWithMeteredProducts(
|
||||
updatedBillingSubscription,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[
|
||||
WorkspaceActivationStatus.SUSPENDED
|
||||
].includes(data.object.status as SubscriptionStatus) &&
|
||||
workspace.activationStatus == WorkspaceActivationStatus.ACTIVE &&
|
||||
!hasActiveWorkspaceCompatibleSubscription
|
||||
this.shouldSuspendWorkspace(data) &&
|
||||
workspace.activationStatus == WorkspaceActivationStatus.ACTIVE
|
||||
) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
activationStatus: WorkspaceActivationStatus.SUSPENDED,
|
||||
@ -163,9 +119,7 @@ export class BillingWebhookSubscriptionService {
|
||||
}
|
||||
|
||||
if (
|
||||
BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[
|
||||
WorkspaceActivationStatus.ACTIVE
|
||||
].includes(data.object.status as SubscriptionStatus) &&
|
||||
!this.shouldSuspendWorkspace(data) &&
|
||||
workspace.activationStatus == WorkspaceActivationStatus.SUSPENDED
|
||||
) {
|
||||
await this.workspaceRepository.update(workspaceId, {
|
||||
@ -188,4 +142,28 @@ export class BillingWebhookSubscriptionService {
|
||||
stripeCustomerId: data.object.customer,
|
||||
};
|
||||
}
|
||||
|
||||
shouldSuspendWorkspace(
|
||||
data:
|
||||
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
||||
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
||||
) {
|
||||
const timeSinceTrialEnd = Date.now() / 1000 - (data.object.trial_end || 0);
|
||||
const hasTrialJustEnded =
|
||||
timeSinceTrialEnd < 60 * 60 * 24 && timeSinceTrialEnd > 0;
|
||||
|
||||
if (
|
||||
[
|
||||
SubscriptionStatus.Canceled,
|
||||
SubscriptionStatus.Unpaid,
|
||||
SubscriptionStatus.Paused, // TODO: remove this once paused subscriptions are deprecated
|
||||
].includes(data.object.status as SubscriptionStatus) ||
|
||||
(hasTrialJustEnded && data.object.status === SubscriptionStatus.PastDue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user