// server/supabase/functions/_shared/quota-policy.ts // Pure quota policy shared by the quota adapter (quota.ts) and its tests. // // Policy (pure): window arithmetic, allow/deny decision, RPC response parsing. // Adapter: quota.ts (subscriptions + daily_usage + consume_quota RPC). // // The weekly window is the rolling 7 calendar days ending today // (today-6 .. today, UTC). The SQL quota functions (`consume_quota`, // `reserve_stt_quota`, the meeting-document RPC) use `CURRENT_DATE - 6`; // this module must stay on the same window so a pre-check never blocks usage // that the atomic SQL consumption has already rolled off. import type { PlanQuotaPeriod } from './core-contract.generated.ts' /** Days before today that still count toward a weekly quota. */ export const WEEKLY_WINDOW_EXTRA_DAYS = 6 export type QuotaConsumedFrom = 'base' | 'overage' | 'unlimited' | 'none' export interface QuotaConsumeResponse { allowed: boolean current: number limit: number overageCredits: number consumedFrom: QuotaConsumedFrom } function isoDate(date: Date): string { return date.toISOString().slice(0, 10) } /** Today's usage date (UTC, matches the database `CURRENT_DATE` on Supabase). */ export function quotaUsageDate(now: Date): string { return isoDate(now) } /** * First usage date (inclusive) that counts toward the period. * daily → today; weekly → today-6 (7 calendar days including today). */ export function quotaWindowStart(period: PlanQuotaPeriod, now: Date): string { if (period !== 'weekly') return isoDate(now) const start = new Date(now.getTime()) start.setUTCDate(start.getUTCDate() - WEEKLY_WINDOW_EXTRA_DAYS) return isoDate(start) } /** * Whether one more call may go through. * limit: -1 = unlimited, 0 = not available, >0 = base allowance in the window. * Past the base allowance a positive overage balance lets the call through; * the atomic `consume_quota` RPC then spends one credit for it. */ export function isQuotaAllowed(limit: number, current: number, overageCredits: number): boolean { if (limit === 0) return false if (limit === -1) return true return current < limit || overageCredits > 0 } const CONSUMED_FROM: readonly QuotaConsumedFrom[] = ['base', 'overage', 'unlimited', 'none'] /** Validate the `consume_quota` RPC payload; throws on any unexpected shape. */ export function parseQuotaConsumeResponse(data: unknown): QuotaConsumeResponse { if (!data || typeof data !== 'object' || Array.isArray(data)) { throw new Error('Invalid quota consumption response.') } const result = data as Record if ( typeof result.allowed !== 'boolean' || typeof result.current !== 'number' || typeof result.limit !== 'number' || typeof result.overage_credits !== 'number' || !(CONSUMED_FROM as readonly unknown[]).includes(result.consumed_from) ) { throw new Error('Invalid quota consumption response.') } return { allowed: result.allowed, current: result.current, limit: result.limit, overageCredits: result.overage_credits, consumedFrom: result.consumed_from as QuotaConsumedFrom, } }