Add self-serve billing: tiers, credits, BTCPay and Zaprite
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Phase 1 of self-serve subscriptions: prepaid-period expiry enforcement +
|
||||
// the extend-from-current-expiry logic both payment rails will call.
|
||||
|
||||
import { test, describe, before } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { identityTier, isSubscriptionExpired } from "../identity.js";
|
||||
import { initCredits, extendUserTier, getUserCreditRow } from "../credits.js";
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
describe("subscription expiry enforcement (pure)", () => {
|
||||
test("isSubscriptionExpired: null / future / past / invalid", () => {
|
||||
assert.equal(isSubscriptionExpired({}), false);
|
||||
assert.equal(isSubscriptionExpired({ subscription_expires_at: null }), false);
|
||||
assert.equal(
|
||||
isSubscriptionExpired({ subscription_expires_at: new Date(Date.now() + DAY).toISOString() }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isSubscriptionExpired({ subscription_expires_at: new Date(Date.now() - DAY).toISOString() }),
|
||||
true,
|
||||
);
|
||||
assert.equal(isSubscriptionExpired({ subscription_expires_at: "not-a-date" }), false);
|
||||
});
|
||||
|
||||
test("identityTier: cloud tier honored until expiry, then Core", () => {
|
||||
const cloud = { kind: "cloud" };
|
||||
// No expiry = operator comp grant → never expires.
|
||||
assert.equal(identityTier(cloud, { tier_snapshot: "max" }), "max");
|
||||
// Future expiry → tier honored.
|
||||
assert.equal(
|
||||
identityTier(cloud, { tier_snapshot: "max", subscription_expires_at: new Date(Date.now() + DAY).toISOString() }),
|
||||
"max",
|
||||
);
|
||||
// Past expiry → effectively Core.
|
||||
assert.equal(
|
||||
identityTier(cloud, { tier_snapshot: "max", subscription_expires_at: new Date(Date.now() - DAY).toISOString() }),
|
||||
"core",
|
||||
);
|
||||
});
|
||||
|
||||
test("identityTier: license path is unaffected by expiry", () => {
|
||||
assert.equal(identityTier({ kind: "license", license: { tier: "pro" } }, null), "pro");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extendUserTier (prepaid periods)", () => {
|
||||
before(async () => {
|
||||
await initCredits({ dataDir: mkdtempSync(path.join(tmpdir(), "relay-tier-")) });
|
||||
});
|
||||
|
||||
test("first purchase sets the tier + ~periodDays out", async () => {
|
||||
const row = await extendUserTier({ userId: "u1", tier: "pro", periodDays: 30 });
|
||||
assert.equal(row.tier_snapshot, "pro");
|
||||
const days = (new Date(row.subscription_expires_at).getTime() - Date.now()) / DAY;
|
||||
assert.ok(days > 29.9 && days < 30.1, `~30 days, got ${days}`);
|
||||
});
|
||||
|
||||
test("early renewal EXTENDS from the current expiry (adds time)", async () => {
|
||||
const first = await extendUserTier({ userId: "u2", tier: "max", periodDays: 30 });
|
||||
const firstExp = new Date(first.subscription_expires_at).getTime();
|
||||
const second = await extendUserTier({ userId: "u2", tier: "max", periodDays: 30 });
|
||||
const addedDays = (new Date(second.subscription_expires_at).getTime() - firstExp) / DAY;
|
||||
assert.ok(addedDays > 29.9 && addedDays < 30.1, `added ~30 days, got ${addedDays}`);
|
||||
});
|
||||
|
||||
test("renewing AFTER expiry starts fresh from now", async () => {
|
||||
const row = await getUserCreditRow("u3");
|
||||
row.tier_snapshot = "pro";
|
||||
row.subscription_expires_at = new Date(Date.now() - 5 * DAY).toISOString(); // expired 5d ago
|
||||
const renewed = await extendUserTier({ userId: "u3", tier: "pro", periodDays: 30 });
|
||||
const days = (new Date(renewed.subscription_expires_at).getTime() - Date.now()) / DAY;
|
||||
assert.ok(days > 29.9 && days < 30.1, `fresh ~30 days from now, got ${days}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user