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