Onboarding - delete PENDING_CREATION workspace if billing is deactivated (#12704)

[More context
details](https://discord.com/channels/1130383047699738754/1384834012882927637/1384834143673778226)
This commit is contained in:
Etienne
2025-06-20 18:18:50 +02:00
committed by GitHub
parent e11bd7aa0f
commit 830e49c5b1
26 changed files with 105 additions and 44 deletions

View File

@ -1,149 +0,0 @@
/* @license Enterprise */
import {
Controller,
Headers,
Logger,
Post,
RawBodyRequest,
Req,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';
import { Response } from 'express';
import Stripe from 'stripe';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
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 { BillingWebhookCustomerService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-customer.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';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller()
@UseFilters(BillingRestApiExceptionFilter)
export class BillingController {
protected readonly logger = new Logger(BillingController.name);
constructor(
private readonly stripeWebhookService: StripeWebhookService,
private readonly billingWebhookSubscriptionService: BillingWebhookSubscriptionService,
private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService,
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly billingWebhookProductService: BillingWebhookProductService,
private readonly billingWebhookPriceService: BillingWebhookPriceService,
private readonly billingWebhookAlertService: BillingWebhookAlertService,
private readonly billingWebhookInvoiceService: BillingWebhookInvoiceService,
private readonly billingWebhookCustomerService: BillingWebhookCustomerService,
) {}
@Post(['webhooks/stripe'])
@UseGuards(PublicEndpointGuard)
async handleWebhooks(
@Headers('stripe-signature') signature: string,
@Req() req: RawBodyRequest<Request>,
@Res() res: Response,
) {
if (!req.rawBody) {
throw new BillingException(
'Missing request body',
BillingExceptionCode.BILLING_MISSING_REQUEST_BODY,
);
}
try {
const event = this.stripeWebhookService.constructEventFromPayload(
signature,
req.rawBody,
);
const result = await this.handleStripeEvent(event);
res.status(200).send(result).end();
} catch (error) {
if (
error instanceof BillingException ||
error instanceof Stripe.errors.StripeError
) {
throw error;
}
const errorMessage =
error instanceof Error ? error.message : JSON.stringify(error);
throw new BillingException(
errorMessage,
BillingExceptionCode.BILLING_UNHANDLED_ERROR,
);
}
}
private async handleStripeEvent(event: Stripe.Event) {
switch (event.type) {
case BillingWebhookEvent.SETUP_INTENT_SUCCEEDED:
return await this.billingSubscriptionService.handleUnpaidInvoices(
event.data,
);
case BillingWebhookEvent.PRICE_UPDATED:
case BillingWebhookEvent.PRICE_CREATED:
return await this.billingWebhookPriceService.processStripeEvent(
event.data,
);
case BillingWebhookEvent.PRODUCT_UPDATED:
case BillingWebhookEvent.PRODUCT_CREATED:
return await this.billingWebhookProductService.processStripeEvent(
event.data,
);
case BillingWebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED:
return await this.billingWebhookEntitlementService.processStripeEvent(
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_CREATED:
return await this.billingWebhookCustomerService.processStripeEvent(
event.data,
);
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED:
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED:
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED: {
const workspaceId = event.data.object.metadata?.workspaceId;
if (!workspaceId) {
throw new BillingException(
'Workspace ID is required for subscription events',
BillingExceptionCode.BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND,
);
}
return await this.billingWebhookSubscriptionService.processStripeEvent(
workspaceId,
event,
);
}
default:
return {};
}
}
}

View File

@ -3,7 +3,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BillingController } from 'src/engine/core-modules/billing/billing.controller';
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
import { BillingAddWorkflowSubscriptionItemCommand } from 'src/engine/core-modules/billing/commands/billing-add-workflow-subscription-item.command';
import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.command';
@ -27,13 +26,6 @@ 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 { BillingWebhookCustomerService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-customer.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';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@ -65,12 +57,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
'core',
),
],
controllers: [BillingController],
providers: [
BillingSubscriptionService,
BillingSubscriptionItemService,
BillingWebhookSubscriptionService,
BillingWebhookEntitlementService,
BillingPortalWorkspaceService,
BillingProductService,
BillingResolver,
@ -78,11 +67,6 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
BillingWorkspaceMemberListener,
BillingFeatureUsedListener,
BillingService,
BillingWebhookProductService,
BillingWebhookPriceService,
BillingWebhookAlertService,
BillingWebhookInvoiceService,
BillingWebhookCustomerService,
BillingRestApiExceptionFilter,
BillingSyncCustomerDataCommand,
BillingUpdateSubscriptionPriceCommand,

View File

@ -10,6 +10,7 @@ import Stripe from 'stripe';
import { isDefined } from 'twenty-shared/utils';
import { Not, Repository } from 'typeorm';
import { getSubscriptionStatus } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-event-to-database-subscription.util';
import {
BillingException,
BillingExceptionCode,
@ -19,6 +20,7 @@ import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-p
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 { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-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';
@ -28,10 +30,8 @@ import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/se
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { getPlanKeyFromSubscription } from 'src/engine/core-modules/billing/utils/get-plan-key-from-subscription.util';
import { getSubscriptionStatus } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
@Injectable()
export class BillingSubscriptionService {

View File

@ -1,658 +0,0 @@
import Stripe from 'stripe';
export const mockStripeSubscriptionUpdatedEventWithoutUpdatedItem: Stripe.CustomerSubscriptionUpdatedEvent =
{
type: 'customer.subscription.updated',
data: {
object: {
id: 'sub_1RFLOkQjnlxerLN36QL6lXER',
object: 'subscription',
application: null,
application_fee_percent: null,
billing_cycle_anchor: 1745007630,
billing_cycle_anchor_config: null,
automatic_tax: {
enabled: false,
liability: null,
},
billing_thresholds: null,
cancel_at: null,
cancel_at_period_end: false,
canceled_at: null,
cancellation_details: {
comment: null,
feedback: null,
reason: null,
},
collection_method: 'charge_automatically',
created: 1745007630,
currency: 'usd',
current_period_end: 1747599630,
current_period_start: 1745007630,
customer: 'cus_S9eiHiQ8lNbIkL',
days_until_due: null,
default_payment_method: null,
default_source: null,
default_tax_rates: [],
description: null,
discount: null,
discounts: [],
ended_at: null,
invoice_settings: {
account_tax_ids: null,
issuer: {
type: 'self',
},
},
items: {
object: 'list',
data: [
{
id: 'si_S9einNoUMK9WAL',
object: 'subscription_item',
billing_thresholds: null,
created: 1745007631,
discounts: [],
metadata: {},
plan: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'plan',
active: true,
aggregate_usage: null,
amount: 1500,
amount_decimal: '1500',
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
interval: 'month',
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: 'licensed',
},
price: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
recurring: {
aggregate_usage: null,
interval: 'month',
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: 'licensed',
},
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'recurring',
unit_amount: 1500,
unit_amount_decimal: '1500',
},
quantity: 1,
subscription: 'sub_1RFLOkQjnlxerLN36QL6lXER',
tax_rates: [],
},
],
has_more: false,
url: '/v1/subscription_items?subscription=sub_1RFLOkQjnlxerLN36QL6lXER',
},
latest_invoice: 'in_1RFLOkQjnlxerLN3aexylENH',
livemode: false,
metadata: {
foo: 'bar',
},
next_pending_invoice_item_invoice: null,
on_behalf_of: null,
pause_collection: null,
payment_settings: {
payment_method_options: null,
payment_method_types: null,
save_default_payment_method: 'off',
},
pending_invoice_item_interval: null,
pending_setup_intent: null,
pending_update: null,
schedule: null,
start_date: 1745007630,
status: 'active',
test_clock: null,
transfer_data: null,
trial_end: null,
trial_settings: {
end_behavior: {
missing_payment_method: 'create_invoice',
},
},
trial_start: null,
},
previous_attributes: {
metadata: {},
},
},
id: 'evt_1RFLOkQjnlxerLN36QL6lXER',
object: 'event',
api_version: '2024-04-10',
created: 1745007630,
livemode: false,
pending_webhooks: 0,
request: null,
};
export const mockStripeSubscriptionUpdatedEventWithUpdatedItemOnly: Stripe.CustomerSubscriptionUpdatedEvent =
{
type: 'customer.subscription.updated',
data: {
object: {
id: 'sub_1RFLOkQjnlxerLN36QL6lXER',
object: 'subscription',
application: null,
application_fee_percent: null,
billing_cycle_anchor: 1745007630,
billing_cycle_anchor_config: null,
automatic_tax: {
enabled: false,
liability: null,
},
billing_thresholds: null,
cancel_at: null,
cancel_at_period_end: false,
canceled_at: null,
cancellation_details: {
comment: null,
feedback: null,
reason: null,
},
collection_method: 'charge_automatically',
created: 1745007630,
currency: 'usd',
current_period_end: 1747599630,
current_period_start: 1745007630,
customer: 'cus_S9eiHiQ8lNbIkL',
days_until_due: null,
default_payment_method: null,
default_source: null,
default_tax_rates: [],
description: null,
discount: null,
discounts: [],
ended_at: null,
invoice_settings: {
account_tax_ids: null,
issuer: {
type: 'self',
},
},
items: {
object: 'list',
data: [
{
id: 'updated_item_id',
object: 'subscription_item',
billing_thresholds: null,
created: 1745007631,
discounts: [],
metadata: {},
plan: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'plan',
active: true,
aggregate_usage: null,
amount: 1500,
amount_decimal: '1500',
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
interval: 'month',
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: 'licensed',
},
price: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
recurring: {
aggregate_usage: null,
interval: 'month',
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: 'licensed',
},
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'recurring',
unit_amount: 1500,
unit_amount_decimal: '1500',
},
quantity: 1,
subscription: 'sub_1RFLOkQjnlxerLN36QL6lXER',
tax_rates: [],
},
],
has_more: false,
url: '/v1/subscription_items?subscription=sub_1RFLOkQjnlxerLN36QL6lXER',
},
latest_invoice: 'in_1RFLOkQjnlxerLN3aexylENH',
livemode: false,
metadata: {
foo: 'bar',
},
next_pending_invoice_item_invoice: null,
on_behalf_of: null,
pause_collection: null,
payment_settings: {
payment_method_options: null,
payment_method_types: null,
save_default_payment_method: 'off',
},
pending_invoice_item_interval: null,
pending_setup_intent: null,
pending_update: null,
schedule: null,
start_date: 1745007630,
status: 'active',
test_clock: null,
transfer_data: null,
trial_end: null,
trial_settings: {
end_behavior: {
missing_payment_method: 'create_invoice',
},
},
trial_start: null,
},
previous_attributes: {
items: {
data: [
{
id: 'updated_item_id',
object: 'subscription_item',
billing_thresholds: null,
created: 1745007631,
discounts: [],
metadata: {},
plan: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'plan',
active: true,
aggregate_usage: null,
amount: 1500,
amount_decimal: '1500',
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
interval: 'month',
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: 'licensed',
},
price: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
recurring: {
aggregate_usage: null,
interval: 'month',
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: 'licensed',
},
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'recurring',
unit_amount: 1500,
unit_amount_decimal: '1500',
},
quantity: 1,
subscription: 'sub_1RFLOkQjnlxerLN36QL6lXER',
tax_rates: [],
},
],
object: 'list',
has_more: false,
url: '/v1/subscription_items?subscription=sub_1RFLOkQjnlxerLN36QL6lXER',
},
},
},
id: 'evt_1RFLOkQjnlxerLN36QL6lXER',
object: 'event',
api_version: '2024-04-10',
created: 1745007630,
livemode: false,
pending_webhooks: 0,
request: null,
};
export const mockStripeSubscriptionUpdatedEventWithDeletedItem: Stripe.CustomerSubscriptionUpdatedEvent =
{
type: 'customer.subscription.updated',
data: {
object: {
id: 'sub_1RFLOkQjnlxerLN36QL6lXER',
object: 'subscription',
application: null,
application_fee_percent: null,
billing_cycle_anchor: 1745007630,
billing_cycle_anchor_config: null,
automatic_tax: {
enabled: false,
liability: null,
},
billing_thresholds: null,
cancel_at: null,
cancel_at_period_end: false,
canceled_at: null,
cancellation_details: {
comment: null,
feedback: null,
reason: null,
},
collection_method: 'charge_automatically',
created: 1745007630,
currency: 'usd',
current_period_end: 1747599630,
current_period_start: 1745007630,
customer: 'cus_S9eiHiQ8lNbIkL',
days_until_due: null,
default_payment_method: null,
default_source: null,
default_tax_rates: [],
description: null,
discount: null,
discounts: [],
ended_at: null,
invoice_settings: {
account_tax_ids: null,
issuer: {
type: 'self',
},
},
items: {
object: 'list',
data: [
{
id: 'updated_item_id',
object: 'subscription_item',
billing_thresholds: null,
created: 1745007631,
discounts: [],
metadata: {},
plan: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'plan',
active: true,
aggregate_usage: null,
amount: 1500,
amount_decimal: '1500',
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
interval: 'month',
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: 'licensed',
},
price: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
recurring: {
aggregate_usage: null,
interval: 'month',
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: 'licensed',
},
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'recurring',
unit_amount: 1500,
unit_amount_decimal: '1500',
},
quantity: 1,
subscription: 'sub_1RFLOkQjnlxerLN36QL6lXER',
tax_rates: [],
},
],
has_more: false,
url: '/v1/subscription_items?subscription=sub_1RFLOkQjnlxerLN36QL6lXER',
},
latest_invoice: 'in_1RFLOkQjnlxerLN3aexylENH',
livemode: false,
metadata: {
foo: 'bar',
},
next_pending_invoice_item_invoice: null,
on_behalf_of: null,
pause_collection: null,
payment_settings: {
payment_method_options: null,
payment_method_types: null,
save_default_payment_method: 'off',
},
pending_invoice_item_interval: null,
pending_setup_intent: null,
pending_update: null,
schedule: null,
start_date: 1745007630,
status: 'active',
test_clock: null,
transfer_data: null,
trial_end: null,
trial_settings: {
end_behavior: {
missing_payment_method: 'create_invoice',
},
},
trial_start: null,
},
previous_attributes: {
items: {
data: [
{
id: 'deleted_item_id',
object: 'subscription_item',
billing_thresholds: null,
created: 1745007631,
discounts: [],
metadata: {},
plan: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'plan',
active: true,
aggregate_usage: null,
amount: 1500,
amount_decimal: '1500',
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
interval: 'month',
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: 'licensed',
},
price: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
recurring: {
aggregate_usage: null,
interval: 'month',
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: 'licensed',
},
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'recurring',
unit_amount: 1500,
unit_amount_decimal: '1500',
},
quantity: 1,
subscription: 'sub_1RFLOkQjnlxerLN36QL6lXER',
tax_rates: [],
},
{
id: 'updated_item_id',
object: 'subscription_item',
billing_thresholds: null,
created: 1745007631,
discounts: [],
metadata: {},
plan: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'plan',
active: true,
aggregate_usage: null,
amount: 1500,
amount_decimal: '1500',
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
interval: 'month',
interval_count: 1,
livemode: false,
metadata: {},
meter: null,
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
tiers_mode: null,
transform_usage: null,
trial_period_days: null,
usage_type: 'licensed',
},
price: {
id: 'price_1RFLOkQjnlxerLN3WzE0SNgt',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1745007630,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_S9eiEm1mzxUNjJ',
recurring: {
aggregate_usage: null,
interval: 'month',
interval_count: 1,
meter: null,
trial_period_days: null,
usage_type: 'licensed',
},
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'recurring',
unit_amount: 1500,
unit_amount_decimal: '1500',
},
quantity: 1,
subscription: 'sub_1RFLOkQjnlxerLN36QL6lXER',
tax_rates: [],
},
],
object: 'list',
has_more: false,
url: '/v1/subscription_items?subscription=sub_1RFLOkQjnlxerLN36QL6lXER',
},
},
},
id: 'evt_1RFLOkQjnlxerLN36QL6lXER',
object: 'event',
api_version: '2024-04-10',
created: 1745007630,
livemode: false,
pending_webhooks: 0,
request: null,
};

