CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/licenses/purchase/compute-cost.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { ONE_MONTH_MS } from "@cocalc/util/consts/billing";
7
import {
8
LicenseIdleTimeouts,
9
requiresMemberhosting,
10
} from "@cocalc/util/consts/site-license";
11
import { BASIC, getCosts, MAX, STANDARD } from "./consts";
12
import { dedicatedPrice } from "./dedicated-price";
13
import type { Cost, PurchaseInfo } from "./types";
14
15
// NOTE: the PurchaseInfo object optionally has a "version" field in it.
16
// If the version is not specified, then it defaults to "1", which is the version
17
// when we started versioning prices. If it is something else, then different
18
// cost parameters may be used in the algorithm below -- that's what's currently
19
// implemented. However... maybe we want a new cost function entirely? That's
20
// possible too:
21
// - just call a new function for your new version below (that's the easy part), and
22
// - there is frontend and other UI code that depends on the structure exported
23
// by contst.ts, and anything that uses that MUST be updated accordingly. E.g.,
24
// there are tables with example costs for various scenarios, stuff about academic
25
// discounts, etc., and a completely different cost function would need to explain
26
// all that differently to users.
27
// OBVIOUSLY: NEVER EVER CHANGE the code or parameters that compute the value of
28
// a specific version of a license! If you make any change, then you must assign a
29
// new version number and also keep the old version around.
30
export function compute_cost(info: PurchaseInfo): Cost {
31
if (info.type === "disk" || info.type === "vm") {
32
return compute_cost_dedicated(info);
33
}
34
35
if (info.type !== "quota") {
36
throw new Error(`can only compute cost for type=quota`);
37
}
38
39
let {
40
version,
41
quantity,
42
user,
43
upgrade,
44
subscription,
45
custom_ram,
46
custom_cpu,
47
custom_dedicated_ram,
48
custom_dedicated_cpu,
49
custom_disk,
50
custom_member,
51
custom_uptime,
52
} = info;
53
54
const start = info.start ? new Date(info.start) : undefined;
55
const end = info.end ? new Date(info.end) : undefined;
56
57
// dedicated cases above should eliminate an unknown user.
58
if (user !== "academic" && user !== "business") {
59
throw new Error(`unknown user ${user}`);
60
}
61
62
// custom_always_running is set in the next if/else block
63
let custom_always_running = false;
64
if (upgrade == "standard") {
65
// set custom_* to what they would be:
66
custom_ram = STANDARD.ram;
67
custom_cpu = STANDARD.cpu;
68
custom_disk = STANDARD.disk;
69
custom_always_running = !!STANDARD.always_running;
70
custom_member = !!STANDARD.member;
71
} else if (upgrade == "basic") {
72
custom_ram = BASIC.ram;
73
custom_cpu = BASIC.cpu;
74
custom_disk = BASIC.disk;
75
custom_always_running = !!BASIC.always_running;
76
custom_member = !!BASIC.member;
77
} else if (upgrade == "max") {
78
custom_ram = MAX.ram;
79
custom_cpu = MAX.cpu;
80
custom_dedicated_ram = MAX.dedicated_ram;
81
custom_dedicated_cpu = MAX.dedicated_cpu;
82
custom_disk = MAX.disk;
83
custom_always_running = !!MAX.always_running;
84
custom_member = !!MAX.member;
85
} else if (custom_uptime == "always_running") {
86
custom_always_running = true;
87
}
88
89
// member hosting is controlled by uptime
90
if (!custom_always_running && requiresMemberhosting(custom_uptime)) {
91
custom_member = true;
92
}
93
94
const COSTS = getCosts(version);
95
96
// We compute the cost for one project for one month.
97
// First we add the cost for RAM and CPU.
98
let cost_per_project_per_month =
99
custom_ram * COSTS.custom_cost.ram +
100
custom_cpu * COSTS.custom_cost.cpu +
101
custom_dedicated_ram * COSTS.custom_cost.dedicated_ram +
102
custom_dedicated_cpu * COSTS.custom_cost.dedicated_cpu;
103
// If the project is always running, multiply the RAM/CPU cost by a factor.
104
if (custom_always_running) {
105
cost_per_project_per_month *= COSTS.custom_cost.always_running;
106
if (custom_member) {
107
// if it is member hosted and always on, we absolutely can't ever use
108
// pre-emptible for this project. On the other hand,
109
// always on non-member means it gets restarted whenever the
110
// pre-empt gets killed, which is still potentially very useful
111
// for long-running computations that can be checkpointed and started.
112
cost_per_project_per_month *= COSTS.gce.non_pre_factor;
113
}
114
} else {
115
// multiply by the idle_timeout factor
116
// the smallest idle_timeout has a factor of 1
117
const idle_timeout_spec = LicenseIdleTimeouts[custom_uptime];
118
if (idle_timeout_spec != null) {
119
cost_per_project_per_month *= idle_timeout_spec.priceFactor;
120
}
121
}
122
123
// If the project is member hosted, multiply the RAM/CPU cost by a factor.
124
if (custom_member) {
125
cost_per_project_per_month *= COSTS.custom_cost.member;
126
}
127
128
// Add the disk cost, which doesn't depend on how frequently the project
129
// is used or the quality of hosting.
130
cost_per_project_per_month += custom_disk * COSTS.custom_cost.disk;
131
132
// Now give the academic and subscription discounts:
133
cost_per_project_per_month *=
134
COSTS.user_discount[user] * COSTS.sub_discount[subscription];
135
136
// It's convenient in all cases to have the actual amount we will be charging
137
// for both monthly and yearly available.
138
const cost_sub_month = cost_per_project_per_month;
139
const cost_sub_year = cost_per_project_per_month * 12;
140
141
let base_cost;
142
143
if (subscription == "no") {
144
// Compute license cost for a partial period which has no subscription.
145
if (start == null) {
146
throw Error("start must be set if subscription=no");
147
}
148
if (end == null) {
149
throw Error("end must be set if subscription=no");
150
}
151
} else if (subscription == "yearly") {
152
// If we're computing the cost for an annual subscription, multiply the monthly subscription
153
// cost by 12.
154
base_cost = 12 * cost_per_project_per_month;
155
} else if (subscription == "monthly") {
156
base_cost = cost_per_project_per_month;
157
} else {
158
throw Error(
159
"BUG -- a subscription must be yearly or monthly or a partial period",
160
);
161
}
162
if (start != null && end != null) {
163
// In all cases -- subscription or not -- if the start and end dates are
164
// explicitly set, then we compute the cost over the given period. This
165
// does not impact cost_sub_month or cost_sub_year.
166
// It is used for computing the cost to edit a license.
167
const months = (end.valueOf() - start.valueOf()) / ONE_MONTH_MS;
168
base_cost = months * cost_per_project_per_month;
169
}
170
171
// cost_per_unit is important for purchasing upgrades for specific intervals.
172
// i.e. above the "cost" is calculated for the total number of projects,
173
// note: later on you have to use round2, since this is the price with full precision.
174
const cost_per_unit = base_cost;
175
const cost_total = quantity * cost_per_unit;
176
177
return {
178
cost_per_unit,
179
cost: cost_total,
180
cost_per_project_per_month,
181
182
// The following are the cost for a subscription for ONE unit for
183
// the given period of time.
184
cost_sub_month,
185
cost_sub_year,
186
quantity,
187
period: subscription == "no" ? "range" : subscription,
188
};
189
}
190
191
export function periodicCost(cost: Cost): number {
192
if (cost.period == "monthly") {
193
return cost.quantity * cost.cost_sub_month;
194
} else if (cost.period == "yearly") {
195
return cost.quantity * cost.cost_sub_year;
196
} else {
197
return cost.cost;
198
}
199
}
200
201
// cost-object for dedicated resource – there are no discounts whatsoever
202
export function compute_cost_dedicated(info) {
203
const { price, monthly } = dedicatedPrice(info);
204
return {
205
cost: price,
206
cost_per_unit: price,
207
cost_per_project_per_month: monthly, // dedicated is always only 1 project
208
cost_sub_month: monthly,
209
cost_sub_year: 12 * monthly,
210
period: info.subscription,
211
quantity: 1,
212
};
213
}
214
215