All files / src/utils roadmap-notify.js

90.9% Statements 30/33
90.9% Branches 20/22
100% Functions 1/1
90% Lines 27/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 79 80 81 82 83 84 85 86 87 88 89 90 91 92                2x 2x 2x   2x               14x             14x         13x   9x 9x 9x   9x 11x 11x         11x 9x 9x   6x 6x                               6x 6x   6x             9x 5x     9x 5x     1x           2x  
/**
 * Roadmap update notification trigger.
 *
 * Queries all subscribers who opted into roadmapUpdate notifications,
 * then creates notification queue entries for dispatch via the
 * notification-dispatch cron job.
 */
 
const { db } = require('./firebase');
const log = require('./log');
const { now } = require('./helpers');
 
const BATCH_LIMIT = 400;
 
/**
 * Notify all roadmapUpdate subscribers about a roadmap change.
 *
 * @param {string} message - Description of what changed (shown to users)
 */
async function notifyRoadmapSubscribers(message) {
  try {
    // Server-side filter on the denormalised `roadmapUpdateOptedIn` flag
    // (Phase 2A finding #2). The previous full-collection scan was a
    // quota grenade — at 5K subs every roadmap edit cost 5K reads
    // regardless of how few opted in. The flag is maintained on every
    // PUT /subscriptions/me and backfilled by the
    // `backfillRoadmapOptedIn` cron for legacy subs.
    const snap = await db
      .collection('subscriptions')
      .where('roadmapUpdateOptedIn', '==', true)
      .get();
 
    if (snap.empty) return;
 
    let batch = db.batch();
    let batchCount = 0;
    let total = 0;
 
    for (const doc of snap.docs) {
      const sub = doc.data();
      const prefs = sub.channelPreferences?.roadmapUpdate;
 
      // Defensive double-check: if the denormalised flag drifted from
      // the actual prefs (race between PUT and notify), trust the prefs.
      // Drift recovers on the next PUT — log and skip this tick.
      if (!prefs) continue;
      const hasAnyChannel = prefs.email || prefs.push || prefs.inApp || prefs.systemMessage;
      if (!hasAnyChannel) continue;
 
      const notifRef = db.collection('notificationQueue').doc();
      batch.set(notifRef, {
        type: 'roadmapUpdate',
        uid: sub.uid || doc.id,
        title: 'Roadmap Update',
        body: message,
        channels: {
          email: !!prefs.email,
          push: !!prefs.push,
          inApp: !!prefs.inApp,
          systemMessage: !!prefs.systemMessage,
        },
        email: sub.email || null,
        pushToken: sub.pushToken || null,
        status: 'queued',
        createdAt: now(),
      });
      batchCount++;
      total++;
 
      Iif (batchCount === BATCH_LIMIT) {
        await batch.commit();
        batch = db.batch();
        batchCount = 0;
      }
    }
 
    if (batchCount > 0) {
      await batch.commit();
    }
 
    if (total > 0) {
      log.info('roadmap-notify', `Queued ${total} roadmap update notifications`);
    }
  } catch (err) {
    log.error('roadmap-notify', 'Failed to notify roadmap subscribers', {
      error: err.message,
    });
  }
}
 
module.exports = { notifyRoadmapSubscribers };