const admin = require('firebase-admin') const { FieldValue } = require('firebase-admin/firestore') const { refList } = require('../../index') const { STRIPE_WEBHOOK_SECRET } = require('../../config/keys') const { SUBSCRIPTION_LEVEL_ALLOWANCES, SUBSCRIPTION_PRICE_METADATA, COIN_PACK_PRODUCT_MAP, } = require('./constants') const refsList = refList const paymentsCollection = admin.firestore().collection('payments') let cachedStripeWebhookSecret = null const toFiniteNumber = (value) => { if (typeof value === 'number' && Number.isFinite(value)) { return value } if (typeof value === 'string') { const normalized = value.trim().replace(',', '.') if (!normalized) { return null } const parsed = Number(normalized) return Number.isFinite(parsed) ? parsed : null } return null } const parseCoinsPerMonth = (metadata) => { if (!metadata || typeof metadata !== 'object') { return null } if (!Object.prototype.hasOwnProperty.call(metadata, 'coinsPerMonth')) { return null } const candidateValue = toFiniteNumber(metadata.coinsPerMonth) if (candidateValue === null || candidateValue <= 0) { return null } return Math.round(candidateValue) } const parseCoinAmount = (metadata) => { if (!metadata || typeof metadata !== 'object') { return null } if (!Object.prototype.hasOwnProperty.call(metadata, 'coins')) { return null } const candidateValue = toFiniteNumber(metadata.coins) if (candidateValue === null || candidateValue <= 0) { return null } return Math.round(candidateValue) } const toDateSafe = (value) => { if (!value) { return null } if (value instanceof Date) { return value } if (typeof value?.toDate === 'function') { try { return value.toDate() } catch (_error) { return null } } if (typeof value === 'number' && Number.isFinite(value)) { if (value > 1e12) { return new Date(value) } return new Date(value * 1000) } return null } const addMonths = (date, months = 1) => { if (!(date instanceof Date) || !Number.isFinite(months)) { return null } const result = new Date(date.getTime()) const initialDay = result.getDate() result.setMonth(result.getMonth() + months) if (result.getDate() !== initialDay) { result.setDate(0) } return result } const computeNextGrantTimestamp = (base, months = 1) => { const baseDate = toDateSafe(base) if (!baseDate) { return null } const nextDate = addMonths(baseDate, months) if (!nextDate) { return null } return admin.firestore.Timestamp.fromDate(nextDate) } const getServerTimestamp = () => { if (typeof FieldValue?.serverTimestamp === 'function') { return FieldValue.serverTimestamp() } const fallback = admin.firestore?.FieldValue if (typeof fallback?.serverTimestamp === 'function') { return fallback.serverTimestamp() } throw new Error('Firestore FieldValue.serverTimestamp indisponible.') } const resolveStripeWebhookSecret = () => { if (cachedStripeWebhookSecret) { return cachedStripeWebhookSecret } const envSecret = typeof process?.env?.STRIPE_WEBHOOK_SECRET === 'string' ? process.env.STRIPE_WEBHOOK_SECRET.trim() : '' const inlineSecret = typeof STRIPE_WEBHOOK_SECRET === 'string' ? STRIPE_WEBHOOK_SECRET.trim() : '' const secret = envSecret || inlineSecret if (!secret) { throw new Error('STRIPE_WEBHOOK_SECRET not configured') } cachedStripeWebhookSecret = secret return cachedStripeWebhookSecret } const toFirestoreTimestamp = (unixSeconds) => { if (typeof unixSeconds !== 'number' || !Number.isFinite(unixSeconds)) { return null } try { return admin.firestore.Timestamp.fromMillis(unixSeconds * 1000) } catch (error) { console.error('[subscription-toFirestoreTimestamp] Conversion error', unixSeconds, error) return null } } const extractFirebaseUid = (metadata) => { if (!metadata || typeof metadata !== 'object') { return null } const candidates = [metadata.firebaseUID, metadata.firebaseUid, metadata.uid, metadata.userId] for (let index = 0; index < candidates.length; index += 1) { const candidate = candidates[index] if (typeof candidate === 'string' && candidate.trim()) { return candidate.trim() } } return null } const formatCoinPack = ({ product, price }) => { if (!product || typeof product !== 'object') { return null } const resolvedPrice = price || (typeof product.default_price === 'object' && product.default_price) || null const priceId = (resolvedPrice && resolvedPrice.id) || (typeof product.default_price === 'string' ? product.default_price : null) const coinAmount = parseCoinAmount(product?.metadata || {}) if (coinAmount === null) { throw new Error(`[formatCoinPack] Missing metadata.coins on product ${product.id}`) } return { productId: product.id, priceId, name: product.name || '', description: product.description || '', coinAmount, currency: resolvedPrice?.currency || (typeof resolvedPrice?.currency === 'string' ? resolvedPrice.currency.toLowerCase() : 'eur'), unitAmount: resolvedPrice?.unit_amount ?? null, metadata: product.metadata || {}, } } const getSubscriptionMetaFromPrice = (priceId) => { if (typeof priceId !== 'string') { return null } return SUBSCRIPTION_PRICE_METADATA[priceId] || null } const buildEventSnapshot = (eventType, entityId) => { const now = admin.firestore.Timestamp.now() return { eventType: eventType || null, entityId: entityId || null, syncedAt: now, } } const formatSubscriptionForClient = (subscription) => { if (!subscription || typeof subscription !== 'object') { return null } return { id: subscription.id, status: subscription.status, customer: subscription.customer, currentPeriodStart: toFirestoreTimestamp(subscription.current_period_start), currentPeriodEnd: toFirestoreTimestamp(subscription.current_period_end), cancelAtPeriodEnd: subscription.cancel_at_period_end === true, created: toFirestoreTimestamp(subscription.created), items: Array.isArray(subscription.items?.data) ? subscription.items.data.map((item) => ({ id: item.id, price: item.price, quantity: item.quantity, })) : [], metadata: subscription.metadata || {}, } } const buildSubscriptionPayload = (subscription) => { if (!subscription || typeof subscription !== 'object') { return null } const items = Array.isArray(subscription.items?.data) ? subscription.items.data.map((item) => ({ id: item.id, priceId: item.price?.id || null, productId: item.price?.product || null, quantity: item.quantity || 0, price: item.price || null, })) : [] const primaryItem = items[0] || null const productId = primaryItem?.productId || null const priceId = primaryItem?.priceId || null const priceMeta = getSubscriptionMetaFromPrice(priceId) || {} const metadataLevel = subscription.metadata?.subscriptionLevel || null const resolvedLevel = metadataLevel || priceMeta?.level || null const metadataPeriod = subscription.metadata?.subscriptionBillingPeriod || null const resolvedPeriod = metadataPeriod || priceMeta?.billingPeriod || null const customerId = typeof subscription.customer === 'string' ? subscription.customer : null return { id: subscription.id, customerId, productId, priceId, items, level: resolvedLevel, billingPeriod: resolvedPeriod, status: subscription.status || null, cancelAtPeriodEnd: subscription.cancel_at_period_end === true, currentPeriodStart: toFirestoreTimestamp(subscription.current_period_start), currentPeriodEnd: toFirestoreTimestamp(subscription.current_period_end), } } const resolveUserContext = async ({ metadata, customerId }) => { const firebaseUid = extractFirebaseUid(metadata) if (firebaseUid) { const userRef = refsList?.users?.doc(firebaseUid) || null if (userRef) { try { const snapshot = await userRef.get() if (snapshot.exists) { return { uid: firebaseUid, userRef, userData: snapshot.data() || null, } } } catch (error) { console.warn( '[subscription-resolveUserContext] Unable to read user', firebaseUid, error?.message || error ) } } } if (customerId) { try { const snapshot = await refsList.users .where('stripeCustomerId', '==', customerId) .limit(1) .get() if (!snapshot.empty) { const doc = snapshot.docs[0] return { uid: doc.id, userRef: doc.ref, userData: doc.data() || null, } } } catch (error) { console.warn( '[subscription-resolveUserContext] Unable to query by customer', customerId, error?.message || error ) } } return { uid: firebaseUid || null, userRef: null, userData: null, } } const upsertPaymentDocument = async (docId, data = {}) => { if (!docId) { return null } const docRef = paymentsCollection.doc(docId) try { await docRef.set( { ...data, updatedAt: getServerTimestamp(), createdAt: getServerTimestamp(), }, { merge: true } ) } catch (error) { console.error('[subscription-upsertPaymentDocument] Failed to persist payment', docId, error) } return docRef } module.exports = { admin, refsList, paymentsCollection, toFiniteNumber, parseCoinsPerMonth, parseCoinAmount, toDateSafe, addMonths, computeNextGrantTimestamp, getServerTimestamp, resolveStripeWebhookSecret, toFirestoreTimestamp, extractFirebaseUid, formatCoinPack, getSubscriptionMetaFromPrice, buildEventSnapshot, formatSubscriptionForClient, buildSubscriptionPayload, resolveUserContext, upsertPaymentDocument, COIN_PACK_PRODUCT_MAP, }