import { eq, sql } from "drizzle-orm" import { db } from "@/lib/db" import { profiles, properties, tenants } from "@/lib/db/schema" import { PLAN_LIMITS, checkLimit } from "@/lib/stripe/plans" import { getUserStorageBytes } from "@/lib/storage" import type { Plan } from "@/types" /** * Server-side plan-limit enforcement helpers. Single source of truth is * PLAN_LIMITS in lib/stripe/plans.ts — never hardcode limit numbers in routes. */ /** The user's current plan (defaults to "starter" if no profile row). */ export async function getUserPlan(userId: string): Promise { const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, userId), columns: { plan: true }, }) return (profile?.plan ?? "starter") as Plan } /** * Returns an error message if storing `incomingBytes` more would exceed the * user's plan storage cap, otherwise null. Reads actual usage from the storage * backend so it stays accurate regardless of which tables reference the files. */ export async function checkStorageLimit( userId: string, incomingBytes: number ): Promise { const plan = await getUserPlan(userId) const maxBytes = PLAN_LIMITS[plan].maxStorageMB * 1024 * 1024 if (!Number.isFinite(maxBytes)) return null // unlimited plan const used = await getUserStorageBytes(userId) if (used + incomingBytes > maxBytes) { const limitMb = PLAN_LIMITS[plan].maxStorageMB return `Storage limit reached (${limitMb} MB on your plan). Upgrade for more space.` } return null } // ── countable resource caps ────────────────────────────────────────────────── // These live here, not inline in route handlers, because there is more than one // way into the app: the session routes AND the public v1 API both create // properties. When the check was written inline, v1 simply did not have it and a // Starter user could mint an API key and create unlimited properties. Every // creation path MUST call the helper for its resource — mirroring the same rule // the upload allowlist follows in lib/storage.ts. /** * Returns a user-facing error message if the owner is at their plan's property * cap, otherwise null. Call before every property insert, on every entry point. */ export async function checkPropertyLimit(ownerId: string): Promise { const plan = await getUserPlan(ownerId) const [{ count }] = await db .select({ count: sql`count(*)::int` }) .from(properties) .where(eq(properties.user_id, ownerId)) if (!checkLimit(plan, "maxProperties", count)) { return "Plan limit reached. Upgrade to add more properties." } return null } /** * Returns a user-facing error message if the owner is at their plan's tenant * cap, otherwise null. Call before every tenant insert, on every entry point. */ export async function checkTenantLimit(ownerId: string): Promise { const plan = await getUserPlan(ownerId) const [{ count }] = await db .select({ count: sql`count(*)::int` }) .from(tenants) .where(eq(tenants.user_id, ownerId)) if (!checkLimit(plan, "maxTenants", count)) { return "Plan limit reached. Upgrade to add more tenants." } return null }