126 lines
5.8 KiB
JavaScript
126 lines
5.8 KiB
JavaScript
|
|
// The plan catalogue and bundle pricing.
|
||
|
|
//
|
||
|
|
// Most of these assert honesty properties rather than arithmetic: that a free
|
||
|
|
// option is never priced, that only a plan where WE hold the bytes is flagged as
|
||
|
|
// creating an obligation, and that every recurring plan tells the buyer what
|
||
|
|
// happens to their NAME when hosting lapses. Those are the claims that get
|
||
|
|
// quietly wrong when copy is edited months later, and they are exactly the ones
|
||
|
|
// no one notices until a customer is upset.
|
||
|
|
|
||
|
|
import test from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
|
||
|
|
import {
|
||
|
|
HOSTING_PLANS, VPS_REFERRALS, PLAN_KIND,
|
||
|
|
planById, paidPlans, freePlans, quoteBundle,
|
||
|
|
} from "../src/lib/plans.js";
|
||
|
|
import { quotePrice } from "../src/lib/orders.js";
|
||
|
|
import { REGISTRAR } from "../src/registrar-config.js";
|
||
|
|
|
||
|
|
const NAME_PRICE = quotePrice({ serviceFeeSats: REGISTRAR.serviceFee.sats });
|
||
|
|
|
||
|
|
test("only one plan is actually hosting-as-a-service", () => {
|
||
|
|
const managed = HOSTING_PLANS.filter((p) => p.kind === PLAN_KIND.managed);
|
||
|
|
assert.equal(managed.length, 1, "selling more 'plans' than we run is how a storefront starts lying");
|
||
|
|
assert.equal(managed[0].id, "sia-managed-1y");
|
||
|
|
assert.equal(managed[0].weStore, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("every free plan is genuinely free and stores nothing of ours", () => {
|
||
|
|
for (const p of freePlans()) {
|
||
|
|
assert.equal(p.priceSats, 0n, `${p.id} is listed free but priced`);
|
||
|
|
assert.equal(p.weStore, false, `${p.id} is listed free but we hold the bytes`);
|
||
|
|
assert.equal(p.recurring, false, `${p.id} is free but recurring — that cannot both be true`);
|
||
|
|
}
|
||
|
|
assert.ok(freePlans().length >= 3, "most options cost the buyer nothing; the page should say so");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("paid plans are exactly the ones where we store bytes", () => {
|
||
|
|
const paid = paidPlans().map((p) => p.id).sort();
|
||
|
|
const stores = HOSTING_PLANS.filter((p) => p.weStore).map((p) => p.id).sort();
|
||
|
|
assert.deepEqual(paid, stores, "we should charge for storage and nothing else");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("every recurring plan explains what happens to the NAME on lapse", () => {
|
||
|
|
for (const p of HOSTING_PLANS.filter((x) => x.recurring)) {
|
||
|
|
assert.ok(p.onLapse, `${p.id} is recurring but says nothing about lapsing`);
|
||
|
|
assert.match(p.onLapse, /name is unaffected/i, "the flagship claim must survive a lapsed subscription");
|
||
|
|
assert.match(p.onLapse, /repoint/i, "the buyer needs to know the remedy is free and in their hands");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("non-recurring plans do not carry lapse language", () => {
|
||
|
|
for (const p of HOSTING_PLANS.filter((x) => !x.recurring)) {
|
||
|
|
assert.equal(p.onLapse, null, `${p.id} cannot lapse, so it must not imply it can`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("referrals are not plans and carry no price of ours", () => {
|
||
|
|
const ids = new Set(HOSTING_PLANS.map((p) => p.id));
|
||
|
|
for (const r of VPS_REFERRALS) {
|
||
|
|
assert.ok(!ids.has(r.id), `${r.id} appears in both lists — a referral is not something we sell`);
|
||
|
|
assert.ok(r.url.startsWith("https://"), "referral needs a real destination");
|
||
|
|
assert.equal(r.priceSats, undefined, "we do not price other people's products");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------- bundle pricing ----------
|
||
|
|
|
||
|
|
test("a name on its own is just the name price", () => {
|
||
|
|
const q = quoteBundle({ namePriceSats: NAME_PRICE });
|
||
|
|
assert.equal(q.totalSats, NAME_PRICE);
|
||
|
|
assert.equal(q.lines.length, 1);
|
||
|
|
assert.equal(q.plan, null);
|
||
|
|
assert.equal(q.createsHostingObligation, false);
|
||
|
|
assert.equal(q.recurring, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("name + managed hosting is one payment with itemised lines", () => {
|
||
|
|
const plan = planById("sia-managed-1y");
|
||
|
|
const q = quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-1y" });
|
||
|
|
assert.equal(q.totalSats, NAME_PRICE + plan.priceSats);
|
||
|
|
assert.equal(q.lines.length, 2, "one payment, two lines — not two orders to reconcile");
|
||
|
|
assert.equal(q.lines[0].id, "name");
|
||
|
|
assert.match(q.lines[1].label, /Managed Sia hosting/);
|
||
|
|
assert.match(q.lines[1].label, /1y/, "the period belongs in the label, where the buyer reads it");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("choosing a free plan adds no charge and no obligation", () => {
|
||
|
|
for (const p of freePlans()) {
|
||
|
|
const q = quoteBundle({ namePriceSats: NAME_PRICE, planId: p.id });
|
||
|
|
assert.equal(q.totalSats, NAME_PRICE, `${p.id} must not add cost`);
|
||
|
|
assert.equal(q.lines.length, 1, `${p.id} must not add a billing line`);
|
||
|
|
assert.equal(q.createsHostingObligation, false, `${p.id} must not make us a host`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("only the managed plan flags a hosting obligation", () => {
|
||
|
|
// This flag is what the UI keys the "we will be storing your files" disclosure
|
||
|
|
// off. Getting it wrong either hides a real liability or scares people off a
|
||
|
|
// free option that never touches our infrastructure.
|
||
|
|
const managed = quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-1y" });
|
||
|
|
assert.equal(managed.createsHostingObligation, true);
|
||
|
|
assert.equal(managed.recurring, true);
|
||
|
|
const onchain = quoteBundle({ namePriceSats: NAME_PRICE, planId: "onchain" });
|
||
|
|
assert.equal(onchain.createsHostingObligation, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("an unknown plan is refused rather than silently ignored", () => {
|
||
|
|
// Silently dropping it would sell a name, take the money, and quietly not
|
||
|
|
// provide the hosting the buyer picked.
|
||
|
|
assert.throws(() => quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-10y" }), /unknown hosting plan/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("bundle totals stay bigint throughout", () => {
|
||
|
|
const q = quoteBundle({ namePriceSats: NAME_PRICE, planId: "sia-managed-1y" });
|
||
|
|
assert.equal(typeof q.totalSats, "bigint");
|
||
|
|
for (const l of q.lines) assert.equal(typeof l.sats, "bigint", `${l.id} must not drift to Number`);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("every plan maps to a real BNS record type", () => {
|
||
|
|
const valid = new Set(["h", "u", "ip", "s3", "tls"]);
|
||
|
|
for (const p of HOSTING_PLANS) {
|
||
|
|
assert.ok(valid.has(p.record), `${p.id} points at "${p.record}", which the resolver does not implement`);
|
||
|
|
}
|
||
|
|
});
|