mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
add price and meter dynamically add foreign keys in billing (#9100)
**TLDR** Solves: https://github.com/twentyhq/private-issues/issues/199 Partially solves: https://github.com/twentyhq/private-issues/issues/221 (more details below) Updates the BillingMeter and BillingPrice tables while listening to the events "price.created" and "price.updated" from the stripe webhook. Also added the foreign keys, that couldn't be added to the BillingEntities. **In Order To test** Billing: - Set IS_BILLING_ENABLED to true - Add your BILLING_STRIPE_SECRET and BILLING_STRIPE_API_KEY - Add your BILLING_STRIPE_BASE_PLAN_PRODUCT_ID (use the one in testMode > Base Plan) Authenticate with your account in the stripe CLI Run the command: stripe listen --forward-to http://localhost:3000/billing/webhooks Run the twenty workker Authenticate yourself on the app choose a plan and run the app normally. In stripe and in posgress the customer table data should be added. **Take Into Consideration** In a previous migration the foreign key to workpaceId was taken down this was due to the separation of the migrations if billing is enabled. Because we want to separate in these two categories: we will be polluting the Common Migrations with relations to tables that don't exists. This will be addressed in a PR in the next sprint (perhaps a decorator?) **Doing** Testing migrations, when we are in main and when billing is enabled.
This commit is contained in:
parent
e492efb79e
commit
55dc5983a2
@ -0,0 +1,67 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddConstraintsOnBillingTables1734450749954
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddConstraintsOnBillingTables1734450749954';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP CONSTRAINT "IndexOnBillingSubscriptionIdAndStripeSubscriptionItemIdUnique"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingCustomer" DROP CONSTRAINT "IndexOnWorkspaceIdAndStripeCustomerIdUnique"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "UQ_6a989264cab5ee2d4b424e78526" UNIQUE ("stripeSubscriptionItemId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "quantity"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "quantity" numeric`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingCustomer" ADD CONSTRAINT "UQ_53c2ef50e9611082f83d760897d" UNIQUE ("workspaceId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IndexOnActiveSubscriptionPerWorkspace" ON "core"."billingSubscription" ("workspaceId") WHERE status IN ('trialing', 'active', 'past_due')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingEntitlement" ADD CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356" FOREIGN KEY ("stripeCustomerId") REFERENCES "core"."billingCustomer"("stripeCustomerId") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_9120b7586c3471463480b58d20a" FOREIGN KEY ("stripeCustomerId") REFERENCES "core"."billingCustomer"("stripeCustomerId") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_9120b7586c3471463480b58d20a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IndexOnActiveSubscriptionPerWorkspace"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingCustomer" DROP CONSTRAINT "UQ_53c2ef50e9611082f83d760897d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "quantity"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "quantity" integer NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP CONSTRAINT "UQ_6a989264cab5ee2d4b424e78526"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingCustomer" ADD CONSTRAINT "IndexOnWorkspaceIdAndStripeCustomerIdUnique" UNIQUE ("workspaceId", "stripeCustomerId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "IndexOnBillingSubscriptionIdAndStripeSubscriptionItemIdUnique" UNIQUE ("billingSubscriptionId", "stripeSubscriptionItemId")`,
|
||||
);
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webh
|
||||
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 { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
@ -33,6 +34,7 @@ export class BillingController {
|
||||
private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly billingWebhookProductService: BillingWebhookProductService,
|
||||
private readonly billingWebhookPriceService: BillingWebhookPriceService,
|
||||
) {}
|
||||
|
||||
@Post('/webhooks')
|
||||
@ -96,6 +98,21 @@ export class BillingController {
|
||||
) {
|
||||
await this.billingWebhookProductService.processStripeEvent(event.data);
|
||||
}
|
||||
if (
|
||||
event.type === WebhookEvent.PRICE_CREATED ||
|
||||
event.type === WebhookEvent.PRICE_UPDATED
|
||||
) {
|
||||
try {
|
||||
await this.billingWebhookPriceService.processStripeEvent(event.data);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BillingException &&
|
||||
error.code === BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND
|
||||
) {
|
||||
res.status(404).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
@ -11,4 +11,5 @@ export class BillingException extends CustomException {
|
||||
|
||||
export enum BillingExceptionCode {
|
||||
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND',
|
||||
BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND',
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/
|
||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
@ -56,6 +57,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
BillingWorkspaceMemberListener,
|
||||
BillingService,
|
||||
BillingWebhookProductService,
|
||||
BillingWebhookPriceService,
|
||||
BillingRestApiExceptionFilter,
|
||||
],
|
||||
exports: [
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@ -18,10 +17,6 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
|
||||
|
||||
@Entity({ name: 'billingCustomer', schema: 'core' })
|
||||
@ObjectType('billingCustomer')
|
||||
@Unique('IndexOnWorkspaceIdAndStripeCustomerIdUnique', [
|
||||
'workspaceId',
|
||||
'stripeCustomerId',
|
||||
])
|
||||
export class BillingCustomer {
|
||||
@IDField(() => UUIDScalarType)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -36,7 +31,7 @@ export class BillingCustomer {
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
@Column({ nullable: false, type: 'uuid', unique: true })
|
||||
workspaceId: string;
|
||||
|
||||
@Column({ nullable: false, unique: true })
|
||||
|
@ -52,7 +52,6 @@ export class BillingEntitlement {
|
||||
(billingCustomer) => billingCustomer.billingEntitlements,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
createForeignKeyConstraints: false, // TODO: remove this once the customer table is populated
|
||||
},
|
||||
)
|
||||
@JoinColumn({
|
||||
|
@ -16,10 +16,6 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
|
||||
'billingSubscriptionId',
|
||||
'stripeProductId',
|
||||
])
|
||||
@Unique('IndexOnBillingSubscriptionIdAndStripeSubscriptionItemIdUnique', [
|
||||
'billingSubscriptionId',
|
||||
'stripeSubscriptionItemId',
|
||||
])
|
||||
export class BillingSubscriptionItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@ -60,9 +56,9 @@ export class BillingSubscriptionItem {
|
||||
@Column({ nullable: false })
|
||||
stripePriceId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
stripeSubscriptionItemId: string; //TODO: add unique
|
||||
@Column({ nullable: false, unique: true })
|
||||
stripeSubscriptionItemId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
quantity: number; //TODO: add nullable and modify stripe service
|
||||
@Column({ nullable: true, type: 'numeric' })
|
||||
quantity: number | null;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
@ -25,6 +26,10 @@ registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
|
||||
registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' });
|
||||
|
||||
@Entity({ name: 'billingSubscription', schema: 'core' })
|
||||
@Index('IndexOnActiveSubscriptionPerWorkspace', ['workspaceId'], {
|
||||
unique: true,
|
||||
where: `status IN ('trialing', 'active', 'past_due')`,
|
||||
})
|
||||
@ObjectType('BillingSubscription')
|
||||
export class BillingSubscription {
|
||||
@IDField(() => UUIDScalarType)
|
||||
@ -76,14 +81,14 @@ export class BillingSubscription {
|
||||
(billingCustomer) => billingCustomer.billingSubscriptions,
|
||||
{
|
||||
nullable: false,
|
||||
createForeignKeyConstraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
)
|
||||
@JoinColumn({
|
||||
referencedColumnName: 'stripeCustomerId',
|
||||
name: 'stripeCustomerId',
|
||||
})
|
||||
billingCustomer: Relation<BillingCustomer>; //let's see if it works
|
||||
billingCustomer: Relation<BillingCustomer>;
|
||||
|
||||
@Column({ nullable: false, default: false })
|
||||
cancelAtPeriodEnd: boolean;
|
||||
|
@ -6,4 +6,6 @@ export enum WebhookEvent {
|
||||
CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED = 'entitlements.active_entitlement_summary.updated',
|
||||
PRODUCT_CREATED = 'product.created',
|
||||
PRODUCT_UPDATED = 'product.updated',
|
||||
PRICE_CREATED = 'price.created',
|
||||
PRICE_UPDATED = 'price.updated',
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
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 { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
||||
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util';
|
||||
@Injectable()
|
||||
export class BillingWebhookPriceService {
|
||||
protected readonly logger = new Logger(BillingWebhookPriceService.name);
|
||||
constructor(
|
||||
private readonly stripeService: StripeService,
|
||||
@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.stripeService.getMeter(meterId);
|
||||
|
||||
await this.billingMeterRepository.upsert(
|
||||
transformStripeMeterDataToMeterRepositoryData(meterData),
|
||||
{
|
||||
conflictPaths: ['stripeMeterId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await this.billingPriceRepository.upsert(
|
||||
transformStripePriceEventToPriceRepositoryData(data),
|
||||
{
|
||||
conflictPaths: ['stripePriceId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ export class BillingWebhookProductService {
|
||||
return hasBillingPlanKey && hasPriceUsageBased;
|
||||
}
|
||||
|
||||
isValidBillingPlanKey(planKey: string | undefined) {
|
||||
isValidBillingPlanKey(planKey?: string) {
|
||||
switch (planKey) {
|
||||
case BillingPlanKey.BASE_PLAN:
|
||||
return true;
|
||||
@ -61,7 +61,7 @@ export class BillingWebhookProductService {
|
||||
}
|
||||
}
|
||||
|
||||
isValidPriceUsageBased(priceUsageBased: string | undefined) {
|
||||
isValidPriceUsageBased(priceUsageBased?: string) {
|
||||
switch (priceUsageBased) {
|
||||
case BillingUsageType.METERED:
|
||||
return true;
|
||||
|
@ -54,7 +54,7 @@ export class BillingWebhookSubscriptionService {
|
||||
data,
|
||||
),
|
||||
{
|
||||
conflictPaths: ['workspaceId', 'stripeCustomerId'],
|
||||
conflictPaths: ['workspaceId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
|
@ -114,10 +114,7 @@ export class StripeService {
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
});
|
||||
} // I prefered to not create a customer with metadat before the checkout, because it would break the tax calculation
|
||||
// Indeed when the checkout session is created, the customer is created and the tax calculation is done
|
||||
// If we create a customer before the checkout session, the tax calculation is not done and the checkout session will fail
|
||||
// I think that it's not risk worth to create a customer before the checkout session, it would only complicate the code for no signigicant gain
|
||||
}
|
||||
|
||||
async collectLastInvoice(stripeSubscriptionId: string) {
|
||||
const subscription = await this.stripe.subscriptions.retrieve(
|
||||
@ -146,7 +143,10 @@ export class StripeService {
|
||||
stripeSubscriptionItem.stripeSubscriptionItemId,
|
||||
{
|
||||
price: stripePriceId,
|
||||
quantity: stripeSubscriptionItem.quantity,
|
||||
quantity:
|
||||
stripeSubscriptionItem.quantity === null
|
||||
? undefined
|
||||
: stripeSubscriptionItem.quantity,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -164,6 +164,10 @@ export class StripeService {
|
||||
return await this.stripe.customers.retrieve(stripeCustomerId);
|
||||
}
|
||||
|
||||
async getMeter(stripeMeterId: string) {
|
||||
return await this.stripe.billing.meters.retrieve(stripeMeterId);
|
||||
}
|
||||
|
||||
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
||||
const productPrices: ProductPriceEntity[] = Object.values(
|
||||
prices
|
||||
|
@ -0,0 +1,40 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
|
||||
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
|
||||
|
||||
export const transformStripeMeterDataToMeterRepositoryData = (
|
||||
data: Stripe.Billing.Meter,
|
||||
) => {
|
||||
return {
|
||||
stripeMeterId: data.id,
|
||||
displayName: data.display_name,
|
||||
eventName: data.event_name,
|
||||
status: getBillingMeterStatus(data.status),
|
||||
customerMapping: data.customer_mapping,
|
||||
eventTimeWindow: data.event_time_window
|
||||
? getBillingMeterEventTimeWindow(data.event_time_window)
|
||||
: undefined,
|
||||
valueSettings: data.value_settings,
|
||||
};
|
||||
};
|
||||
|
||||
const getBillingMeterStatus = (data: Stripe.Billing.Meter.Status) => {
|
||||
switch (data) {
|
||||
case 'active':
|
||||
return BillingMeterStatus.ACTIVE;
|
||||
case 'inactive':
|
||||
return BillingMeterStatus.INACTIVE;
|
||||
}
|
||||
};
|
||||
|
||||
const getBillingMeterEventTimeWindow = (
|
||||
data: Stripe.Billing.Meter.EventTimeWindow,
|
||||
) => {
|
||||
switch (data) {
|
||||
case 'day':
|
||||
return BillingMeterEventTimeWindow.DAY;
|
||||
case 'hour':
|
||||
return BillingMeterEventTimeWindow.HOUR;
|
||||
}
|
||||
};
|
@ -0,0 +1,113 @@
|
||||
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 transformStripePriceEventToPriceRepositoryData = (
|
||||
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;
|
||||
}
|
||||
};
|
@ -13,8 +13,9 @@ export const transformStripeProductEventToProductRepositoryData = (
|
||||
defaultStripePriceId: data.object.default_price
|
||||
? String(data.object.default_price)
|
||||
: undefined,
|
||||
unitLabel: data.object.unit_label ?? undefined,
|
||||
url: data.object.url ?? 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,
|
||||
};
|
||||
};
|
||||
|
@ -17,7 +17,10 @@ export const transformStripeSubscriptionEventToSubscriptionItemRepositoryData =
|
||||
stripeSubscriptionItemId: item.id,
|
||||
quantity: item.quantity,
|
||||
metadata: item.metadata,
|
||||
billingThresholds: item.billing_thresholds ?? undefined,
|
||||
billingThresholds:
|
||||
item.billing_thresholds === null
|
||||
? undefined
|
||||
: item.billing_thresholds,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -25,8 +25,14 @@ export const transformStripeSubscriptionEventToSubscriptionRepositoryData = (
|
||||
BillingSubscriptionCollectionMethod[
|
||||
data.object.collection_method.toUpperCase()
|
||||
],
|
||||
automaticTax: data.object.automatic_tax ?? undefined,
|
||||
cancellationDetails: data.object.cancellation_details ?? undefined,
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user