← back to Dw Signup Fulfillment
TK-11114: backfill tooling — scope (68 affected since go-live) + safe sender
228c2ea3e5e5518159ce14adc73152d7983c1c49 · 2026-09-02 12:09:51 -0700 · Steve Abrams
Read-only scoper enumerates genuine retail signups with no sample_verify_sent (never
got the verify letter), excluding test/internal + trade: 68 since 2026-08-28. Sender
bypasses the 24h webhook freshness gate (direct verify.startVerification), dry-run by
default, idempotent (live flag re-check + ledger), --only/--apply gates. Real 68-email
send stays hard-gated (send-to-list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A verification/tk11114/backfill-scope.jsA verification/tk11114/backfill-send.js
Diff
commit 228c2ea3e5e5518159ce14adc73152d7983c1c49
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 12:09:51 2026 -0700
TK-11114: backfill tooling — scope (68 affected since go-live) + safe sender
Read-only scoper enumerates genuine retail signups with no sample_verify_sent (never
got the verify letter), excluding test/internal + trade: 68 since 2026-08-28. Sender
bypasses the 24h webhook freshness gate (direct verify.startVerification), dry-run by
default, idempotent (live flag re-check + ledger), --only/--apply gates. Real 68-email
send stays hard-gated (send-to-list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
verification/tk11114/backfill-scope.js | 43 ++++++++++++++++++++++
verification/tk11114/backfill-send.js | 65 ++++++++++++++++++++++++++++++++++
2 files changed, 108 insertions(+)
diff --git a/verification/tk11114/backfill-scope.js b/verification/tk11114/backfill-scope.js
new file mode 100644
index 0000000..1b8a297
--- /dev/null
+++ b/verification/tk11114/backfill-scope.js
@@ -0,0 +1,43 @@
+'use strict';
+// TK-11114 backfill SCOPE — READ-ONLY. Enumerates customers created since go-live and
+// classifies each: AFFECTED (no custom.sample_verify_sent=true → never got the verify letter),
+// GOT_IT (flag set), TEST/INTERNAL (excluded), TRADE (excluded — not a retail-samples signup).
+// No writes, no sends. Prints a JSON summary + the AFFECTED list for Steve's go.
+const path = require('path');
+const shopify = require(path.join(__dirname, '..', '..', 'lib', 'shopify'));
+
+const SINCE = process.env.SINCE || '2026-08-28T00:00:00Z';
+function isInternalOrTest(e){ e=(e||'').toLowerCase(); return /@designerwallcoverings\.com$/.test(e)||/dwgolive|\btest@|example\.com$/.test(e)||/\+dwgolive/.test(e); }
+
+async function listSince(sinceIso){
+ const out=[]; let url=`/customers.json?limit=250&created_at_min=${encodeURIComponent(sinceIso)}&fields=id,email,created_at,tags,state`;
+ // single page is enough for <250; paginate defensively via since_id
+ let lastId=0;
+ for(let i=0;i<10;i++){
+ const u = url + (lastId?`&since_id=${lastId}`:'');
+ const r = await shopify.request('GET', u, undefined);
+ const cs = (r&&r.json&&r.json.customers)||[];
+ if(!cs.length) break;
+ out.push(...cs); lastId = cs[cs.length-1].id;
+ if(cs.length<250) break;
+ }
+ return out;
+}
+
+(async()=>{
+ const custs = await listSince(SINCE);
+ const res={ since:SINCE, total:custs.length, affected:[], got_it:0, test_internal:0, trade:0 };
+ for(const c of custs){
+ const tags=(c.tags||'').toLowerCase();
+ if(isInternalOrTest(c.email)){ res.test_internal++; continue; }
+ if(/\btrade\b/.test(tags)){ res.trade++; continue; }
+ const flag = await shopify.getCustomerMetafield(c.id,'custom','sample_verify_sent');
+ if(flag && String(flag).toLowerCase()==='true'){ res.got_it++; continue; }
+ res.affected.push({ id:c.id, email:c.email, created_at:c.created_at, tags:c.tags||'' });
+ }
+ res.affected_count=res.affected.length;
+ require('fs').writeFileSync(path.join(__dirname,'backfill-affected.json'), JSON.stringify(res,null,2));
+ console.log(JSON.stringify({ since:res.since, total:res.total, affected_count:res.affected_count, got_it:res.got_it, test_internal:res.test_internal, trade:res.trade },null,2));
+ console.log('--- AFFECTED (first 20, email masked) ---');
+ for(const a of res.affected.slice(0,20)) console.log(`${a.created_at} ${String(a.email).replace(/(.{2}).*@/,'$1***@')} id=${a.id}`);
+})().catch(e=>{ console.error('ERR', e.message); process.exit(1); });
diff --git a/verification/tk11114/backfill-send.js b/verification/tk11114/backfill-send.js
new file mode 100644
index 0000000..640a75b
--- /dev/null
+++ b/verification/tk11114/backfill-send.js
@@ -0,0 +1,65 @@
+'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); });
← a287362 auto-data-snapshot: 2026-09-02T12:07:47 (1 data files) — ver
·
back to Dw Signup Fulfillment
·
TK-11114: backfill sent (68/68) — outage fully remediated d8959b9 →