From e8373405aa56306827c0019805642f04e9e1a632 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Aug 2025 21:08:04 +0300 Subject: [PATCH 1/3] feat(backend): Add `cancelSubscriptionItem` to BillingApi --- .changeset/eight-sheep-matter.md | 5 + .../backend/src/api/endpoints/BillingApi.ts | 22 ++++ .../backend/src/api/resources/CommercePlan.ts | 2 +- .../api/resources/CommerceSubscriptionItem.ts | 104 ++++++++++++++++++ .../backend/src/api/resources/Deserializer.ts | 3 + packages/backend/src/api/resources/JSON.ts | 51 ++++++++- .../backend/src/api/resources/Webhooks.ts | 12 +- packages/backend/src/api/resources/index.ts | 1 + packages/backend/src/index.ts | 7 +- 9 files changed, 192 insertions(+), 15 deletions(-) create mode 100644 .changeset/eight-sheep-matter.md create mode 100644 packages/backend/src/api/resources/CommerceSubscriptionItem.ts diff --git a/.changeset/eight-sheep-matter.md b/.changeset/eight-sheep-matter.md new file mode 100644 index 00000000000..cd8895d4189 --- /dev/null +++ b/.changeset/eight-sheep-matter.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +[Billing Beta] Add `cancelSubscriptionItem` to BillingApi. diff --git a/packages/backend/src/api/endpoints/BillingApi.ts b/packages/backend/src/api/endpoints/BillingApi.ts index 64f7ecd09a4..f30777d79d0 100644 --- a/packages/backend/src/api/endpoints/BillingApi.ts +++ b/packages/backend/src/api/endpoints/BillingApi.ts @@ -2,6 +2,7 @@ import type { ClerkPaginationRequest } from '@clerk/types'; import { joinPaths } from '../../util/path'; import type { CommercePlan } from '../resources/CommercePlan'; +import type { CommerceSubscriptionItem } from '../resources/CommerceSubscriptionItem'; import type { PaginatedResourceResponse } from '../resources/Deserializer'; import { AbstractAPI } from './AbstractApi'; @@ -11,6 +12,14 @@ type GetOrganizationListParams = ClerkPaginationRequest<{ payerType: 'org' | 'user'; }>; +type CancelSubscriptionItemParams = { + /** + * If true, the subscription item will be canceled immediately. If false or undefined, the subscription item will be canceled at the end of the current billing period. + * @default undefined + */ + endNow?: boolean; +}; + export class BillingAPI extends AbstractAPI { /** * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. @@ -23,4 +32,17 @@ export class BillingAPI extends AbstractAPI { queryParams: params, }); } + + /** + * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. + * It is advised to pin the SDK version to avoid breaking changes. + */ + public async cancelSubscriptionItem(subscriptionItemId: string, params?: CancelSubscriptionItemParams) { + this.requireId(subscriptionItemId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, 'subscription_items', subscriptionItemId), + queryParams: params, + }); + } } diff --git a/packages/backend/src/api/resources/CommercePlan.ts b/packages/backend/src/api/resources/CommercePlan.ts index 06a73a44029..8cf2ffbc968 100644 --- a/packages/backend/src/api/resources/CommercePlan.ts +++ b/packages/backend/src/api/resources/CommercePlan.ts @@ -1,7 +1,7 @@ import { Feature } from './Feature'; import type { CommercePlanJSON } from './JSON'; -type CommerceMoneyAmount = { +export type CommerceMoneyAmount = { amount: number; amountFormatted: string; currency: string; diff --git a/packages/backend/src/api/resources/CommerceSubscriptionItem.ts b/packages/backend/src/api/resources/CommerceSubscriptionItem.ts new file mode 100644 index 00000000000..ae171ec9a3f --- /dev/null +++ b/packages/backend/src/api/resources/CommerceSubscriptionItem.ts @@ -0,0 +1,104 @@ +import type { CommerceMoneyAmountJSON } from '@clerk/types'; + +import { type CommerceMoneyAmount, CommercePlan } from './CommercePlan'; +import type { CommerceSubscriptionItemJSON } from './JSON'; + +/** + * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. + * It is advised to pin the SDK version to avoid breaking changes. + */ +export class CommerceSubscriptionItem { + constructor( + /** + * The unique identifier for the subscription item. + */ + readonly id: string, + /** + * The status of the subscription item. + */ + readonly status: CommerceSubscriptionItemJSON['status'], + /** + * The plan period for the subscription item. + */ + readonly planPeriod: 'month' | 'annual', + /** + * The start of the current period. + */ + readonly periodStart: number, + /** + * The next payment information. + */ + readonly nextPayment: { + amount: number; + date: number; + } | null, + /** + * The current amount for the subscription item. + */ + readonly amount: CommerceMoneyAmount | null | undefined, + /** + * The plan associated with this subscription item. + */ + readonly plan: CommercePlan, + /** + * The plan ID. + */ + readonly planId: string, + /** + * The end of the current period. + */ + readonly periodEnd?: number, + /** + * When the subscription item was canceled. + */ + readonly canceledAt?: number, + /** + * When the subscription item became past due. + */ + readonly pastDueAt?: number, + /** + * The lifetime amount paid for this subscription item. + */ + readonly lifetimePaid?: CommerceMoneyAmount | null, + ) {} + + static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem { + console.log('data', data); + + function formatAmountJSON(amount: null): null; + function formatAmountJSON(amount: undefined): undefined; + function formatAmountJSON(amount: CommerceMoneyAmountJSON): CommerceMoneyAmount; + function formatAmountJSON( + amount: CommerceMoneyAmountJSON | null | undefined, + ): CommerceMoneyAmount | null | undefined; + function formatAmountJSON( + amount: CommerceMoneyAmountJSON | null | undefined, + ): CommerceMoneyAmount | null | undefined { + if (!amount) { + return amount; + } + + return { + amount: amount.amount, + amountFormatted: amount.amount_formatted, + currency: amount.currency, + currencySymbol: amount.currency_symbol, + }; + } + + return new CommerceSubscriptionItem( + data.id, + data.status, + data.plan_period, + data.period_start, + data.next_payment, + formatAmountJSON(data.amount), + CommercePlan.fromJSON(data.plan), + data.plan_id, + data.period_end, + data.canceled_at, + data.past_due_at, + formatAmountJSON(data.lifetime_paid), + ); + } +} diff --git a/packages/backend/src/api/resources/Deserializer.ts b/packages/backend/src/api/resources/Deserializer.ts index 5608df9e537..55e59e19d29 100644 --- a/packages/backend/src/api/resources/Deserializer.ts +++ b/packages/backend/src/api/resources/Deserializer.ts @@ -38,6 +38,7 @@ import { } from '.'; import { AccountlessApplication } from './AccountlessApplication'; import { CommercePlan } from './CommercePlan'; +import { CommerceSubscriptionItem } from './CommerceSubscriptionItem'; import { Feature } from './Feature'; import type { PaginatedResponseJSON } from './JSON'; import { ObjectType } from './JSON'; @@ -183,6 +184,8 @@ function jsonToObject(item: any): any { return WaitlistEntry.fromJSON(item); case ObjectType.CommercePlan: return CommercePlan.fromJSON(item); + case ObjectType.CommerceSubscriptionItem: + return CommerceSubscriptionItem.fromJSON(item); case ObjectType.Feature: return Feature.fromJSON(item); default: diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 4a744facad9..2c5d1ae605d 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -843,9 +843,44 @@ export interface CommercePlanJSON extends ClerkResourceJSON { features: FeatureJSON[]; } +type CommerceSubscriptionItemStatus = + | 'abandoned' + | 'active' + | 'canceled' + | 'ended' + | 'expired' + | 'incomplete' + | 'past_due' + | 'upcoming'; + +/** + * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. + * It is advised to pin the SDK version to avoid breaking changes. + */ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { object: typeof ObjectType.CommerceSubscriptionItem; - status: 'abandoned' | 'active' | 'canceled' | 'ended' | 'expired' | 'incomplete' | 'past_due' | 'upcoming'; + status: CommerceSubscriptionItemStatus; + plan_period: 'month' | 'annual'; + period_start: number; + period_end?: number; + canceled_at?: number; + past_due_at?: number; + lifetime_paid: CommerceMoneyAmountJSON; + next_payment: { + amount: number; + date: number; + } | null; + amount: CommerceMoneyAmountJSON | null; + plan: CommercePlanJSON; + plan_id: string; +} + +/** + * Webhooks specific interface for CommerceSubscriptionItem. + */ +export interface CommerceSubscriptionItemWebhookEventJSON extends ClerkResourceJSON { + object: typeof ObjectType.CommerceSubscriptionItem; + status: CommerceSubscriptionItemStatus; credit: { amount: CommerceMoneyAmountJSON; cycle_days_remaining: number; @@ -882,7 +917,10 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { plan_id: string; } -export interface CommercePaymentAttemptJSON extends ClerkResourceJSON { +/** + * Webhooks specific interface for CommercePaymentAttempt. + */ +export interface CommercePaymentAttemptWebhookEventJSON extends ClerkResourceJSON { object: typeof ObjectType.CommercePaymentAttempt; instance_id: string; payment_id: string; @@ -912,10 +950,13 @@ export interface CommercePaymentAttemptJSON extends ClerkResourceJSON { card_type?: string; last4?: string; }; - subscription_items: CommerceSubscriptionItemJSON[]; + subscription_items: CommerceSubscriptionItemWebhookEventJSON[]; } -export interface CommerceSubscriptionJSON extends ClerkResourceJSON { +/** + * Webhooks specific interface for CommerceSubscription. + */ +export interface CommerceSubscriptionWebhookEventJSON extends ClerkResourceJSON { object: typeof ObjectType.CommerceSubscription; status: 'abandoned' | 'active' | 'canceled' | 'ended' | 'expired' | 'incomplete' | 'past_due' | 'upcoming'; active_at?: number; @@ -928,7 +969,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON { payer_id: string; payer: CommercePayerJSON; payment_source_id: string; - items: CommerceSubscriptionItemJSON[]; + items: CommerceSubscriptionItemWebhookEventJSON[]; } export interface WebhooksSvixJSON { diff --git a/packages/backend/src/api/resources/Webhooks.ts b/packages/backend/src/api/resources/Webhooks.ts index 18d7c333c59..b9b60018fa7 100644 --- a/packages/backend/src/api/resources/Webhooks.ts +++ b/packages/backend/src/api/resources/Webhooks.ts @@ -1,7 +1,7 @@ import type { - CommercePaymentAttemptJSON, - CommerceSubscriptionItemJSON, - CommerceSubscriptionJSON, + CommercePaymentAttemptWebhookEventJSON, + CommerceSubscriptionItemWebhookEventJSON, + CommerceSubscriptionWebhookEventJSON, DeletedObjectJSON, EmailJSON, OrganizationDomainJSON, @@ -67,12 +67,12 @@ export type WaitlistEntryWebhookEvent = Webhook<'waitlistEntry.created' | 'waitl export type CommercePaymentAttemptWebhookEvent = Webhook< 'paymentAttempt.created' | 'paymentAttempt.updated', - CommercePaymentAttemptJSON + CommercePaymentAttemptWebhookEventJSON >; export type CommerceSubscriptionWebhookEvent = Webhook< 'subscription.created' | 'subscription.updated' | 'subscription.active' | 'subscription.past_due', - CommerceSubscriptionJSON + CommerceSubscriptionWebhookEventJSON >; export type CommerceSubscriptionItemWebhookEvent = Webhook< @@ -85,7 +85,7 @@ export type CommerceSubscriptionItemWebhookEvent = Webhook< | 'subscriptionItem.abandoned' | 'subscriptionItem.incomplete' | 'subscriptionItem.past_due', - CommerceSubscriptionItemJSON + CommerceSubscriptionItemWebhookEventJSON >; export type WebhookEvent = diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index edc03a18db4..7f00decb20b 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -58,6 +58,7 @@ export * from './Verification'; export * from './WaitlistEntry'; export * from './Web3Wallet'; export * from './CommercePlan'; +export * from './CommerceSubscriptionItem'; export type { EmailWebhookEvent, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index de6f3ef4b98..084f8c3bcf3 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -100,11 +100,8 @@ export type { PaginatedResponseJSON, TestingTokenJSON, WebhooksSvixJSON, - CommercePayerJSON, CommercePlanJSON, CommerceSubscriptionItemJSON, - CommercePaymentAttemptJSON, - CommerceSubscriptionJSON, } from './api/resources/JSON'; /** @@ -147,6 +144,7 @@ export type { User, TestingToken, CommercePlan, + CommerceSubscriptionItem, } from './api/resources'; /** @@ -166,6 +164,9 @@ export type { WaitlistEntryWebhookEvent, WebhookEvent, WebhookEventType, + CommercePaymentAttemptWebhookEvent, + CommerceSubscriptionWebhookEvent, + CommerceSubscriptionItemWebhookEvent, } from './api/resources/Webhooks'; /** From 67f538a0886512b0d04f86fe2c480c3d38fe7578 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Aug 2025 21:14:24 +0300 Subject: [PATCH 2/3] remove necessary pieces --- .../backend/src/api/resources/CommerceSubscriptionItem.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/backend/src/api/resources/CommerceSubscriptionItem.ts b/packages/backend/src/api/resources/CommerceSubscriptionItem.ts index ae171ec9a3f..4178b926876 100644 --- a/packages/backend/src/api/resources/CommerceSubscriptionItem.ts +++ b/packages/backend/src/api/resources/CommerceSubscriptionItem.ts @@ -63,11 +63,6 @@ export class CommerceSubscriptionItem { ) {} static fromJSON(data: CommerceSubscriptionItemJSON): CommerceSubscriptionItem { - console.log('data', data); - - function formatAmountJSON(amount: null): null; - function formatAmountJSON(amount: undefined): undefined; - function formatAmountJSON(amount: CommerceMoneyAmountJSON): CommerceMoneyAmount; function formatAmountJSON( amount: CommerceMoneyAmountJSON | null | undefined, ): CommerceMoneyAmount | null | undefined; From 5115fe6d163fa19de447f649db8f428886ff835d Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 22 Aug 2025 22:32:23 +0300 Subject: [PATCH 3/3] update snapshot --- .typedoc/__tests__/__snapshots__/file-structure.test.ts.snap | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap index 6102b6a89c1..6a75f9fcb75 100644 --- a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap +++ b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap @@ -222,8 +222,13 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = ` "backend/auth-object.mdx", "backend/authenticate-request-options.mdx", "backend/client.mdx", + "backend/commerce-payment-attempt-webhook-event-json.mdx", "backend/commerce-plan-json.mdx", "backend/commerce-plan.mdx", + "backend/commerce-subscription-item-json.mdx", + "backend/commerce-subscription-item-webhook-event-json.mdx", + "backend/commerce-subscription-item.mdx", + "backend/commerce-subscription-webhook-event-json.mdx", "backend/email-address.mdx", "backend/external-account.mdx", "backend/get-auth-fn.mdx",