Path: blob/master/src/packages/util/licenses/purchase/compute-cost.ts
1450 views
/*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";13import { round2up } from "@cocalc/util/misc";14import { decimalMultiply } from "@cocalc/util/stripe/calc";1516// NOTE: the PurchaseInfo object optionally has a "version" field in it.17// If the version is not specified, then it defaults to "1", which is the version18// when we started versioning prices. If it is something else, then different19// cost parameters may be used in the algorithm below -- that's what's currently20// implemented. However... maybe we want a new cost function entirely? That's21// possible too:22// - just call a new function for your new version below (that's the easy part), and23// - there is frontend and other UI code that depends on the structure exported24// by contst.ts, and anything that uses that MUST be updated accordingly. E.g.,25// there are tables with example costs for various scenarios, stuff about academic26// discounts, etc., and a completely different cost function would need to explain27// all that differently to users.28// OBVIOUSLY: NEVER EVER CHANGE the code or parameters that compute the value of29// a specific version of a license! If you make any change, then you must assign a30// new version number and also keep the old version around.31export function compute_cost(info: PurchaseInfo): Cost {32if (info.type === "disk" || info.type === "vm") {33return compute_cost_dedicated(info);34}3536if (info.type !== "quota") {37throw new Error(`can only compute costa for type=quota`);38}3940let {41version,42quantity,43user,44upgrade,45subscription,46custom_ram = 0,47custom_cpu = 0,48custom_dedicated_ram = 0,49custom_dedicated_cpu = 0,50custom_disk = 0,51custom_member = 0,52custom_uptime,53} = info;54const start = info.start ? new Date(info.start) : undefined;55const end = info.end ? new Date(info.end) : undefined;5657// dedicated cases above should eliminate an unknown user.58if (user !== "academic" && user !== "business") {59throw new Error(`unknown user ${user}`);60}6162// custom_always_running is set in the next if/else block63let custom_always_running = false;64if (upgrade == "standard") {65// set custom_* to what they would be:66custom_ram = STANDARD.ram;67custom_cpu = STANDARD.cpu;68custom_disk = STANDARD.disk;69custom_always_running = !!STANDARD.always_running;70custom_member = !!STANDARD.member;71} else if (upgrade == "basic") {72custom_ram = BASIC.ram;73custom_cpu = BASIC.cpu;74custom_disk = BASIC.disk;75custom_always_running = !!BASIC.always_running;76custom_member = !!BASIC.member;77} else if (upgrade == "max") {78custom_ram = MAX.ram;79custom_cpu = MAX.cpu;80custom_dedicated_ram = MAX.dedicated_ram;81custom_dedicated_cpu = MAX.dedicated_cpu;82custom_disk = MAX.disk;83custom_always_running = !!MAX.always_running;84custom_member = !!MAX.member;85} else if (custom_uptime == "always_running") {86custom_always_running = true;87}8889// member hosting is controlled by uptime90if (!custom_always_running && requiresMemberhosting(custom_uptime)) {91custom_member = true;92}9394const COSTS = getCosts(version);9596// We compute the cost for one project for one month.97// First we add the cost for RAM and CPU.98let cost_per_project_per_month =99custom_ram * COSTS.custom_cost.ram +100custom_cpu * COSTS.custom_cost.cpu +101custom_dedicated_ram * COSTS.custom_cost.dedicated_ram +102custom_dedicated_cpu * COSTS.custom_cost.dedicated_cpu;103// If the project is always running, multiply the RAM/CPU cost by a factor.104if (custom_always_running) {105cost_per_project_per_month *= COSTS.custom_cost.always_running;106if (custom_member) {107// if it is member hosted and always on, we absolutely can't ever use108// pre-emptible for this project. On the other hand,109// always on non-member means it gets restarted whenever the110// pre-empt gets killed, which is still potentially very useful111// for long-running computations that can be checkpointed and started.112cost_per_project_per_month *= COSTS.gce.non_pre_factor;113}114} else {115// multiply by the idle_timeout factor116// the smallest idle_timeout has a factor of 1117const idle_timeout_spec = LicenseIdleTimeouts[custom_uptime];118if (idle_timeout_spec != null) {119cost_per_project_per_month *= idle_timeout_spec.priceFactor;120}121}122123// If the project is member hosted, multiply the RAM/CPU cost by a factor.124if (custom_member) {125cost_per_project_per_month *= COSTS.custom_cost.member;126}127128// Add the disk cost, which doesn't depend on how frequently the project129// is used or the quality of hosting.130cost_per_project_per_month += custom_disk * COSTS.custom_cost.disk;131132// Now give the academic and subscription discounts:133cost_per_project_per_month *=134COSTS.user_discount[user] * COSTS.sub_discount[subscription];135136cost_per_project_per_month = round2up(cost_per_project_per_month);137138// It's convenient in all cases to have the actual amount we will be charging139// for both monthly and yearly available.140const cost_sub_month = cost_per_project_per_month;141const cost_sub_year = decimalMultiply(cost_per_project_per_month, 12);142143let base_cost;144145if (subscription == "no") {146// Compute license cost for a partial period which has no subscription.147if (start == null) {148throw Error("start must be set if subscription=no");149}150if (end == null) {151throw Error("end must be set if subscription=no");152}153} else if (subscription == "yearly") {154// If we're computing the cost for an annual subscription, multiply the monthly subscription155// cost by 12.156base_cost = decimalMultiply(cost_per_project_per_month, 12);157} else if (subscription == "monthly") {158base_cost = cost_per_project_per_month;159} else {160throw Error(161"BUG -- a subscription must be yearly or monthly or a partial period",162);163}164if (start != null && end != null) {165// In all cases -- subscription or not -- if the start and end dates are166// explicitly set, then we compute the cost over the given period. This167// does not impact cost_sub_month or cost_sub_year.168// It is used for computing the cost to edit a license.169const months = (end.valueOf() - start.valueOf()) / ONE_MONTH_MS;170base_cost = round2up(decimalMultiply(cost_per_project_per_month, months));171}172173// cost_per_unit is important for purchasing upgrades for specific intervals.174// i.e. above the "cost" is calculated for the total number of projects,175const cost_per_unit = base_cost;176const cost_total = decimalMultiply(cost_per_unit, quantity);177178return {179cost_per_unit,180cost: cost_total,181cost_per_project_per_month,182183// The following are the cost for a subscription for ONE unit for184// the given period of time.185cost_sub_month,186cost_sub_year,187quantity,188period: subscription == "no" ? "range" : subscription,189};190}191192export function periodicCost(cost: Cost): number {193if (cost.period == "monthly") {194return decimalMultiply(cost.quantity, cost.cost_sub_month);195} else if (cost.period == "yearly") {196return decimalMultiply(cost.quantity, cost.cost_sub_year);197} else {198return cost.cost;199}200}201202// cost-object for dedicated resource – there are no discounts whatsoever203export function compute_cost_dedicated(info) {204const { price, monthly } = dedicatedPrice(info);205return {206cost: price,207cost_per_unit: price,208cost_per_project_per_month: monthly, // dedicated is always only 1 project209cost_sub_month: monthly,210cost_sub_year: 12 * monthly,211period: info.subscription,212quantity: 1,213};214}215216217