UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

293 lines (231 loc) 9.43 kB
import { randomUUID } from "node:crypto"; import { Alepha } from "alepha"; import { AlephaOrmPostgres } from "alepha/orm/postgres"; import { BadRequestError } from "alepha/server"; import { describe, it } from "vitest"; import { AlephaApiSubscriptions } from "../index.ts"; import { createSubscriptionSchema } from "../schemas/createSubscriptionSchema.ts"; import type { PlanDefinition } from "../schemas/planDefinitionSchema.ts"; import { SubscriptionConfig } from "../services/SubscriptionConfig.ts"; import { SubscriptionService } from "../services/SubscriptionService.ts"; // ----------------------------------------------------------------------------------------------------------------- class TestSubscriptionConfig extends SubscriptionConfig { public async seedPlans(plans: PlanDefinition[]): Promise<void> { await this.plans.set({ plans }); } } // ----------------------------------------------------------------------------------------------------------------- const testPlans: PlanDefinition[] = [ { id: "free", name: "Free", available: true, pricing: [{ interval: "monthly", amount: 0, currency: "usd" }], features: ["dashboard"], limits: { seats: 1, "api-calls": 100 }, order: 0, }, { id: "pro", name: "Pro", available: true, pricing: [ { interval: "monthly", amount: 2900, currency: "usd" }, { interval: "yearly", amount: 29000, currency: "usd" }, ], trial: { days: 14, requirePaymentMethod: false }, features: ["dashboard", "analytics", "export"], limits: { seats: 10, "api-calls": 10000 }, order: 1, }, ]; // ----------------------------------------------------------------------------------------------------------------- const setup = async () => { const alepha = Alepha.create() .with(AlephaOrmPostgres) .with({ provide: SubscriptionConfig, use: TestSubscriptionConfig }) .with(AlephaApiSubscriptions); const service = alepha.inject(SubscriptionService); const config = alepha.inject( TestSubscriptionConfig, ) as TestSubscriptionConfig; await alepha.start(); await config.seedPlans(testPlans); return { alepha, service, config }; }; // ----------------------------------------------------------------------------------------------------------------- describe("SubscriptionService", () => { // --------------------------------------------------------------------------------------------------------------- describe("public createSubscription body", () => { it("does not accept a client-controlled skipTrial", ({ expect }) => { // skipTrial goes straight to `status:active` with no payment; a client // must never be able to set it via the public subscribe endpoint. const parsed = createSubscriptionSchema.parse({ planId: "pro", interval: "monthly", skipTrial: true, } as Record<string, unknown>); expect((parsed as Record<string, unknown>).skipTrial).toBeUndefined(); }); }); describe("subscribe", () => { it("should create a trialing subscription with trial days", async ({ expect, }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "pro", "monthly"); expect(sub.status).toBe("trialing"); expect(sub.planId).toBe("pro"); expect(sub.interval).toBe("monthly"); expect(sub.organizationId).toBe(orgId); expect(sub.trialStart).toBeDefined(); expect(sub.trialEnd).toBeDefined(); }); it("should create an active subscription when skipTrial", async ({ expect, }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); expect(sub.status).toBe("active"); expect(sub.planId).toBe("pro"); expect(sub.trialStart).toBeUndefined(); expect(sub.trialEnd).toBeUndefined(); }); it("should reject when plan not found", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); await expect( service.subscribe(orgId, "nonexistent", "monthly"), ).rejects.toThrow(BadRequestError); }); it("should reject duplicate subscription for same org", async ({ expect, }) => { const { service } = await setup(); const orgId = randomUUID(); await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); await expect(service.subscribe(orgId, "free", "monthly")).rejects.toThrow( BadRequestError, ); }); }); // --------------------------------------------------------------------------------------------------------------- describe("cancel", () => { it("should cancel at period end", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); await service.cancel(sub.id, { immediate: false }); const updated = await service.getSubscription(sub.id); expect(updated.status).toBe("cancelled"); expect(updated.cancelAtPeriodEnd).toBe(true); expect(updated.cancelledAt).toBeDefined(); }); it("should cancel immediately with expired status", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); await service.cancel(sub.id, { immediate: true }); const updated = await service.getSubscription(sub.id); expect(updated.status).toBe("expired"); expect(updated.cancelAtPeriodEnd).toBe(false); expect(updated.cancelledAt).toBeDefined(); }); }); // --------------------------------------------------------------------------------------------------------------- describe("resume", () => { it("should resume a cancelled subscription back to active", async ({ expect, }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); await service.cancel(sub.id, { immediate: false }); const cancelled = await service.getSubscription(sub.id); expect(cancelled.status).toBe("cancelled"); await service.resume(sub.id); const resumed = await service.getSubscription(sub.id); expect(resumed.status).toBe("active"); expect(resumed.cancelAtPeriodEnd).toBe(false); }); }); // --------------------------------------------------------------------------------------------------------------- describe("changePlan", () => { it("should schedule plan change at period end", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "free", "monthly", { skipTrial: true, }); await service.changePlan(sub.id, "pro", "monthly", { immediate: false, }); const updated = await service.getSubscription(sub.id); expect(updated.pendingPlanId).toBe("pro"); expect(updated.planId).toBe("free"); }); it("should apply immediate plan change", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); const sub = await service.subscribe(orgId, "free", "monthly", { skipTrial: true, }); await service.changePlan(sub.id, "pro", "monthly", { immediate: true, }); const updated = await service.getSubscription(sub.id); expect(updated.planId).toBe("pro"); expect(updated.pendingPlanId).toBeUndefined(); }); }); // --------------------------------------------------------------------------------------------------------------- describe("entitlements", () => { it("should return true for included feature", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); const canAnalytics = await service.can(orgId, "analytics"); expect(canAnalytics).toBe(true); }); it("should return false for excluded feature", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); await service.subscribe(orgId, "free", "monthly", { skipTrial: true, }); const canAnalytics = await service.can(orgId, "analytics"); expect(canAnalytics).toBe(false); }); it("should return correct limit value", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); await service.subscribe(orgId, "pro", "monthly", { skipTrial: true, }); const seats = await service.limit(orgId, "seats"); expect(seats).toBe(10); const apiCalls = await service.limit(orgId, "api-calls"); expect(apiCalls).toBe(10000); }); it("should return 0 limit for no subscription", async ({ expect }) => { const { service } = await setup(); const orgId = randomUUID(); const seats = await service.limit(orgId, "seats"); expect(seats).toBe(0); }); }); });