Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-bats-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

WIP
22 changes: 22 additions & 0 deletions packages/backend/src/api/endpoints/BillingApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 {
public async getPlanList(params?: GetOrganizationListParams) {
return this.request<PaginatedResourceResponse<CommercePlan[]>>({
method: 'GET',
path: joinPaths(basePath, 'plans'),
queryParams: params,
});
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/api/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
WaitlistEntryAPI,
WebhookAPI,
} from './endpoints';
import { BillingAPI } from './endpoints/BillingApi';
import { buildRequest } from './request';

export type CreateBackendApiOptions = Parameters<typeof buildRequest>[0];
Expand All @@ -52,6 +53,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
),
betaFeatures: new BetaFeaturesAPI(request),
blocklistIdentifiers: new BlocklistIdentifierAPI(request),
billing: new BillingAPI(request),
clients: new ClientAPI(request),
domains: new DomainAPI(request),
emailAddresses: new EmailAddressAPI(request),
Expand Down
97 changes: 97 additions & 0 deletions packages/backend/src/api/resources/CommercePlan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Feature } from './Feature';
import type { CommercePlanJSON } from './JSON';

type CommerceFee = {
amount: number;
amountFormatted: string;
currency: string;
currencySymbol: string;
};

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 `<PriceTable/>` 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)),
);
}
}
6 changes: 6 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/backend/src/api/resources/Feature.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
98 changes: 59 additions & 39 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -792,71 +794,61 @@ 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 {
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;
Expand All @@ -866,8 +858,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;
}

Expand All @@ -882,13 +890,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[];
}

Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export * from './User';
export * from './Verification';
export * from './WaitlistEntry';
export * from './Web3Wallet';
export * from './CommercePlan';

export type {
EmailWebhookEvent,
Expand Down
7 changes: 1 addition & 6 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,6 @@ export type {
TestingTokenJSON,
WebhooksSvixJSON,
CommercePayerJSON,
CommercePayeeJSON,
CommerceAmountJSON,
CommerceTotalsJSON,
CommercePaymentSourceJSON,
CommercePaymentFailedReasonJSON,
CommerceSubscriptionCreditJSON,
CommercePlanJSON,
CommerceSubscriptionItemJSON,
CommercePaymentAttemptJSON,
Expand Down Expand Up @@ -150,6 +144,7 @@ export type {
Token,
User,
TestingToken,
CommercePlan,
} from './api/resources';

/**
Expand Down
Loading