UNPKG

alepha

Version:

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

191 lines (181 loc) 6.35 kB
import { $inject, z } from "alepha"; import { $secure } from "alepha/security"; import { $action, NotFoundError, okSchema } from "alepha/server"; import { cancelSubscriptionSchema } from "../schemas/cancelSubscriptionSchema.ts"; import { changePlanSchema } from "../schemas/changePlanSchema.ts"; import { createSubscriptionSchema } from "../schemas/createSubscriptionSchema.ts"; import { entitlementsSchema } from "../schemas/entitlementsSchema.ts"; import { planResourceSchema } from "../schemas/planResourceSchema.ts"; import { subscriptionEventResourceSchema } from "../schemas/subscriptionEventResourceSchema.ts"; import { subscriptionResourceSchema } from "../schemas/subscriptionResourceSchema.ts"; import { SubscriptionConfig } from "../services/SubscriptionConfig.ts"; import { SubscriptionService } from "../services/SubscriptionService.ts"; export class SubscriptionController { protected readonly url = "/subscriptions"; protected readonly group = "subscriptions"; protected readonly service = $inject(SubscriptionService); protected readonly config = $inject(SubscriptionConfig); /** * List available subscription plans with pricing. */ public readonly getPlans = $action({ path: `${this.url}/plans`, group: this.group, description: "List available subscription plans", schema: { response: z.array(planResourceSchema), }, handler: async () => { const plans = await this.config.getPlans(); return plans .filter((p) => p.available) .map((p) => ({ id: p.id, name: p.name, description: p.description, pricing: p.pricing, features: p.features, limits: p.limits, trial: p.trial, order: p.order, })); }, }); /** * Get the current organization's subscription. */ public readonly getMySubscription = $action({ path: `${this.url}/mine`, group: this.group, use: [$secure()], description: "Get the current organization subscription", schema: { response: subscriptionResourceSchema, }, handler: async ({ user }) => { const sub = await this.service.getByOrganization(user.organization!); if (!sub) throw new NotFoundError("No subscription found for your organization"); return sub; }, }); /** * Create a new subscription for the current organization. */ public readonly subscribe = $action({ method: "POST", path: this.url, group: this.group, use: [$secure({ permissions: ["subscription:create"] })], description: "Create a new subscription", schema: { body: createSubscriptionSchema, response: subscriptionResourceSchema, }, handler: ({ body, user }) => // NB: no `skipTrial` — the public subscribe path always honors the // plan's trial. Comped/paid activations go through the service directly. this.service.subscribe(user.organization!, body.planId, body.interval, { metadata: body.metadata, }), }); /** * Change the plan for the current organization's subscription. */ public readonly changePlan = $action({ method: "POST", path: `${this.url}/mine/change-plan`, group: this.group, use: [$secure({ permissions: ["subscription:update"] })], description: "Upgrade or downgrade the subscription plan", schema: { body: changePlanSchema, response: subscriptionResourceSchema, }, handler: async ({ body, user }) => { const sub = await this.service.getByOrganization(user.organization!); if (!sub) throw new NotFoundError("No subscription found for your organization"); await this.service.changePlan(sub.id, body.planId, body.interval, { immediate: body.immediate, }); return this.service.getSubscription(sub.id); }, }); /** * Cancel the current organization's subscription. */ public readonly cancel = $action({ method: "POST", path: `${this.url}/mine/cancel`, group: this.group, use: [$secure({ permissions: ["subscription:update"] })], description: "Cancel the current subscription", schema: { body: cancelSubscriptionSchema, response: okSchema, }, handler: async ({ body, user }) => { const sub = await this.service.getByOrganization(user.organization!); if (!sub) throw new NotFoundError("No subscription found for your organization"); await this.service.cancel(sub.id, { reason: body.reason, immediate: body.immediate, }); return { ok: true }; }, }); /** * Resume a cancelled subscription before the period ends. */ public readonly resume = $action({ method: "POST", path: `${this.url}/mine/resume`, group: this.group, use: [$secure({ permissions: ["subscription:update"] })], description: "Resume a cancelled subscription", schema: { response: okSchema, }, handler: async ({ user }) => { const sub = await this.service.getByOrganization(user.organization!); if (!sub) throw new NotFoundError("No subscription found for your organization"); await this.service.resume(sub.id); return { ok: true }; }, }); /** * Get the billing event history for the current organization's subscription. */ public readonly getSubscriptionHistory = $action({ path: `${this.url}/mine/history`, group: this.group, use: [$secure()], description: "Get the subscription billing event history", schema: { response: z.array(subscriptionEventResourceSchema), }, handler: async ({ user }) => { const sub = await this.service.getByOrganization(user.organization!); if (!sub) throw new NotFoundError("No subscription found for your organization"); return this.service.getHistory(sub.id); }, }); /** * Get the feature and usage limit entitlements for the current organization. */ public readonly getEntitlements = $action({ path: `${this.url}/mine/entitlements`, group: this.group, use: [$secure()], description: "Get the feature and limit entitlements for the current organization", schema: { response: entitlementsSchema, }, handler: ({ user }) => this.service.getEntitlements(user.organization!), }); }