feat(billing): refacto billing (#14243)
… prices for metered billing --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+7
@@ -22,6 +22,7 @@ import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-we
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
|
||||
import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service';
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
@@ -47,6 +48,7 @@ export class BillingWebhookController {
|
||||
private readonly billingWebhookAlertService: BillingWebhookAlertService,
|
||||
private readonly billingWebhookInvoiceService: BillingWebhookInvoiceService,
|
||||
private readonly billingWebhookCustomerService: BillingWebhookCustomerService,
|
||||
private readonly billingWebhookSubscriptionScheduleService: BillingWebhookSubscriptionScheduleService,
|
||||
) {}
|
||||
|
||||
@Post(['webhooks/stripe'])
|
||||
@@ -100,6 +102,11 @@ export class BillingWebhookController {
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.SUBSCRIPTION_SCHEDULE_UPDATED:
|
||||
return await this.billingWebhookSubscriptionScheduleService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
|
||||
case BillingWebhookEvent.PRODUCT_UPDATED:
|
||||
case BillingWebhookEvent.PRODUCT_CREATED:
|
||||
return await this.billingWebhookProductService.processStripeEvent(
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-we
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
|
||||
import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
@@ -57,6 +58,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
BillingWebhookInvoiceService,
|
||||
BillingWebhookCustomerService,
|
||||
BillingWebhookSubscriptionService,
|
||||
BillingWebhookSubscriptionScheduleService,
|
||||
BillingWebhookEntitlementService,
|
||||
],
|
||||
})
|
||||
|
||||
+30
-52
@@ -3,8 +3,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
@@ -14,17 +14,13 @@ import {
|
||||
} 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
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookAlertService {
|
||||
protected readonly logger = new Logger(BillingWebhookAlertService.name);
|
||||
constructor(
|
||||
@InjectRepository(BillingSubscription)
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
@InjectRepository(BillingProduct)
|
||||
private readonly billingProductRepository: Repository<BillingProduct>,
|
||||
@InjectRepository(BillingSubscriptionItem)
|
||||
@@ -34,57 +30,39 @@ export class BillingWebhookAlertService {
|
||||
async processStripeEvent(data: Stripe.BillingAlertTriggeredEvent.Data) {
|
||||
const { customer: stripeCustomerId, alert } = data.object;
|
||||
|
||||
const stripeMeterId = alert.usage_threshold?.meter as string | undefined;
|
||||
const stripeMeterId = alert.usage_threshold?.meter;
|
||||
|
||||
if (alert.title === TRIAL_PERIOD_ALERT_TITLE && isDefined(stripeMeterId)) {
|
||||
const subscription = await this.billingSubscriptionRepository.findOne({
|
||||
where: { stripeCustomerId, status: SubscriptionStatus.Trialing },
|
||||
relations: [
|
||||
'billingSubscriptionItems',
|
||||
'billingSubscriptionItems.billingProduct',
|
||||
],
|
||||
});
|
||||
assertIsDefinedOrThrow(stripeMeterId);
|
||||
|
||||
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 subscription =
|
||||
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
|
||||
{ stripeCustomerId },
|
||||
);
|
||||
|
||||
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,
|
||||
const product = await this.billingProductRepository.findOne({
|
||||
where: {
|
||||
billingPrices: {
|
||||
stripeMeterId:
|
||||
typeof stripeMeterId === 'string'
|
||||
? stripeMeterId
|
||||
: stripeMeterId.id,
|
||||
},
|
||||
{ hasReachedCurrentPeriodCap: true },
|
||||
},
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new BillingException(
|
||||
`Product associated to meter ${stripeMeterId} not found`,
|
||||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.billingSubscriptionItemRepository.update(
|
||||
{
|
||||
billingSubscriptionId: subscription.id,
|
||||
stripeProductId: product.stripeProductId,
|
||||
},
|
||||
{ hasReachedCurrentPeriodCap: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -17,12 +17,14 @@ import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-p
|
||||
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 { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookPriceService {
|
||||
protected readonly logger = new Logger(BillingWebhookPriceService.name);
|
||||
constructor(
|
||||
private readonly stripeBillingMeterService: StripeBillingMeterService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
@InjectRepository(BillingPrice)
|
||||
private readonly billingPriceRepository: Repository<BillingPrice>,
|
||||
@InjectRepository(BillingMeter)
|
||||
@@ -61,7 +63,9 @@ export class BillingWebhookPriceService {
|
||||
}
|
||||
|
||||
await this.billingPriceRepository.upsert(
|
||||
transformStripePriceEventToDatabasePrice(data),
|
||||
transformStripePriceEventToDatabasePrice(
|
||||
await this.stripePriceService.getPriceByPriceId(data.object.id),
|
||||
),
|
||||
{
|
||||
conflictPaths: ['stripePriceId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import Stripe from 'stripe';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service';
|
||||
import { transformStripeSubscriptionScheduleEventToDatabaseSubscriptionPhase } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-schedule-event-to-database-subscription-phase.util';
|
||||
|
||||
@Injectable()
|
||||
export class BillingWebhookSubscriptionScheduleService {
|
||||
protected readonly logger = new Logger(
|
||||
BillingWebhookSubscriptionScheduleService.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(BillingSubscription)
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
private readonly stripeSubscriptionScheduleService: StripeSubscriptionScheduleService,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(
|
||||
data:
|
||||
| Stripe.SubscriptionScheduleUpdatedEvent.Data
|
||||
| Stripe.SubscriptionScheduleCanceledEvent.Data,
|
||||
) {
|
||||
const schedule = data.object as Stripe.SubscriptionSchedule;
|
||||
|
||||
if (!isDefined(schedule.subscription)) {
|
||||
throw new Error('Subscription is not defined');
|
||||
}
|
||||
|
||||
const subscriptionId =
|
||||
typeof schedule.subscription === 'string'
|
||||
? schedule.subscription
|
||||
: schedule.subscription.id;
|
||||
|
||||
const subscriptionWithSchedule =
|
||||
await this.stripeSubscriptionScheduleService.getSubscriptionWithSchedule(
|
||||
subscriptionId,
|
||||
);
|
||||
|
||||
await this.billingSubscriptionRepository.update(
|
||||
{ stripeSubscriptionId: subscriptionWithSchedule.id },
|
||||
{
|
||||
phases:
|
||||
transformStripeSubscriptionScheduleEventToDatabaseSubscriptionPhase(
|
||||
schedule,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
stripeSubscriptionId: subscriptionWithSchedule.id,
|
||||
phasesCount: subscriptionWithSchedule.schedule?.phases?.length ?? 0,
|
||||
scheduleId: subscriptionWithSchedule.schedule?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -29,6 +29,8 @@ import {
|
||||
CleanWorkspaceDeletionWarningUserVarsJob,
|
||||
type CleanWorkspaceDeletionWarningUserVarsJobData,
|
||||
} from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
|
||||
import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service';
|
||||
import { StripeBillingAlertService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-alert.service';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@@ -50,6 +52,8 @@ export class BillingWebhookSubscriptionService {
|
||||
private readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly stripeSubscriptionScheduleService: StripeSubscriptionScheduleService,
|
||||
private readonly stripeBillingAlertService: StripeBillingAlertService,
|
||||
) {}
|
||||
|
||||
async processStripeEvent(
|
||||
@@ -83,7 +87,12 @@ export class BillingWebhookSubscriptionService {
|
||||
);
|
||||
|
||||
await this.billingSubscriptionRepository.upsert(
|
||||
transformStripeSubscriptionEventToDatabaseSubscription(workspaceId, data),
|
||||
transformStripeSubscriptionEventToDatabaseSubscription(
|
||||
workspaceId,
|
||||
await this.stripeSubscriptionScheduleService.getSubscriptionWithSchedule(
|
||||
data.object.id,
|
||||
),
|
||||
),
|
||||
{
|
||||
conflictPaths: ['stripeSubscriptionId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
@@ -146,6 +155,15 @@ export class BillingWebhookSubscriptionService {
|
||||
await this.billingSubscriptionService.setBillingThresholdsAndTrialPeriodWorkflowCredits(
|
||||
updatedBillingSubscription.id,
|
||||
);
|
||||
const gte =
|
||||
this.billingSubscriptionService.getTrialPeriodFreeWorkflowCredits(
|
||||
updatedBillingSubscription,
|
||||
);
|
||||
|
||||
await this.stripeBillingAlertService.createUsageThresholdAlertForCustomerMeter(
|
||||
updatedBillingSubscription.stripeCustomerId,
|
||||
gte,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+19
-40
@@ -3,35 +3,32 @@
|
||||
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-price-event-to-database-price.util';
|
||||
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';
|
||||
|
||||
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,
|
||||
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', () => {
|
||||
@@ -55,7 +52,6 @@ describe('transformStripePriceEventToDatabasePrice', () => {
|
||||
interval: SubscriptionInterval.Month,
|
||||
currencyOptions: undefined,
|
||||
tiers: undefined,
|
||||
tiersMode: undefined,
|
||||
recurring: {
|
||||
usage_type: 'licensed',
|
||||
interval: 'month',
|
||||
@@ -124,25 +120,9 @@ describe('transformStripePriceEventToDatabasePrice', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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],
|
||||
];
|
||||
|
||||
@@ -172,7 +152,6 @@ describe('transformStripePriceEventToDatabasePrice', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
+25
-26
@@ -9,34 +9,32 @@ describe('transformStripeSubscriptionEventToDatabaseSubscription', () => {
|
||||
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',
|
||||
},
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
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', () => {
|
||||
@@ -66,6 +64,7 @@ describe('transformStripeSubscriptionEventToDatabaseSubscription', () => {
|
||||
trialEnd: undefined,
|
||||
cancelAt: undefined,
|
||||
canceledAt: undefined,
|
||||
phases: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+21
-47
@@ -4,53 +4,40 @@ import type 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,
|
||||
data: Stripe.Price,
|
||||
) => {
|
||||
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)
|
||||
stripePriceId: data.id,
|
||||
active: data.active,
|
||||
stripeProductId: String(data.product),
|
||||
stripeMeterId: data.recurring?.meter,
|
||||
currency: data.currency.toUpperCase(),
|
||||
nickname: data.nickname === null ? undefined : data.nickname,
|
||||
taxBehavior: data.tax_behavior
|
||||
? getTaxBehavior(data.tax_behavior)
|
||||
: undefined,
|
||||
type: getBillingPriceType(data.object.type),
|
||||
billingScheme: getBillingPriceBillingScheme(data.object.billing_scheme),
|
||||
type: getBillingPriceType(data.type),
|
||||
billingScheme: getBillingPriceBillingScheme(data.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,
|
||||
data.unit_amount_decimal === null ? undefined : data.unit_amount_decimal,
|
||||
unitAmount: data.unit_amount ? Number(data.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)
|
||||
data.transform_quantity === null ? undefined : data.transform_quantity,
|
||||
usageType: data.recurring?.usage_type
|
||||
? getBillingPriceUsageType(data.recurring.usage_type)
|
||||
: undefined,
|
||||
interval: data.object.recurring?.interval
|
||||
? getBillingPriceInterval(data.object.recurring.interval)
|
||||
interval: data.recurring?.interval
|
||||
? getBillingPriceInterval(data.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,
|
||||
data.currency_options === null ? undefined : data.currency_options,
|
||||
tiers: data.tiers === null ? undefined : data.tiers,
|
||||
recurring: data.recurring === null ? undefined : data.recurring,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -92,23 +79,10 @@ const getBillingPriceUsageType = (data: Stripe.Price.Recurring.UsageType) => {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
+33
-28
@@ -4,52 +4,57 @@ import type 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';
|
||||
import { type SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { type SubscriptionWithSchedule } from 'src/engine/core-modules/billing/types/billing-subscription-with-schedule.type';
|
||||
import { transformStripeSubscriptionScheduleEventToDatabaseSubscriptionPhase } from 'src/engine/core-modules/billing-webhook/utils/transform-stripe-subscription-schedule-event-to-database-subscription-phase.util';
|
||||
|
||||
export const transformStripeSubscriptionEventToDatabaseSubscription = (
|
||||
workspaceId: string,
|
||||
data:
|
||||
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||
| Stripe.CustomerSubscriptionCreatedEvent.Data
|
||||
| Stripe.CustomerSubscriptionDeletedEvent.Data,
|
||||
subscription: SubscriptionWithSchedule,
|
||||
) => {
|
||||
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,
|
||||
stripeCustomerId: String(subscription.customer),
|
||||
stripeSubscriptionId: subscription.id,
|
||||
status: getSubscriptionStatus(subscription.status),
|
||||
interval: subscription.items.data[0].plan.interval as SubscriptionInterval,
|
||||
phases: subscription.schedule
|
||||
? transformStripeSubscriptionScheduleEventToDatabaseSubscriptionPhase(
|
||||
subscription.schedule,
|
||||
)
|
||||
: [],
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
currency: subscription.currency.toUpperCase(),
|
||||
currentPeriodEnd: getDateFromTimestamp(subscription.current_period_end),
|
||||
currentPeriodStart: getDateFromTimestamp(subscription.current_period_start),
|
||||
metadata: subscription.metadata,
|
||||
collectionMethod:
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
BillingSubscriptionCollectionMethod[
|
||||
data.object.collection_method.toUpperCase()
|
||||
subscription.collection_method.toUpperCase()
|
||||
],
|
||||
automaticTax:
|
||||
data.object.automatic_tax === null
|
||||
subscription.automatic_tax === null
|
||||
? undefined
|
||||
: data.object.automatic_tax,
|
||||
: subscription.automatic_tax,
|
||||
cancellationDetails:
|
||||
data.object.cancellation_details === null
|
||||
subscription.cancellation_details === null
|
||||
? undefined
|
||||
: data.object.cancellation_details,
|
||||
endedAt: data.object.ended_at
|
||||
? getDateFromTimestamp(data.object.ended_at)
|
||||
: subscription.cancellation_details,
|
||||
endedAt: subscription.ended_at
|
||||
? getDateFromTimestamp(subscription.ended_at)
|
||||
: undefined,
|
||||
trialStart: data.object.trial_start
|
||||
? getDateFromTimestamp(data.object.trial_start)
|
||||
trialStart: subscription.trial_start
|
||||
? getDateFromTimestamp(subscription.trial_start)
|
||||
: undefined,
|
||||
trialEnd: data.object.trial_end
|
||||
? getDateFromTimestamp(data.object.trial_end)
|
||||
trialEnd: subscription.trial_end
|
||||
? getDateFromTimestamp(subscription.trial_end)
|
||||
: undefined,
|
||||
cancelAt: data.object.cancel_at
|
||||
? getDateFromTimestamp(data.object.cancel_at)
|
||||
cancelAt: subscription.cancel_at
|
||||
? getDateFromTimestamp(subscription.cancel_at)
|
||||
: undefined,
|
||||
canceledAt: data.object.canceled_at
|
||||
? getDateFromTimestamp(data.object.canceled_at)
|
||||
canceledAt: subscription.canceled_at
|
||||
? getDateFromTimestamp(subscription.canceled_at)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { type BillingSubscriptionSchedulePhase } from 'src/engine/core-modules/billing/dtos/billing-subscription-schedule-phase.dto';
|
||||
|
||||
export function transformStripeSubscriptionScheduleEventToDatabaseSubscriptionPhase(
|
||||
schedule: Stripe.SubscriptionSchedule,
|
||||
): Array<BillingSubscriptionSchedulePhase> {
|
||||
return schedule.phases.slice(-2).map((phase) => ({
|
||||
start_date: phase.start_date,
|
||||
end_date: phase.end_date,
|
||||
items: phase.items.map((item) => ({
|
||||
price: typeof item.price === 'string' ? item.price : item.price.id,
|
||||
...(isDefined(item.quantity) ? { quantity: item.quantity } : {}),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user