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/util/licenses/purchase/compute-cost.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { ONE_MONTH_MS } from "@cocalc/util/consts/billing";6import {7LicenseIdleTimeouts,8requiresMemberhosting,9} from "@cocalc/util/consts/site-license";10import { BASIC, getCosts, MAX, STANDARD } from "./consts";11import { dedicatedPrice } from "./dedicated-price";12import type { Cost, PurchaseInfo } from "./types";1314// NOTE: the PurchaseInfo object optionally has a "version" field in it.15// If the version is not specified, then it defaults to "1", which is the version16// when we started versioning prices. If it is something else, then different17// cost parameters may be used in the algorithm below -- that's what's currently18// implemented. However... maybe we want a new cost function entirely? That's19// possible too:20// - just call a new function for your new version below (that's the easy part), and21// - there is frontend and other UI code that depends on the structure exported22// by contst.ts, and anything that uses that MUST be updated accordingly. E.g.,23// there are tables with example costs for various scenarios, stuff about academic24// discounts, etc., and a completely different cost function would need to explain25// all that differently to users.26// OBVIOUSLY: NEVER EVER CHANGE the code or parameters that compute the value of27// a specific version of a license! If you make any change, then you must assign a28// new version number and also keep the old version around.29export function compute_cost(info: PurchaseInfo): Cost {30if (info.type === "disk" || info.type === "vm") {31return compute_cost_dedicated(info);32}3334if (info.type !== "quota") {35throw new Error(`can only compute cost for type=quota`);36}3738let {39version,40quantity,41user,42upgrade,43subscription,44custom_ram,45custom_cpu,46custom_dedicated_ram,47custom_dedicated_cpu,48custom_disk,49custom_member,50custom_uptime,51} = info;5253const start = info.start ? new Date(info.start) : undefined;54const end = info.end ? new Date(info.end) : undefined;5556// dedicated cases above should eliminate an unknown user.57if (user !== "academic" && user !== "business") {58throw new Error(`unknown user ${user}`);59}6061// custom_always_running is set in the next if/else block62let custom_always_running = false;63if (upgrade == "standard") {64// set custom_* to what they would be:65custom_ram = STANDARD.ram;66custom_cpu = STANDARD.cpu;67custom_disk = STANDARD.disk;68custom_always_running = !!STANDARD.always_running;69custom_member = !!STANDARD.member;70} else if (upgrade == "basic") {71custom_ram = BASIC.ram;72custom_cpu = BASIC.cpu;73custom_disk = BASIC.disk;74custom_always_running = !!BASIC.always_running;75custom_member = !!BASIC.member;76} else if (upgrade == "max") {77custom_ram = MAX.ram;78custom_cpu = MAX.cpu;79custom_dedicated_ram = MAX.dedicated_ram;80custom_dedicated_cpu = MAX.dedicated_cpu;81custom_disk = MAX.disk;82custom_always_running = !!MAX.always_running;83custom_member = !!MAX.member;84} else if (custom_uptime == "always_running") {85custom_always_running = true;86}8788// member hosting is controlled by uptime89if (!custom_always_running && requiresMemberhosting(custom_uptime)) {90custom_member = true;91}9293const COSTS = getCosts(version);9495// We compute the cost for one project for one month.96// First we add the cost for RAM and CPU.97let cost_per_project_per_month =98custom_ram * COSTS.custom_cost.ram +99custom_cpu * COSTS.custom_cost.cpu +100custom_dedicated_ram * COSTS.custom_cost.dedicated_ram +101custom_dedicated_cpu * COSTS.custom_cost.dedicated_cpu;102// If the project is always running, multiply the RAM/CPU cost by a factor.103if (custom_always_running) {104cost_per_project_per_month *= COSTS.custom_cost.always_running;105if (custom_member) {106// if it is member hosted and always on, we absolutely can't ever use107// pre-emptible for this project. On the other hand,108// always on non-member means it gets restarted whenever the109// pre-empt gets killed, which is still potentially very useful110// for long-running computations that can be checkpointed and started.111cost_per_project_per_month *= COSTS.gce.non_pre_factor;112}113} else {114// multiply by the idle_timeout factor115// the smallest idle_timeout has a factor of 1116const idle_timeout_spec = LicenseIdleTimeouts[custom_uptime];117if (idle_timeout_spec != null) {118cost_per_project_per_month *= idle_timeout_spec.priceFactor;119}120}121122// If the project is member hosted, multiply the RAM/CPU cost by a factor.123if (custom_member) {124cost_per_project_per_month *= COSTS.custom_cost.member;125}126127// Add the disk cost, which doesn't depend on how frequently the project128// is used or the quality of hosting.129cost_per_project_per_month += custom_disk * COSTS.custom_cost.disk;130131// Now give the academic and subscription discounts:132cost_per_project_per_month *=133COSTS.user_discount[user] * COSTS.sub_discount[subscription];134135// It's convenient in all cases to have the actual amount we will be charging136// for both monthly and yearly available.137const cost_sub_month = cost_per_project_per_month;138const cost_sub_year = cost_per_project_per_month * 12;139140let base_cost;141142if (subscription == "no") {143// Compute license cost for a partial period which has no subscription.144if (start == null) {145throw Error("start must be set if subscription=no");146}147if (end == null) {148throw Error("end must be set if subscription=no");149}150} else if (subscription == "yearly") {151// If we're computing the cost for an annual subscription, multiply the monthly subscription152// cost by 12.153base_cost = 12 * cost_per_project_per_month;154} else if (subscription == "monthly") {155base_cost = cost_per_project_per_month;156} else {157throw Error(158"BUG -- a subscription must be yearly or monthly or a partial period",159);160}161if (start != null && end != null) {162// In all cases -- subscription or not -- if the start and end dates are163// explicitly set, then we compute the cost over the given period. This164// does not impact cost_sub_month or cost_sub_year.165// It is used for computing the cost to edit a license.166const months = (end.valueOf() - start.valueOf()) / ONE_MONTH_MS;167base_cost = months * cost_per_project_per_month;168}169170// cost_per_unit is important for purchasing upgrades for specific intervals.171// i.e. above the "cost" is calculated for the total number of projects,172// note: later on you have to use round2, since this is the price with full precision.173const cost_per_unit = base_cost;174const cost_total = quantity * cost_per_unit;175176return {177cost_per_unit,178cost: cost_total,179cost_per_project_per_month,180181// The following are the cost for a subscription for ONE unit for182// the given period of time.183cost_sub_month,184cost_sub_year,185quantity,186period: subscription == "no" ? "range" : subscription,187};188}189190export function periodicCost(cost: Cost): number {191if (cost.period == "monthly") {192return cost.quantity * cost.cost_sub_month;193} else if (cost.period == "yearly") {194return cost.quantity * cost.cost_sub_year;195} else {196return cost.cost;197}198}199200// cost-object for dedicated resource – there are no discounts whatsoever201export function compute_cost_dedicated(info) {202const { price, monthly } = dedicatedPrice(info);203return {204cost: price,205cost_per_unit: price,206cost_per_project_per_month: monthly, // dedicated is always only 1 project207cost_sub_month: monthly,208cost_sub_year: 12 * monthly,209period: info.subscription,210quantity: 1,211};212}213214215