← back to Prestige Car Wash

reports/deployed-20260720/publish-due.js

71 lines

'use strict';
/**
 * Prestige social scheduler worker — Phase 1 (DARK / INERT).
 *
 * Reads data/schedule.json, finds posts that are DUE (when <= now) and not yet
 * sent, and hands each to its platform adapter. Adapters are "inert until keyed":
 * with no token in .env they return {skipped:'needs-connection'} and the post
 * STAYS queued — i.e. exactly today's manual copy&open behavior. Every attempt is
 * appended to reports/social-posts.jsonl.
 *
 * Nothing is auto-posted in Phase 1. Real posting only happens once a platform
 * adapter is IMPLEMENTED, keyed, AND its go-live is approved
 * (see reports/SCOPE-social-autopost-*.md). Yelp has no posting API — manual forever.
 *
 * Not yet installed on any timer — run manually: `node scripts/publish-due.js`.
 */
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const DATA = path.join(__dirname, '..', 'data');
const REPORTS = path.join(__dirname, '..', 'reports');
const FILE = path.join(DATA, 'schedule.json');

// One adapter per platform, each gated behind an env token (Stripe-style inert).
function needsKey(env) {
  return async (_post) => {
    if (!process.env[env]) return { ok: false, skipped: 'needs-connection', env };
    // Adapter body intentionally NOT implemented in Phase 1 — go-live is gated.
    return { ok: false, skipped: 'adapter-not-enabled', env };
  };
}
const adapters = {
  'Instagram':               needsKey('PCW_IG_ACCESS_TOKEN'),
  'Facebook Page':           needsKey('PCW_FB_PAGE_TOKEN'),
  'TikTok':                  needsKey('PCW_TIKTOK_ACCESS_TOKEN'),
  'YouTube':                 needsKey('PCW_YT_REFRESH_TOKEN'),
  'Google Business Profile': needsKey('PCW_GBP_ACCESS_TOKEN')
  // 'Yelp Business': no posting API — always manual, no adapter.
};

function log(entry) {
  try { fs.mkdirSync(REPORTS, { recursive: true }); } catch (e) {}
  fs.appendFileSync(path.join(REPORTS, 'social-posts.jsonl'),
    JSON.stringify({ ...entry, at: new Date().toISOString() }) + '\n');
}

async function main() {
  let doc;
  try { doc = JSON.parse(fs.readFileSync(FILE, 'utf8')); }
  catch (e) { console.log('[publish-due] no schedule.json — nothing to do'); return; }
  const posts = Array.isArray(doc.posts) ? doc.posts : [];
  const now = Date.now();
  let due = 0, attempted = 0;
  for (const p of posts) {
    if (p.status === 'sent') continue;
    if (!p.when || new Date(p.when).getTime() > now) continue;
    due++;
    for (const plat of (p.socials || [])) {
      const fn = adapters[plat];
      const r = fn ? await fn(p) : { ok: false, skipped: 'no-adapter (manual only)' };
      attempted++;
      log({ post_id: p.id, platform: plat, result: r });
    }
    // Phase 1: nothing actually posts, so status is NOT flipped to 'sent' — the
    // post stays visible in the admin as "ready to post" (manual copy&open) until
    // an adapter is enabled + approved.
  }
  console.log(`[publish-due] ${posts.length} queued · ${due} due · ${attempted} adapter-calls · 0 auto-posted (Phase 1 dark)`);
}
main();