← back to Professional Directory

scripts/aggregate-ratings.js

72 lines

#!/usr/bin/env node
/**
 * Recompute aggregated_ratings from reviews. Run nightly via launchd.
 *
 *   - Excludes suppressed_by_target=true and hidden_by_admin=true.
 *   - Includes reviews where go_live_at <= now() OR go_live_at IS NULL (seeded).
 *   - Counts user reviews + seeded reviews separately.
 */
const { pool, query } = require('../agents/shared/db');

async function main() {
  const t0 = Date.now();
  console.log('[agg] recomputing aggregated_ratings…');

  // Wipe + rebuild — small enough table that a full recompute is fine.
  await query(`TRUNCATE aggregated_ratings RESTART IDENTITY`);

  // Per-professional aggregation.
  await query(`
    INSERT INTO aggregated_ratings (target_professional_id,
                                    service_avg, price_avg, quality_avg, overall_avg,
                                    n_reviews, n_user_reviews, n_seeded_reviews)
    SELECT target_professional_id,
           ROUND(AVG(service_score)::numeric, 2),
           ROUND(AVG(price_score)::numeric, 2),
           ROUND(AVG(quality_score)::numeric, 2),
           ROUND(AVG(overall_score)::numeric, 2),
           COUNT(*),
           COUNT(*) FILTER (WHERE source = 'user'),
           COUNT(*) FILTER (WHERE source <> 'user')
      FROM reviews
     WHERE target_professional_id IS NOT NULL
       AND suppressed_by_target = false
       AND hidden_by_admin = false
       AND (go_live_at IS NULL OR go_live_at <= now())
     GROUP BY target_professional_id
  `);

  // Per-organization aggregation.
  await query(`
    INSERT INTO aggregated_ratings (target_organization_id,
                                    service_avg, price_avg, quality_avg, overall_avg,
                                    n_reviews, n_user_reviews, n_seeded_reviews)
    SELECT target_organization_id,
           ROUND(AVG(service_score)::numeric, 2),
           ROUND(AVG(price_score)::numeric, 2),
           ROUND(AVG(quality_score)::numeric, 2),
           ROUND(AVG(overall_score)::numeric, 2),
           COUNT(*),
           COUNT(*) FILTER (WHERE source = 'user'),
           COUNT(*) FILTER (WHERE source <> 'user')
      FROM reviews
     WHERE target_organization_id IS NOT NULL
       AND suppressed_by_target = false
       AND hidden_by_admin = false
       AND (go_live_at IS NULL OR go_live_at <= now())
     GROUP BY target_organization_id
  `);

  const stats = (await query(`
    SELECT COUNT(*) AS rows,
           SUM(n_reviews)::int AS total_reviews,
           SUM(n_user_reviews)::int AS user_reviews,
           SUM(n_seeded_reviews)::int AS seeded_reviews
      FROM aggregated_ratings`)).rows[0];

  console.log(`[agg] done in ${(Date.now() - t0) / 1000}s. rows=${stats.rows} total_reviews=${stats.total_reviews} user=${stats.user_reviews} seeded=${stats.seeded_reviews}`);
  await pool.end();
}

main().catch(async e => { console.error(e); try { await pool.end(); } catch (_) {}; process.exit(1); });