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