import { supabaseAdmin } from "./supabase.js"; type AssignSubscriptionInput = { userId: string; subscriptionId: string; startDate: string; endDate: string; }; /** * Persist a paid subscription after a payment provider's webhook has been * authenticated and its payment has been confirmed. Never call this directly * from a browser request. */ export async function assignPaidSubscription(input: AssignSubscriptionInput) { const { userId, subscriptionId, startDate, endDate } = input; const isoDate = /^\d{4}-\d{2}-\d{2}$/; if (!isoDate.test(startDate) || !isoDate.test(endDate) || endDate < startDate) { throw new Error("Invalid subscription period."); } const { data: plan, error: planError } = await supabaseAdmin .from("subscriptions") .select("subscription_id") .eq("subscription_id", subscriptionId) .maybeSingle(); if (planError) throw new Error(`Unable to validate subscription plan: ${planError.message}`); if (!plan) throw new Error("Subscription plan was not found."); const { data, error } = await supabaseAdmin .from("users_subscriptions") .upsert({ user_id: userId, subscription_id: subscriptionId, startdate: startDate, enddate: endDate }, { onConflict: "user_id" }) .select("user_id,subscription_id,startdate,enddate") .single(); if (error) throw new Error(`Unable to assign paid subscription: ${error.message}`); invalidateSubscriptionCache(userId); return data; } async function loadActiveSubscription(userId: string) { const today = new Date().toISOString().slice(0, 10); const { data: assignment, error } = await supabaseAdmin.from("users_subscriptions") .select("subscription_id,startdate,enddate,plan:subscriptions(subscription_id,plan,price,player_limit,session_limit)") .eq("user_id", userId).maybeSingle(); if (error) throw new Error(`Unable to check subscription: ${error.message}`); if (!assignment?.subscription_id || assignment.startdate > today || assignment.enddate < today) return null; // The FK is many-to-one; untyped Supabase clients infer embedded rows as arrays. const plan = assignment.plan as unknown as { subscription_id: string; plan: string; price: number; player_limit: number | null; session_limit: number | null; } | null; return plan ? { ...plan, startdate: assignment.startdate, enddate: assignment.enddate } : null; } // Brief, per-user cache shared by polling, media and session requests. const accessCache = new Map>; expires: number }>(); const accessRequests = new Map>>>(); export function invalidateSubscriptionCache(userId: string) { accessCache.delete(userId); accessRequests.delete(userId); } export async function activeSubscriptionFor(userId: string, options: { refresh?: boolean } = {}) { const cached = accessCache.get(userId); if (!options.refresh && cached && cached.expires > Date.now()) return cached.value; const existing = accessRequests.get(userId); if (existing) return existing; const pending = loadActiveSubscription(userId).then((value) => { if (accessRequests.get(userId) === pending) { if (accessCache.size >= 1000) accessCache.delete(accessCache.keys().next().value!); const tomorrow = new Date(); tomorrow.setUTCHours(24, 0, 0, 0); accessCache.set(userId, { value, expires: Math.min(Date.now() + 30_000, tomorrow.getTime()) }); } return value; }).finally(() => { if (accessRequests.get(userId) === pending) accessRequests.delete(userId); }); accessRequests.set(userId, pending); return pending; }