Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/billing/actions.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Billing actions.78These are mainly for interfacing with Stripe. They are9all async (no callbacks!).10*/1112import { fromJS, Map } from "immutable";13import { redux, Actions, Store } from "../app-framework";14import { reuse_in_flight_methods } from "@cocalc/util/async-utils";15import {16server_minutes_ago,17server_time,18server_days_ago,19} from "@cocalc/util/misc";20import { webapp_client } from "../webapp-client";21import { StripeClient } from "../client/stripe";22import { getManagedLicenses } from "../account/licenses/util";2324import { BillingStoreState } from "./store";2526require("./store"); // ensure 'billing' store is created so can set this.store below.2728export class BillingActions extends Actions<BillingStoreState> {29private store: Store<BillingStoreState>;30private last_subscription_attempt?: any;31private stripe: StripeClient;3233constructor(name: string, redux: any) {34super(name, redux);35const store = redux.getStore("billing");36if (store == null) throw Error("bug -- billing store should be defined");37this.store = store;38this.stripe = webapp_client.stripe;39reuse_in_flight_methods(this, ["update_customer"]);40}4142public clear_error(): void {43this.setState({ error: "" });44}4546public async update_customer(): Promise<void> {47const is_commercial = redux48.getStore("customize")49.get("is_commercial", false);50if (!is_commercial) return;51this.setState({ action: "Updating billing information" });52try {53const resp = await this.stripe.get_customer();54if (!resp.stripe_publishable_key) {55this.setState({ no_stripe: true });56throw Error(57"WARNING: Stripe is not configured -- billing not available"58);59}60this.setState({61customer: resp.customer,62loaded: true,63stripe_publishable_key: resp.stripe_publishable_key,64});65if (resp.customer) {66// TODO: only call get_invoices if the customer already exists in the system!67// FUTURE: -- this {limit:100} will change when we use webhooks and our own database of info...68const invoices = await this.stripe.get_invoices({69limit: 100,70});71this.setState({ invoices });72}73} catch (err) {74this.setState({ error: err });75throw err;76} finally {77this.setState({ action: "" });78}79}8081// Call a webapp_client.stripe. function with given opts, returning82// the result (which matters only for coupons?).83// This is wrapped as an async call, and also sets the action and error84// states of the Store so the UI can reflect what is happening.85// Also, after update_customer gets called, to update the UI.86// If there is an error, this also throws that error (so it is NOT just87// reflected in the UI).88private async stripe_action(89f: Function,90desc: string,91...args92): Promise<any> {93this.setState({ action: desc });94try {95return await f.bind(this.stripe)(...args);96} catch (err) {97this.setState({ error: `${err}` });98throw err;99} finally {100this.setState({ action: "" });101await this.update_customer();102}103}104105public clear_action(): void {106this.setState({ action: "", error: "" });107}108109public async delete_payment_method(card_id: string): Promise<void> {110await this.stripe_action(111this.stripe.delete_source,112"Deleting a payment method",113card_id114);115}116117public async set_as_default_payment_method(card_id: string): Promise<void> {118await this.stripe_action(119this.stripe.set_default_source,120"Setting payment method as default",121card_id122);123}124125public async submit_payment_method(token: string): Promise<void> {126await this.stripe_action(127this.stripe.create_source,128"Creating a new payment method (sending token)",129token130);131}132133public async cancel_subscription(subscription_id: string): Promise<void> {134await this.stripe_action(135this.stripe.cancel_subscription,136"Cancel a subscription",137{ subscription_id }138);139}140141public async create_subscription(plan: string): Promise<void> {142const lsa = this.last_subscription_attempt;143if (144lsa != null &&145lsa.plan == plan &&146lsa.timestamp > server_minutes_ago(2)147) {148this.setState({149action: "",150error:151"Too many subscription attempts in the last minute. Please **REFRESH YOUR BROWSER** THEN DOUBLE CHECK YOUR SUBSCRIPTION LIST.",152});153return;154}155let coupon: any;156this.setState({ error: "" });157const applied_coupons = this.store.get("applied_coupons");158if (applied_coupons != null && applied_coupons.size > 0) {159coupon = applied_coupons.first();160}161const opts = {162plan,163coupon_id: coupon?.id,164};165await this.stripe_action(166this.stripe.create_subscription,167"Create a subscription",168opts169);170this.last_subscription_attempt = { timestamp: server_time(), plan };171}172173public async apply_coupon(coupon_id: string): Promise<any> {174try {175const coupon = await this.stripe_action(176this.stripe.get_coupon,177`Applying coupon: ${coupon_id}`,178coupon_id179);180const applied_coupons = this.store181.get("applied_coupons", Map<string, any>())182.set(coupon.id, coupon);183if (applied_coupons == null) throw Error("BUG -- can't happen");184this.setState({ applied_coupons, coupon_error: "" });185} catch (err) {186return this.setState({ coupon_error: `${err}` });187}188}189190public clear_coupon_error(): void {191this.setState({ coupon_error: "" });192}193194public remove_all_coupons(): void {195this.setState({ applied_coupons: Map<string, any>(), coupon_error: "" });196}197198public remove_coupon(coupon_id: string): void {199this.setState({200applied_coupons: this.store201.get("applied_coupons", Map<string, any>())202.delete(coupon_id),203});204}205206// Cancel all subscriptions, remove credit cards, etc. -- this is not a normal action,207// and is used only when deleting an account.208public async cancel_everything(): Promise<void> {209// update info about this customer210await this.update_customer();211// delete stuff212// delete payment methods213const payment_methods = this.store.getIn(["customer", "sources", "data"]);214if (payment_methods != null) {215for (const x of payment_methods.toJS() as any) {216await this.delete_payment_method(x.id);217}218}219const subscriptions = this.store.getIn([220"customer",221"subscriptions",222"data",223]);224if (subscriptions != null) {225for (const x of subscriptions.toJS() as any) {226await this.cancel_subscription(x.id);227}228}229}230231// Set this while we are paying for the course.232public set_is_paying_for_course(233project_id: string,234is_paying: boolean235): void {236let course_pay = this.store.get("course_pay");237let continue_first_purchase = this.store.get("continue_first_purchase");238if (is_paying) {239course_pay = course_pay.add(project_id);240} else {241course_pay = course_pay.remove(project_id);242continue_first_purchase = false;243}244this.setState({ course_pay, continue_first_purchase });245}246247public set_selected_plan(plan: string, period?: string): void {248if (period != null) {249if (period.slice(0, 4) == "year") {250plan += "-year";251} else if (period.slice(0, 4) == "week") {252plan += "-week";253}254}255this.setState({ selected_plan: plan });256}257258public async update_managed_licenses(): Promise<void> {259// Update the license state in the frontend260const v = await getManagedLicenses();261const all_managed_license_ids = fromJS(v.map((x) => x.id)) as any;262263const day_ago = server_days_ago(1);264const managed_license_ids = fromJS(265v266.filter((x) => x.expires == null || x.expires >= day_ago)267.map((x) => x.id)268) as any;269270const x: { [license_id: string]: object } = {};271for (const license of v) {272x[license.id] = license;273}274const managed_licenses = fromJS(x) as any;275this.setState({276managed_licenses,277managed_license_ids,278all_managed_license_ids,279});280}281}282283export const actions = redux.createActions("billing", BillingActions);284285286