View File

@ -1,89 +0,0 @@
/* @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',
'billingSubscriptionItems.billingProduct',
],
});
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,
);
}
const subscriptionItem = subscription.billingSubscriptionItems.find(
(item) =>
item.billingProduct.stripeProductId === product.stripeProductId,
);
const trialPeriodFreeWorkflowCredits = isDefined(
subscriptionItem?.metadata.trialPeriodFreeWorkflowCredits,
)
? Number(subscriptionItem?.metadata.trialPeriodFreeWorkflowCredits)
: 0;
if (
!isDefined(alert.usage_threshold?.gte) ||
trialPeriodFreeWorkflowCredits !== alert.usage_threshold.gte
) {
return;
}
await this.billingSubscriptionItemRepository.update(
{
billingSubscriptionId: subscription.id,
stripeProductId: product.stripeProductId,
},
{ hasReachedCurrentPeriodCap: true },
);
}
}
}

View File

@ -1,46 +0,0 @@
/* @license Enterprise */
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
@Injectable()
export class BillingWebhookCustomerService {
protected readonly logger = new Logger(BillingWebhookCustomerService.name);
constructor(
@InjectRepository(BillingCustomer, 'core')
private readonly billingCustomerRepository: Repository<BillingCustomer>,
) {}
async processStripeEvent(data: Stripe.CustomerCreatedEvent.Data) {
const { id: stripeCustomerId, metadata } = data.object;
const workspaceId = metadata?.workspaceId;
if (!workspaceId) {
throw new BillingException(
'Workspace ID is required for customer events',
BillingExceptionCode.BILLING_CUSTOMER_EVENT_WORKSPACE_NOT_FOUND,
);
}
await this.billingCustomerRepository.upsert(
{
stripeCustomerId,
workspaceId,
},
{
conflictPaths: ['workspaceId'],
skipUpdateIfNoValuesChanged: true,
},
);
}
}

