← back to Dw Signup Fulfillment

verification/tk11114-remediation/backfill.js

51 lines

'use strict';
// TK-11114 verify-email BACKFILL executor.
// DRY-RUN BY DEFAULT: classifies each id (eligible / skip-reason) and sends NOTHING.
// Sending requires BOTH `--send` AND env TK11114_SEND_CONFIRM=YES-SEND (double-lock),
// and is paced 1/sec. Idempotent: only sends when sample_verify_sent is unset,
// customer exists, has a real email, and is not a tk11114-test record.
process.env.DRY_RUN = '0'; // force real Shopify reads (this file never fabricates)
const fs = require('fs');
const path = require('path');
const shopify = require(path.join(__dirname, '..', '..', 'lib', 'shopify'));
const config = require(path.join(__dirname, '..', '..', 'lib', 'config'));
const verify = require(path.join(__dirname, '..', '..', 'lib', 'verify'));

const args = process.argv.slice(2);
const SEND = args.includes('--send') && process.env.TK11114_SEND_CONFIRM === 'YES-SEND';
const fileArg = args.find(a => !a.startsWith('--')) || path.join(__dirname, 'tk11114_affected.txt');

function truthy(mf) { return mf !== null && mf !== undefined && mf !== '' && mf !== false && mf !== '0'; }
const sleep = ms => new Promise(r => setTimeout(r, ms));

(async () => {
  if (!config.SHOPIFY_FULFILLMENT_TOKEN) { console.log(JSON.stringify({ ok: false, error: 'no fulfillment token resolved' })); process.exit(1); }
  const ids = fs.readFileSync(fileArg, 'utf8').split(/\s+/).map(s => s.trim()).filter(Boolean);
  const mode = SEND ? 'LIVE-SEND' : 'DRY-RUN';
  console.log(`== TK-11114 backfill [${mode}] · ${ids.length} candidate ids · source=${fileArg} ==`);
  const buckets = { eligible: [], already_sent: [], no_email: [], test_record: [], not_found: [], sent_ok: [], send_failed: [] };
  for (const id of ids) {
    let r; try { r = await shopify.getCustomer(id); } catch (e) { buckets.not_found.push({ id, err: e.message }); continue; }
    if (!r || r.status === 404 || !(r.json && r.json.customer)) { buckets.not_found.push({ id, status: r && r.status }); continue; }
    const c = r.json.customer;
    const tags = (c.tags || '');
    if (/tk11114-test/i.test(tags)) { buckets.test_record.push({ id }); continue; }
    if (!c.email) { buckets.no_email.push({ id }); continue; }
    let sentFlag = null; try { sentFlag = await shopify.getCustomerMetafield(id, 'custom', 'sample_verify_sent'); } catch (e) {}
    if (truthy(sentFlag)) { buckets.already_sent.push({ id }); continue; }
    // ELIGIBLE
    if (!SEND) { buckets.eligible.push({ id, email_domain: String(c.email).split('@')[1] || '?' }); continue; }
    // LIVE send path (double-locked)
    try {
      const res = await verify.startVerification({ email: c.email, customerId: id, firstName: c.first_name || '' });
      if (res && res.ok) buckets.sent_ok.push({ id });
      else buckets.send_failed.push({ id, reason: res && res.reason, status: res && res.status, errorCode: res && res.errorCode });
    } catch (e) { buckets.send_failed.push({ id, err: e.message }); }
    await sleep(1000); // pace
  }
  const summary = Object.fromEntries(Object.entries(buckets).map(([k, v]) => [k, v.length]));
  console.log('== SUMMARY =='); console.log(JSON.stringify(summary, null, 2));
  console.log('== eligible ids =='); console.log(buckets.eligible.map(x => x.id).join(' ') || '(none)');
  if (SEND) { console.log('== send_failed detail =='); console.log(JSON.stringify(buckets.send_failed, null, 2)); }
})().catch(e => { console.error('backfill error:', e.message); process.exit(1); });