Path: blob/master/src/packages/next/components/store/site-license-cost.tsx
1450 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Icon } from "@cocalc/frontend/components/icon";6import { untangleUptime } from "@cocalc/util/consts/site-license";7import {8describeQuotaOnLine,9describe_quota,10} from "@cocalc/util/licenses/describe-quota";11import type {12CostInputPeriod,13PurchaseInfo,14Subscription,15} from "@cocalc/util/licenses/purchase/types";16import { money } from "@cocalc/util/licenses/purchase/utils";17import { plural, round2, round2up } from "@cocalc/util/misc";18import { appendAfterNowToDate, getDays } from "@cocalc/util/stripe/timecalcs";19import Timestamp, { processTimestamp } from "components/misc/timestamp";20import { ReactNode } from "react";21import { useTimeFixer } from "./util";22import { Tooltip, Typography } from "antd";23import { currency } from "@cocalc/util/misc";24const { Text } = Typography;25import { periodicCost } from "@cocalc/util/licenses/purchase/compute-cost";26import { decimalMultiply } from "@cocalc/util/stripe/calc";2728interface Props {29cost: CostInputPeriod;30simple?: boolean;31oneLine?: boolean;32simpleShowPeriod?: boolean;33discountTooltip?: boolean;34noDiscount?: boolean;35}3637export function DisplayCost({38cost,39simple = false,40oneLine = false,41simpleShowPeriod = true,42}: Props) {43if (cost == null || isNaN(cost.cost)) {44return <>–</>;45}4647if (simple) {48return (49<>50{cost.cost_sub_first_period != null &&51cost.cost_sub_first_period != cost.cost && (52<>53{" "}54{money(round2up(cost.cost_sub_first_period))} due today, then55{oneLine ? <>, </> : <br />}56</>57)}58{money(round2up(periodicCost(cost)))}59{cost.period != "range" ? (60<>61{oneLine ? " " : <br />}62{simpleShowPeriod && cost.period}63</>64) : (65""66)}67{oneLine ? null : <br />}{" "}68</>69);70}71const desc = `${money(round2up(periodicCost(cost)))} ${72cost.period != "range" ? cost.period : ""73}`;7475return (76<span>77{describeItem({ info: cost.input })}78<hr />79<Icon name="money-check" /> Total Cost: {desc}80</span>81);82}8384interface DescribeItemProps {85info;86variant?: "short" | "long";87voucherPeriod?: boolean;88}8990// TODO: this should be a component. Rename it to DescribeItem and use it91// properly, e.g., <DescribeItem info={cost.input}/> above.9293export function describeItem({94info,95variant = "long",96voucherPeriod,97}: DescribeItemProps): ReactNode {98if (info.type == "cash-voucher") {99// see also packages/util/upgrades/describe.ts for text version of this100// that appears on invoices.101return (102<>103{info.numVouchers ?? 1} {plural(info.numVouchers ?? 1, "Voucher Code")}{" "}104{info.numVouchers > 1 ? " each " : ""} worth{" "}105{currency(info.amount)}. Total Value:{" "}106{currency(decimalMultiply(info.amount, info.numVouchers ?? 1))}107{info.whenPay == "admin" ? " (admin: no charge)" : ""}108</>109);110}111if (info.type !== "quota") {112throw Error("at this point, we only deal with type=quota");113}114115if (info.quantity == null) {116throw new Error("should not happen");117}118119const { always_running, idle_timeout } = untangleUptime(120info.custom_uptime ?? "short",121);122123const quota = {124ram: info.custom_ram,125cpu: info.custom_cpu,126disk: info.custom_disk,127always_running,128idle_timeout,129member: info.custom_member,130user: info.user,131};132133if (variant === "short") {134return (135<>136<Text strong={true}>{describeQuantity({ quota: info, variant })}</Text>{" "}137{describeQuotaOnLine(quota)},{" "}138{describePeriod({ quota: info, variant, voucherPeriod })}139</>140);141} else {142return (143<>144{describe_quota(quota, false)}{" "}145{describeQuantity({ quota: info, variant })} (146{describePeriod({ quota: info, variant, voucherPeriod })})147</>148);149}150}151152interface DescribeQuantityProps {153quota: Partial<PurchaseInfo>;154variant?: "short" | "long";155}156157function describeQuantity(props: DescribeQuantityProps): ReactNode {158const { quota: info, variant = "long" } = props;159const { quantity = 1 } = info;160161if (variant === "short") {162return `${quantity}x`;163} else {164return `for ${quantity} running ${plural(quantity, "project")}`;165}166}167168interface PeriodProps {169quota: {170subscription?: Omit<Subscription, "no">;171start?: Date | string | null;172end?: Date | string | null;173};174variant?: "short" | "long";175// voucherPeriod: description used for a voucher -- just give number of days, since the exact dates themselves are discarded.176voucherPeriod?: boolean;177}178179/**180* ATTN: this is not a general purpose period description generator. It's very specific181* to the purchases in the store!182*/183export function describePeriod({184quota,185variant = "long",186voucherPeriod,187}: PeriodProps): ReactNode {188const { subscription, start: startRaw, end: endRaw } = quota;189190const { fromServerTime, serverTimeDate } = useTimeFixer();191192if (subscription == "no") {193if (startRaw == null || endRaw == null)194throw new Error(`start date not set!`);195const start = fromServerTime(startRaw);196const end = fromServerTime(endRaw);197198if (start == null || end == null) {199throw new Error(`this should never happen`);200}201202// days are calculated based on the actual selection203const days = round2(getDays({ start, end }));204205if (voucherPeriod) {206return (207<>208license lasts {days} {plural(days, "day")}209</>210);211}212213// but the displayed end mimics what will happen later on the backend214// i.e. if the day already started, we append the already elapsed period to the end215const endDisplay = appendAfterNowToDate({216now: serverTimeDate,217start,218end,219});220221if (variant === "short") {222const tsStart = processTimestamp({ datetime: start, absolute: true });223const tsEnd = processTimestamp({ datetime: endDisplay, absolute: true });224if (tsStart === "-" || tsEnd === "-") {225return "-";226}227const timespanStr = `${tsStart.absoluteTimeFull} - ${tsEnd.absoluteTimeFull}`;228return (229<Tooltip230trigger={["hover", "click"]}231title={timespanStr}232placement="bottom"233>234{`${days} ${plural(days, "day")}`}235</Tooltip>236);237} else {238return (239<>240<Timestamp datetime={start} absolute /> to{" "}241<Timestamp datetime={endDisplay} absolute />, {days}{" "}242{plural(days, "day")}243</>244);245}246} else {247if (variant === "short") {248return `${subscription}`;249} else {250return `${subscription} subscription`;251}252}253}254255256