Files
songs2vid/lib/quota.ts
T

134 lines
3.8 KiB
TypeScript

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<ReservationSplit> {
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();
}