import { admin } from '../auth/firebase.js'; import { FREE_DAILY_PDF_LIMIT, PREMIUM_DAILY_PDF_LIMIT, USAGE_WRITE_TIMEOUT_MS } from '../config.js'; const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; const DAY_MS = 24 * 60 * 60 * 1000; function utcISODate(ms = Date.now()) { return new Date(ms).toISOString().slice(0, 10); } function shiftISODate(isoDate, days) { return utcISODate(Date.parse(`${isoDate}T00:00:00Z`) + days * DAY_MS); } // The quota "day" is the user's local day, so the client sends its local date. // Accept it only when it is a real date within a day of UTC now (every timezone is), else use UTC. function resolveUsageDate(clientDate) { if (typeof clientDate === 'string' && DATE_RE.test(clientDate)) { const diff = Math.abs(Date.parse(`${clientDate}T00:00:00Z`) - Date.parse(`${utcISODate()}T00:00:00Z`)); if (diff <= DAY_MS) return clientDate; } return utcISODate(); } async function incrementUsage(uid, today) { const db = admin.firestore(); const usageRef = db.doc(`users/${uid}/usage/pdf_exports`); const userRef = db.doc(`users/${uid}`); const yesterday = shiftISODate(today, -1); await db.runTransaction(async (tx) => { const [usageSnap, userSnap] = await Promise.all([tx.get(usageRef), tx.get(userRef)]); const prev = usageSnap.exists ? usageSnap.data() : {}; const user = userSnap.exists ? userSnap.data() : {}; const isPremium = user.premium === true || user.isPremium === true; const count = (prev.date === today ? prev.count || 0 : 0) + 1; let streak = prev.streak || 0; let lastStreakDate = prev.lastStreakDate || null; if (lastStreakDate !== today) { streak = lastStreakDate === yesterday ? streak + 1 : 1; lastStreakDate = today; } tx.set(usageRef, { date: today, count, maxDaily: isPremium ? PREMIUM_DAILY_PDF_LIMIT : FREE_DAILY_PDF_LIMIT, streak, lastStreakDate, totalGenerations: admin.firestore.FieldValue.increment(1), updatedAt: admin.firestore.FieldValue.serverTimestamp() }, { merge: true }); }); } /** * Counts one finished PDF generation against the user's daily usage doc. * Never throws: a Firestore problem is logged and must not fail the PDF the user already got. */ export async function recordPdfGeneration(uid, clientDate) { if (!uid) return; let timer; try { await Promise.race([ incrementUsage(uid, resolveUsageDate(clientDate)), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('usage write timed out')), USAGE_WRITE_TIMEOUT_MS); }) ]); } catch (err) { console.error(`[Usage] Failed to record PDF generation for ${uid}:`, err.message); } finally { clearTimeout(timer); } }