← back to Dw Signup Fulfillment
scripts/tk11411-resend.js
123 lines
'use strict';
// TK-11411 — re-send the mail lost during the George 401 outage (2026-09-10).
//
// George rejected every send with "Invalid credentials" for the duration of the
// outage. Two cohorts lost different letters:
// RETAIL — signed up for free samples, never got "confirm your email", so they
// never clicked, so they never got the verified-sample tag, so they
// cannot claim samples. Remedy: re-run the SAME verification start the
// /claim route uses (lib/verify.startVerification) — mints a fresh
// token and sends the letter.
// TRADE — their approval genuinely LANDED (Shopify carries trade+trade_approved
// AND the local row says approved) but the "you're approved" letter
// failed, so they were never told. Remedy: send that letter only. Do
// NOT re-run trade.approve() — they are already approved and it would
// re-tag and re-assign a rep.
//
// SAFE BY DESIGN:
// * DRY-RUN by default. --apply is required to send, and it refuses while
// config.DRY_RUN is on (a bare `node ...` inherits DRY_RUN=1 and would
// otherwise silently "succeed" without sending).
// * COHORT IS DERIVED AT RUN TIME, never hardcoded: a customer that already has
// verified-sample is SKIPPED, so anyone who recovered on their own is not
// re-mailed.
// * IDEMPOTENT via data/tk11411-resend-ledger.jsonl — an address already recorded
// as sent is skipped on every later run.
// * Ledger is append-only and written AFTER a confirmed real send.
const fs = require('fs');
const path = require('path');
const config = require('../lib/config');
const shopify = require('../lib/shopify');
const email = require('../lib/email');
const verify = require('../lib/verify');
const args = process.argv.slice(2);
const APPLY = args.includes('--apply');
const LEDGER = path.join(__dirname, '..', 'data', 'tk11411-resend-ledger.jsonl');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// The addresses observed with source=retail-verify / designer-welcome /
// trade-application 401s in the outage logs. Classification is NOT taken from
// here — it is derived below from live Shopify + the local store.
const COHORT = ['j.dmce@post.harvard.edu', 'juliewendl@aol.com', 'kristopher.woodcock@gmail.com',
'lchandlee@aol.com', 'lizbeth@luxupholstery.com', 'margaret@designmadesimple.design',
'noelle@silkandslate.com', 'office@leddyinteriors.com', 'sdkett20@gmail.com',
'sheldonlycan@gmail.com', 'skl@liocowine.com', 'team@michellewalshdesigns.com',
'tori@bellewoodgroup.com', 'yanchapaxi@auxarchitecture.com'];
function alreadySent(addr) {
try {
return fs.readFileSync(LEDGER, 'utf8').split('\n').filter(Boolean)
.some((l) => { try { return JSON.parse(l).email === addr; } catch { return false; } });
} catch { return false; }
}
function record(row) { fs.appendFileSync(LEDGER, JSON.stringify(row) + '\n'); }
function tradeRows() {
try {
return fs.readFileSync(path.join(__dirname, '..', 'data', 'trade-applications.jsonl'), 'utf8')
.split('\n').filter(Boolean).map((l) => JSON.parse(l));
} catch { return []; }
}
(async () => {
if (APPLY && config.DRY_RUN) {
console.error('REFUSING --apply while DRY_RUN is on: nothing would actually send. Re-run with DRY_RUN=0.');
process.exit(1);
}
// FAIL LOUD, ONCE, BEFORE ANY SEND. PUBLIC_URL lives in ecosystem.config.js, which pm2
// injects into the SERVICE — a bare `node scripts/...` does NOT inherit it, so verify.js
// refuses every retail send with no_public_url. Without this pre-flight that surfaces as
// 11 identical per-customer failures after the trade cohort has already been mailed,
// i.e. a half-completed run. Same class as the DRY_RUN footgun documented at the top of
// scripts/recover-stuck-apps.js — which I had read, and repeated anyway.
if (APPLY && !config.PUBLIC_URL) {
console.error('REFUSING --apply: PUBLIC_URL is unset, so every retail verify letter would');
console.error('carry a dead localhost link. Run with the value the service uses, e.g.');
console.error(" PUBLIC_URL=$(node -e \"console.log(require('./ecosystem.config.js').apps[0].env.PUBLIC_URL)\") DRY_RUN=0 node scripts/tk11411-resend.js --apply");
process.exit(1);
}
console.log(`TK-11411 resend mode=${APPLY ? 'APPLY' : 'DRY-RUN'} DRY_RUN(config)=${config.DRY_RUN}`);
const rows = tradeRows();
let sent = 0, skipped = 0;
for (const addr of COHORT) {
if (alreadySent(addr)) { console.log(` [skip] ${addr} — already re-sent (ledger)`); skipped++; continue; }
const id = await shopify.findCustomerByEmail(addr);
if (!id) { console.log(` [skip] ${addr} — no Shopify customer`); skipped++; continue; }
const custId = String(id).replace(/\D/g, '');
const res = await shopify.getCustomer(custId);
const c = (res && res.json && (res.json.customer || (res.json.data && res.json.data.customer))) || {};
const tags = Array.isArray(c.tags) ? c.tags.join(',') : (c.tags || '');
if (/verified-sample/.test(tags)) { console.log(` [skip] ${addr} — already verified-sample (recovered on its own)`); skipped++; continue; }
// TRADE: approved on BOTH sides, only the letter was lost.
const approvedRow = rows.find((r) => String(r.email).toLowerCase() === addr && r.status === 'approved');
const isTrade = /(^|,)\s*trade_approved\s*(,|$)/.test(tags) && !!approvedRow;
if (isTrade) {
const repName = (approvedRow.assigned_rep && approvedRow.assigned_rep.name) || 'your DW rep';
const tpl = email.tradeApprovedEmail({ applicant: approvedRow, repName });
if (!APPLY) { console.log(` [dry] ${addr} — TRADE: would re-send "${tpl.subject}"`); continue; }
const m = await email.sendEmail({ to: addr, subject: tpl.subject, html: tpl.html, source: 'tk11411-trade-approved-resend' });
const ok = m && m.ok === true && !m.dryRun;
console.log(` [${ok ? 'sent' : 'FAIL'}] ${addr} — TRADE approval letter${ok ? '' : ' :: ' + JSON.stringify(m && (m.error || m.status))}`);
if (ok) { record({ ts: new Date().toISOString(), email: addr, cohort: 'trade', subject: tpl.subject }); sent++; }
await sleep(400);
continue;
}
// RETAIL: re-run the same verification start /claim uses.
if (!APPLY) { console.log(` [dry] ${addr} — RETAIL: would re-send the confirm-your-email verify letter`); continue; }
const r = await verify.startVerification({ email: addr, customerId: custId, firstName: c.first_name || addr.split('@')[0] });
const ok = r && r.ok !== false && !r.dryRun;
console.log(` [${ok ? 'sent' : 'FAIL'}] ${addr} — RETAIL verify letter${ok ? '' : ' :: ' + JSON.stringify(r && (r.reason || r.status))}`);
if (ok) { record({ ts: new Date().toISOString(), email: addr, cohort: 'retail' }); sent++; }
await sleep(400);
}
console.log(`\n${APPLY ? 'Sent' : 'Would send'}: ${APPLY ? sent : COHORT.length - skipped} · skipped: ${skipped}`);
if (!APPLY) console.log('Dry-run only. Re-run with DRY_RUN=0 --apply to actually send.');
})().catch((e) => { console.error('tk11411-resend error:', e.message); process.exit(1); });