/** * Voice Quota DB-Layer * * Tracks daily TTS seconds per user. Quota resets automatically when the * calendar day (UTC) changes since the last reset. * * Schema fields on `profiles`: * voice_seconds_used_today INTEGER NOT NULL DEFAULT 0 * voice_quota_reset_at TIMESTAMP */ import { usePrisma } from "../utils/prisma"; import { getPlanLimits } from "../utils/plan-features"; /** Midnight UTC for the current day (used as reset boundary). */ function todayUtcMidnight(): Date { const d = new Date(); d.setUTCHours(0, 0, 0, 0); return d; } /** * Returns remaining quota seconds for the user today. * Automatically resets counter if last reset was before today (UTC). * Returns Infinity when dailyQuotaSeconds === 0 (Legend unlimited). */ export async function getRemainingVoiceQuota( userId: string, plan: string, ): Promise { const limits = getPlanLimits(plan); const { dailyQuotaSeconds } = limits.voice; // Unlimited plan — short-circuit if (dailyQuotaSeconds === 0) return Infinity; const db = usePrisma(); const midnight = todayUtcMidnight(); const profile = await db.profile.findUnique({ where: { id: userId }, select: { voiceSecondsUsedToday: true, voiceQuotaResetAt: true, }, }); if (!profile) throw createError({ statusCode: 404, message: "Profil nicht gefunden" }); // Reset if no reset-stamp yet OR stamp is before today's midnight UTC const needsReset = !profile.voiceQuotaResetAt || profile.voiceQuotaResetAt < midnight; if (needsReset) { // Idempotent: even if two concurrent requests hit this, the result is the // same full quota — no race condition risk here. await db.profile.update({ where: { id: userId }, data: { voiceSecondsUsedToday: 0, voiceQuotaResetAt: midnight, }, }); return dailyQuotaSeconds; } const used = profile.voiceSecondsUsedToday ?? 0; return Math.max(0, dailyQuotaSeconds - used); } /** * Increment the used-seconds counter. No-op for unlimited plans (Legend). * Does NOT validate against quota — caller must check `getRemainingVoiceQuota` * first. */ export async function consumeVoiceQuota( userId: string, plan: string, seconds: number, ): Promise { const limits = getPlanLimits(plan); if (limits.voice.dailyQuotaSeconds === 0) return; // unlimited — no tracking needed const db = usePrisma(); const midnight = todayUtcMidnight(); await db.profile.update({ where: { id: userId }, data: { voiceSecondsUsedToday: { increment: Math.max(0, Math.round(seconds)) }, // Ensure reset-stamp is set (idempotent with getRemainingVoiceQuota reset) voiceQuotaResetAt: midnight, }, }); } /** * Estimate audio duration in seconds for a given text length. * Rule of thumb: German TTS ~13 chars/sec at default speed. * Minimum 1 second. */ export function estimateAudioSeconds(text: string): number { return Math.max(1, Math.ceil(text.length / 13)); }