View File

@ -1,56 +0,0 @@
/* @license Enterprise */
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
@Injectable()
export class BillingWebhookEntitlementService {
constructor(
@InjectRepository(BillingCustomer, 'core')
private readonly billingCustomerRepository: Repository<BillingCustomer>,
@InjectRepository(BillingEntitlement, 'core')
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
) {}
async processStripeEvent(
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
) {
const billingCustomer = await this.billingCustomerRepository.findOne({
where: { stripeCustomerId: data.object.customer },
});
if (!billingCustomer) {
throw new BillingException(
'Billing customer not found',
BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND,
);
}
const workspaceId = billingCustomer.workspaceId;
await this.billingEntitlementRepository.upsert(
transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
workspaceId,
data,
),
{
conflictPaths: ['workspaceId', 'key'],
skipUpdateIfNoValuesChanged: true,
},
);
return {
stripeEntitlementCustomerId: data.object.customer,
};
}
}

View File

@ -1,35 +0,0 @@
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 },
);
}
}
}

View File

@ -1,75 +0,0 @@
/* @license Enterprise */
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
@Injectable()
export class BillingWebhookPriceService {
protected readonly logger = new Logger(BillingWebhookPriceService.name);
constructor(
private readonly stripeBillingMeterService: StripeBillingMeterService,
@InjectRepository(BillingPrice, 'core')
private readonly billingPriceRepository: Repository<BillingPrice>,
@InjectRepository(BillingMeter, 'core')
private readonly billingMeterRepository: Repository<BillingMeter>,
@InjectRepository(BillingProduct, 'core')
private readonly billingProductRepository: Repository<BillingProduct>,
) {}
async processStripeEvent(
data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data,
) {
const stripeProductId = String(data.object.product);
const product = await this.billingProductRepository.findOne({
where: { stripeProductId },
});
if (!product) {
throw new BillingException(
'Billing product not found',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
);
}
const meterId = data.object.recurring?.meter;
if (meterId) {
const meterData = await this.stripeBillingMeterService.getMeter(meterId);
await this.billingMeterRepository.upsert(
transformStripeMeterToDatabaseMeter(meterData),
{
conflictPaths: ['stripeMeterId'],
skipUpdateIfNoValuesChanged: true,
},
);
}
await this.billingPriceRepository.upsert(
transformStripePriceEventToDatabasePrice(data),
{
conflictPaths: ['stripePriceId'],
skipUpdateIfNoValuesChanged: true,
},
);
return {
stripePriceId: data.object.id,
stripeMeterId: meterId,
};
}
}

