resume-server / src /services /usageService.js
hari7775's picture
Deploy: Updated generator service (v1.0.3)
c89bb66
Raw History Blame Contribute Delete
2.91 kB
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);
}
}