Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/site-license-cost.tsx
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 { Icon } from "@cocalc/frontend/components/icon";
7
import { untangleUptime } from "@cocalc/util/consts/site-license";
8
import {
9
describeQuotaOnLine,
10
describe_quota,
11
} from "@cocalc/util/licenses/describe-quota";
12
import type {
13
CostInputPeriod,
14
PurchaseInfo,
15
Subscription,
16
} from "@cocalc/util/licenses/purchase/types";
17
import { money } from "@cocalc/util/licenses/purchase/utils";
18
import { plural, round2, round2up } from "@cocalc/util/misc";
19
import { appendAfterNowToDate, getDays } from "@cocalc/util/stripe/timecalcs";
20
import Timestamp, { processTimestamp } from "components/misc/timestamp";
21
import { ReactNode } from "react";
22
import { useTimeFixer } from "./util";
23
import { Tooltip, Typography } from "antd";
24
import { currency } from "@cocalc/util/misc";
25
const { Text } = Typography;
26
import { periodicCost } from "@cocalc/util/licenses/purchase/compute-cost";
27
import { decimalMultiply } from "@cocalc/util/stripe/calc";
28
29
interface Props {
30
cost: CostInputPeriod;
31
simple?: boolean;
32
oneLine?: boolean;
33
simpleShowPeriod?: boolean;
34
discountTooltip?: boolean;
35
noDiscount?: boolean;
36
}
37
38
export function DisplayCost({
39
cost,
40
simple = false,
41
oneLine = false,
42
simpleShowPeriod = true,
43
}: Props) {
44
if (cost == null || isNaN(cost.cost)) {
45
return <>&ndash;</>;
46
}
47
48
if (simple) {
49
return (
50
<>
51
{cost.cost_sub_first_period != null &&
52
cost.cost_sub_first_period != cost.cost && (
53
<>
54
{" "}
55
{money(round2up(cost.cost_sub_first_period))} due today, then
56
{oneLine ? <>, </> : <br />}
57
</>
58
)}
59
{money(round2up(periodicCost(cost)))}
60
{cost.period != "range" ? (
61
<>
62
{oneLine ? " " : <br />}
63
{simpleShowPeriod && cost.period}
64
</>
65
) : (
66
""
67
)}
68
{oneLine ? null : <br />}{" "}
69
</>
70
);
71
}
72
const desc = `${money(round2up(periodicCost(cost)))} ${
73
cost.period != "range" ? cost.period : ""
74
}`;
75
76
return (
77
<span>
78
{describeItem({ info: cost.input })}
79
<hr />
80
<Icon name="money-check" /> Total Cost: {desc}
81
</span>
82
);
83
}
84
85
interface DescribeItemProps {
86
info;
87
variant?: "short" | "long";
88
voucherPeriod?: boolean;
89
}
90
91
// TODO: this should be a component. Rename it to DescribeItem and use it
92
// properly, e.g., <DescribeItem info={cost.input}/> above.
93
94
export function describeItem({
95
info,
96
variant = "long",
97
voucherPeriod,
98
}: DescribeItemProps): ReactNode {
99
if (info.type == "cash-voucher") {
100
// see also packages/util/upgrades/describe.ts for text version of this
101
// that appears on invoices.
102
return (
103
<>
104
{info.numVouchers ?? 1} {plural(info.numVouchers ?? 1, "Voucher Code")}{" "}
105
{info.numVouchers > 1 ? " each " : ""} worth{" "}
106
{currency(info.amount)}. Total Value:{" "}
107
{currency(decimalMultiply(info.amount, info.numVouchers ?? 1))}
108
{info.whenPay == "admin" ? " (admin: no charge)" : ""}
109
</>
110
);
111
}
112
if (info.type !== "quota") {
113
throw Error("at this point, we only deal with type=quota");
114
}
115
116
if (info.quantity == null) {
117
throw new Error("should not happen");
118
}
119
120
const { always_running, idle_timeout } = untangleUptime(
121
info.custom_uptime ?? "short",
122
);
123
124
const quota = {
125
ram: info.custom_ram,
126
cpu: info.custom_cpu,
127
disk: info.custom_disk,
128
always_running,
129
idle_timeout,
130
member: info.custom_member,
131
user: info.user,
132
};
133
134
if (variant === "short") {
135
return (
136
<>
137
<Text strong={true}>{describeQuantity({ quota: info, variant })}</Text>{" "}
138
{describeQuotaOnLine(quota)},{" "}
139
{describePeriod({ quota: info, variant, voucherPeriod })}
140
</>
141
);
142
} else {
143
return (
144
<>
145
{describe_quota(quota, false)}{" "}
146
{describeQuantity({ quota: info, variant })} (
147
{describePeriod({ quota: info, variant, voucherPeriod })})
148
</>
149
);
150
}
151
}
152
153
interface DescribeQuantityProps {
154
quota: Partial<PurchaseInfo>;
155
variant?: "short" | "long";
156
}
157
158
function describeQuantity(props: DescribeQuantityProps): ReactNode {
159
const { quota: info, variant = "long" } = props;
160
const { quantity = 1 } = info;
161
162
if (variant === "short") {
163
return `${quantity}x`;
164
} else {
165
return `for ${quantity} running ${plural(quantity, "project")}`;
166
}
167
}
168
169
interface PeriodProps {
170
quota: {
171
subscription?: Omit<Subscription, "no">;
172
start?: Date | string | null;
173
end?: Date | string | null;
174
};
175
variant?: "short" | "long";
176
// voucherPeriod: description used for a voucher -- just give number of days, since the exact dates themselves are discarded.
177
voucherPeriod?: boolean;
178
}
179
180
/**
181
* ATTN: this is not a general purpose period description generator. It's very specific
182
* to the purchases in the store!
183
*/
184
export function describePeriod({
185
quota,
186
variant = "long",
187
voucherPeriod,
188
}: PeriodProps): ReactNode {
189
const { subscription, start: startRaw, end: endRaw } = quota;
190
191
const { fromServerTime, serverTimeDate } = useTimeFixer();
192
193
if (subscription == "no") {
194
if (startRaw == null || endRaw == null)
195
throw new Error(`start date not set!`);
196
const start = fromServerTime(startRaw);
197
const end = fromServerTime(endRaw);
198
199
if (start == null || end == null) {
200
throw new Error(`this should never happen`);
201
}
202
203
// days are calculated based on the actual selection
204
const days = round2(getDays({ start, end }));
205
206
if (voucherPeriod) {
207
return (
208
<>
209
license lasts {days} {plural(days, "day")}
210
</>
211
);
212
}
213
214
// but the displayed end mimics what will happen later on the backend
215
// i.e. if the day already started, we append the already elapsed period to the end
216
const endDisplay = appendAfterNowToDate({
217
now: serverTimeDate,
218
start,
219
end,
220
});
221
222
if (variant === "short") {
223
const tsStart = processTimestamp({ datetime: start, absolute: true });
224
const tsEnd = processTimestamp({ datetime: endDisplay, absolute: true });
225
if (tsStart === "-" || tsEnd === "-") {
226
return "-";
227
}
228
const timespanStr = `${tsStart.absoluteTimeFull} - ${tsEnd.absoluteTimeFull}`;
229
return (
230
<Tooltip
231
trigger={["hover", "click"]}
232
title={timespanStr}
233
placement="bottom"
234
>
235
{`${days} ${plural(days, "day")}`}
236
</Tooltip>
237
);
238
} else {
239
return (
240
<>
241
<Timestamp datetime={start} absolute /> to{" "}
242
<Timestamp datetime={endDisplay} absolute />, {days}{" "}
243
{plural(days, "day")}
244
</>
245
);
246
}
247
} else {
248
if (variant === "short") {
249
return `${subscription}`;
250
} else {
251
return `${subscription} subscription`;
252
}
253
}
254
}
255
256