import {BaseAPI, IOptions} from 'app/common/BaseAPI'; import {FullUser} from 'app/common/LoginSessionAPI'; import {StringUnion} from 'app/common/StringUnion'; import {addCurrentOrgToPath} from 'app/common/urlUtils'; import {BillingAccount, ManagerDelta, OrganizationWithoutAccessInfo} from 'app/common/UserAPI'; export const BillingSubPage = StringUnion('payment', 'plans'); export type BillingSubPage = typeof BillingSubPage.type; export const BillingPage = StringUnion(...BillingSubPage.values, 'billing'); export type BillingPage = typeof BillingPage.type; export const BillingTask = StringUnion('signUp', 'updatePlan', 'addCard', 'updateCard', 'updateAddress'); export type BillingTask = typeof BillingTask.type; // Note that IBillingPlan includes selected fields from the Stripe plan object along with // custom metadata fields that are present on plans we store in Stripe. // For reference: https://stripe.com/docs/api/plans/object export interface IBillingPlan { id: string; // the Stripe plan id nickname: string; currency: string; // lowercase three-letter ISO currency code interval: string; // billing frequency - one of day, week, month or year amount: number; // amount in cents charged at each interval metadata: { family?: string; // groups plans for filtering by GRIST_STRIPE_FAMILY env variable isStandard: boolean; // indicates that the plan should be returned by the API to be offered. supportAvailable: boolean; gristProduct: string; // name of grist product that should be used with this plan. unthrottledApi: boolean; customSubdomain: boolean; workspaces: boolean; maxDocs?: number; // if given, limit of docs that can be created maxUsersPerDoc?: number; // if given, limit of users each doc can be shared with }; trial_period_days: number|null; // Number of days in the trial period, or null if there is none. product: string; // the Stripe product id. } // Stripe customer address information. Used to maintain the company address. // For reference: https://stripe.com/docs/api/customers/object#customer_object-address export interface IBillingAddress { line1: string; line2?: string; city?: string; state?: string; postal_code?: string; country?: string; } export interface IBillingCard { funding?: 'credit'|'debit'|'prepaid'|'unknown'; brand?: string; country?: string; // uppercase two-letter ISO country code last4?: string; // last 4 digits of the card number name?: string|null; } export interface IBillingSubscription { // All standard plan options. plans: IBillingPlan[]; // Index in the plans array of the plan currently in effect. planIndex: number; // Index in the plans array of the plan to be in effect after the current period end. // Equal to the planIndex when the plan has not been downgraded or cancelled. upcomingPlanIndex: number; // Timestamp in milliseconds indicating when the current plan period ends. // Null if the account is not signed up with Stripe. periodEnd: number|null; // Whether the subscription is in the trial period. isInTrial: boolean; // Value in cents remaining for the current subscription. This indicates the amount that // will be discounted from a subscription upgrade. valueRemaining: number; // The payment card, or null if none is attached. card: IBillingCard|null; // The company address. address: IBillingAddress|null; // The effective tax rate of the customer for the given address. taxRate: number; // The current number of users with whom the paid org is shared. userCount: number; // The next total in cents that Stripe is going to charge (includes tax and discount). nextTotal: number; // Name of the discount if any. discountName: string|null; // Last plan we had a subscription for, if any. lastPlanId: string|null; // Whether there is a valid plan in effect isValidPlan: boolean; // Stripe status, documented at https://stripe.com/docs/api/subscriptions/object#subscription_object-status // such as "active", "trialing" (reflected in isInTrial), "incomplete", etc. status?: string; lastInvoiceUrl?: string; // URL of the Stripe-hosted page with the last invoice. lastChargeError?: string; // The last charge error, if any, to show in case of a bad status. lastChargeTime?: number; // The time of the last charge attempt. } export interface IBillingOrgSettings { name: string; domain: string; } // Full description of billing account, including nested list of orgs and managers. export interface FullBillingAccount extends BillingAccount { orgs: OrganizationWithoutAccessInfo[]; managers: FullUser[]; } export interface BillingAPI { isDomainAvailable(domain: string): Promise; getTaxRate(address: IBillingAddress): Promise; getPlans(): Promise; getSubscription(): Promise; getBillingAccount(): Promise; // The signUp function takes the tokenId generated when card data is submitted to Stripe. // See: https://stripe.com/docs/stripe-js/reference#stripe-create-token signUp(planId: string, tokenId: string, address: IBillingAddress, settings: IBillingOrgSettings): Promise; setCard(tokenId: string): Promise; removeCard(): Promise; setSubscription(planId: string, tokenId?: string): Promise; updateAddress(address?: IBillingAddress, settings?: IBillingOrgSettings): Promise; updateBillingManagers(delta: ManagerDelta): Promise; } export class BillingAPIImpl extends BaseAPI implements BillingAPI { constructor(private _homeUrl: string, options: IOptions = {}) { super(options); } public async isDomainAvailable(domain: string): Promise { const resp = await this.request(`${this._url}/api/billing/domain`, { method: 'POST', body: JSON.stringify({ domain }) }); return resp.json(); } public async getTaxRate(address: IBillingAddress): Promise { const resp = await this.request(`${this._url}/api/billing/tax`, { method: 'POST', body: JSON.stringify({ address }) }); return resp.json(); } public async getPlans(): Promise { const resp = await this.request(`${this._url}/api/billing/plans`, {method: 'GET'}); return resp.json(); } // Returns an IBillingSubscription public async getSubscription(): Promise { const resp = await this.request(`${this._url}/api/billing/subscription`, {method: 'GET'}); return resp.json(); } public async getBillingAccount(): Promise { const resp = await this.request(`${this._url}/api/billing`, {method: 'GET'}); return resp.json(); } // Returns the new Stripe customerId. public async signUp( planId: string, tokenId: string, address: IBillingAddress, settings: IBillingOrgSettings ): Promise { const resp = await this.request(`${this._url}/api/billing/signup`, { method: 'POST', body: JSON.stringify({ tokenId, planId, address, settings }) }); const parsed = await resp.json(); return parsed.data; } public async setSubscription(planId: string, tokenId?: string): Promise { await this.request(`${this._url}/api/billing/subscription`, { method: 'POST', body: JSON.stringify({ tokenId, planId }) }); } public async removeSubscription(): Promise { await this.request(`${this._url}/api/billing/subscription`, {method: 'DELETE'}); } public async setCard(tokenId: string): Promise { await this.request(`${this._url}/api/billing/card`, { method: 'POST', body: JSON.stringify({ tokenId }) }); } public async removeCard(): Promise { await this.request(`${this._url}/api/billing/card`, {method: 'DELETE'}); } public async updateAddress(address?: IBillingAddress, settings?: IBillingOrgSettings): Promise { await this.request(`${this._url}/api/billing/address`, { method: 'POST', body: JSON.stringify({ address, settings }) }); } public async updateBillingManagers(delta: ManagerDelta): Promise { await this.request(`${this._url}/api/billing/managers`, { method: 'PATCH', body: JSON.stringify({delta}) }); } private get _url(): string { return addCurrentOrgToPath(this._homeUrl); } }