fix subscription item update (#11648)
closes https://twenty-v7.sentry.io/issues/6550388239/?environment=prod&project=4507072499810304&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D&referrer=issue-stream&stream_index=6
This commit is contained in:
@ -19,7 +19,7 @@ export const useGetWorkflowNodeExecutionUsage = () => {
|
||||
return {
|
||||
usageQuantity: 0,
|
||||
freeUsageQuantity: 0,
|
||||
includedFreeQuantity: 0,
|
||||
includedFreeQuantity: 10000,
|
||||
paidUsageQuantity: 0,
|
||||
unitPriceCents: 0,
|
||||
totalCostCents: 0,
|
||||
|
||||
@ -106,7 +106,9 @@ export const SettingsBilling = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsBillingMonthlyCreditsSection />
|
||||
{hasNotCanceledCurrentSubscription && (
|
||||
<SettingsBillingMonthlyCreditsSection />
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Manage your subscription`}
|
||||
|
||||
@ -14,6 +14,7 @@ export enum BillingExceptionCode {
|
||||
BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND',
|
||||
BILLING_PRICE_NOT_FOUND = 'BILLING_PRICE_NOT_FOUND',
|
||||
BILLING_METER_NOT_FOUND = 'BILLING_METER_NOT_FOUND',
|
||||
BILLING_SUBSCRIPTION_NOT_FOUND = 'BILLING_SUBSCRIPTION_NOT_FOUND',
|
||||
BILLING_SUBSCRIPTION_ITEM_NOT_FOUND = 'BILLING_SUBSCRIPTION_ITEM_NOT_FOUND',
|
||||
BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND',
|
||||
BILLING_CUSTOMER_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_CUSTOMER_EVENT_WORKSPACE_NOT_FOUND',
|
||||
|
||||
@ -45,6 +45,7 @@ export class BillingRestApiExceptionFilter implements ExceptionFilter {
|
||||
case BillingExceptionCode.BILLING_PLAN_NOT_FOUND:
|
||||
case BillingExceptionCode.BILLING_METER_NOT_FOUND:
|
||||
case BillingExceptionCode.BILLING_SUBSCRIPTION_ITEM_NOT_FOUND:
|
||||
case BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
|
||||
@ -94,6 +94,13 @@ export class BillingUsageService {
|
||||
{ workspaceId: workspace.id },
|
||||
);
|
||||
|
||||
if (!isDefined(subscription)) {
|
||||
throw new BillingException(
|
||||
'Not-canceled subscription not found',
|
||||
BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const meteredSubscriptionItemDetails =
|
||||
await this.billingSubscriptionItemService.getMeteredSubscriptionItemDetails(
|
||||
subscription.id,
|
||||
|
||||
@ -77,6 +77,7 @@ export class BillingService {
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(subscription) ||
|
||||
![SubscriptionStatus.Active, SubscriptionStatus.Trialing].includes(
|
||||
subscription.status,
|
||||
)
|
||||
|
||||
@ -0,0 +1,658 @@
|
||||
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,
|
||||
};
|
||||
@ -6,7 +6,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import Stripe from 'stripe';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
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';
|
||||
@ -15,6 +15,7 @@ import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billin
|
||||
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';
|
||||
@ -100,15 +101,9 @@ export class BillingWebhookSubscriptionService {
|
||||
throw new Error('Billing subscription not found');
|
||||
}
|
||||
|
||||
await this.billingSubscriptionItemRepository.upsert(
|
||||
transformStripeSubscriptionEventToDatabaseSubscriptionItem(
|
||||
updatedBillingSubscription.id,
|
||||
data,
|
||||
),
|
||||
{
|
||||
conflictPaths: ['billingSubscriptionId', 'stripeProductId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
await this.updateBillingSubscriptionItems(
|
||||
updatedBillingSubscription.id,
|
||||
event,
|
||||
);
|
||||
|
||||
if (
|
||||
@ -174,4 +169,33 @@ export class BillingWebhookSubscriptionService {
|
||||
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
/* @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;
|
||||
};
|
||||
Reference in New Issue
Block a user