View File

@ -1,40 +0,0 @@
/* @license Enterprise */
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Repository } from 'typeorm';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
@Injectable()
export class BillingWebhookProductService {
protected readonly logger = new Logger(BillingWebhookProductService.name);
constructor(
@InjectRepository(BillingProduct, 'core')
private readonly billingProductRepository: Repository<BillingProduct>,
) {}
async processStripeEvent(
data: Stripe.ProductCreatedEvent.Data | Stripe.ProductUpdatedEvent.Data,
) {
const metadata = data.object.metadata;
const productRepositoryData = isStripeValidProductMetadata(metadata)
? {
...transformStripeProductEventToDatabaseProduct(data),
metadata,
}
: transformStripeProductEventToDatabaseProduct(data);
await this.billingProductRepository.upsert(productRepositoryData, {
conflictPaths: ['stripeProductId'],
skipUpdateIfNoValuesChanged: true,
});
return {
stripeProductId: data.object.id,
};
}
}

View File

@ -1,201 +0,0 @@
/* @license Enterprise */
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.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';
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 { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util';
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 { 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';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
CleanWorkspaceDeletionWarningUserVarsJob,
CleanWorkspaceDeletionWarningUserVarsJobData,
} from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
@Injectable()
export class BillingWebhookSubscriptionService {
protected readonly logger = new Logger(
BillingWebhookSubscriptionService.name,
);
constructor(
private readonly stripeCustomerService: StripeCustomerService,
@InjectMessageQueue(MessageQueue.workspaceQueue)
private readonly messageQueueService: MessageQueueService,
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
@InjectRepository(BillingSubscriptionItem, 'core')
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(BillingCustomer, 'core')
private readonly billingCustomerRepository: Repository<BillingCustomer>,
private readonly billingSubscriptionService: BillingSubscriptionService,
@InjectRepository(FeatureFlag, 'core')
private readonly featureFlagRepository: Repository<FeatureFlag>,
) {}
async processStripeEvent(
workspaceId: string,
event:
| Stripe.CustomerSubscriptionUpdatedEvent
| Stripe.CustomerSubscriptionCreatedEvent
| Stripe.CustomerSubscriptionDeletedEvent,
) {
const { data, type } = event;
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
withDeleted: true,
});
if (
!workspace ||
(isDefined(workspace?.deletedAt) &&
type !== BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED)
) {
return { noWorkspace: true };
}
await this.billingCustomerRepository.upsert(
transformStripeSubscriptionEventToDatabaseCustomer(workspaceId, data),
{
conflictPaths: ['workspaceId'],
skipUpdateIfNoValuesChanged: true,
},
);
await this.billingSubscriptionRepository.upsert(
transformStripeSubscriptionEventToDatabaseSubscription(workspaceId, data),
{
conflictPaths: ['stripeSubscriptionId'],
skipUpdateIfNoValuesChanged: true,
},
);
const billingSubscriptions = await this.billingSubscriptionRepository.find({
where: { workspaceId },
});
const updatedBillingSubscription = billingSubscriptions.find(
(subscription) => subscription.stripeSubscriptionId === data.object.id,
);
if (!updatedBillingSubscription) {
throw new Error('Billing subscription not found');
}
await this.updateBillingSubscriptionItems(
updatedBillingSubscription.id,
event,
);
if (
this.shouldSuspendWorkspace(data) &&
workspace.activationStatus == WorkspaceActivationStatus.ACTIVE
) {
await this.workspaceRepository.update(workspaceId, {
activationStatus: WorkspaceActivationStatus.SUSPENDED,
});
}
if (
!this.shouldSuspendWorkspace(data) &&
workspace.activationStatus == WorkspaceActivationStatus.SUSPENDED
) {
await this.workspaceRepository.update(workspaceId, {
activationStatus: WorkspaceActivationStatus.ACTIVE,
});
await this.messageQueueService.add<CleanWorkspaceDeletionWarningUserVarsJobData>(
CleanWorkspaceDeletionWarningUserVarsJob.name,
{ workspaceId },
);
}
await this.stripeCustomerService.updateCustomerMetadataWorkspaceId(
String(data.object.customer),
workspaceId,
);
if (event.type === BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED) {
await this.billingSubscriptionService.setBillingThresholdsAndTrialPeriodWorkflowCredits(
updatedBillingSubscription.id,
);
}
return {
stripeSubscriptionId: data.object.id,
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;
}
async updateBillingSubscriptionItems(
subscriptionId: string,
event:
| Stripe.CustomerSubscriptionUpdatedEvent
| Stripe.CustomerSubscriptionCreatedEvent
| Stripe.CustomerSubscriptionDeletedEvent,
) {
const deletedSubscriptionItemIds =
getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent(event);
if (deletedSubscriptionItemIds.length > 0) {
await this.billingSubscriptionItemRepository.delete({
billingSubscriptionId: subscriptionId,
stripeSubscriptionItemId: In(deletedSubscriptionItemIds),
});
}
await this.billingSubscriptionItemRepository.upsert(
transformStripeSubscriptionEventToDatabaseSubscriptionItem(
subscriptionId,
event.data,
),
{
conflictPaths: ['stripeSubscriptionItemId'],
skipUpdateIfNoValuesChanged: true,
},
);
}
}

