import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' /** * Proactive daily question reminder. * * Schedule: 4:00 PM America/Chicago (2 hours before the question expires at 6 PM). * * Logic: * 1. Query all `daily_question` docs (collection group) with expiresAt in the * next 3 hours — these are the ones expiring today. * 2. For each, count the answers subcollection. If 0 answers (neither partner * has responded), the couple needs a nudge. * 3. Skip couples where a gentle_reminder or automated_daily_reminder was * already sent today (avoid over-notifying couples where a partner already * manually nudged the other). * 4. Send FCM to all users in the couple + write to notification_queue. * 5. Record automated_daily_reminder/{docId} so this function is idempotent * on re-runs. */ export const sendDailyQuestionProactiveReminder = functions.pubsub .schedule('0 16 * * *') .timeZone('America/Chicago') .onRun(async () => { const db = admin.firestore() const messaging = admin.messaging() const now = Date.now() const threeHoursMs = 3 * 60 * 60 * 1000 // Docs expiring within the next 3 hours — the active window for today's question. const expiringSnap = await db .collectionGroup('daily_question') .where('expiresAt', '>', admin.firestore.Timestamp.fromMillis(now)) .where('expiresAt', '<', admin.firestore.Timestamp.fromMillis(now + threeHoursMs)) .get() let notified = 0 let skipped = 0 await Promise.all( expiringSnap.docs.map(async (questionDoc) => { const coupleRef = questionDoc.ref.parent.parent if (!coupleRef) return const reminderId = `auto_${questionDoc.id}` const reminderRef = coupleRef.collection('daily_reminders').doc(reminderId) // Idempotency: skip if we already sent this reminder. const alreadySent = await reminderRef.get() if (alreadySent.exists) { skipped++; return } // Skip if a manual gentle_reminder was sent today (same date key). const dateKey = questionDoc.id // daily_question docs are keyed by date (YYYY-MM-DD) const gentleRef = coupleRef.collection('gentle_reminders').doc(dateKey) const gentleSent = await gentleRef.get() if (gentleSent.exists) { skipped++; return } // Check answer count — only remind if nobody has answered yet. const answersSnap = await questionDoc.ref.collection('answers').limit(1).get() if (!answersSnap.empty) { skipped++; return } // Fetch couple members. const coupleDoc = await coupleRef.get() const userIds = (coupleDoc.data()?.userIds ?? []) as string[] if (userIds.length === 0) { skipped++; return } // Claim the reminder slot atomically. try { await db.runTransaction(async (tx) => { const fresh = await tx.get(reminderRef) if (fresh.exists) throw new Error('already_sent') tx.set(reminderRef, { sentAt: admin.firestore.FieldValue.serverTimestamp(), questionDate: dateKey, }) }) } catch { skipped++ return } // Send FCM + notification_queue entry to every user in the couple. await Promise.all( userIds.map((userId) => sendReminder(db, messaging, userId, coupleRef.id, dateKey) ) ) notified += userIds.length }) ) console.log( `[sendDailyQuestionProactiveReminder] scanned ${expiringSnap.size} docs; notified ${notified} users; skipped ${skipped}` ) }) async function sendReminder( db: admin.firestore.Firestore, messaging: admin.messaging.Messaging, userId: string, coupleId: string, questionDate: string ): Promise { // In-app notification record. await db .collection('users') .doc(userId) .collection('notification_queue') .add({ type: 'daily_question_reminder', title: "Tonight's question is waiting.", body: 'Answer together before it expires.', read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), }) // FCM push. const tokens = await getUserTokens(db, userId) if (tokens.length === 0) return const sendResults = await Promise.allSettled( tokens.map((token) => messaging.send({ token, notification: { title: "Tonight's question is waiting.", body: 'Answer together before it expires.', }, data: { type: 'daily_question_reminder', couple_id: coupleId, question_date: questionDate, }, }) ) ) sendResults.forEach((result, i) => { if (result.status === 'rejected') { console.warn( `[sendDailyQuestionProactiveReminder] FCM failed for token ${tokens[i]}:`, result.reason ) } }) } async function getUserTokens( db: admin.firestore.Firestore, userId: string ): Promise { const tokens: string[] = [] const userDoc = await db.collection('users').doc(userId).get() const legacyToken = userDoc.data()?.fcmToken if (typeof legacyToken === 'string' && legacyToken.length > 0) { tokens.push(legacyToken) } const tokenSnap = await db .collection('users') .doc(userId) .collection('fcmTokens') .get() tokenSnap.docs.forEach((doc) => { const t = doc.data()?.token if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { tokens.push(t) } }) return tokens }