221 lines
9.0 KiB
TypeScript
221 lines
9.0 KiB
TypeScript
import { test, expect, describe } from "bun:test";
|
|
import { computeApplyPlan, computeExpiry, insertIntoStack } from "./monitor";
|
|
|
|
const day = 86400000;
|
|
const now = new Date("2026-03-24T00:00:00Z");
|
|
function daysFromNow(d: number) { return new Date(now.getTime() + d * day); }
|
|
|
|
describe("insertIntoStack", () => {
|
|
test("inserts into empty stack", () => {
|
|
expect(insertIntoStack([], { plan: "pro", remaining_days: 20 }))
|
|
.toEqual([{ plan: "pro", remaining_days: 20 }]);
|
|
});
|
|
|
|
test("maintains tier-descending order", () => {
|
|
const stack = [{ plan: "pro2x", remaining_days: 10 }];
|
|
expect(insertIntoStack(stack, { plan: "pro4x", remaining_days: 5 }))
|
|
.toEqual([{ plan: "pro4x", remaining_days: 5 }, { plan: "pro2x", remaining_days: 10 }]);
|
|
});
|
|
|
|
test("inserts lower tier after higher", () => {
|
|
const stack = [{ plan: "pro2x", remaining_days: 10 }];
|
|
expect(insertIntoStack(stack, { plan: "pro", remaining_days: 30 }))
|
|
.toEqual([{ plan: "pro2x", remaining_days: 10 }, { plan: "pro", remaining_days: 30 }]);
|
|
});
|
|
|
|
test("merges same plan by adding days", () => {
|
|
const stack = [{ plan: "pro", remaining_days: 20 }];
|
|
expect(insertIntoStack(stack, { plan: "pro", remaining_days: 30 }))
|
|
.toEqual([{ plan: "pro", remaining_days: 50 }]);
|
|
});
|
|
|
|
test("merges same plan - null wins (lifetime)", () => {
|
|
const stack = [{ plan: "lifetime", remaining_days: null }];
|
|
expect(insertIntoStack(stack, { plan: "lifetime", remaining_days: null }))
|
|
.toEqual([{ plan: "lifetime", remaining_days: null }]);
|
|
});
|
|
|
|
test("merges timed into permanent → permanent", () => {
|
|
const stack = [{ plan: "pro", remaining_days: 20 }];
|
|
// This shouldn't happen in practice but tests null-wins logic
|
|
expect(insertIntoStack(stack, { plan: "pro", remaining_days: null }))
|
|
.toEqual([{ plan: "pro", remaining_days: null }]);
|
|
});
|
|
});
|
|
|
|
describe("computeApplyPlan", () => {
|
|
test("free user buys Pro 1mo", () => {
|
|
const acc = { plan: "free", plan_expires_at: null, plan_stack: [] };
|
|
const result = computeApplyPlan(acc, { plan: "pro", months: 1 }, now);
|
|
expect(result.plan).toBe("pro");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 30 * day);
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("pro user renews same plan", () => {
|
|
const expires = daysFromNow(20);
|
|
const acc = { plan: "pro", plan_expires_at: expires, plan_stack: [] };
|
|
const result = computeApplyPlan(acc, { plan: "pro", months: 1 }, now);
|
|
expect(result.plan).toBe("pro");
|
|
expect(result.plan_expires_at!.getTime()).toBe(expires.getTime() + 30 * day);
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("pro (20d left) upgrades to pro2x 1mo", () => {
|
|
const acc = { plan: "pro", plan_expires_at: daysFromNow(20), plan_stack: [] };
|
|
const result = computeApplyPlan(acc, { plan: "pro2x", months: 1 }, now);
|
|
expect(result.plan).toBe("pro2x");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 30 * day);
|
|
expect(result.plan_stack).toEqual([{ plan: "pro", remaining_days: 20 }]);
|
|
});
|
|
|
|
test("pro2x (10d left) upgrades to pro4x, existing pro base", () => {
|
|
const acc = {
|
|
plan: "pro2x", plan_expires_at: daysFromNow(10),
|
|
plan_stack: [{ plan: "pro", remaining_days: 20 }]
|
|
};
|
|
const result = computeApplyPlan(acc, { plan: "pro4x", months: 1 }, now);
|
|
expect(result.plan).toBe("pro4x");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 30 * day);
|
|
// pro2x (tier 2) should be before pro (tier 1) in stack
|
|
expect(result.plan_stack).toEqual([
|
|
{ plan: "pro2x", remaining_days: 10 },
|
|
{ plan: "pro", remaining_days: 20 },
|
|
]);
|
|
});
|
|
|
|
test("lifetime user buys pro2x 1mo", () => {
|
|
const acc = { plan: "lifetime", plan_expires_at: null, plan_stack: [] };
|
|
const result = computeApplyPlan(acc, { plan: "pro2x", months: 1 }, now);
|
|
expect(result.plan).toBe("pro2x");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 30 * day);
|
|
expect(result.plan_stack).toEqual([{ plan: "lifetime", remaining_days: null }]);
|
|
});
|
|
|
|
test("pro4x (15d left) buys lifetime - goes to stack", () => {
|
|
const acc = { plan: "pro4x", plan_expires_at: daysFromNow(15), plan_stack: [] };
|
|
const result = computeApplyPlan(acc, { plan: "lifetime", months: null }, now);
|
|
expect(result.plan).toBe("pro4x"); // stays active (higher tier)
|
|
expect(result.plan_expires_at!.getTime()).toBe(daysFromNow(15).getTime());
|
|
expect(result.plan_stack).toEqual([{ plan: "lifetime", remaining_days: null }]);
|
|
});
|
|
|
|
test("pro2x (10d left) buys pro 1mo with lifetime in stack", () => {
|
|
const acc = {
|
|
plan: "pro2x", plan_expires_at: daysFromNow(10),
|
|
plan_stack: [{ plan: "lifetime", remaining_days: null }]
|
|
};
|
|
const result = computeApplyPlan(acc, { plan: "pro", months: 1 }, now);
|
|
expect(result.plan).toBe("pro2x"); // stays active
|
|
expect(result.plan_stack).toEqual([
|
|
{ plan: "pro", remaining_days: 30 },
|
|
{ plan: "lifetime", remaining_days: null },
|
|
]);
|
|
});
|
|
|
|
test("expired pro activates new plan without saving expired", () => {
|
|
const acc = { plan: "pro", plan_expires_at: daysFromNow(-5), plan_stack: [] };
|
|
const result = computeApplyPlan(acc, { plan: "pro2x", months: 1 }, now);
|
|
expect(result.plan).toBe("pro2x");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 30 * day);
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("free with existing stack - preserves stack", () => {
|
|
const acc = { plan: "free", plan_expires_at: null, plan_stack: [{ plan: "lifetime", remaining_days: null }] };
|
|
const result = computeApplyPlan(acc, { plan: "pro", months: 1 }, now);
|
|
expect(result.plan).toBe("pro");
|
|
expect(result.plan_stack).toEqual([{ plan: "lifetime", remaining_days: null }]);
|
|
});
|
|
|
|
test("buying same lower tier twice merges days", () => {
|
|
const acc = {
|
|
plan: "pro2x", plan_expires_at: daysFromNow(20),
|
|
plan_stack: [{ plan: "pro", remaining_days: 15 }]
|
|
};
|
|
const result = computeApplyPlan(acc, { plan: "pro", months: 1 }, now);
|
|
expect(result.plan).toBe("pro2x"); // stays
|
|
expect(result.plan_stack).toEqual([{ plan: "pro", remaining_days: 45 }]); // 15 + 30
|
|
});
|
|
});
|
|
|
|
describe("computeExpiry", () => {
|
|
test("returns null for non-pro plans", () => {
|
|
expect(computeExpiry({ plan: "free", plan_expires_at: null, plan_stack: [] }, now)).toBeNull();
|
|
expect(computeExpiry({ plan: "lifetime", plan_expires_at: null, plan_stack: [] }, now)).toBeNull();
|
|
});
|
|
|
|
test("returns null for non-expired pro", () => {
|
|
const acc = { plan: "pro", plan_expires_at: daysFromNow(10), plan_stack: [] };
|
|
expect(computeExpiry(acc, now)).toBeNull();
|
|
});
|
|
|
|
test("expired pro with empty stack → free", () => {
|
|
const acc = { plan: "pro", plan_expires_at: daysFromNow(-1), plan_stack: [] };
|
|
const result = computeExpiry(acc, now)!;
|
|
expect(result.plan).toBe("free");
|
|
expect(result.plan_expires_at).toBeNull();
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("expired pro with lifetime in stack → promote lifetime", () => {
|
|
const acc = {
|
|
plan: "pro2x", plan_expires_at: daysFromNow(-1),
|
|
plan_stack: [{ plan: "lifetime", remaining_days: null }]
|
|
};
|
|
const result = computeExpiry(acc, now)!;
|
|
expect(result.plan).toBe("lifetime");
|
|
expect(result.plan_expires_at).toBeNull();
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("expired pro with timed base → promote with computed expiry", () => {
|
|
const acc = {
|
|
plan: "pro2x", plan_expires_at: daysFromNow(-1),
|
|
plan_stack: [{ plan: "pro", remaining_days: 20 }]
|
|
};
|
|
const result = computeExpiry(acc, now)!;
|
|
expect(result.plan).toBe("pro");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 20 * day);
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("expired pro with multi-layer stack → promotes top, keeps rest", () => {
|
|
const acc = {
|
|
plan: "pro4x", plan_expires_at: daysFromNow(-1),
|
|
plan_stack: [
|
|
{ plan: "pro2x", remaining_days: 15 },
|
|
{ plan: "lifetime", remaining_days: null },
|
|
]
|
|
};
|
|
const result = computeExpiry(acc, now)!;
|
|
expect(result.plan).toBe("pro2x");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 15 * day);
|
|
expect(result.plan_stack).toEqual([{ plan: "lifetime", remaining_days: null }]);
|
|
});
|
|
|
|
test("expired pro skips zero-day layers", () => {
|
|
const acc = {
|
|
plan: "pro4x", plan_expires_at: daysFromNow(-1),
|
|
plan_stack: [
|
|
{ plan: "pro2x", remaining_days: 0 },
|
|
{ plan: "pro", remaining_days: 10 },
|
|
]
|
|
};
|
|
const result = computeExpiry(acc, now)!;
|
|
expect(result.plan).toBe("pro");
|
|
expect(result.plan_expires_at!.getTime()).toBe(now.getTime() + 10 * day);
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
|
|
test("expired pro with all zero-day layers → free", () => {
|
|
const acc = {
|
|
plan: "pro", plan_expires_at: daysFromNow(-1),
|
|
plan_stack: [{ plan: "pro2x", remaining_days: 0 }]
|
|
};
|
|
const result = computeExpiry(acc, now)!;
|
|
expect(result.plan).toBe("free");
|
|
expect(result.plan_stack).toEqual([]);
|
|
});
|
|
});
|