UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

515 lines 24.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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.set_purchase_metadata = exports.charge_user_for_license = void 0; var util_1 = require("smc-webapp/site-licenses/purchase/util"); var site_licenses_1 = require("smc-util/db-schema/site-licenses"); function charge_user_for_license(stripe, info, dbg) { return __awaiter(this, void 0, void 0, function () { var product_id; return __generator(this, function (_a) { switch (_a.label) { case 0: dbg("getting product_id"); return [4 /*yield*/, stripe_get_product(stripe, info)]; case 1: product_id = _a.sent(); dbg("got product_id", product_id); if (!(info.subscription == "no")) return [3 /*break*/, 3]; return [4 /*yield*/, stripe_purchase_product(stripe, product_id, info, dbg)]; case 2: return [2 /*return*/, _a.sent()]; case 3: return [4 /*yield*/, stripe_create_subscription(stripe, product_id, info, dbg)]; case 4: return [2 /*return*/, _a.sent()]; } }); }); } exports.charge_user_for_license = charge_user_for_license; function get_days(info) { if (info.start == null || info.end == null) throw Error("bug"); return Math.round((info.end.valueOf() - info.start.valueOf()) / (24 * 60 * 60 * 1000)); } // When we change pricing, the products in stripe will already // exist with old prices (often grandfathered) so we may want to // instead change the version so new products get created // automatically. var VERSION = 0; function get_product_id(info) { /* We generate a unique identifier that represents the parameters of the purchase. The following parameters determine what "product" they are purchasing: - custom_always_running - custom_cpu - custom_dedicated_cpu - custom_disk - custom_member - custom_ram - custom_dedicated_ram - period: subscription or set number of days We encode these in a string which serves to identify the product. */ var period; if (info.subscription == "no") { period = get_days(info).toString(); } else { period = "0"; // 0 means "subscription" -- same product for all types of subscription billing; } return "license_a" + (info.custom_always_running ? 1 : 0) + "b" + (info.user == "business" ? 1 : 0) + "c" + info.custom_cpu + "d" + info.custom_disk + "m" + (info.custom_member ? 1 : 0) + "p" + period + "r" + info.custom_ram + (info.custom_dedicated_ram ? "y" + info.custom_dedicated_ram : "") + (info.custom_dedicated_cpu ? "z" + Math.round(10 * info.custom_dedicated_cpu) : "") + "_v" + VERSION; } function get_product_name(info) { /* Similar to get_product_id above, but meant to be human readable. This name is what customers see on invoices, so it's very valuable as it reflects what they bought clearly. */ var period; if (info.subscription == "no") { period = get_days(info) + " days"; } else { period = "subscription"; } var desc = site_licenses_1.describe_quota({ user: info.user, ram: info.custom_ram, cpu: info.custom_cpu, dedicated_ram: info.custom_dedicated_ram, dedicated_cpu: info.custom_dedicated_cpu, disk: info.custom_disk, member: info.custom_member, always_running: info.always_running, }); desc += " - " + period; return desc; } function get_product_metadata(info) { var _a, _b; return { user: info.user, ram: info.custom_ram, cpu: info.custom_cpu, dedicated_ram: info.custom_dedicated_ram, dedicated_cpu: info.custom_dedicated_cpu, disk: info.custom_disk, always_running: info.custom_always_running, member: info.custom_member, subscription: info.subscription, start: (_a = info.start) === null || _a === void 0 ? void 0 : _a.toISOString(), end: (_b = info.end) === null || _b === void 0 ? void 0 : _b.toISOString(), }; } function stripe_create_price(stripe, info) { return __awaiter(this, void 0, void 0, function () { var product; return __generator(this, function (_a) { switch (_a.label) { case 0: product = get_product_id(info); // Add the pricing info: // - if sub then we set the price for monthly and yearly // and build in the 25% discount since subscriptions are // self-service by default. // - if number of days, we set price for that many days. if (info.cost == null) throw Error("cost must be defined"); if (!(info.subscription == "no")) return [3 /*break*/, 2]; // create the one-time cost return [4 /*yield*/, stripe.conn.prices.create({ currency: "usd", unit_amount: Math.round((info.cost.cost / info.quantity) * 100), product: product, })]; case 1: // create the one-time cost _a.sent(); return [3 /*break*/, 5]; case 2: // create the two recurring subscription costs. Build // in the self-service discount, which is: // COSTS.online_discount return [4 /*yield*/, stripe.conn.prices.create({ currency: "usd", unit_amount: Math.round(util_1.COSTS.online_discount * info.cost.cost_sub_month * 100), product: product, recurring: { interval: "month" }, })]; case 3: // create the two recurring subscription costs. Build // in the self-service discount, which is: // COSTS.online_discount _a.sent(); return [4 /*yield*/, stripe.conn.prices.create({ currency: "usd", unit_amount: Math.round(util_1.COSTS.online_discount * info.cost.cost_sub_year * 100), product: product, recurring: { interval: "year" }, })]; case 4: _a.sent(); _a.label = 5; case 5: return [2 /*return*/]; } }); }); } function stripe_get_product(stripe, info) { return __awaiter(this, void 0, void 0, function () { var product_id, metadata, name_1, statement_descriptor, n; return __generator(this, function (_a) { switch (_a.label) { case 0: product_id = get_product_id(info); return [4 /*yield*/, stripe_product_exists(stripe, product_id)]; case 1: if (!!(_a.sent())) return [3 /*break*/, 3]; metadata = get_product_metadata(info); name_1 = get_product_name(info); statement_descriptor = "COCALC LICENSE "; if (info.subscription != "no") { statement_descriptor += "SUB"; } else { n = get_days(info); // n<100 logic to fit in 22 characters statement_descriptor += "" + n + (n < 100 ? " " : "") + "DAYS"; } return [4 /*yield*/, stripe.conn.products.create({ id: product_id, name: name_1, metadata: metadata, statement_descriptor: statement_descriptor, })]; case 2: _a.sent(); stripe_create_price(stripe, info); _a.label = 3; case 3: return [2 /*return*/, product_id]; } }); }); } function stripe_product_exists(stripe, product_id) { return __awaiter(this, void 0, void 0, function () { var _1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, stripe.conn.products.retrieve(product_id)]; case 1: _a.sent(); return [2 /*return*/, true]; case 2: _1 = _a.sent(); return [2 /*return*/, false]; case 3: return [2 /*return*/]; } }); }); } function stripe_purchase_product(stripe, product_id, info, dbg) { var _a, _b; return __awaiter(this, void 0, void 0, function () { var quantity, customer, coupon, prices, price, prices_1, period, tax_percent, options, invoice_id, invoice; return __generator(this, function (_c) { switch (_c.label) { case 0: quantity = info.quantity; dbg("stripe_purchase_product", product_id, quantity); return [4 /*yield*/, stripe.need_customer_id()]; case 1: customer = _c.sent(); return [4 /*yield*/, get_self_service_discount_coupon(stripe.conn)]; case 2: coupon = _c.sent(); dbg("stripe_purchase_product: get price"); return [4 /*yield*/, stripe.conn.prices.list({ product: product_id, type: "one_time", active: true, })]; case 3: prices = _c.sent(); price = (_a = prices.data[0]) === null || _a === void 0 ? void 0 : _a.id; if (!(price == null)) return [3 /*break*/, 6]; dbg("stripe_purchase_product: missing -- try to create it"); return [4 /*yield*/, stripe_create_price(stripe, info)]; case 4: _c.sent(); return [4 /*yield*/, stripe.conn.prices.list({ product: product_id, type: "one_time", active: true, })]; case 5: prices_1 = _c.sent(); price = (_b = prices_1.data[0]) === null || _b === void 0 ? void 0 : _b.id; if (price == null) { dbg("stripe_purchase_product: still missing -- give up"); throw Error("price for one-time purchase missing -- product_id=\"" + product_id + "\""); } _c.label = 6; case 6: dbg("stripe_purchase_product: got price", JSON.stringify(price)); if (info.start == null || info.end == null) { throw Error("start and end must be defined"); } period = { start: Math.round(info.start.valueOf() / 1000), end: Math.round(info.end.valueOf() / 1000), }; // gets automatically put on the invoice created below. return [4 /*yield*/, stripe.conn.invoiceItems.create({ customer: customer, price: price, quantity: quantity, period: period })]; case 7: // gets automatically put on the invoice created below. _c.sent(); return [4 /*yield*/, stripe.sales_tax(customer)]; case 8: tax_percent = _c.sent(); options = { customer: customer, auto_advance: true, collection_method: "charge_automatically", tax_percent: tax_percent ? Math.round(tax_percent * 100 * 100) / 100 : undefined, }; dbg("stripe_purchase_product options=", JSON.stringify(options)); return [4 /*yield*/, stripe.conn.customers.update(customer, { coupon: coupon })]; case 9: _c.sent(); return [4 /*yield*/, stripe.conn.invoices.create(options)]; case 10: invoice_id = (_c.sent()).id; return [4 /*yield*/, stripe.conn.invoices.finalizeInvoice(invoice_id, { auto_advance: true, })]; case 11: _c.sent(); return [4 /*yield*/, stripe.conn.invoices.pay(invoice_id, { payment_method: info.payment_method, })]; case 12: invoice = _c.sent(); // remove coupon so it isn't automatically applied return [4 /*yield*/, stripe.conn.customers.deleteDiscount(customer)]; case 13: // remove coupon so it isn't automatically applied _c.sent(); return [4 /*yield*/, stripe.update_database()]; case 14: _c.sent(); if (!!invoice.paid) return [3 /*break*/, 16]; // We void it so user doesn't get charged later. Of course, // we plan to rewrite this to keep trying and once they pay it // somehow, then they get their license. But that's a TODO! return [4 /*yield*/, stripe.conn.invoices.voidInvoice(invoice_id)]; case 15: // We void it so user doesn't get charged later. Of course, // we plan to rewrite this to keep trying and once they pay it // somehow, then they get their license. But that's a TODO! _c.sent(); throw Error("created invoice but not able to pay it -- invoice has been voided; please try again when you have a valid payment method on file"); case 16: return [2 /*return*/, { type: "invoice", id: invoice_id }]; } }); }); } function stripe_create_subscription(stripe, product_id, info, dbg) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function () { var quantity, subscription, customer, prices, price, _e, _f, x, prices_2, _g, _h, x, tax_percent, options, id; var e_1, _j, e_2, _k; return __generator(this, function (_l) { switch (_l.label) { case 0: quantity = info.quantity, subscription = info.subscription; return [4 /*yield*/, stripe.need_customer_id()]; case 1: customer = _l.sent(); return [4 /*yield*/, stripe.conn.prices.list({ product: product_id, type: "recurring", active: true, })]; case 2: prices = _l.sent(); price = undefined; try { for (_e = __values(prices.data), _f = _e.next(); !_f.done; _f = _e.next()) { x = _f.value; if (subscription.startsWith((_b = (_a = x.recurring) === null || _a === void 0 ? void 0 : _a.interval) !== null && _b !== void 0 ? _b : "none")) { price = x === null || x === void 0 ? void 0 : x.id; break; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_f && !_f.done && (_j = _e.return)) _j.call(_e); } finally { if (e_1) throw e_1.error; } } if (!(price == null)) return [3 /*break*/, 5]; return [4 /*yield*/, stripe_create_price(stripe, info)]; case 3: _l.sent(); return [4 /*yield*/, stripe.conn.prices.list({ product: product_id, type: "recurring", active: true, })]; case 4: prices_2 = _l.sent(); try { for (_g = __values(prices_2.data), _h = _g.next(); !_h.done; _h = _g.next()) { x = _h.value; if (subscription.startsWith((_d = (_c = x.recurring) === null || _c === void 0 ? void 0 : _c.interval) !== null && _d !== void 0 ? _d : "none")) { price = x === null || x === void 0 ? void 0 : x.id; break; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_h && !_h.done && (_k = _g.return)) _k.call(_g); } finally { if (e_2) throw e_2.error; } } if (price == null) { dbg("stripe_purchase_product: still missing -- give up"); throw Error("price for subscription purchase missing -- product_id=\"" + product_id + "\", subscription=\"" + subscription + "\""); } _l.label = 5; case 5: return [4 /*yield*/, stripe.sales_tax(customer)]; case 6: tax_percent = _l.sent(); options = { customer: customer, // see https://github.com/sagemathinc/cocalc/issues/5234 for // why this payment_behavior. payment_behavior: "error_if_incomplete", items: [{ price: price, quantity: quantity }], tax_percent: tax_percent ? Math.round(tax_percent * 100 * 100) / 100 : undefined, }; return [4 /*yield*/, stripe.conn.subscriptions.create(options)]; case 7: id = (_l.sent()).id; return [4 /*yield*/, stripe.update_database()]; case 8: _l.sent(); return [2 /*return*/, { type: "subscription", id: id }]; } }); }); } // Gets a coupon that matches the current online discount. var known_coupons = {}; function get_self_service_discount_coupon(conn) { return __awaiter(this, void 0, void 0, function () { var percent_off, id, _2; return __generator(this, function (_a) { switch (_a.label) { case 0: percent_off = Math.round(100 * (1 - util_1.COSTS.online_discount)); id = "coupon_self_service_" + percent_off; if (known_coupons[id]) { return [2 /*return*/, id]; } _a.label = 1; case 1: _a.trys.push([1, 3, , 5]); return [4 /*yield*/, conn.coupons.retrieve(id)]; case 2: _a.sent(); return [3 /*break*/, 5]; case 3: _2 = _a.sent(); // coupon doesn't exist, so we have to create it. return [4 /*yield*/, conn.coupons.create({ id: id, percent_off: percent_off, name: "Self-service discount", duration: "forever", })]; case 4: // coupon doesn't exist, so we have to create it. _a.sent(); return [3 /*break*/, 5]; case 5: known_coupons[id] = true; return [2 /*return*/, id]; } }); }); } function set_purchase_metadata(stripe, purchase, metadata) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { if (purchase.type == "subscription") { stripe.conn.subscriptions.update(purchase.id, { metadata: metadata }); } else if (purchase.type == "invoice") { stripe.conn.invoices.update(purchase.id, { metadata: metadata }); } return [2 /*return*/]; }); }); } exports.set_purchase_metadata = set_purchase_metadata; //# sourceMappingURL=charge.js.map