View File

@ -1,33 +0,0 @@
import {
mockStripeSubscriptionUpdatedEventWithDeletedItem,
mockStripeSubscriptionUpdatedEventWithoutUpdatedItem,
mockStripeSubscriptionUpdatedEventWithUpdatedItemOnly,
} from 'src/engine/core-modules/billing/webhooks/__mocks__/stripe-subscription-updated-events';
import { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util';
describe('getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent', () => {
it('should return an empty array if subscription items are not updated', () => {
const result =
getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent(
mockStripeSubscriptionUpdatedEventWithoutUpdatedItem,
);
expect(result).toEqual([]);
});
it('should return an empty array if subscription items are updated but not deleted', () => {
const result =
getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent(
mockStripeSubscriptionUpdatedEventWithUpdatedItemOnly,
);
expect(result).toEqual([]);
});
it('should return subscription item ids if subscription items are deleted', () => {
const result =
getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent(
mockStripeSubscriptionUpdatedEventWithDeletedItem,
);
expect(result).toEqual(['deleted_item_id']);
});
});

View File

@ -1,96 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => {
it('should return the SSO key with true value', () => {
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
object: {
customer: 'cus_123',
entitlements: {
data: [
{
lookup_key: 'SSO',
feature: 'SSO',
livemode: false,
id: 'ent_123',
object: 'entitlements.active_entitlement',
},
],
object: 'list',
has_more: false,
url: '',
},
livemode: false,
object: 'entitlements.active_entitlement_summary',
},
};
const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
'workspaceId',
data,
);
expect(result).toEqual([
{
workspaceId: 'workspaceId',
key: BillingEntitlementKey.SSO,
value: true,
stripeCustomerId: 'cus_123',
},
{
key: BillingEntitlementKey.CUSTOM_DOMAIN,
stripeCustomerId: 'cus_123',
value: false,
workspaceId: 'workspaceId',
},
]);
});
it('should return the SSO key with false value,should only render the values that are listed in BillingEntitlementKeys', () => {
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
object: {
customer: 'cus_123',
entitlements: {
data: [
{
id: 'ent_123',
object: 'entitlements.active_entitlement',
lookup_key: 'DIFFERENT_KEY',
feature: 'DIFFERENT_FEATURE',
livemode: false,
},
],
object: 'list',
has_more: false,
url: '',
},
livemode: false,
object: 'entitlements.active_entitlement_summary',
},
};
const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
'workspaceId',
data,
);
expect(result).toEqual([
{
workspaceId: 'workspaceId',
key: BillingEntitlementKey.SSO,
value: false,
stripeCustomerId: 'cus_123',
},
{
key: 'CUSTOM_DOMAIN',
stripeCustomerId: 'cus_123',
value: false,
workspaceId: 'workspaceId',
},
]);
});
});

View File

@ -1,216 +0,0 @@
/* @license Enterprise */
import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum';
import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum';
import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum';
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
describe('transformStripePriceEventToDatabasePrice', () => {
const createMockPriceData = (overrides = {}) => ({
object: {
id: 'price_123',
active: true,
product: 'prod_123',
meter: null,
currency: 'usd',
nickname: null,
tax_behavior: null,
type: 'recurring',
billing_scheme: 'per_unit',
unit_amount_decimal: '1000',
unit_amount: 1000,
transform_quantity: null,
recurring: {
usage_type: 'licensed',
interval: 'month',
},
currency_options: null,
tiers: null,
tiers_mode: null,
...overrides,
},
});
it('should transform basic price data correctly', () => {
const mockData = createMockPriceData();
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result).toEqual({
stripePriceId: 'price_123',
active: true,
stripeProductId: 'prod_123',
stripeMeterId: undefined,
currency: 'USD',
nickname: undefined,
taxBehavior: undefined,
type: BillingPriceType.RECURRING,
billingScheme: BillingPriceBillingScheme.PER_UNIT,
unitAmountDecimal: '1000',
unitAmount: 1000,
transformQuantity: undefined,
usageType: BillingUsageType.LICENSED,
interval: SubscriptionInterval.Month,
currencyOptions: undefined,
tiers: undefined,
tiersMode: undefined,
recurring: {
usage_type: 'licensed',
interval: 'month',
},
});
});
it('should handle all tax behaviors correctly', () => {
const taxBehaviors = [
['exclusive', BillingPriceTaxBehavior.EXCLUSIVE],
['inclusive', BillingPriceTaxBehavior.INCLUSIVE],
['unspecified', BillingPriceTaxBehavior.UNSPECIFIED],
];
taxBehaviors.forEach(([stripeTaxBehavior, expectedTaxBehavior]) => {
const mockData = createMockPriceData({
tax_behavior: stripeTaxBehavior,
});
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.taxBehavior).toBe(expectedTaxBehavior);
});
});
it('should handle all price types correctly', () => {
const priceTypes = [
['one_time', BillingPriceType.ONE_TIME],
['recurring', BillingPriceType.RECURRING],
];
priceTypes.forEach(([stripeType, expectedType]) => {
const mockData = createMockPriceData({ type: stripeType });
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.type).toBe(expectedType);
});
});
it('should handle all billing schemes correctly', () => {
const billingSchemes = [
['per_unit', BillingPriceBillingScheme.PER_UNIT],
['tiered', BillingPriceBillingScheme.TIERED],
];
billingSchemes.forEach(([stripeScheme, expectedScheme]) => {
const mockData = createMockPriceData({ billing_scheme: stripeScheme });
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.billingScheme).toBe(expectedScheme);
});
});
it('should handle all usage types correctly', () => {
const usageTypes = [
['licensed', BillingUsageType.LICENSED],
['metered', BillingUsageType.METERED],
];
usageTypes.forEach(([stripeUsageType, expectedUsageType]) => {
const mockData = createMockPriceData({
recurring: { usage_type: stripeUsageType, interval: 'month' },
});
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.usageType).toBe(expectedUsageType);
});
});
it('should handle all tiers modes correctly', () => {
const tiersModes = [
['graduated', BillingPriceTiersMode.GRADUATED],
['volume', BillingPriceTiersMode.VOLUME],
];
tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => {
const mockData = createMockPriceData({ tiers_mode: stripeTiersMode });
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.tiersMode).toBe(expectedTiersMode);
});
});
it('should handle all intervals correctly', () => {
const intervals = [
['month', SubscriptionInterval.Month],
['day', SubscriptionInterval.Day],
['week', SubscriptionInterval.Week],
['year', SubscriptionInterval.Year],
];
intervals.forEach(([stripeInterval, expectedInterval]) => {
const mockData = createMockPriceData({
recurring: { usage_type: 'licensed', interval: stripeInterval },
});
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.interval).toBe(expectedInterval);
});
});
it('should handle tiered pricing configuration', () => {
const mockTiers = [
{ up_to: 10, unit_amount: 1000 },
{ up_to: 20, unit_amount: 800 },
];
const mockData = createMockPriceData({
billing_scheme: 'tiered',
tiers: mockTiers,
tiers_mode: 'graduated',
});
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED);
expect(result.tiers).toEqual(mockTiers);
expect(result.tiersMode).toBe(BillingPriceTiersMode.GRADUATED);
});
it('should handle metered pricing with transform quantity', () => {
const mockTransformQuantity = {
divide_by: 100,
round: 'up',
};
const mockData = createMockPriceData({
recurring: {
usage_type: 'metered',
interval: 'month',
meter: 'meter_123',
},
transform_quantity: mockTransformQuantity,
});
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.stripeMeterId).toBe('meter_123');
expect(result.usageType).toBe(BillingUsageType.METERED);
expect(result.transformQuantity).toEqual(mockTransformQuantity);
});
it('should handle currency options', () => {
const mockCurrencyOptions = {
eur: {
unit_amount: 850,
unit_amount_decimal: '850',
},
};
const mockData = createMockPriceData({
currency_options: mockCurrencyOptions,
});
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.currencyOptions).toEqual(mockCurrencyOptions);
});
});

