diff --git a/.changeset/early-bats-shout.md b/.changeset/early-bats-shout.md new file mode 100644 index 00000000000..0b5f0b361eb --- /dev/null +++ b/.changeset/early-bats-shout.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Add billing API for fetching available plans. diff --git a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap index 91dc98edd0f..be155abe172 100644 --- a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap +++ b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap @@ -221,6 +221,8 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = ` "backend/auth-object.mdx", "backend/authenticate-request-options.mdx", "backend/client.mdx", + "backend/commerce-plan-json.mdx", + "backend/commerce-plan.mdx", "backend/email-address.mdx", "backend/external-account.mdx", "backend/get-auth-fn.mdx", diff --git a/packages/backend/src/api/endpoints/BillingApi.ts b/packages/backend/src/api/endpoints/BillingApi.ts new file mode 100644 index 00000000000..64f7ecd09a4 --- /dev/null +++ b/packages/backend/src/api/endpoints/BillingApi.ts @@ -0,0 +1,26 @@ +import type { ClerkPaginationRequest } from '@clerk/types'; + +import { joinPaths } from '../../util/path'; +import type { CommercePlan } from '../resources/CommercePlan'; +import type { PaginatedResourceResponse } from '../resources/Deserializer'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/commerce'; + +type GetOrganizationListParams = ClerkPaginationRequest<{ + payerType: 'org' | 'user'; +}>; + +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. + * It is advised to pin the SDK version to avoid breaking changes. + */ + public async getPlanList(params?: GetOrganizationListParams) { + return this.request>({ + method: 'GET', + path: joinPaths(basePath, 'plans'), + queryParams: params, + }); + } +} diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index db0b1cfeac1..f2228b49ca7 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -29,6 +29,7 @@ import { WaitlistEntryAPI, WebhookAPI, } from './endpoints'; +import { BillingAPI } from './endpoints/BillingApi'; import { buildRequest } from './request'; export type CreateBackendApiOptions = Parameters[0]; @@ -52,6 +53,11 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { ), betaFeatures: new BetaFeaturesAPI(request), blocklistIdentifiers: new BlocklistIdentifierAPI(request), + /** + * @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. + */ + billing: new BillingAPI(request), clients: new ClientAPI(request), domains: new DomainAPI(request), emailAddresses: new EmailAddressAPI(request), diff --git a/packages/backend/src/api/resources/CommercePlan.ts b/packages/backend/src/api/resources/CommercePlan.ts new file mode 100644 index 00000000000..931729e74e0 --- /dev/null +++ b/packages/backend/src/api/resources/CommercePlan.ts @@ -0,0 +1,101 @@ +import { Feature } from './Feature'; +import type { CommercePlanJSON } from './JSON'; + +type CommerceFee = { + amount: number; + amountFormatted: string; + currency: string; + currencySymbol: string; +}; + +/** + * @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 CommercePlan { + constructor( + /** + * The unique identifier for the plan. + */ + readonly id: string, + /** + * The id of the product the plan belongs to. + */ + readonly productId: string, + /** + * The name of the plan. + */ + readonly name: string, + /** + * The URL-friendly identifier of the plan. + */ + readonly slug: string, + /** + * The description of the plan. + */ + readonly description: string | undefined, + /** + * Whether the plan is the default plan. + */ + readonly isDefault: boolean, + /** + * Whether the plan is recurring. + */ + readonly isRecurring: boolean, + /** + * Whether the plan has a base fee. + */ + readonly hasBaseFee: boolean, + /** + * Whether the plan is displayed in the `` component. + */ + readonly publiclyVisible: boolean, + /** + * The monthly fee of the plan. + */ + readonly fee: CommerceFee, + /** + * The annual fee of the plan. + */ + readonly annualFee: CommerceFee, + /** + * The annual fee of the plan on a monthly basis. + */ + readonly annualMonthlyFee: CommerceFee, + /** + * The type of payer for the plan. + */ + readonly forPayerType: 'org' | 'user', + /** + * The features the plan offers. + */ + readonly features: Feature[], + ) {} + + static fromJSON(data: CommercePlanJSON): CommercePlan { + const formatAmountJSON = (fee: CommercePlanJSON['fee']) => { + return { + amount: fee.amount, + amountFormatted: fee.amount_formatted, + currency: fee.currency, + currencySymbol: fee.currency_symbol, + }; + }; + return new CommercePlan( + data.id, + data.product_id, + data.name, + data.slug, + data.description, + data.is_default, + data.is_recurring, + data.has_base_fee, + data.publicly_visible, + formatAmountJSON(data.fee), + formatAmountJSON(data.annual_fee), + formatAmountJSON(data.annual_monthly_fee), + data.for_payer_type, + data.features.map(feature => Feature.fromJSON(feature)), + ); + } +} diff --git a/packages/backend/src/api/resources/Deserializer.ts b/packages/backend/src/api/resources/Deserializer.ts index 72bfbc59ce6..5608df9e537 100644 --- a/packages/backend/src/api/resources/Deserializer.ts +++ b/packages/backend/src/api/resources/Deserializer.ts @@ -37,6 +37,8 @@ import { User, } from '.'; import { AccountlessApplication } from './AccountlessApplication'; +import { CommercePlan } from './CommercePlan'; +import { Feature } from './Feature'; import type { PaginatedResponseJSON } from './JSON'; import { ObjectType } from './JSON'; import { WaitlistEntry } from './WaitlistEntry'; @@ -179,6 +181,10 @@ function jsonToObject(item: any): any { return User.fromJSON(item); case ObjectType.WaitlistEntry: return WaitlistEntry.fromJSON(item); + case ObjectType.CommercePlan: + return CommercePlan.fromJSON(item); + case ObjectType.Feature: + return Feature.fromJSON(item); default: return item; } diff --git a/packages/backend/src/api/resources/Feature.ts b/packages/backend/src/api/resources/Feature.ts new file mode 100644 index 00000000000..f1fa1d48774 --- /dev/null +++ b/packages/backend/src/api/resources/Feature.ts @@ -0,0 +1,15 @@ +import type { FeatureJSON } from './JSON'; + +export class Feature { + constructor( + readonly id: string, + readonly name: string, + readonly description: string, + readonly slug: string, + readonly avatarUrl: string, + ) {} + + static fromJSON(data: FeatureJSON): Feature { + return new Feature(data.id, data.name, data.description, data.slug, data.avatar_url); + } +} diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 2bfde1520e9..a38f75ca216 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -69,6 +69,8 @@ export const ObjectType = { CommercePaymentAttempt: 'commerce_payment_attempt', CommerceSubscription: 'commerce_subscription', CommerceSubscriptionItem: 'commerce_subscription_item', + CommercePlan: 'commerce_plan', + Feature: 'feature', } as const; export type ObjectType = (typeof ObjectType)[keyof typeof ObjectType]; @@ -791,71 +793,65 @@ export interface CommercePayerJSON extends ClerkResourceJSON { updated_at: number; } -export interface CommercePayeeJSON { +interface CommercePayeeJSON { id: string; gateway_type: string; gateway_external_id: string; gateway_status: 'active' | 'pending' | 'restricted' | 'disconnected'; } -export interface CommerceAmountJSON { +interface CommerceFeeJSON { amount: number; amount_formatted: string; currency: string; currency_symbol: string; } -export interface CommerceTotalsJSON { - subtotal: CommerceAmountJSON; - tax_total: CommerceAmountJSON; - grand_total: CommerceAmountJSON; +interface CommerceTotalsJSON { + subtotal: CommerceFeeJSON; + tax_total: CommerceFeeJSON; + grand_total: CommerceFeeJSON; } -export interface CommercePaymentSourceJSON { - id: string; - gateway: string; - gateway_external_id: string; - gateway_external_account_id?: string; - payment_method: string; - status: 'active' | 'disconnected'; - card_type?: string; - last4?: string; -} - -export interface CommercePaymentFailedReasonJSON { - code: string; - decline_code: string; -} - -export interface CommerceSubscriptionCreditJSON { - amount: CommerceAmountJSON; - cycle_days_remaining: number; - cycle_days_total: number; - cycle_remaining_percent: number; +export interface FeatureJSON extends ClerkResourceJSON { + object: typeof ObjectType.Feature; + name: string; + description: string; + slug: string; + avatar_url: string; } -export interface CommercePlanJSON { +/** + * @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 CommercePlanJSON extends ClerkResourceJSON { + object: typeof ObjectType.CommercePlan; id: string; - instance_id: string; product_id: string; name: string; slug: string; description?: string; is_default: boolean; is_recurring: boolean; - amount: number; - period: 'month' | 'annual'; - interval: number; has_base_fee: boolean; - currency: string; - annual_monthly_amount: number; publicly_visible: boolean; + fee: CommerceFeeJSON; + annual_fee: CommerceFeeJSON; + annual_monthly_fee: CommerceFeeJSON; + for_payer_type: 'org' | 'user'; + features: FeatureJSON[]; } export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { object: typeof ObjectType.CommerceSubscriptionItem; status: 'abandoned' | 'active' | 'canceled' | 'ended' | 'expired' | 'incomplete' | 'past_due' | 'upcoming'; - credit: CommerceSubscriptionCreditJSON; + credit: { + amount: CommerceFeeJSON; + cycle_days_remaining: number; + cycle_days_total: number; + cycle_remaining_percent: number; + }; proration_date: string; plan_period: 'month' | 'annual'; period_start: number; @@ -865,8 +861,24 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON { lifetime_paid: number; next_payment_amount: number; next_payment_date: number; - amount: CommerceAmountJSON; - plan: CommercePlanJSON; + amount: CommerceFeeJSON; + plan: { + id: string; + instance_id: string; + product_id: string; + name: string; + slug: string; + description?: string; + is_default: boolean; + is_recurring: boolean; + amount: number; + period: 'month' | 'annual'; + interval: number; + has_base_fee: boolean; + currency: string; + annual_monthly_amount: number; + publicly_visible: boolean; + }; plan_id: string; } @@ -881,13 +893,25 @@ export interface CommercePaymentAttemptJSON extends ClerkResourceJSON { updated_at: number; paid_at?: number; failed_at?: number; - failed_reason?: CommercePaymentFailedReasonJSON; + failed_reason?: { + code: string; + decline_code: string; + }; billing_date: number; charge_type: 'checkout' | 'recurring'; payee: CommercePayeeJSON; payer: CommercePayerJSON; totals: CommerceTotalsJSON; - payment_source: CommercePaymentSourceJSON; + payment_source: { + id: string; + gateway: string; + gateway_external_id: string; + gateway_external_account_id?: string; + payment_method: string; + status: 'active' | 'disconnected'; + card_type?: string; + last4?: string; + }; subscription_items: CommerceSubscriptionItemJSON[]; } diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 566efbdebbf..edc03a18db4 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -57,6 +57,7 @@ export * from './User'; export * from './Verification'; export * from './WaitlistEntry'; export * from './Web3Wallet'; +export * from './CommercePlan'; export type { EmailWebhookEvent, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7d072d4e841..21d29130e78 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -101,12 +101,6 @@ export type { TestingTokenJSON, WebhooksSvixJSON, CommercePayerJSON, - CommercePayeeJSON, - CommerceAmountJSON, - CommerceTotalsJSON, - CommercePaymentSourceJSON, - CommercePaymentFailedReasonJSON, - CommerceSubscriptionCreditJSON, CommercePlanJSON, CommerceSubscriptionItemJSON, CommercePaymentAttemptJSON, @@ -150,6 +144,7 @@ export type { Token, User, TestingToken, + CommercePlan, } from './api/resources'; /**