Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
6.6 KiB
TypeScript
149 lines
6.6 KiB
TypeScript
/**
|
|
* Server Pricing Suite
|
|
*
|
|
* The server — never the client — decides what a plan costs. These tests pin
|
|
* the pure checkout builder against the shared price catalog (src/lib/billing/
|
|
* pricing.ts): bogus plans fall back to annual, the catalog amounts (499 /
|
|
* 3999 / 5999) are used verbatim when no Stripe Price ID is configured, and an
|
|
* env Price ID wins when present. No real Stripe calls are made.
|
|
*/
|
|
|
|
import { describe, test, expect } from "./runner";
|
|
import { buildCheckoutParams } from "../../src/lib/billing/checkout";
|
|
import { PLAN_CONFIGS } from "../../src/lib/billing/pricing";
|
|
|
|
const user = { id: "usr_test_checkout_1" };
|
|
|
|
describe("ServerPricing — plan resolution", () => {
|
|
test("unknown or missing plans fall back to annual", () => {
|
|
expect(buildCheckoutParams(user, "enterprise-ultra", {}).metadata.plan).toBe("annual");
|
|
expect(buildCheckoutParams(user, undefined, {}).metadata.plan).toBe("annual");
|
|
expect(buildCheckoutParams(user, null, {}).metadata.plan).toBe("annual");
|
|
expect(buildCheckoutParams(user, "", {}).metadata.plan).toBe("annual");
|
|
});
|
|
|
|
test("a bogus plan is charged the annual price, never an invented one", () => {
|
|
const params = buildCheckoutParams(user, "FREE_FOREVER", {});
|
|
expect(params.metadata.plan).toBe("annual");
|
|
expect(params.lineItems[0].price_data?.unit_amount).toBe(PLAN_CONFIGS.annual.unitAmountMinor);
|
|
expect(params.lineItems[0].price_data?.unit_amount).toBe(3999);
|
|
});
|
|
|
|
test("known plan ids resolve to themselves", () => {
|
|
expect(buildCheckoutParams(user, "weekly", {}).metadata.plan).toBe("weekly");
|
|
expect(buildCheckoutParams(user, "annual", {}).metadata.plan).toBe("annual");
|
|
expect(buildCheckoutParams(user, "lifetime", {}).metadata.plan).toBe("lifetime");
|
|
});
|
|
});
|
|
|
|
describe("ServerPricing — catalog amounts", () => {
|
|
test("without an env Price ID, unit_amount is exactly the catalog value per plan", () => {
|
|
expect(buildCheckoutParams(user, "weekly", {}).lineItems[0].price_data?.unit_amount).toBe(499);
|
|
expect(buildCheckoutParams(user, "annual", {}).lineItems[0].price_data?.unit_amount).toBe(3999);
|
|
expect(buildCheckoutParams(user, "lifetime", {}).lineItems[0].price_data?.unit_amount).toBe(5999);
|
|
});
|
|
|
|
test("the amounts stay pinned to PLAN_CONFIGS and never drift apart", () => {
|
|
expect(buildCheckoutParams(user, "weekly", {}).lineItems[0].price_data?.unit_amount).toBe(
|
|
PLAN_CONFIGS.weekly.unitAmountMinor
|
|
);
|
|
expect(buildCheckoutParams(user, "annual", {}).lineItems[0].price_data?.unit_amount).toBe(
|
|
PLAN_CONFIGS.annual.unitAmountMinor
|
|
);
|
|
expect(buildCheckoutParams(user, "lifetime", {}).lineItems[0].price_data?.unit_amount).toBe(
|
|
PLAN_CONFIGS.lifetime.unitAmountMinor
|
|
);
|
|
});
|
|
|
|
test("a client-supplied amount cannot influence the charged price", () => {
|
|
// The builder accepts no amount parameter; the body can only carry a plan
|
|
// id, so the catalog amount is what gets charged no matter what else a
|
|
// tampered request tries to inject.
|
|
const params = buildCheckoutParams(user, "annual", {} as any);
|
|
expect(params.lineItems[0].price_data?.unit_amount).toBe(PLAN_CONFIGS.annual.unitAmountMinor);
|
|
expect(params.lineItems[0].price_data?.unit_amount).toBe(3999);
|
|
});
|
|
});
|
|
|
|
describe("ServerPricing — env Price ID precedence", () => {
|
|
test("a configured env Price ID wins and suppresses price_data entirely", () => {
|
|
const weekly = buildCheckoutParams(user, "weekly", {
|
|
STRIPE_WEEKLY_PRICE_ID: "price_weekly_test",
|
|
});
|
|
expect(weekly.lineItems[0].price).toBe("price_weekly_test");
|
|
expect(weekly.lineItems[0].price_data).toBeUndefined();
|
|
|
|
const annual = buildCheckoutParams(user, "annual", {
|
|
STRIPE_ANNUAL_PRICE_ID: "price_annual_test",
|
|
});
|
|
expect(annual.lineItems[0].price).toBe("price_annual_test");
|
|
expect(annual.lineItems[0].price_data).toBeUndefined();
|
|
|
|
const lifetime = buildCheckoutParams(user, "lifetime", {
|
|
STRIPE_LIFETIME_PRICE_ID: "price_lifetime_test",
|
|
});
|
|
expect(lifetime.lineItems[0].price).toBe("price_lifetime_test");
|
|
expect(lifetime.lineItems[0].price_data).toBeUndefined();
|
|
});
|
|
|
|
test("the env Price ID comes from the plan's own catalog entry", () => {
|
|
const weekly = buildCheckoutParams(user, "weekly", {
|
|
[PLAN_CONFIGS.weekly.priceIdEnv]: "price_weekly_test",
|
|
});
|
|
expect(weekly.lineItems[0].price).toBe("price_weekly_test");
|
|
expect(weekly.lineItems[0].price_data).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("ServerPricing — subscription shape", () => {
|
|
test("weekly is a subscription with a 3-day trial and weekly billing", () => {
|
|
const params = buildCheckoutParams(user, "weekly", {});
|
|
expect(params.mode).toBe("subscription");
|
|
expect(params.subscriptionData?.trial_period_days).toBe(3);
|
|
expect(params.subscriptionData?.metadata?.plan).toBe("weekly");
|
|
expect(params.lineItems[0].price_data?.recurring?.interval).toBe("week");
|
|
});
|
|
|
|
test("annual is a subscription billed yearly, without any trial", () => {
|
|
const params = buildCheckoutParams(user, "annual", {});
|
|
expect(params.mode).toBe("subscription");
|
|
expect(params.subscriptionData?.trial_period_days).toBeUndefined();
|
|
expect(params.subscriptionData?.metadata?.plan).toBe("annual");
|
|
expect(params.lineItems[0].price_data?.recurring?.interval).toBe("year");
|
|
});
|
|
|
|
test("lifetime is a one-off payment without subscription data", () => {
|
|
const params = buildCheckoutParams(user, "lifetime", {});
|
|
expect(params.mode).toBe("payment");
|
|
expect(params.subscriptionData).toBeUndefined();
|
|
expect(params.lineItems[0].price_data?.recurring).toBeUndefined();
|
|
});
|
|
|
|
test("only the weekly pass ever carries a trial", () => {
|
|
const plans = ["weekly", "annual", "lifetime"] as const;
|
|
for (const plan of plans) {
|
|
const params = buildCheckoutParams(user, plan, {});
|
|
const trial = params.subscriptionData?.trial_period_days;
|
|
expect(trial).toBe(plan === "weekly" ? 3 : undefined);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("ServerPricing — metadata & reference", () => {
|
|
test("metadata always carries the resolved plan and the user id", () => {
|
|
for (const plan of ["weekly", "annual", "lifetime"]) {
|
|
const params = buildCheckoutParams(user, plan, {});
|
|
expect(params.metadata.plan).toBe(plan);
|
|
expect(params.metadata.userId).toBe(user.id);
|
|
expect(params.clientReferenceId).toBe(user.id);
|
|
}
|
|
});
|
|
|
|
test("the fallback plan keeps the authenticated user's id attached", () => {
|
|
const params = buildCheckoutParams(user, "hacker-plan", {});
|
|
expect(params.metadata.plan).toBe("annual");
|
|
expect(params.metadata.userId).toBe(user.id);
|
|
expect(params.clientReferenceId).toBe(user.id);
|
|
});
|
|
});
|