View File

@ -1,91 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
describe('transformStripeProductEventToDatabaseProduct', () => {
it('should return the correct data', () => {
const data: Stripe.ProductCreatedEvent.Data = {
object: {
id: 'prod_123',
name: 'Product 1',
active: true,
description: 'Description 1',
images: ['image1.jpg', 'image2.jpg'],
marketing_features: [
{
name: 'feature1',
},
],
created: 1719859200,
updated: 1719859200,
type: 'service',
livemode: false,
package_dimensions: null,
shippable: false,
object: 'product',
default_price: 'price_123',
unit_label: 'Unit',
url: 'https://example.com',
tax_code: 'tax_code_1',
metadata: { key: 'value' },
},
};
const result = transformStripeProductEventToDatabaseProduct(data);
expect(result).toEqual({
stripeProductId: 'prod_123',
name: 'Product 1',
active: true,
description: 'Description 1',
images: ['image1.jpg', 'image2.jpg'],
marketingFeatures: [{ name: 'feature1' }],
defaultStripePriceId: 'price_123',
unitLabel: 'Unit',
url: 'https://example.com',
taxCode: 'tax_code_1',
});
});
it('should return the correct data with null values', () => {
const data: Stripe.ProductUpdatedEvent.Data = {
object: {
id: 'prod_456',
name: 'Product 2',
object: 'product',
active: false,
description: '',
images: [],
created: 1719859200,
updated: 1719859200,
type: 'service',
livemode: false,
package_dimensions: null,
shippable: false,
marketing_features: [],
default_price: null,
unit_label: null,
url: null,
tax_code: null,
metadata: {},
},
};
const result = transformStripeProductEventToDatabaseProduct(data);
expect(result).toEqual({
stripeProductId: 'prod_456',
name: 'Product 2',
active: false,
description: '',
images: [],
marketingFeatures: [],
defaultStripePriceId: undefined,
unitLabel: undefined,
url: undefined,
taxCode: undefined,
});
});
});

View File

@ -1,86 +0,0 @@
/* @license Enterprise */
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
describe('transformStripeSubscriptionEventToDatabaseCustomer', () => {
const mockWorkspaceId = 'workspace_123';
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
const createMockSubscriptionData = (overrides = {}) => ({
object: {
id: 'sub_123',
customer: 'cus_123',
status: 'active',
items: {
data: [
{
plan: {
interval: 'month',
},
},
],
},
cancel_at_period_end: false,
currency: 'usd',
current_period_end: mockTimestamp,
current_period_start: mockTimestamp - 2592000, // 30 days before end
metadata: {},
collection_method: 'charge_automatically',
automatic_tax: null,
cancellation_details: null,
ended_at: null,
trial_start: null,
trial_end: null,
cancel_at: null,
canceled_at: null,
...overrides,
},
});
it('should transform basic customer data correctly', () => {
const mockData = createMockSubscriptionData('cus_123');
const result = transformStripeSubscriptionEventToDatabaseCustomer(
mockWorkspaceId,
mockData as any,
);
expect(result).toEqual({
workspaceId: 'workspace_123',
stripeCustomerId: 'cus_123',
});
});
it('should work with different subscription event types', () => {
const mockData = createMockSubscriptionData('cus_123');
// Test with different event types (they should all transform the same way)
['updated', 'created', 'deleted'].forEach(() => {
const result = transformStripeSubscriptionEventToDatabaseCustomer(
mockWorkspaceId,
mockData as any,
);
expect(result).toEqual({
workspaceId: 'workspace_123',
stripeCustomerId: 'cus_123',
});
});
});
it('should handle different workspace IDs', () => {
const mockData = createMockSubscriptionData('cus_123');
const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc'];
testWorkspaces.forEach((testWorkspaceId) => {
const result = transformStripeSubscriptionEventToDatabaseCustomer(
testWorkspaceId,
mockData as any,
);
expect(result).toEqual({
workspaceId: testWorkspaceId,
stripeCustomerId: 'cus_123',
});
});
});
});

View File

