[object Object]

← back to Dw Signup Fulfillment

TK-11114: dry-run-safe verify-email backfill executor (double-locked send)

cb069ce1c8f5365c0822effd1605d92c6828b2e9 · 2026-09-02 11:50:22 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuW5Mim1jbLsbaaEWsnq7D

Files touched

Diff

commit cb069ce1c8f5365c0822effd1605d92c6828b2e9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 11:50:22 2026 -0700

    TK-11114: dry-run-safe verify-email backfill executor (double-locked send)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01HuW5Mim1jbLsbaaEWsnq7D
---
 verification/tk11114-remediation/backfill.js | 50 ++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/verification/tk11114-remediation/backfill.js b/verification/tk11114-remediation/backfill.js
new file mode 100644
index 0000000..5bf3074
--- /dev/null
+++ b/verification/tk11114-remediation/backfill.js
@@ -0,0 +1,50 @@
+'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); });

← 49e1750 TK-10830: one-shot guard on Kelly reply (prevent duplicate s  ·  back to Dw Signup Fulfillment  ·  TK-11114: durable persist confirmed — signup email now resta 3efc18c →