Skip to content

Commit 6ff416f

Browse files
authored
feat(backend): Add billing api and an endpoint for fetching plans (#6449)
1 parent 4edef81 commit 6ff416f

File tree

10 files changed

+226
-45
lines changed

10 files changed

+226
-45
lines changed

.changeset/early-bats-shout.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': minor
3+
---
4+
5+
Add billing API for fetching available plans.

.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
221221
"backend/auth-object.mdx",
222222
"backend/authenticate-request-options.mdx",
223223
"backend/client.mdx",
224+
"backend/commerce-plan-json.mdx",
225+
"backend/commerce-plan.mdx",
224226
"backend/email-address.mdx",
225227
"backend/external-account.mdx",
226228
"backend/get-auth-fn.mdx",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { ClerkPaginationRequest } from '@clerk/types';
2+
3+
import { joinPaths } from '../../util/path';
4+
import type { CommercePlan } from '../resources/CommercePlan';
5+
import type { PaginatedResourceResponse } from '../resources/Deserializer';
6+
import { AbstractAPI } from './AbstractApi';
7+
8+
const basePath = '/commerce';
9+
10+
type GetOrganizationListParams = ClerkPaginationRequest<{
11+
payerType: 'org' | 'user';
12+
}>;
13+
14+
export class BillingAPI extends AbstractAPI {
15+
/**
16+
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
17+
* It is advised to pin the SDK version to avoid breaking changes.
18+
*/
19+
public async getPlanList(params?: GetOrganizationListParams) {
20+
return this.request<PaginatedResourceResponse<CommercePlan[]>>({
21+
method: 'GET',
22+
path: joinPaths(basePath, 'plans'),
23+
queryParams: params,
24+
});
25+
}
26+
}

packages/backend/src/api/factory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
WaitlistEntryAPI,
3030
WebhookAPI,
3131
} from './endpoints';
32+
import { BillingAPI } from './endpoints/BillingApi';
3233
import { buildRequest } from './request';
3334

3435
export type CreateBackendApiOptions = Parameters<typeof buildRequest>[0];
@@ -52,6 +53,11 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
5253
),
5354
betaFeatures: new BetaFeaturesAPI(request),
5455
blocklistIdentifiers: new BlocklistIdentifierAPI(request),
56+
/**
57+
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
58+
* It is advised to pin the SDK version to avoid breaking changes.
59+
*/
60+
billing: new BillingAPI(request),
5561
clients: new ClientAPI(request),
5662
domains: new DomainAPI(request),
5763
emailAddresses: new EmailAddressAPI(request),
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Feature } from './Feature';
2+
import type { CommercePlanJSON } from './JSON';
3+
4+
type CommerceFee = {
5+
amount: number;
6+
amountFormatted: string;
7+
currency: string;
8+
currencySymbol: string;
9+
};
10+
11+
/**
12+
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
13+
* It is advised to pin the SDK version to avoid breaking changes.
14+
*/
15+
export class CommercePlan {
16+
constructor(
17+
/**
18+
* The unique identifier for the plan.
19+
*/
20+
readonly id: string,
21+
/**
22+
* The id of the product the plan belongs to.
23+
*/
24+
readonly productId: string,
25+
/**
26+
* The name of the plan.
27+
*/
28+
readonly name: string,
29+
/**
30+
* The URL-friendly identifier of the plan.
31+
*/
32+
readonly slug: string,
33+
/**
34+
* The description of the plan.
35+
*/
36+
readonly description: string | undefined,
37+
/**
38+
* Whether the plan is the default plan.
39+
*/
40+
readonly isDefault: boolean,
41+
/**
42+
* Whether the plan is recurring.
43+
*/
44+
readonly isRecurring: boolean,
45+
/**
46+
* Whether the plan has a base fee.
47+
*/
48+
readonly hasBaseFee: boolean,
49+
/**
50+
* Whether the plan is displayed in the `<PriceTable/>` component.
51+
*/
52+
readonly publiclyVisible: boolean,
53+
/**
54+
* The monthly fee of the plan.
55+
*/
56+
readonly fee: CommerceFee,
57+
/**
58+
* The annual fee of the plan.
59+
*/
60+
readonly annualFee: CommerceFee,
61+
/**
62+
* The annual fee of the plan on a monthly basis.
63+
*/
64+
readonly annualMonthlyFee: CommerceFee,
65+
/**
66+
* The type of payer for the plan.
67+
*/
68+
readonly forPayerType: 'org' | 'user',
69+
/**
70+
* The features the plan offers.
71+
*/
72+
readonly features: Feature[],
73+
) {}
74+
75+
static fromJSON(data: CommercePlanJSON): CommercePlan {
76+
const formatAmountJSON = (fee: CommercePlanJSON['fee']) => {
77+
return {
78+
amount: fee.amount,
79+
amountFormatted: fee.amount_formatted,
80+
currency: fee.currency,
81+
currencySymbol: fee.currency_symbol,
82+
};
83+
};
84+
return new CommercePlan(
85+
data.id,
86+
data.product_id,
87+
data.name,
88+
data.slug,
89+
data.description,
90+
data.is_default,
91+
data.is_recurring,
92+
data.has_base_fee,
93+
data.publicly_visible,
94+
formatAmountJSON(data.fee),
95+
formatAmountJSON(data.annual_fee),
96+
formatAmountJSON(data.annual_monthly_fee),
97+
data.for_payer_type,
98+
data.features.map(feature => Feature.fromJSON(feature)),
99+
);
100+
}
101+
}