@ -1,197 +0,0 @@
/* @license Enterprise */
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
describe('transformStripeSubscriptionEventToDatabaseSubscription', () => {
const mockWorkspaceId = 'workspace-123';
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
const createMockSubscriptionData = (overrides = {}) => ({
object: {
id: 'sub_123',
customer: 'cus_123',
status: 'active',
items: {
data: [
{
plan: {
interval: 'month',
},
},
],
},
cancel_at_period_end: false,
currency: 'usd',
current_period_end: mockTimestamp,
current_period_start: mockTimestamp - 2592000, // 30 days before end
metadata: {},
collection_method: 'charge_automatically',
automatic_tax: null,
cancellation_details: null,
ended_at: null,
trial_start: null,
trial_end: null,
cancel_at: null,
canceled_at: null,
...overrides,
},
});
it('should transform basic subscription data correctly', () => {
const mockData = createMockSubscriptionData();
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result).toEqual({
workspaceId: mockWorkspaceId,
stripeCustomerId: 'cus_123',
stripeSubscriptionId: 'sub_123',
status: SubscriptionStatus.Active,
interval: 'month',
cancelAtPeriodEnd: false,
currency: 'USD',
currentPeriodEnd: new Date(mockTimestamp * 1000),
currentPeriodStart: new Date((mockTimestamp - 2592000) * 1000),
metadata: {},
collectionMethod:
BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY,
automaticTax: undefined,
cancellationDetails: undefined,
endedAt: undefined,
trialStart: undefined,
trialEnd: undefined,
cancelAt: undefined,
canceledAt: undefined,
});
});
it('should handle all subscription statuses correctly', () => {
const statuses = [
['active', SubscriptionStatus.Active],
['canceled', SubscriptionStatus.Canceled],
['incomplete', SubscriptionStatus.Incomplete],
['incomplete_expired', SubscriptionStatus.IncompleteExpired],
['past_due', SubscriptionStatus.PastDue],
['paused', SubscriptionStatus.Paused],
['trialing', SubscriptionStatus.Trialing],
['unpaid', SubscriptionStatus.Unpaid],
];
statuses.forEach(([stripeStatus, expectedStatus]) => {
const mockData = createMockSubscriptionData({
status: stripeStatus,
});
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.status).toBe(expectedStatus);
});
});
it('should handle subscription with trial periods', () => {
const trialStart = mockTimestamp - 604800; // 7 days before
const trialEnd = mockTimestamp + 604800; // 7 days after
const mockData = createMockSubscriptionData({
trial_start: trialStart,
trial_end: trialEnd,
});
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.trialStart).toEqual(new Date(trialStart * 1000));
expect(result.trialEnd).toEqual(new Date(trialEnd * 1000));
});
it('should handle subscription cancellation details', () => {
const cancelAt = mockTimestamp + 2592000; // 30 days after
const canceledAt = mockTimestamp;
const mockData = createMockSubscriptionData({
cancel_at: cancelAt,
canceled_at: canceledAt,
cancel_at_period_end: true,
cancellation_details: {
comment: 'Customer requested cancellation',
feedback: 'too_expensive',
reason: 'customer_request',
},
});
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.cancelAt).toEqual(new Date(cancelAt * 1000));
expect(result.canceledAt).toEqual(new Date(canceledAt * 1000));
expect(result.cancelAtPeriodEnd).toBe(true);
expect(result.cancellationDetails).toEqual({
comment: 'Customer requested cancellation',
feedback: 'too_expensive',
reason: 'customer_request',
});
});
it('should handle automatic tax information', () => {
const mockData = createMockSubscriptionData({
automatic_tax: {
enabled: true,
status: 'calculated',
},
});
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.automaticTax).toEqual({
enabled: true,
status: 'calculated',
});
});
it('should handle different collection methods', () => {
const methods = [
[
'charge_automatically',
BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY,
],
['send_invoice', BillingSubscriptionCollectionMethod.SEND_INVOICE],
];
methods.forEach(([stripeMethod, expectedMethod]) => {
const mockData = createMockSubscriptionData({
collection_method: stripeMethod,
});
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.collectionMethod).toBe(expectedMethod);
});
});
it('should handle different currencies', () => {
const mockData = createMockSubscriptionData({
currency: 'eur',
});
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.currency).toBe('EUR');
});
});

View File

@ -1,29 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
import { isDefined } from 'twenty-shared/utils';
export const getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent = (
event:
| Stripe.CustomerSubscriptionUpdatedEvent
| Stripe.CustomerSubscriptionCreatedEvent
| Stripe.CustomerSubscriptionDeletedEvent,
): string[] => {
const hasUpdatedSubscriptionItems = isDefined(
event.data.previous_attributes?.items?.data,
);
if (!hasUpdatedSubscriptionItems) {
return [];
}
const subscriptionItemIds =
event.data.object.items.data.map((item) => item.id) ?? [];
const deletedSubscriptionItemIds =
event.data.previous_attributes?.items?.data
.filter((item) => !subscriptionItemIds.includes(item.id))
.map((item) => item.id) ?? [];
return deletedSubscriptionItemIds;
};

View File

@ -1,24 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
export const transformStripeEntitlementUpdatedEventToDatabaseEntitlement = (
workspaceId: string,
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
) => {
const stripeCustomerId = data.object.customer;
const activeEntitlementsKeys = data.object.entitlements.data.map(
(entitlement) => entitlement.lookup_key,
);
return Object.values(BillingEntitlementKey).map((key) => {
return {
workspaceId,
key,
value: activeEntitlementsKeys.includes(key),
stripeCustomerId,
};
});
};

View File

