smc-hub
Version:
CoCalc: Backend webserver component
172 lines (162 loc) • 5.06 kB
text/typescript
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
/*
Ensure that plans are all correctly defined in stripe.
Stripe API docs https://stripe.com/docs/api/node#create_plan
*/
import Stripe from "stripe";
// STOPGAP FIX: relative dirs necessary for manage service
import { upgrades } from "smc-util/upgrade-spec";
import { init_stripe } from "./connection";
import { PostgreSQL } from "../postgres/types";
// Create all plans that are missing
export async function create_missing_plans(
logger: { debug: Function },
database: PostgreSQL
) {
const dbg = (m?) => logger.debug(`create_missing_plans: ${m}`);
dbg();
// We have deprecated the old "upgrade" subscriptions, and the
// code below would just ensure that all upgrade subscription types
// described in code were also represented in stripe. However,
// the existence of licenses which automatically created hundreds
// of subscriptions broke this code (since only 100 plans get returned).
// There's no point in fixing it now, since we won't be creating
// any new subscriptions.
dbg("DEPRECATED");
// This is just kept for reference
if (false) {
dbg("initialize stripe connection");
const stripe = await init_stripe(database, logger);
dbg("get already created plans");
const plans = await stripe.plans.list({ limit: 999 });
const known: { [id: string]: boolean } = {};
for (const plan of plans.data) {
known[plan.id] = true;
}
dbg("create any missing plans");
for (const name in upgrades.subscription) {
await create_plan(name, database, logger, known);
}
}
}
// Create a specific plan (error if plan already defined)
async function create_plan(
name: string, // the name of the plan, one of the keys of upgrades.subscription;
// NOTE: there are multiple stripe plans associated to a single cocalc
// plan, due to different intervals.
database: PostgreSQL,
logger: { debug: Function },
known: { [id: string]: boolean } // map from known plan ids to true -- these are skipped
): Promise<void> {
const spec = upgrades.subscription[name];
if (spec == null) {
throw Error(`unknown plan "${name}"`);
}
const dbg = (m?) => logger.debug(`create_plan(name="${name}"): ${m}`);
dbg();
dbg("initialize stripe connection");
const stripe: Stripe = await init_stripe(database, logger);
const plans = spec_to_plans(name, spec, known);
if (plans.length === 0) {
dbg("no missing stripe plans");
return;
}
dbg(`creating ${plans.length} missing stripe plans`);
for (const plan of plans) {
const { interval } = plan;
if (
interval != "day" &&
interval != "week" &&
interval != "month" &&
interval != "year"
) {
// make TS happy, and a good consistency check -- this check above makes it so interval
// has the right type, which is why we do the object merge below (to fix the typing).
throw Error(`invalid plan interval "${plan.interval}"`);
}
await stripe.plans.create({ ...plan, ...{ interval } });
}
}
function spec_to_plans(name: string, spec, known: { [id: string]: boolean }) {
const v: {
id: string;
interval: string;
interval_count: number;
amount: number;
product: {
name: string;
statement_descriptor: string;
};
currency: string;
}[] = [];
let the_desc = spec.desc;
const i = the_desc.indexOf("\n");
if (i !== -1) {
the_desc = the_desc.slice(0, i);
}
for (const period in spec.price) {
let desc, id, interval, interval_count;
const amount = spec.price[period];
switch (period) {
case "month":
id = name;
interval = "month";
interval_count = 1;
desc = the_desc;
break;
case "month4":
id = name;
interval = "month";
interval_count = 4;
desc = the_desc;
break;
case "year":
case "year1":
id = `${name}-year`;
interval = "year";
interval_count = 1;
desc = `One Year ${the_desc}`;
break;
case "week":
id = `${name}-week`;
interval = "week";
interval_count = 1;
desc = `One Week ${the_desc}`;
break;
default:
throw Error(`unknown period '${period}'`);
}
if (known[id]) {
continue;
}
let { statement } = spec;
if (statement == null) {
throw Error(
`plan statement must be defined but it is not for name='${name}'`
);
}
if (statement.length > 17) {
throw Error(
`statement '${statement}' must be at most 17 characters, but is ${statement.length} characters for name='${name}'`
);
}
if (interval === "year") {
statement += " YEAR";
}
v.push({
id,
interval,
interval_count,
amount: amount * 100,
product: {
name: desc,
statement_descriptor: statement,
},
currency: "usd",
});
}
return v;
}