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);
    }
}