Skip to content

Commit 7b579fe

Browse files
authored
Merge pull request #8486 from sagemathinc/price-upd-20250805
pricing: incorporate summer changes
2 parents f5d4324 + b9af6e5 commit 7b579fe

File tree

11 files changed

+146
-28
lines changed

11 files changed

+146
-28
lines changed

src/packages/frontend/purchases/license-editor.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ single license. It doesn't manage actually coordinating purchases, showing pric
44
or anything like that.
55
*/
66

7-
import type { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";
8-
import type { Changes } from "@cocalc/util/purchases/cost-to-edit-license";
97
import {
108
Alert,
119
DatePicker,
1210
InputNumber,
13-
Switch,
1411
Select,
12+
Switch,
1513
Table,
1614
Tag,
1715
} from "antd";
1816
import dayjs from "dayjs";
19-
import { MAX } from "@cocalc/util/licenses/purchase/consts";
2017
import { useMemo, useState } from "react";
2118

19+
import { MAX } from "@cocalc/util/licenses/purchase/consts";
20+
import { MIN_DISK_GB } from "@cocalc/util/upgrades/consts";
21+
import type { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";
22+
import type { Changes } from "@cocalc/util/purchases/cost-to-edit-license";
23+
2224
type Field =
2325
| "start"
2426
| "end"
@@ -217,7 +219,7 @@ export default function LicenseEditor({
217219
value: (
218220
<InputNumber
219221
disabled={disabledFields?.has("custom_disk")}
220-
min={3}
222+
min={Math.min(info.custom_disk || MIN_DISK_GB, MIN_DISK_GB)}
221223
max={MAX.disk}
222224
step={1}
223225
value={info.custom_disk}

src/packages/next/components/store/quota-config-presets.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6+
import { ReactNode } from "react";
7+
68
import { IconName } from "@cocalc/frontend/components/icon";
79
import { Uptime } from "@cocalc/util/consts/site-license";
10+
import { MAX_DISK_GB } from "@cocalc/util/upgrades/consts";
811
import { Paragraph } from "components/misc";
912
import A from "components/misc/A";
10-
import { ReactNode } from "react";
13+
import { STANDARD_DISK } from "@cocalc/util/consts/billing";
1114

1215
export type Preset = "standard" | "instructor" | "research";
1316

@@ -19,7 +22,7 @@ export const PRESET_MATCH_FIELDS: Record<string, string> = {
1922
ram: "memory",
2023
uptime: "idle timeout",
2124
member: "member hosting",
22-
};
25+
} as const;
2326

2427
export interface PresetConfig {
2528
icon: IconName;
@@ -42,7 +45,6 @@ type PresetEntries = {
4245
// some constants to keep text and preset in sync
4346
const STANDARD_CPU = 1;
4447
const STANDARD_RAM = 4;
45-
const STANDARD_DISK = 3;
4648

4749
const PRESET_STANDARD_NAME = "Standard";
4850

@@ -149,7 +151,7 @@ export const SITE_LICENSE: PresetEntries = {
149151
),
150152
cpu: 1,
151153
ram: 2 * STANDARD_RAM,
152-
disk: 15,
154+
disk: Math.min(Math.max(15, 4 * STANDARD_DISK), MAX_DISK_GB),
153155
uptime: "medium",
154156
member: true,
155157
},
@@ -191,8 +193,8 @@ export const SITE_LICENSE: PresetEntries = {
191193
</>
192194
),
193195
cpu: 2,
194-
ram: 10,
195-
disk: 10,
196+
ram: 2 * STANDARD_RAM,
197+
disk: Math.min(Math.max(15, 4 * STANDARD_DISK), MAX_DISK_GB),
196198
uptime: "day",
197199
member: true,
198200
},
@@ -246,7 +248,7 @@ export const COURSE = {
246248
),
247249
cpu: 1,
248250
ram: 8,
249-
disk: 2 * STANDARD_DISK,
251+
disk: Math.min(2 * STANDARD_DISK, MAX_DISK_GB),
250252
uptime: "medium",
251253
member: true,
252254
},

src/packages/next/components/store/quota-config.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import { HelpIcon } from "@cocalc/frontend/components/help-icon";
2222
import { Icon } from "@cocalc/frontend/components/icon";
2323
import { displaySiteLicense } from "@cocalc/util/consts/site-license";
2424
import { plural, unreachable } from "@cocalc/util/misc";
25-
import { BOOST, DISK_DEFAULT_GB, REGULAR } from "@cocalc/util/upgrades/consts";
25+
import {
26+
BOOST,
27+
DISK_DEFAULT_GB,
28+
MAX_DISK_GB,
29+
MIN_DISK_GB,
30+
REGULAR,
31+
} from "@cocalc/util/upgrades/consts";
2632
import type { LicenseSource } from "@cocalc/util/upgrades/shopping";
2733

2834
import PricingItem, { Line } from "components/landing/pricing-item";
@@ -281,8 +287,29 @@ export const QuotaConfig: React.FC<Props> = (props: Props) => {
281287
);
282288
}
283289

290+
function generateDiskPresets(min: number, max: number): number[] {
291+
if (min >= max) return [min];
292+
293+
const range = max - min;
294+
const presets = [min]; // Always include minimum
295+
296+
// Create 3-4 evenly spaced values
297+
const step = Math.ceil(range / 4);
298+
let current = min + step;
299+
while (current < max) {
300+
presets.push(current);
301+
current += step;
302+
}
303+
304+
presets.push(max); // Always include maximum
305+
return [...new Set(presets)].sort((a, b) => a - b); // Remove duplicates and sort
306+
}
307+
284308
function disk() {
285-
// 2022-06: price increase "version 2": minimum disk we sell (also the free quota) is 3gb, not 1gb
309+
// Generate dynamic presets based on MIN_DISK_GB and MAX_DISK_GB
310+
const presets = boost
311+
? [0, ...generateDiskPresets(MIN_DISK_GB, PARAMS.disk.max).slice(0, 3)] // For boost, include 0 and limit to 3 additional values
312+
: generateDiskPresets(MIN_DISK_GB, MAX_DISK_GB);
286313
return (
287314
<Form.Item
288315
label="Disk space"
@@ -314,9 +341,7 @@ export const QuotaConfig: React.FC<Props> = (props: Props) => {
314341
onChange();
315342
}}
316343
units={"G Disk"}
317-
presets={
318-
boost ? [0, 3, 6, PARAMS.disk.max] : [3, 5, 10, PARAMS.disk.max]
319-
}
344+
presets={presets}
320345
/>
321346
</Form.Item>
322347
);

src/packages/util/consts/billing.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ export const AVG_YEAR_DAYS = 12 * AVG_MONTH_DAYS;
1010
export const ONE_MONTH_MS = AVG_MONTH_DAYS * ONE_DAY_MS;
1111

1212
// throughout the UI, we show this price as the minimum (per month)
13-
export const LICENSE_MIN_PRICE = "about $6/month";
13+
// It is nice if it is vague enough to match price changes.
14+
export const LICENSE_MIN_PRICE = "a few $ per month";
1415

1516
// Trial Banner in the UI
1617
export const EVALUATION_PERIOD_DAYS = 3;
1718
export const BANNER_NON_DISMISSIBLE_DAYS = 7;
19+
20+
// The "standard license" disk size.
21+
// used in next/store and student-pay
22+
// Aug 2025: changed from 3 to 10 GB in anticipation of the new file server
23+
// However, the actual presets are capped at the maximum disk size.
24+
// That disk max is in util/upgrades/consts.ts
25+
export const STANDARD_DISK = 10;

src/packages/util/licenses/purchase/compute-cost.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { compute_cost } from "./compute-cost";
22
import { decimalMultiply } from "@cocalc/util/stripe/calc";
33

4-
describe("use the compute-cost function with v1 pricing, no version, and a test version to compute the price of a license", () => {
4+
const MONTHLY_V1 = 27.15;
5+
6+
describe("compute-cost v1 pricing", () => {
57
// This is a monthly business subscription for 3 projects with 1 cpu, 2 GB ram and 3 GB disk,
68
// using v1 pricing. On the website right now it says this should cost:
79
// "Cost: USD $27.15 monthly USD $9.05 per project"
8-
const monthly1 = 27.15;
10+
const monthly1 = MONTHLY_V1;
911
const info1 = {
1012
version: "1",
1113
end: new Date("2024-01-06T22:00:02.582Z"),
@@ -103,3 +105,53 @@ describe("a couple more consistency checks with prod", () => {
103105
expect(cost.cost).toBe(amount);
104106
});
105107
});
108+
109+
describe("compute-cost v3 pricing", () => {
110+
// This is a monthly business subscription for 3 projects with 1 cpu, 2 GB ram and 3 GB disk,
111+
// using v3 pricing.
112+
const monthly3 = 31.5;
113+
const info1 = {
114+
version: "3",
115+
end: new Date("2024-01-06T22:00:02.582Z"),
116+
type: "quota",
117+
user: "business",
118+
boost: false,
119+
start: new Date("2023-12-05T17:15:55.781Z"),
120+
upgrade: "custom",
121+
quantity: 3,
122+
account_id: "6aae57c6-08f1-4bb5-848b-3ceb53e61ede",
123+
custom_cpu: 1,
124+
custom_ram: 2,
125+
custom_disk: 3,
126+
subscription: "monthly",
127+
custom_member: true,
128+
custom_uptime: "short",
129+
custom_dedicated_cpu: 0,
130+
custom_dedicated_ram: 0,
131+
} as const;
132+
133+
it("computes the cost", () => {
134+
const cost1 = compute_cost(info1);
135+
expect(decimalMultiply(cost1.cost_sub_month, cost1.quantity)).toBe(
136+
monthly3,
137+
);
138+
});
139+
140+
it("version 1 is the default", () => {
141+
const info = { ...info1 };
142+
// @ts-ignore
143+
delete info["version"];
144+
const cost = compute_cost(info);
145+
expect(decimalMultiply(cost.cost_sub_month, cost.quantity)).toBe(
146+
MONTHLY_V1,
147+
);
148+
});
149+
150+
it("computes correct cost with a different version of pricing params", () => {
151+
const info = { ...info1 };
152+
// @ts-ignore
153+
info.version = "test_1";
154+
const cost = compute_cost(info);
155+
expect(decimalMultiply(cost.cost_sub_month, cost.quantity)).toBe(54.3);
156+
});
157+
});

src/packages/util/licenses/purchase/compute-cost.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ export function compute_cost(info: PurchaseInfo): Cost {
134134
cost_per_project_per_month *=
135135
COSTS.user_discount[user] * COSTS.sub_discount[subscription];
136136

137-
cost_per_project_per_month = round2up(cost_per_project_per_month);
137+
// If the numbers were picked to give clean prices, it is possible to get
138+
// things like 12.50000001 and we do NOT want to round it up to 12.51.
139+
cost_per_project_per_month = round2up(cost_per_project_per_month - 0.00001);
138140

139141
// It's convenient in all cases to have the actual amount we will be charging
140142
// for both monthly and yearly available.

src/packages/util/licenses/purchase/consts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import costVersions from "./cost-versions";
1111
// is used when the version is not defined.
1212
const FALLBACK_VERSION = "1";
1313

14-
export const CURRENT_VERSION = "1";
14+
export const CURRENT_VERSION = "3";
1515

1616
// Another gamble implicit in this is that pre's are available. When they
1717
// aren't, cocalc.com switches to uses MUCH more expensive non-preemptibles.

src/packages/util/licenses/purchase/cost-versions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ const COST = {
6767
ALWAYS_RUNNING_FACTOR: 2,
6868
},
6969

70+
3: {
71+
SUB_DISCOUNT: { no: 1, monthly: 0.9, yearly: 0.75 },
72+
GCE_COSTS: {
73+
ram: 0.625, // for pre-emptibles
74+
cpu: 5, // for pre-emptibles
75+
disk: 0.04, // per GB/month
76+
non_pre_factor: 3.5, // Roughly Google's factor for non-preemptible's
77+
},
78+
// 2025-08: Andrey increases it to 1
79+
COST_MULTIPLIER: 1,
80+
NONMEMBER_DENSITY: 2,
81+
ACADEMIC_DISCOUNT: 0.6,
82+
// 2025-08: in anticipation of new file storage
83+
DISK_FACTOR: 6.25,
84+
RAM_OVERCOMMIT: 5,
85+
CPU_OVERCOMMIT: 10,
86+
ALWAYS_RUNNING_FACTOR: 2,
87+
},
88+
7089
// this version is PURELY for testing purposes
7190
test_1: {
7291
SUB_DISCOUNT: { no: 1, monthly: 0.9, yearly: 0.85 },

src/packages/util/licenses/purchase/purchase-info.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import dayjs from "dayjs";
2+
3+
import type { Date0 } from "@cocalc/util/types/store";
14
import type {
25
Period,
36
SiteLicenseDescriptionDB,
47
} from "@cocalc/util/upgrades/shopping";
5-
import type { PurchaseInfo, StartEndDates, Subscription } from "./types";
68
import { CURRENT_VERSION } from "./consts";
7-
import type { Date0 } from "@cocalc/util/types/store";
8-
import dayjs from "dayjs";
9+
import type { PurchaseInfo, StartEndDates, Subscription } from "./types";
910

1011
// this ALWAYS returns purchaseInfo that is the *current* version.
1112
export default function getPurchaseInfo(

src/packages/util/licenses/purchase/student-pay.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { PurchaseInfo } from "./types";
21
import dayjs from "dayjs";
32

3+
import { STANDARD_DISK } from "@cocalc/util/consts/billing";
4+
import type { PurchaseInfo } from "./types";
5+
46
export const DEFAULT_PURCHASE_INFO = {
57
type: "quota",
68
user: "academic",
@@ -11,7 +13,7 @@ export const DEFAULT_PURCHASE_INFO = {
1113
custom_dedicated_cpu: 0,
1214
custom_ram: 4,
1315
custom_dedicated_ram: 0,
14-
custom_disk: 3,
16+
custom_disk: STANDARD_DISK,
1517
custom_member: true,
1618
custom_uptime: "short",
1719
start: new Date(),

0 commit comments

Comments
 (0)