Spaces:
Running
Running
File size: 2,913 Bytes
c89bb66 7a555a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 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);
}
}
|