// 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}`); }); test("early renewal PRESERVES the monthly credit counter (no free reset)", async () => { await extendUserTier({ userId: "u4", tier: "pro", periodDays: 30 }); const row = await getUserCreditRow("u4"); row.monthly_consumed = 7; // simulate credits already spent this cycle row.monthly_gemini_consumed = 3; // Pay early / renew while the subscription is still in force. await extendUserTier({ userId: "u4", tier: "pro", periodDays: 30 }); const after = await getUserCreditRow("u4"); assert.equal(after.monthly_consumed, 7, "consumed credits must survive an early renewal"); assert.equal(after.monthly_gemini_consumed, 3); }); test("resubscribing AFTER a lapse starts a fresh cycle (counter reset)", async () => { await extendUserTier({ userId: "u5", tier: "pro", periodDays: 30 }); const row = await getUserCreditRow("u5"); row.monthly_consumed = 9; row.subscription_expires_at = new Date(Date.now() - 5 * DAY).toISOString(); // lapsed await extendUserTier({ userId: "u5", tier: "pro", periodDays: 30 }); const after = await getUserCreditRow("u5"); assert.equal(after.monthly_consumed, 0, "a lapsed resubscribe starts a clean cycle"); }); });