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:
@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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']);
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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),
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user