feat: add Team billing tier
Widens the tier enum from free/pro to free/pro/team and every "free" | "pro" cast that assumed exactly two tiers (~30 call sites: every AI route's withAiQuota/checkAndIncrementTierLimit call, admin user/invite management, upload quota checks, OpenAPI schemas). Team sits above Pro with genuinely unlimited recipes/public-recipes (the -1 sentinel, which Pro doesn't actually use — Pro uses large finite numbers instead) and a higher AI-call/storage cap. Seeded via db:seed, editable afterward from Admin > Tiers. role (user/moderator/admin — permissions) and tier (free/pro/team — billing limits) stay separate concepts, as they already were; this does not touch role-based permissions. Requires migration 0043 to run against a live DB — not applied in this sandbox (no Docker here); run `pnpm db:migrate` then `pnpm db:seed`. v0.44.0
This commit is contained in:
@@ -685,14 +685,14 @@ export function generateOpenApiSpec(): object {
|
||||
|
||||
const InviteRef = registry.register("Invite", z.object({
|
||||
id: z.string(), token: z.string(), email: z.string().nullable(),
|
||||
role: z.enum(["user", "moderator", "admin"]), tier: z.enum(["free", "pro"]),
|
||||
role: z.enum(["user", "moderator", "admin"]), tier: z.enum(["free", "pro", "team"]),
|
||||
createdById: z.string(), createdAt: z.string().datetime(),
|
||||
expiresAt: z.string().datetime().nullable(), usedAt: z.string().datetime().nullable(),
|
||||
usedById: z.string().nullable(),
|
||||
}));
|
||||
const CreateInviteRef = registry.register("CreateInvite", z.object({
|
||||
email: z.string().optional(), role: z.enum(["user", "moderator", "admin"]).default("user"),
|
||||
tier: z.enum(["free", "pro"]).default("free"), expiresInDays: z.number().default(7),
|
||||
tier: z.enum(["free", "pro", "team"]).default("free"), expiresInDays: z.number().default(7),
|
||||
}));
|
||||
|
||||
const AdminReportRef = registry.register("AdminReport", z.object({
|
||||
@@ -734,20 +734,20 @@ export function generateOpenApiSpec(): object {
|
||||
storageMb: z.number().int().optional(), maxPublicRecipes: z.number().int().optional(),
|
||||
}).describe("Each field must be a non-negative integer, or -1 for unlimited."));
|
||||
const TierDefinitionRef = registry.register("TierDefinition", z.object({
|
||||
tier: z.enum(["free", "pro"]), maxRecipes: z.number().int(), aiCallsPerMonth: z.number().int(),
|
||||
tier: z.enum(["free", "pro", "team"]), maxRecipes: z.number().int(), aiCallsPerMonth: z.number().int(),
|
||||
storageMb: z.number().int(), maxPublicRecipes: z.number().int(),
|
||||
}));
|
||||
|
||||
const AdminCreateUserBodyRef = registry.register("AdminCreateUserBody", z.object({
|
||||
email: z.string(), name: z.string(), role: z.enum(["user", "moderator", "admin"]).default("user"),
|
||||
tier: z.enum(["free", "pro"]).default("free"),
|
||||
tier: z.enum(["free", "pro", "team"]).default("free"),
|
||||
}));
|
||||
const AdminCreatedUserRef = registry.register("AdminCreatedUser", z.object({
|
||||
user: z.object({ id: z.string(), email: z.string(), name: z.string(), role: z.string(), tier: z.string() }),
|
||||
}));
|
||||
|
||||
const AdminUpdateUserBodyRef = registry.register("AdminUpdateUserBody", z.object({
|
||||
role: z.enum(["user", "moderator", "admin"]).optional(), tier: z.enum(["free", "pro"]).optional(),
|
||||
role: z.enum(["user", "moderator", "admin"]).optional(), tier: z.enum(["free", "pro", "team"]).optional(),
|
||||
}));
|
||||
const AdminUpdatedUserRef = registry.register("AdminUpdatedUser", z.object({
|
||||
user: z.object({ id: z.string(), role: z.string(), tier: z.string() }),
|
||||
@@ -758,7 +758,7 @@ export function generateOpenApiSpec(): object {
|
||||
aiCallsUsed: z.number().int(), recipeCount: z.number().int(), storageUsedMb: z.number().int(),
|
||||
}));
|
||||
|
||||
const tierParam = z.object({ tier: z.enum(["free", "pro"]) });
|
||||
const tierParam = z.object({ tier: z.enum(["free", "pro", "team"]) });
|
||||
|
||||
registry.registerPath({ method: "get", path: "/api/v1/admin/invites", summary: "List invites", description: "Admin only.", security: adminSecurity, responses: { 200: { description: "Invites", content: { "application/json": { schema: z.object({ invites: z.array(InviteRef) }) } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "post", path: "/api/v1/admin/invites", summary: "Create an invite", description: "Admin only.", security: adminSecurity, request: { body: { content: { "application/json": { schema: CreateInviteRef } }, required: true } }, responses: { 200: { description: "Created", content: { "application/json": { schema: z.object({ invite: InviteRef }) } } }, 400: { description: "Invalid role or tier", content: { "application/json": { schema: ApiErrorRef } } }, 403: { description: "Forbidden", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
|
||||
Reference in New Issue
Block a user