@ -1,115 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum';
import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum';
import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum';
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
export const transformStripePriceEventToDatabasePrice = (
data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data,
) => {
return {
stripePriceId: data.object.id,
active: data.object.active,
stripeProductId: String(data.object.product),
stripeMeterId: data.object.recurring?.meter,
currency: data.object.currency.toUpperCase(),
nickname: data.object.nickname === null ? undefined : data.object.nickname,
taxBehavior: data.object.tax_behavior
? getTaxBehavior(data.object.tax_behavior)
: undefined,
type: getBillingPriceType(data.object.type),
billingScheme: getBillingPriceBillingScheme(data.object.billing_scheme),
unitAmountDecimal:
data.object.unit_amount_decimal === null
? undefined
: data.object.unit_amount_decimal,
unitAmount: data.object.unit_amount
? Number(data.object.unit_amount)
: undefined,
transformQuantity:
data.object.transform_quantity === null
? undefined
: data.object.transform_quantity,
usageType: data.object.recurring?.usage_type
? getBillingPriceUsageType(data.object.recurring.usage_type)
: undefined,
interval: data.object.recurring?.interval
? getBillingPriceInterval(data.object.recurring.interval)
: undefined,
currencyOptions:
data.object.currency_options === null
? undefined
: data.object.currency_options,
tiers: data.object.tiers === null ? undefined : data.object.tiers,
tiersMode: data.object.tiers_mode
? getBillingPriceTiersMode(data.object.tiers_mode)
: undefined,
recurring:
data.object.recurring === null ? undefined : data.object.recurring,
};
};
const getTaxBehavior = (data: Stripe.Price.TaxBehavior) => {
switch (data) {
case 'exclusive':
return BillingPriceTaxBehavior.EXCLUSIVE;
case 'inclusive':
return BillingPriceTaxBehavior.INCLUSIVE;
case 'unspecified':
return BillingPriceTaxBehavior.UNSPECIFIED;
}
};
const getBillingPriceType = (data: Stripe.Price.Type) => {
switch (data) {
case 'one_time':
return BillingPriceType.ONE_TIME;
case 'recurring':
return BillingPriceType.RECURRING;
}
};
const getBillingPriceBillingScheme = (data: Stripe.Price.BillingScheme) => {
switch (data) {
case 'per_unit':
return BillingPriceBillingScheme.PER_UNIT;
case 'tiered':
return BillingPriceBillingScheme.TIERED;
}
};
const getBillingPriceUsageType = (data: Stripe.Price.Recurring.UsageType) => {
switch (data) {
case 'licensed':
return BillingUsageType.LICENSED;
case 'metered':
return BillingUsageType.METERED;
}
};
const getBillingPriceTiersMode = (data: Stripe.Price.TiersMode) => {
switch (data) {
case 'graduated':
return BillingPriceTiersMode.GRADUATED;
case 'volume':
return BillingPriceTiersMode.VOLUME;
}
};
const getBillingPriceInterval = (data: Stripe.Price.Recurring.Interval) => {
switch (data) {
case 'month':
return SubscriptionInterval.Month;
case 'day':
return SubscriptionInterval.Day;
case 'week':
return SubscriptionInterval.Week;
case 'year':
return SubscriptionInterval.Year;
}
};

View File

@ -1,23 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
export const transformStripeProductEventToDatabaseProduct = (
data: Stripe.ProductUpdatedEvent.Data | Stripe.ProductCreatedEvent.Data,
) => {
return {
stripeProductId: data.object.id,
name: data.object.name,
active: data.object.active,
description: data.object.description ?? '',
images: data.object.images,
marketingFeatures: data.object.marketing_features,
defaultStripePriceId: data.object.default_price
? String(data.object.default_price)
: undefined,
unitLabel:
data.object.unit_label === null ? undefined : data.object.unit_label,
url: data.object.url === null ? undefined : data.object.url,
taxCode: data.object.tax_code ? String(data.object.tax_code) : undefined,
};
};

View File

@ -1,16 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
export const transformStripeSubscriptionEventToDatabaseCustomer = (
workspaceId: string,
data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data
| Stripe.CustomerSubscriptionCreatedEvent.Data
| Stripe.CustomerSubscriptionDeletedEvent.Data,
) => {
return {
workspaceId,
stripeCustomerId: String(data.object.customer),
};
};

View File

@ -1,25 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
export const transformStripeSubscriptionEventToDatabaseSubscriptionItem = (
billingSubscriptionId: string,
data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data
| Stripe.CustomerSubscriptionCreatedEvent.Data
| Stripe.CustomerSubscriptionDeletedEvent.Data,
) => {
return data.object.items.data.map((item) => {
return {
billingSubscriptionId,
stripeSubscriptionId: data.object.id,
stripeProductId: String(item.price.product),
stripePriceId: item.price.id,
stripeSubscriptionItemId: item.id,
quantity: item.quantity,
metadata: item.metadata,
billingThresholds:
item.billing_thresholds === null ? undefined : item.billing_thresholds,
};
});
};

View File

@ -1,80 +0,0 @@
/* @license Enterprise */
import Stripe from 'stripe';
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
export const transformStripeSubscriptionEventToDatabaseSubscription = (
workspaceId: string,
data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data
| Stripe.CustomerSubscriptionCreatedEvent.Data
| Stripe.CustomerSubscriptionDeletedEvent.Data,
) => {
return {
workspaceId,
stripeCustomerId: String(data.object.customer),
stripeSubscriptionId: data.object.id,
status: getSubscriptionStatus(data.object.status),
interval: data.object.items.data[0].plan.interval,
cancelAtPeriodEnd: data.object.cancel_at_period_end,
currency: data.object.currency.toUpperCase(),
currentPeriodEnd: getDateFromTimestamp(data.object.current_period_end),
currentPeriodStart: getDateFromTimestamp(data.object.current_period_start),
metadata: data.object.metadata,
collectionMethod:
// @ts-expect-error legacy noImplicitAny
BillingSubscriptionCollectionMethod[
data.object.collection_method.toUpperCase()
],
automaticTax:
data.object.automatic_tax === null
? undefined
: data.object.automatic_tax,
cancellationDetails:
data.object.cancellation_details === null
? undefined
: data.object.cancellation_details,
endedAt: data.object.ended_at
? getDateFromTimestamp(data.object.ended_at)
: undefined,
trialStart: data.object.trial_start
? getDateFromTimestamp(data.object.trial_start)
: undefined,
trialEnd: data.object.trial_end
? getDateFromTimestamp(data.object.trial_end)
: undefined,
cancelAt: data.object.cancel_at
? getDateFromTimestamp(data.object.cancel_at)
: undefined,
canceledAt: data.object.canceled_at
? getDateFromTimestamp(data.object.canceled_at)
: undefined,
};
};
export const getSubscriptionStatus = (status: Stripe.Subscription.Status) => {
switch (status) {
case 'active':
return SubscriptionStatus.Active;
case 'canceled':
return SubscriptionStatus.Canceled;
case 'incomplete':
return SubscriptionStatus.Incomplete;
case 'incomplete_expired':
return SubscriptionStatus.IncompleteExpired;
case 'past_due':
return SubscriptionStatus.PastDue;
case 'paused':
return SubscriptionStatus.Paused;
case 'trialing':
return SubscriptionStatus.Trialing;
case 'unpaid':
return SubscriptionStatus.Unpaid;
}
};
const getDateFromTimestamp = (timestamp: number) => {
return new Date(timestamp * 1000);
};