UNPKG

@cocalc/server

Version:

CoCalc server functionality: functions used by either the hub and the next.js server

586 lines 25.7 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StripeClient = exports.Stripe = void 0; const hof_1 = require("async-await-utils/hof"); const awaiting_1 = require("awaiting"); const async_utils_1 = require("@cocalc/util/async-utils"); const message = __importStar(require("@cocalc/util/message")); const upgrades_1 = require("@cocalc/util/upgrades"); const name_1 = __importDefault(require("@cocalc/util/stripe/name")); const database_1 = require("@cocalc/database"); const stripe_1 = __importDefault(require("stripe")); exports.Stripe = stripe_1.default; const private_1 = __importDefault(require("@cocalc/server/accounts/profile/private")); const connection_1 = __importDefault(require("./connection")); const sales_tax_1 = require("./sales-tax"); const logger_1 = __importDefault(require("@cocalc/backend/logger")); const logger = (0, logger_1.default)("stripe-client"); function get_string_field(mesg, name) { if (mesg == null) throw Error("invalid message; must not be null"); const x = mesg[name]; if (typeof x != "string") throw Error(`mesg[${name}] must be a string`); return x; } function get_nonnull_field(mesg, name) { if (mesg == null) throw Error("invalid message; must not be null"); const x = mesg[name]; if (x == null) throw Error(`mesg[${name}] must be defined`); return x; } class StripeClient { constructor(client) { if (!client.account_id) { throw Error("account_id must be specified"); } if (!client.database) { // TODO: Importing this at the top level doesn't work with // next.js right now for some reason. I'm sure this problem // will vanish when @cocalc/database melts away or is rewritten // in typescript... client.database = (0, database_1.db)(); } // Set defaults for some attributes of client, in case not specified if (client.dbg == null) { client.dbg = (f) => { return (...args) => logger.debug(`Stripe(account_id=${client.account_id})`, f, ...args); }; } if (client.push_to_client == null) { client.push_to_client = () => { }; // no op } if (client.error_to_client == null) { client.error_to_client = () => { }; // no op } if (client.assert_user_is_in_group == null) { client.assert_user_is_in_group = () => { }; // no op } this.client = client; this.get_customer_id = (0, hof_1.reuseInFlight)(this.get_customer_id.bind(this)); } dbg(f) { return this.client.dbg(`stripe.${f}`); } // Returns the stripe customer id for this account from our database, // or undefined if there is no known stripe customer id. // Throws an error if something goes wrong. // If called multiple times simultaneously, only does one DB query. async get_customer_id() { // If no customer info yet with stripe, then NOT an error; instead, // customer_id is undefined (but will check every time in this case). const dbg = this.dbg("get_customer_id"); dbg(); if (this.stripe_customer_id != null) { dbg("using cached this.stripe_customer_id"); return this.stripe_customer_id; } const account_id = this.client.account_id; if (account_id == null) { throw Error("You must be signed in to use billing related functions."); } dbg("getting stripe_customer_id from database..."); const stripe_customer_id = await (0, async_utils_1.callback2)(this.client.database.get_stripe_customer_id, { account_id }); if (stripe_customer_id != null) { // cache it, since it won't change. this.stripe_customer_id = stripe_customer_id; } return stripe_customer_id; } // Raise an exception if user is not yet registered with stripe. async need_customer_id() { this.dbg("need_customer_id")(); const customer_id = await this.get_customer_id(); if (customer_id == null) { throw Error("stripe customer not defined"); } return customer_id; } async stripe_api_pager_options(mesg) { return { customer: await this.need_customer_id(), limit: mesg.limit, ending_before: mesg.ending_before, starting_after: mesg.starting_after, }; } async get_customer(customer_id) { const dbg = this.dbg("get_customer"); if (customer_id == null) { dbg("getting customer id"); customer_id = await this.need_customer_id(); } dbg("now getting stripe customer object"); return await (await (0, connection_1.default)()).customers.retrieve(customer_id); } async handle_mesg(mesg) { try { if (mesg.event.slice(0, 7) != "stripe_") { throw Error("mesg event must start with stripe_"); } const f = this[`mesg_${mesg.event.slice(7)}`]; if (f == null) { throw Error(`no such message type ${mesg.event}`); } else { let resp = await f.bind(this)(mesg); if (resp == null) { resp = {}; } resp.id = mesg.id; this.client.push_to_client(resp); } } catch (err) { let error; if (err.stack != null) { error = err.stack.split("\n")[0]; } else { error = `${err}`; } this.dbg("handle_mesg")("Error", error, err.stack); this.client.error_to_client({ id: mesg.id, error }); } } async mesg_get_customer(_mesg) { const dbg = this.dbg("mesg_get_customer"); dbg("get information from stripe: subscriptions, payment methods, etc."); // note -- we explicitly put the "publishable_key" property there... return message.stripe_customer({ stripe_publishable_key: (await (0, connection_1.default)()).publishable_key, customer: await this.update_database(), }); } async mesg_create_source(mesg) { const dbg = this.dbg("mesg_create_source"); dbg("create a payment method (credit card) in stripe for this user"); const token = get_string_field(mesg, "token"); dbg("looking up customer"); const customer_id = await this.get_customer_id(); if (customer_id == null) { await this.create_new_stripe_customer_from_card_token(token); } else { await this.add_card_to_existing_stripe_customer(token); } } async create_new_stripe_customer_from_card_token(token) { const dbg = this.dbg("create_new_stripe_customer_from_card_token"); dbg("create new stripe customer from card token"); // Check user is not anonymous. Our user interface never provides // anything stripe related to anon users. However, an "abuser" // might still try to trigger this. Anonymous accounts may be easier // to create, e.g., no captcha, so we want to limit their potential for damage. const { is_anonymous } = await (0, private_1.default)(this.client.account_id); if (is_anonymous) { throw Error("anonymous users are not allowed to create a stripe customer"); } dbg("get identifying info about user"); const r = await (0, async_utils_1.callback2)(this.client.database.get_account, { columns: ["email_address", "first_name", "last_name"], account_id: this.client.account_id, }); const email = r.email_address; const description = (0, name_1.default)(r.first_name, r.last_name); dbg(`they are ${description} with email ${email}`); dbg("creating stripe customer"); const x = { source: token, description, name: description, email, metadata: { account_id: this.client.account_id, }, }; const customer_id = (await (await (0, connection_1.default)()).customers.create(x)) .id; dbg("success; now save customer_id to database"); await (0, async_utils_1.callback2)(this.client.database.set_stripe_customer_id, { account_id: this.client.account_id, customer_id, }); await this.update_database(); } async add_card_to_existing_stripe_customer(token) { const dbg = this.dbg("add_card_to_existing_stripe_customer"); dbg("add card to existing stripe customer"); const customer_id = await this.need_customer_id(); await (await (0, connection_1.default)()).customers.createSource(customer_id, { source: token }); await this.update_database(); } async mesg_delete_source(mesg) { const dbg = this.dbg("mesg_delete_source"); dbg("delete a payment method for this user"); const card_id = get_string_field(mesg, "card_id"); const customer_id = await this.get_customer_id(); if (customer_id == null) throw Error("no customer information so can't delete source"); await (await (0, connection_1.default)()).customers.deleteSource(customer_id, card_id); await this.update_database(); } async mesg_set_default_source(mesg) { const dbg = this.dbg("mesg_set_default_source"); dbg("set a payment method for this user to be the default"); const card_id = get_string_field(mesg, "card_id"); const customer_id = await this.need_customer_id(); dbg("now setting the default source in stripe"); await (await (0, connection_1.default)()).customers.update(customer_id, { default_source: card_id, }); await this.update_database(); } // update_database queries stripe for customer record, stores // it in the database, and returns the new customer record // if it exists async update_database() { this.dbg("update_database")(); const customer_id = await this.get_customer_id(); if (customer_id == null) return; return await (0, async_utils_1.callback2)(this.client.database.stripe_update_customer, { account_id: this.client.account_id, stripe: await (0, connection_1.default)(), customer_id, }); } async mesg_update_source(mesg) { const dbg = this.dbg("mesg_update_source"); dbg("modify a payment method"); const card_id = get_string_field(mesg, "card_id"); const info = get_nonnull_field(mesg, "info"); await (await (0, connection_1.default)()).sources.update(card_id, info); await this.update_database(); } async sales_tax(customer_id) { return await (0, sales_tax_1.stripe_sales_tax)(customer_id, this.dbg("sales_tax")); } async mesg_create_subscription(mesg) { const dbg = this.dbg("mesg_create_subscription"); dbg("create a subscription for this user, using some billing method"); const plan = get_string_field(mesg, "plan"); const schema = require("@cocalc/util/schema").PROJECT_UPGRADES.subscription[plan.split("-")[0]]; if (schema == null) throw Error(`unknown plan -- '${plan}'`); const customer_id = await this.need_customer_id(); const quantity = mesg.quantity ? mesg.quantity : 1; dbg("determine applicable tax"); const tax_percent = await this.sales_tax(customer_id); // CRITICAL regarding setting tax_percent below: if we don't just multiply by 100, since then sometimes // stripe comes back with an error like this // "Error: Invalid decimal: 8.799999999999999; must contain at maximum two decimal places." const options = { customer: customer_id, payment_behavior: "error_if_incomplete", items: [{ quantity, plan }], coupon: mesg.coupon_id, cancel_at_period_end: schema.cancel_at_period_end, tax_percent: tax_percent ? Math.round(tax_percent * 100 * 100) / 100 : undefined, }; dbg("add customer subscription to stripe"); await (await (0, connection_1.default)()).subscriptions.create(options); dbg("added subscription; now save info in our database about it..."); await this.update_database(); if (options.coupon != null) { dbg("add coupon to customer history"); const { coupon, coupon_history } = await this.validate_coupon(options.coupon); // SECURITY NOTE: incrementing a counter... subject to attack? // I.e., use a coupon more times than should be able to? // NOT a big worry since we never issue or use coupons anyways... coupon_history[coupon.id] += 1; await (0, async_utils_1.callback2)(this.client.database.update_coupon_history, { account_id: this.client.account_id, coupon_history, }); } } async mesg_cancel_subscription(mesg) { const dbg = this.dbg("mesg_cancel_subscription"); dbg("cancel a subscription for this user"); const subscription_id = get_string_field(mesg, "subscription_id"); // TODO/SECURITY: We should check that this subscription actually // belongs to this user. As it is, they could be cancelling somebody // else's subscription! dbg("cancel the subscription at stripe"); await (await (0, connection_1.default)()).subscriptions.update(subscription_id, { cancel_at_period_end: mesg.at_period_end, }); await this.update_database(); } async mesg_update_subscription(mesg) { const dbg = this.dbg("mesg_update_subscription"); dbg("edit a subscription for this user"); const subscription_id = get_string_field(mesg, "subscription_id"); dbg("Update the subscription."); const changes = { quantity: mesg.quantity, plan: mesg.plan, coupon: mesg.coupon_id, }; await (await (0, connection_1.default)()).subscriptions.update(subscription_id, changes); await this.update_database(); if (mesg.coupon_id != null) { const { coupon, coupon_history } = await this.validate_coupon(mesg.coupon_id); coupon_history[coupon.id] += 1; await (0, async_utils_1.callback2)(this.client.database.update_coupon_history, { account_id: this.client.account_id, coupon_history, }); } } async mesg_get_subscriptions(mesg) { const dbg = this.dbg("mesg_get_subscriptions"); dbg("get a list of all the subscriptions that this customer has"); const options = await this.stripe_api_pager_options(mesg); options.status = "all"; const subscriptions = await (await (0, connection_1.default)()).subscriptions.list(options); return message.stripe_subscriptions({ subscriptions }); } async mesg_get_coupon(mesg) { const dbg = this.dbg("mesg_get_coupon"); dbg(`get the coupon with id=${mesg.coupon_id}`); const coupon_id = get_string_field(mesg, "coupon_id"); const { coupon } = await this.validate_coupon(coupon_id); return message.stripe_coupon({ coupon }); } // Checks these coupon criteria: // - Exists // - Is valid // - Used by this account less than the max per account (hard coded default is 1) async validate_coupon(coupon_id) { const dbg = this.dbg("validate_coupon"); dbg("retrieve the coupon"); const coupon = await (await (0, connection_1.default)()).coupons.retrieve(coupon_id); dbg("check account coupon_history"); let coupon_history = await (0, async_utils_1.callback2)(this.client.database.get_coupon_history, { account_id: this.client.account_id, }); if (!coupon.valid) throw Error("Sorry! This coupon has expired."); if (coupon_history == null) { coupon_history = {}; } const times_used = coupon_history[coupon.id] != null ? coupon_history[coupon.id] : 0; if (times_used >= (coupon.metadata.max_per_account != null ? coupon.metadata.max_per_account : 1)) { throw Error("You've already used this coupon."); } coupon_history[coupon.id] = times_used; return { coupon, coupon_history }; } async mesg_get_charges(mesg) { const dbg = this.dbg("mesg_get_charges"); dbg("get a list of charges for this customer"); const options = await this.stripe_api_pager_options(mesg); const charges = await (await (0, connection_1.default)()).charges.list(options); return message.stripe_charges({ charges }); } async mesg_get_invoices(mesg) { const dbg = this.dbg("mesg_get_invoices"); dbg("get a list of invoices for this customer"); const options = await this.stripe_api_pager_options(mesg); const invoices = await (await (0, connection_1.default)()).invoices.list(options); return message.stripe_invoices({ invoices }); } // This is not actually used **YET**. async mesg_admin_create_invoice_item(mesg) { const dbg = this.dbg("mesg_admin_create_invoice_item"); dbg(); await (0, awaiting_1.callback)(this.client.assert_user_is_in_group.bind(this.client), "admin"); dbg("check for existing stripe customer_id"); const r = await (0, async_utils_1.callback2)(this.client.database.get_account, { columns: [ "stripe_customer_id", "email_address", "first_name", "last_name", "account_id", ], account_id: mesg.account_id, email_address: mesg.email_address, }); let customer_id = r.stripe_customer_id; const email = r.email_address; const description = (0, name_1.default)(r.first_name, r.last_name); mesg.account_id = r.account_id; const conn = await (0, connection_1.default)(); if (customer_id != null) { dbg("already signed up for stripe -- sync local user account with stripe"); await (0, async_utils_1.callback2)(this.client.database.stripe_update_customer, { account_id: mesg.account_id, stripe: conn, customer_id, }); } else { dbg("create stripe entry for this customer"); const { is_anonymous } = await (0, private_1.default)(mesg.account_id); if (is_anonymous) { throw Error("anonymous users are not allowed to create a stripe customer"); } const x = { description, email, metadata: { account_id: mesg.account_id, }, }; const customer = await conn.customers.create(x); customer_id = customer.id; dbg("store customer id in our database"); await (0, async_utils_1.callback2)(this.client.database.set_stripe_customer_id, { account_id: mesg.account_id, customer_id, }); } if (!(mesg.amount != null && mesg.description != null)) { dbg("no amount or no description, so not creating an invoice"); return; } dbg("now create the invoice item"); await conn.invoiceItems.create({ customer: customer_id, amount: mesg.amount * 100, currency: "usd", description: mesg.description, }); } async mesg_get_available_upgrades(_mesg) { const dbg = this.dbg("mesg_get_available_upgrades"); dbg("get stripe customer data"); const customer = await this.get_customer(); if (customer == null || customer.subscriptions == null) { // no upgrades since not even a stripe account. return message.available_upgrades({ total: {}, excess: {}, available: {}, }); } const stripe_data = customer.subscriptions.data; dbg("get user project upgrades"); const projects = await (0, async_utils_1.callback2)(this.client.database.get_user_project_upgrades, { account_id: this.client.account_id, }); const { excess, available } = (0, upgrades_1.available_upgrades)(stripe_data, projects); const total = (0, upgrades_1.get_total_upgrades)(stripe_data); return message.available_upgrades({ total, excess, available, }); } async mesg_remove_all_upgrades(mesg) { const dbg = this.dbg("mesg_remove_all_upgrades"); dbg(); if (this.client.account_id == null) throw Error("you must be signed in"); await (0, async_utils_1.callback2)(this.client.database.remove_all_user_project_upgrades, { account_id: this.client.account_id, projects: mesg.projects, }); } async mesg_sync_site_license_subscriptions() { const dbg = this.dbg("mesg_sync_site_license_subscriptions"); dbg(); if (this.client.account_id == null) throw Error("you must be signed in"); await this.client.database.sync_site_license_subscriptions(this.client.account_id); } async cancelEverything() { const dbg = this.dbg("cancelEverything"); const customer_id = await this.get_customer_id(); dbg("customer_id = ", customer_id); if (customer_id == null) { // nothing to do, since no stripe info about this user. return; } const conn = await (0, connection_1.default)(); dbg("Cancel the credit cards"); const customer = await this.get_customer(customer_id); const payment_methods = customer.sources?.data; if (payment_methods != null) { for (const { id } of payment_methods) { await conn.customers.deleteSource(customer_id, id); } } dbg("Cancel the subscriptions (at period end)"); const subscriptions = customer.subscriptions?.data; if (subscriptions != null) { for (const { id } of subscriptions) { await conn.subscriptions.update(id, { cancel_at_period_end: true, }); } } dbg("Sync the database to indicate that everything is canceled."); await this.update_database(); } async getPaymentMethods() { const dbg = this.dbg("get_sources"); dbg("get a list of all the payment sources that this customer has"); const customer = await this.need_customer_id(); const conn = await (0, connection_1.default)(); await conn.paymentMethods.list({ customer, type: "card" }); } async setDefaultSource(default_source) { const conn = await (0, connection_1.default)(); await conn.customers.update(await this.need_customer_id(), { default_source, }); await this.update_database(); } async deletePaymentMethod(id) { const conn = await (0, connection_1.default)(); await conn.customers.deleteSource(await this.need_customer_id(), id); await this.update_database(); } async createPaymentMethod(token) { await this.mesg_create_source({ token }); } async cancelSubscription(id) { // TODO/SECURITY: see comment in mesg_cancel_subscription const conn = await (0, connection_1.default)(); await conn.subscriptions.update(id, { cancel_at_period_end: true }); await this.update_database(); } } exports.StripeClient = StripeClient; //# sourceMappingURL=client.js.map