packages/backend/src/api/resources/Deserializer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {
3737
User,
3838
} from '.';
3939
import { AccountlessApplication } from './AccountlessApplication';
40+
import { CommercePlan } from './CommercePlan';
41+
import { Feature } from './Feature';
4042
import type { PaginatedResponseJSON } from './JSON';
4143
import { ObjectType } from './JSON';
4244
import { WaitlistEntry } from './WaitlistEntry';
@@ -179,6 +181,10 @@ function jsonToObject(item: any): any {
179181
return User.fromJSON(item);
180182
case ObjectType.WaitlistEntry:
181183
return WaitlistEntry.fromJSON(item);
184+
case ObjectType.CommercePlan:
185+
return CommercePlan.fromJSON(item);
186+
case ObjectType.Feature:
187+
return Feature.fromJSON(item);
182188
default:
183189
return item;
184190
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { FeatureJSON } from './JSON';
2+
3+
export class Feature {
4+
constructor(
5+
readonly id: string,
6+
readonly name: string,
7+
readonly description: string,
8+
readonly slug: string,
9+
readonly avatarUrl: string,
10+
) {}
11+
12+
static fromJSON(data: FeatureJSON): Feature {
13+
return new Feature(data.id, data.name, data.description, data.slug, data.avatar_url);
14+
}
15+
}

packages/backend/src/api/resources/JSON.ts

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export const ObjectType = {
6969
CommercePaymentAttempt: 'commerce_payment_attempt',
7070
CommerceSubscription: 'commerce_subscription',
7171
CommerceSubscriptionItem: 'commerce_subscription_item',
72+
CommercePlan: 'commerce_plan',
73+
Feature: 'feature',
7274
} as const;
7375

7476
export type ObjectType = (typeof ObjectType)[keyof typeof ObjectType];
@@ -791,71 +793,65 @@ export interface CommercePayerJSON extends ClerkResourceJSON {
791793
updated_at: number;
792794
}
793795

794-
export interface CommercePayeeJSON {
796+
interface CommercePayeeJSON {
795797
id: string;
796798
gateway_type: string;
797799
gateway_external_id: string;
798800
gateway_status: 'active' | 'pending' | 'restricted' | 'disconnected';
799801
}
800802

801-
export interface CommerceAmountJSON {
803+
interface CommerceFeeJSON {
802804
amount: number;
803805
amount_formatted: string;
804806
currency: string;
805807
currency_symbol: string;
806808
}
807809

808-
export interface CommerceTotalsJSON {
809-
subtotal: CommerceAmountJSON;
810-
tax_total: CommerceAmountJSON;
811-
grand_total: CommerceAmountJSON;
810+
interface CommerceTotalsJSON {
811+
subtotal: CommerceFeeJSON;
812+
tax_total: CommerceFeeJSON;
813+
grand_total: CommerceFeeJSON;
812814
}
813815

814-
export interface CommercePaymentSourceJSON {
815-
id: string;
816-
gateway: string;
817-
gateway_external_id: string;
818-
gateway_external_account_id?: string;
819-
payment_method: string;
820-
status: 'active' | 'disconnected';
821-
card_type?: string;
822-
last4?: string;
823-
}
824-
825-
export interface CommercePaymentFailedReasonJSON {
826-
code: string;
827-
decline_code: string;
828-
}
829-
830-
export interface CommerceSubscriptionCreditJSON {
831-
amount: CommerceAmountJSON;
832-
cycle_days_remaining: number;
833-
cycle_days_total: number;
834-
cycle_remaining_percent: number;
816+
export interface FeatureJSON extends ClerkResourceJSON {
817+
object: typeof ObjectType.Feature;
818+
name: string;
819+
description: string;
820+
slug: string;
821+
avatar_url: string;
835822
}
836823

837-
export interface CommercePlanJSON {
824+
/**
825+
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
826+
* It is advised to pin the SDK version to avoid breaking changes.
827+
*/
828+
export interface CommercePlanJSON extends ClerkResourceJSON {
829+
object: typeof ObjectType.CommercePlan;
838830
id: string;
839-
instance_id: string;
840831
product_id: string;
841832
name: string;
842833
slug: string;
843834
description?: string;
844835
is_default: boolean;
845836
is_recurring: boolean;
846-
amount: number;
847-
period: 'month' | 'annual';
848-
interval: number;
849837
has_base_fee: boolean;
850-
currency: string;
851-
annual_monthly_amount: number;
852838
publicly_visible: boolean;
839+
fee: CommerceFeeJSON;
840+
annual_fee: CommerceFeeJSON;
841+
annual_monthly_fee: CommerceFeeJSON;
842+
for_payer_type: 'org' | 'user';
843+
features: FeatureJSON[];
853844
}
854845

855846
export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
856847
object: typeof ObjectType.CommerceSubscriptionItem;
857848
status: 'abandoned' | 'active' | 'canceled' | 'ended' | 'expired' | 'incomplete' | 'past_due' | 'upcoming';
858-
credit: CommerceSubscriptionCreditJSON;
849+
credit: {
850+
amount: CommerceFeeJSON;
851+
cycle_days_remaining: number;
852+
cycle_days_total: number;
853+
cycle_remaining_percent: number;
854+
};
859855
proration_date: string;
860856
plan_period: 'month' | 'annual';
861857
period_start: number;
@@ -865,8 +861,24 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
865861
lifetime_paid: number;
866862
next_payment_amount: number;
867863
next_payment_date: number;
868-
amount: CommerceAmountJSON;
869-
plan: CommercePlanJSON;
864+
amount: CommerceFeeJSON;
865+
plan: {
866+
id: string;
867+
instance_id: string;
868+
product_id: string;
869+
name: string;
870+
slug: string;
871+
description?: string;
872+
is_default: boolean;
873+
is_recurring: boolean;
874+
amount: number;
875+
period: 'month' | 'annual';
876+
interval: number;
877+
has_base_fee: boolean;
878+
currency: string;
879+
annual_monthly_amount: number;
880+
publicly_visible: boolean;
881+
};
870882
plan_id: string;
871883
}
872884

@@ -881,13 +893,25 @@ export interface CommercePaymentAttemptJSON extends ClerkResourceJSON {
881893
updated_at: number;
882894
paid_at?: number;
883895
failed_at?: number;
884-
failed_reason?: CommercePaymentFailedReasonJSON;
896+
failed_reason?: {
897+
code: string;
898+
decline_code: string;
899+
};
885900
billing_date: number;
886901
charge_type: 'checkout' | 'recurring';
887902
payee: CommercePayeeJSON;
888903
payer: CommercePayerJSON;
889904
totals: CommerceTotalsJSON;
890-
payment_source: CommercePaymentSourceJSON;
905+
payment_source: {
906+
id: string;
907+
gateway: string;
908+
gateway_external_id: string;
909+
gateway_external_account_id?: string;
910+
payment_method: string;
911+
status: 'active' | 'disconnected';
912+
card_type?: string;
913+
last4?: string;
914+
};
891915
subscription_items: CommerceSubscriptionItemJSON[];
892916
}
893917

packages/backend/src/api/resources/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export * from './User';
5757
export * from './Verification';
5858
export * from './WaitlistEntry';
5959
export * from './Web3Wallet';
60+
export * from './CommercePlan';
6061

6162
export type {
6263
EmailWebhookEvent,

packages/backend/src/index.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,6 @@ export type {
101101
TestingTokenJSON,
102102
WebhooksSvixJSON,
103103
CommercePayerJSON,
104-
CommercePayeeJSON,
105-
CommerceAmountJSON,
106-
CommerceTotalsJSON,
107-
CommercePaymentSourceJSON,
108-
CommercePaymentFailedReasonJSON,
109-
CommerceSubscriptionCreditJSON,
110104
CommercePlanJSON,
111105
CommerceSubscriptionItemJSON,
112106
CommercePaymentAttemptJSON,
@@ -150,6 +144,7 @@ export type {
150144
Token,
151145
User,
152146
TestingToken,
147+
CommercePlan,
153148
} from './api/resources';
154149

155150
/**

0 commit comments

Comments
 (0)