alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
293 lines (231 loc) • 9.43 kB
text/typescript
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);
});
});
});