← back to Dw Signup Fulfillment

verification/tk11114/backfill-send.js

66 lines

'use strict';
// TK-11114 backfill SENDER — re-send the verify letter to signups that never got it
// during the outage. Bypasses the webhook's 24h freshness gate by calling
// verify.startVerification() directly (that's the only guard that would wrongly reject
// a legitimate re-send of an owed letter). SAFE BY DESIGN:
//   • DRY_RUN default: prints WOULD-send per customer; sends nothing without --apply.
//   • Real send happens only when config.DRY_RUN=0 (i.e. on Kamatera prod) AND --apply.
//   • Idempotent: skips anyone with custom.sample_verify_sent=true (re-checked live) AND
//     anyone already in the local ledger; sets the flag + ledgers on success so re-runs
//     never double-send.
//   • Excludes internal/test addresses.
//   • --only <id> / --limit N for a controlled single-customer test before the batch.
//   • --list <path> defaults to backfill-affected.json (the read-only scope output).
const fs = require('fs');
const path = require('path');
const shopify = require(path.join(__dirname, '..', '..', 'lib', 'shopify'));
const verify = require(path.join(__dirname, '..', '..', 'lib', 'verify'));
const config = require(path.join(__dirname, '..', '..', 'lib', 'config'));

const args = process.argv.slice(2);
const APPLY = args.includes('--apply');
const ONLY = (args[args.indexOf('--only') + 1]) && args.includes('--only') ? args[args.indexOf('--only') + 1] : null;
const LIMIT = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1], 10) : Infinity;
const LISTP = args.includes('--list') ? args[args.indexOf('--list') + 1] : path.join(__dirname, 'backfill-affected.json');
const LEDGER = path.join(__dirname, 'backfill-ledger.jsonl');

function isInternalOrTest(e){ e=(e||'').toLowerCase(); return /@designerwallcoverings\.com$/.test(e)||/dwgolive|\btest@|example\.com$/.test(e)||/\+dwgolive/.test(e); }
function ledgerDone(){ const s=new Set(); try{ for(const l of fs.readFileSync(LEDGER,'utf8').split('\n')){ if(!l.trim())continue; const r=JSON.parse(l); if(r.ok) s.add(String(r.id)); } }catch{} return s; }
function ledgerAppend(row){ fs.appendFileSync(LEDGER, JSON.stringify(row)+'\n'); }

(async()=>{
  const scope = JSON.parse(fs.readFileSync(LISTP,'utf8'));
  let list = scope.affected || [];
  if (ONLY) list = list.filter(a=>String(a.id)===String(ONLY)).concat(list.some(a=>String(a.id)===String(ONLY))?[]:[{id:ONLY,email:'(direct)'}]);
  const done = ledgerDone();
  console.log(`backfill: ${list.length} in scope · mode=${config.DRY_RUN?'DRY_RUN (no real send)':'LIVE'} · apply=${APPLY} · only=${ONLY||'-'} · limit=${LIMIT===Infinity?'all':LIMIT}`);
  if (!config.DRY_RUN && !APPLY) { console.log('LIVE env but no --apply → refusing to send. Re-run with --apply to actually send.'); }
  let n=0, sent=0, skipped=0, failed=0;
  for (const a of list){
    if (n>=LIMIT) break;
    const id=String(a.id);
    if (done.has(id)) { skipped++; continue; }
    // live re-fetch: real email + freshness-independent; also re-check the flag (idempotent)
    const r = await shopify.getCustomer(id);
    const c = r && r.json && r.json.customer;
    if (!c || !c.email) { failed++; console.log(`  [skip] id=${id} not found / no email`); continue; }
    if (isInternalOrTest(c.email)) { skipped++; console.log(`  [skip] id=${id} internal/test`); continue; }
    const flag = await shopify.getCustomerMetafield(id,'custom','sample_verify_sent');
    if (flag && String(flag).toLowerCase()==='true') { skipped++; console.log(`  [skip] id=${id} already sent`); continue; }
    n++;
    const masked = String(c.email).replace(/(.{2}).*@/,'$1***@');
    if (config.DRY_RUN || !APPLY) { console.log(`  [would-send] id=${id} ${masked}`); continue; }
    const started = await verify.startVerification({ email:c.email, customerId:c.id, firstName:c.first_name });
    if (started.ok) {
      await shopify.setCustomerMetafield(id, { namespace:'custom', key:'sample_verify_sent', value:'true', type:'boolean' });
      sent++; ledgerAppend({ id, email:c.email, ok:true, ts:new Date().toISOString() });
      console.log(`  [sent] id=${id} ${masked}`);
    } else {
      failed++; ledgerAppend({ id, email:c.email, ok:false, reason:started.reason||'unknown', ts:new Date().toISOString() });
      console.log(`  [FAIL] id=${id} ${masked} reason=${started.reason||'unknown'}`);
    }
    await new Promise(r=>setTimeout(r,500)); // gentle pacing for George + Shopify
  }
  console.log(`DONE: would/sent=${n} sent=${sent} skipped=${skipped} failed=${failed}`);
})().catch(e=>{ console.error('ERR', e.message); process.exit(1); });