import { Plan } from "@prisma/client"; import { prisma } from "./db"; import { getNextMonthlyQuotaReset, getPlanLimits } from "./plans"; /** * OSS / self-hosted quota: never blocks on credits or plan limits. * Batch size still enforced via SELFHOSTED_LIMITS.maxBatchSize. * videosUsed is incremented for display only. */ export function formatQuotaResetCountdown(_resetsAt: Date | string): string { return "never"; } export function formatQuotaResetDisplay(_plan: Plan, _resetsAt: Date | string): string { return "never (self-hosted)"; } export async function ensureQuotaReset(userId: string) { return prisma.user.findUniqueOrThrow({ where: { id: userId } }); } export async function getQuotaInfo(userId: string) { const user = await ensureQuotaReset(userId); const limits = getPlanLimits(user.plan); return { used: user.videosUsed, limit: limits.monthlyQuota, baseLimit: limits.monthlyQuota, bonusQuota: 0, remaining: limits.monthlyQuota, videoCredits: 0, extraCredits: 0, creditBalanceMax: 0, totalAvailable: limits.monthlyQuota, resetsAt: user.quotaResetAt.toISOString(), resetsIn: formatQuotaResetDisplay(user.plan, user.quotaResetAt), plan: user.plan, maxBatchSize: limits.maxBatchSize, watermarkOptional: limits.watermarkOptional, customWatermark: limits.customWatermark, perItemImages: limits.perItemImages, artTrackLayouts: limits.artTrackLayouts, maxResolutionHeight: limits.maxResolutionHeight, selfHosted: true, }; } export type ReservationSplit = { fromQuota: number; fromCredits: number; }; /** Always succeed (except empty). Credits are never used in OSS. */ export function planReservation( _plan: Plan, _remainingQuota: number, _videoCredits: number, count: number, ): ReservationSplit | null { if (count <= 0) return { fromQuota: 0, fromCredits: 0 }; return { fromQuota: count, fromCredits: 0 }; } export async function checkQuota(userId: string, requestedCount: number) { const info = await getQuotaInfo(userId); if (requestedCount > info.maxBatchSize) { return { ok: false as const, error: `Batch limit exceeded. Max ${info.maxBatchSize} files per batch.`, used: info.used, limit: info.limit, remaining: info.remaining, videoCredits: 0, resetsAt: info.resetsAt, }; } return { ok: true as const, ...info, reservation: { fromQuota: requestedCount, fromCredits: 0 }, }; } /** Track usage for display; never deduct credits or block. */ export async function reserveQuota(userId: string, count: number): Promise { if (count <= 0) return { fromQuota: 0, fromCredits: 0 }; const limits = getPlanLimits(); if (count > limits.maxBatchSize) { throw new Error(`Batch limit exceeded. Max ${limits.maxBatchSize} files per batch.`); } await prisma.user.update({ where: { id: userId }, data: { videosUsed: { increment: count } }, }); return { fromQuota: count, fromCredits: 0 }; } export async function releaseQuota(userId: string, count: number) { if (count <= 0) return; await prisma.$executeRaw` UPDATE "User" SET "videosUsed" = GREATEST(0, "videosUsed" - ${count}) WHERE id = ${userId} `; } /** No-op in OSS (no prepaid credits). */ export async function releaseCredits(_userId: string, _count: number) { return; } export async function releaseReservation( userId: string, _billingSource: "QUOTA" | "CREDIT", count = 1, ) { await releaseQuota(userId, count); } export async function releaseReservationSplit(userId: string, split: ReservationSplit) { if (split.fromQuota > 0) await releaseQuota(userId, split.fromQuota); } export async function incrementQuota(userId: string, count: number) { await reserveQuota(userId, count); } export function getInitialQuotaResetAt(): Date { return getNextMonthlyQuotaReset(); }