[object Object]

← back to Dw Signup Fulfillment

Verify trade entitlement before approval and preserve retry progress

d291ec9e62ff125cecb4a04420a00f6d1fcd9045 · 2026-09-04 23:57:09 -0700 · Steve Abrams

Files touched

Diff

commit d291ec9e62ff125cecb4a04420a00f6d1fcd9045
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 4 23:57:09 2026 -0700

    Verify trade entitlement before approval and preserve retry progress
---
 lib/shopify.js                 |   10 +
 lib/trade.js                   |  191 +++++--
 scripts/selftest.js            |   25 +-
 scripts/trade-approval-test.js |  202 +++++++
 verification/e2e-proof.json    | 1214 ++++++++++++++++++++++++++++++++++++++--
 5 files changed, 1510 insertions(+), 132 deletions(-)

diff --git a/lib/shopify.js b/lib/shopify.js
index 32c2d7d..be11423 100644
--- a/lib/shopify.js
+++ b/lib/shopify.js
@@ -208,6 +208,16 @@ async function addTags(customerId, newTags) {
   const wanted = (Array.isArray(newTags) ? newTags : [newTags]).map(t => t.trim()).filter(Boolean);
   let existing = [];
   const cur = await getCustomer(customerId);
+  // Never replace tags after a failed/malformed merge read. Approval's final
+  // readback cannot recover unrelated tags that an unsafe merge already erased.
+  if (!cur?.dryRun) {
+    const customer = cur?.json?.customer;
+    if (cur?.ok !== true || !Number.isFinite(cur.status) || cur.status < 200 || cur.status >= 300 ||
+      !customer || String(customer.id) !== String(customerId).replace('gid://shopify/Customer/', '') ||
+      typeof customer.tags !== 'string' || cur.json.errors) {
+      return { ok: false, status: cur?.status || 0, error: 'customer_tags_read_failed', json: cur?.json || null };
+    }
+  }
   if (cur?.json?.customer?.tags) existing = cur.json.customer.tags.split(',').map(t => t.trim()).filter(Boolean);
   const merged = Array.from(new Set([...existing, ...wanted])).join(', ');
   return updateCustomer(customerId, { tags: merged });
diff --git a/lib/trade.js b/lib/trade.js
index a58d839..6134e09 100644
--- a/lib/trade.js
+++ b/lib/trade.js
@@ -155,73 +155,147 @@ function get(id) {
   return readAll().find(a => a.id === id) || null;
 }
 
-// APPROVE — runs the full fan-out, all writes dry-run-safe via lib/shopify.
+// Approval is serialized per application inside this service process. Completed
+// email receipts are checkpointed; uncertain delivery needs manual reconciliation.
+const approvalsInFlight = new Set();
+function customerKey(value) {
+  const match = String(value || '').match(/^(?:gid:\/\/shopify\/Customer\/)?([1-9]\d*)$/);
+  return match ? match[1] : null;
+}
+function hasApiErrors(value) {
+  if (!value || typeof value !== 'object') return false;
+  return Object.entries(value).some(([key, child]) =>
+    ((key === 'errors' || key === 'userErrors') && child &&
+      (Array.isArray(child) ? child.length > 0 : true)) || hasApiErrors(child));
+}
+function shopifyError(result) {
+  if (!result || result.ok !== true || result.status < 200 || result.status >= 300 || !Number.isFinite(result.status)) return 'shopify_http_error';
+  if (result.dryRun || result.noToken) return 'shopify_simulated';
+  if (!result.json || typeof result.json !== 'object' || hasApiErrors(result.json)) return 'shopify_response_error';
+  return null;
+}
+function tagsIncludeTrade(tags) {
+  const values = Array.isArray(tags) ? tags : typeof tags === 'string' ? tags.split(',') : [];
+  return values.some(tag => typeof tag === 'string' && tag.trim() === 'trade_approved');
+}
+// Do not retain a pre-await snapshot of the entire file: new applications may
+// arrive while Shopify or email is in flight.
+function checkpointApproval(app) {
+  const latest = readAll();
+  const index = latest.findIndex(row => row.id === app.id);
+  if (index < 0 || latest[index].status !== 'pending') throw new Error('application_changed');
+  latest[index] = { ...latest[index], ...app };
+  rewriteAll(latest);
+}
+
 async function approve(id) {
-  const rows = readAll();
-  const app = rows.find(a => a.id === id);
+  if (approvalsInFlight.has(id)) return { ok: false, error: 'approval_in_progress', id };
+  const app = get(id);
   if (!app) return { ok: false, error: 'not found' };
   if (app.status !== 'pending') return { ok: false, error: `already ${app.status}` };
-
+  // An ok:true result makes existing one-click callers display "approved". A
+  // simulation therefore explicitly returns ok:false and leaves the row pending.
+  if (config.DRY_RUN) return {
+    ok: false, id, status: 'pending', dryRun: true, simulated: true,
+    error: 'dry_run_simulated', message: 'Approval simulation only; no entitlement or email was changed.', steps: [],
+  };
+  approvalsInFlight.add(id);
   const steps = [];
+  let phase = 'resolve_customer';
+  const fail = error => {
+    app.status = 'pending';
+    app.decision = null;
+    app.decided_at = null;
+    app.approval_error = { error, step: phase, at: new Date().toISOString() };
+    checkpointApproval(app);
+    return { ok: false, id, status: 'pending', error, failedStep: phase, steps };
+  };
+  try {
+    const rep = app.assigned_rep || reps.assignRep();
+    let custId = customerKey(app.shopify_customer_id);
+    const resolvedBy = custId ? 'application' : 'email_lookup';
+    if (!custId) custId = customerKey(await shopify.findCustomerByEmail(app.email));
+    if (!custId) return fail('cannot_resolve_customer');
+    app.shopify_customer_id = custId;
+    app.assigned_rep = { id: rep.id, name: rep.name, email: rep.email };
+    const progress = app.approval_progress || { customer_id: custId, emails: {} };
+    if (customerKey(progress.customer_id) !== custId) return fail('approval_customer_changed');
+    app.approval_progress = progress;
+    progress.emails = progress.emails || {};
+    checkpointApproval(app);
+    steps.push({ step: 'assign_rep', rep: app.assigned_rep }, { step: 'resolve_customer', customer: custId, via: resolvedBy });
 
-  // (a) assign to the DW House Account (fixed — not round-robin)
-  const rep = reps.assignRep();
-  steps.push({ step: 'assign_rep', rep: { id: rep.id, name: rep.name, email: rep.email } });
-
-  // Resolve the Shopify customer id. Public-form applications carry no id, so look it
-  // up by email (read_customers scope). NEVER write against a placeholder id — if we
-  // can't resolve a real customer, HARD-FAIL the approve with a clear admin message so
-  // the customer is never left silently untagged (which would still charge them for
-  // samples). (Closes the original memo §5 "customer-id-by-email" TODO + Cody #4.)
-  let custId = app.shopify_customer_id;
-  let resolvedBy = custId ? 'application' : null;
-  if (!custId) {
-    custId = await shopify.findCustomerByEmail(app.email);
-    if (custId) resolvedBy = 'email_lookup';
-  }
-  if (!custId) {
-    return {
-      ok: false,
-      error: 'cannot_resolve_customer',
-      message: `No Shopify customer found for ${app.email}. The applicant must have a store account (signed up) before trade approval. Not writing anything.`,
-      email: app.email,
-    };
-  }
-  steps.push({ step: 'resolve_customer', customer: custId, via: resolvedBy });
-
-  // (b) `trade_approved` is the authoritative unlimited-samples entitlement.
-  // Preserve the legacy `trade` tag during cutover so existing Regios pricing and
-  // unrelated trade workflows cannot regress before their own migration.
-  const tagRes = await shopify.addTags(custId, ['trade', 'trade_approved']);
-  steps.push({ step: 'tag_trade_approved', customer: custId, result: summarizeShopify(tagRes) });
-
-  // (c) metafield custom.assigned_rep
-  const mfRes = await shopify.setCustomerMetafield(custId, {
-    namespace: 'custom', key: 'assigned_rep', value: `${rep.name} <${rep.email}>`, type: 'single_line_text_field',
-  });
-  steps.push({ step: 'set_metafield', customer: custId, result: summarizeShopify(mfRes) });
-
-  // (d) notify rep
-  const repTpl = email.repNotifyEmail({ repName: rep.name, applicant: app });
-  const repMail = await email.sendEmail({ to: rep.email, subject: repTpl.subject, html: repTpl.html, source: 'trade-rep-notify' });
-  steps.push({ step: 'email_rep', to: rep.email, subject: repTpl.subject, dryRun: repMail.dryRun || false });
-
-  // (e) email applicant approved
-  const appTpl = email.tradeApprovedEmail({ applicant: app, repName: rep.name });
-  const appMail = await email.sendEmail({ to: app.email, subject: appTpl.subject, html: appTpl.html, source: 'trade-approved' });
-  steps.push({ step: 'email_applicant', to: app.email, subject: appTpl.subject, dryRun: appMail.dryRun || false });
-
-  app.status = 'approved';
-  app.decision = 'approved';
-  app.decided_at = new Date().toISOString();
-  app.shopify_customer_id = custId; // persist the resolved id for audit
-  app.assigned_rep = { id: rep.id, name: rep.name, email: rep.email };
-  rewriteAll(rows);
+    phase = 'tag_trade_approved';
+    const tagRes = await shopify.addTags(custId, ['trade', 'trade_approved']);
+    steps.push({ step: phase, customer: custId, result: summarizeShopify(tagRes) });
+    const tagError = shopifyError(tagRes);
+    if (tagError) return fail(tagError);
+    const tagged = tagRes.json.customer || tagRes.json.data?.tagsAdd?.node;
+    if (!tagged || customerKey(tagged.id) !== custId) return fail('invalid_tag_response');
+
+    phase = 'set_metafield';
+    const assignedValue = `${rep.name} <${rep.email}>`;
+    const mfRes = await shopify.setCustomerMetafield(custId, {
+      namespace: 'custom', key: 'assigned_rep', value: assignedValue, type: 'single_line_text_field',
+    });
+    steps.push({ step: phase, customer: custId, result: summarizeShopify(mfRes) });
+    const mfError = shopifyError(mfRes);
+    if (mfError) return fail(mfError);
+    const mf = mfRes.json.metafield || mfRes.json.data?.metafieldsSet?.metafields?.find(m => m.namespace === 'custom' && m.key === 'assigned_rep');
+    if (!mf || !mf.id || mf.namespace !== 'custom' || mf.key !== 'assigned_rep' || mf.value !== assignedValue ||
+      (mf.owner_id != null && customerKey(mf.owner_id) !== custId)) return fail('invalid_metafield_response');
+
+    phase = 'verify_entitlement';
+    const readback = await shopify.getCustomer(custId);
+    steps.push({ step: phase, customer: custId, result: summarizeShopify(readback) });
+    const readError = shopifyError(readback);
+    if (readError) return fail(readError);
+    const customer = readback.json.customer || readback.json.data?.customer;
+    if (!customer || customerKey(customer.id) !== custId || !tagsIncludeTrade(customer.tags)) return fail('entitlement_not_verified');
+    progress.entitlement_verified_at = new Date().toISOString();
+    checkpointApproval(app);
 
-  return { ok: true, id, status: 'approved', rep: app.assigned_rep, steps };
+    for (const target of ['rep', 'applicant']) {
+      phase = `email_${target}`;
+      const receipt = progress.emails[target];
+      if (receipt?.state === 'sent') {
+        steps.push({ step: phase, skipped: true, reason: 'already_sent', sent_at: receipt.sent_at });
+        continue;
+      }
+      if (receipt?.state === 'sending' || receipt?.state === 'unknown') return fail('email_delivery_unknown');
+      const tpl = target === 'rep' ? email.repNotifyEmail({ repName: rep.name, applicant: app }) : email.tradeApprovedEmail({ applicant: app, repName: rep.name });
+      const to = target === 'rep' ? rep.email : app.email;
+      // Persist intent BEFORE send. A crash or transport exception cannot silently
+      // lead to duplicate delivery on the next attempt.
+      progress.emails[target] = { state: 'sending', attempted_at: new Date().toISOString() };
+      checkpointApproval(app);
+      const mail = await email.sendEmail({ to, subject: tpl.subject, html: tpl.html, source: target === 'rep' ? 'trade-rep-notify' : 'trade-approved' });
+      if (!mail || mail.ok !== true || mail.dryRun || mail.noToken || !Number.isFinite(mail.status) || mail.status < 200 || mail.status >= 300) {
+        const knownFailure = mail?.ok === false && Number.isFinite(mail.status) && mail.status >= 400;
+        progress.emails[target].state = knownFailure ? 'failed' : 'unknown';
+        return fail(knownFailure ? 'email_failed' : 'email_delivery_unknown');
+      }
+      progress.emails[target] = { state: 'sent', sent_at: new Date().toISOString(), status: mail.status };
+      checkpointApproval(app);
+      steps.push({ step: phase, to, subject: tpl.subject, dryRun: false });
+    }
+    app.status = 'approved';
+    app.decision = 'approved';
+    app.decided_at = new Date().toISOString();
+    app.approval_error = null;
+    checkpointApproval(app);
+    return { ok: true, id, status: 'approved', rep: app.assigned_rep, steps };
+  } catch (_) {
+    // Do not leak transport errors (which can contain secrets or customer data).
+    try { return fail('approval_exception'); }
+    catch (_) { return { ok: false, id, error: 'approval_checkpoint_failed', failedStep: phase, steps }; }
+  } finally {
+    approvalsInFlight.delete(id);
+  }
 }
 
 async function reject(id) {
+  if (approvalsInFlight.has(id)) return { ok: false, error: 'approval_in_progress', id };
   const rows = readAll();
   const app = rows.find(a => a.id === id);
   if (!app) return { ok: false, error: 'not found' };
@@ -239,6 +313,7 @@ async function reject(id) {
 }
 
 function summarizeShopify(r) {
+  if (!r) return { ok: false };
   if (r.dryRun) return { WOULD: `${r.method} ${r.url}`, payload: r.body, noToken: r.noToken || false };
   return { status: r.status, ok: r.ok };
 }
diff --git a/scripts/selftest.js b/scripts/selftest.js
index c0dfbb8..fc59f56 100644
--- a/scripts/selftest.js
+++ b/scripts/selftest.js
@@ -141,20 +141,10 @@ async function main() {
   const pendingBefore = trade.listPending().length;
   const approve = await trade.approve(created.id);
   console.log('  approve result: ' + JSON.stringify(approve, null, 2));
-  if (approve.ok && approve.status === 'approved') ok('approved; assigned rep = ' + approve.rep.name + ' <' + approve.rep.email + '>');
-  else fail('approve failed');
-  const tagStep = approve.steps.find(s => s.step === 'tag_trade_approved');
-  if (tagStep && tagStep.result && tagStep.result.WOULD) ok('WOULD preserve `trade` and add authoritative `trade_approved`: ' + tagStep.result.WOULD);
-  else fail('no trade-tag step');
-  const mfStep = approve.steps.find(s => s.step === 'set_metafield');
-  if (mfStep && mfStep.result && mfStep.result.WOULD) ok('WOULD set custom.assigned_rep metafield: ' + mfStep.result.WOULD);
-  else fail('no metafield step');
-  const repMailStep = approve.steps.find(s => s.step === 'email_rep');
-  if (repMailStep && repMailStep.dryRun) ok('WOULD email rep ' + repMailStep.to + ' (dry-run)'); else fail('no rep email');
-  const appMailStep = approve.steps.find(s => s.step === 'email_applicant');
-  if (appMailStep && appMailStep.dryRun) ok('WOULD email applicant ' + appMailStep.to + ' (dry-run)'); else fail('no applicant email');
-  if (trade.listPending().length === pendingBefore - 1) ok('application moved out of pending queue');
-  else fail('application still pending after approve');
+  if (!approve.ok && approve.simulated && approve.dryRun && approve.status === 'pending') ok('approval explicitly simulated; no entitlement or email claimed');
+  else fail('dry-run approval must report a pending simulation: ' + JSON.stringify(approve));
+  if (approve.steps.length === 0 && trade.get(created.id).status === 'pending' && trade.listPending().length === pendingBefore) ok('dry run leaves application pending and performs no approval side effects');
+  else fail('dry run unexpectedly advanced approval');
 
   // ---------------------------------------------------------------------------
   // TK-11185 — server-side find-or-create at apply-time so every public application is
@@ -212,15 +202,14 @@ async function main() {
   if (s3persisted && s3.app.shopify_customer_id === null && s3.app.link_status === 'unlinked' && s3.app.link_error === 'user_errors') ok('create-failure: application STILL persisted (unlinked, link_error recorded) — applicant sees success, no black hole');
   else fail('graceful degrade failed: ' + JSON.stringify(s3));
 
-  // Scenario 4 — a linked new app can now be APPROVED (no cannot_resolve_customer).
+  // Scenario 4 — a linked app remains pending under honest approval simulation.
   shopify.findOrCreateCustomer = async () => ({ ok: true, id: '800000009', created: true, via: 'created' });
   const s4 = await trade.applyAndLink({ email: 'Approve@Me.com', business_name: 'Approve Me Co' });
   shopify.getCustomer = async () => ({ ok: true, json: { customer: { id: 800000009, tags: 'trade' } } }); // addTags merge read
   const s4ap = await trade.approve(s4.app.id);
   shopify.getCustomer = _gc; // restore
-  const s4resolve = s4ap.steps && s4ap.steps.find(s => s.step === 'resolve_customer');
-  if (s4ap.ok && s4ap.status === 'approved' && s4ap.error !== 'cannot_resolve_customer' && s4resolve && s4resolve.via === 'application') ok('approve() resolves via the STAMPED id (via=application) — cannot_resolve_customer can never fire for a linked app');
-  else fail('approve did not resolve via stamped id: ' + JSON.stringify(s4ap));
+  if (!s4ap.ok && s4ap.simulated && s4ap.status === 'pending' && trade.get(s4.app.id).shopify_customer_id === '800000009') ok('linked customer ID is preserved while dry-run approval remains pending');
+  else fail('linked application simulation failed: ' + JSON.stringify(s4ap));
 
   shopify.findOrCreateCustomer = _foc; // restore
 
diff --git a/scripts/trade-approval-test.js b/scripts/trade-approval-test.js
new file mode 100644
index 0000000..3cfd295
--- /dev/null
+++ b/scripts/trade-approval-test.js
@@ -0,0 +1,202 @@
+#!/usr/bin/env node
+'use strict';
+// Real trade + Shopify modules, actual retained temp JSONL store, deterministic
+// transport adapter. Real config is NEVER imported; no network module is exposed.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const os = require('node:os');
+const vm = require('node:vm');
+const crypto = require('node:crypto');
+const repo = path.resolve(__dirname, '..');
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tk11285-flow-'));
+const checks = [];
+function fixture(name, fault = null, settings = {}) {
+  const dir = path.join(root, name);
+  fs.mkdirSync(path.join(dir, 'lib'), { recursive: true });
+  const calls = [], mails = [], state = { fault, customer: { id: 123, tags: 'existing', email: 'fixture@example.invalid' } };
+  const config = { DRY_RUN: false, SHOP_DOMAIN: 'fixture.invalid', SHOPIFY_API_VERSION: 'fixture', SHOPIFY_FULFILLMENT_TOKEN: 'fixture-token', ...settings };
+  const response = (json, status = 200) => ({ ok: status >= 200 && status < 300, status, json: async () => structuredClone(json), headers: { get: () => null } });
+  async function fetchAdapter(url, options) {
+    assert.equal(new URL(url).hostname, 'fixture.invalid');
+    const method = options.method;
+    calls.push(method);
+    const currentFault = state.fault;
+    if (currentFault === 'throw' && method === 'PUT') throw Error('fixture transport failed');
+    if ((currentFault === 'http500' && method === 'PUT') || (currentFault === 'mf500' && method === 'POST') || (currentFault === 'read500' && method === 'GET')) return response({}, 500);
+    if (method === 'PUT') {
+      if (currentFault === 'graphql_top') return response({ errors: [{ message: 'Denied' }] });
+      if (currentFault === 'graphql_user') return response({ data: { tagsAdd: { node: { id: 'gid://shopify/Customer/123' }, userErrors: [{ message: 'Denied' }] } } });
+      if (currentFault === 'malformed') return response(null);
+      if (currentFault === 'empty') return response({});
+      state.customer.tags = JSON.parse(options.body).customer.tags;
+      return response({ customer: state.customer });
+    }
+    if (method === 'POST') {
+      if (currentFault === 'mf_user') return response({ data: { metafieldsSet: { userErrors: [{ message: 'Denied' }] } } });
+      return response({ metafield: { id: 789, owner_id: 123, ...JSON.parse(options.body).metafield } }, 201);
+    }
+    const customer = { ...state.customer };
+    // These faults affect the final readback only, leaving tag-merge intact.
+    if (calls.includes('POST') && currentFault === 'missing_tag') customer.tags = 'trade';
+    if (calls.includes('POST') && currentFault === 'wrong_id') customer.id = 999;
+    if (calls.includes('POST') && currentFault === 'missing_customer') return response({ customer: null });
+    return response({ customer });
+  }
+  const email = {
+    repNotifyEmail: () => ({ subject: 'Fixture rep', html: 'Fixture' }),
+    tradeApprovedEmail: () => ({ subject: 'Fixture approval', html: 'Fixture' }),
+    async sendEmail(payload) {
+      mails.push(payload.source);
+      if (state.fault === 'mail503' && payload.source === 'trade-approved') return { ok: false, status: 503 };
+      if (state.fault === 'mail_throw' && payload.source === 'trade-approved') throw Error('ambiguous delivery');
+      return { ok: true, status: 200 };
+    },
+  };
+  const reps = { assignRep: () => ({ id: 'fixture-rep', name: 'Fixture Rep', email: 'rep@example.invalid' }) };
+  const compile = (name, deps) => {
+    const module = { exports: {} };
+    vm.runInNewContext(fs.readFileSync(path.join(repo, 'lib', name + '.js'), 'utf8'), {
+      module, exports: module.exports, __dirname: path.join(dir, 'lib'), Buffer,
+      console: { log() {} }, setTimeout, clearTimeout, fetch: fetchAdapter,
+      require(specifier) { if (!(specifier in deps)) throw Error('Blocked dependency: ' + specifier); return deps[specifier]; },
+    }, { filename: name + '.js' });
+    return module.exports;
+  };
+  const shopify = compile('shopify', { './config': config });
+  const storeFs = { ...fs, writeFileSync(file, data, ...options) {
+    if (state.fault === 'final_checkpoint' && String(data).includes('"status":"approved"')) {
+      state.fault = null;
+      throw Error('fixture one-shot final persistence failure');
+    }
+    return fs.writeFileSync(file, data, ...options);
+  } };
+  function loadTrade() { return compile('trade', { fs: storeFs, path, crypto, './config': config, './reps': reps, './email': email, './shopify': shopify }); }
+  let trade = loadTrade();
+  const app = trade.apply({ email: state.customer.email, business_name: 'Fixture', shopify_customer_id: 123 });
+  return { state, mails, calls, shopify, config, app, trade, reload: loadTrade, persisted: () => JSON.parse(fs.readFileSync(trade.APPS_PATH, 'utf8').trim()), store: trade.APPS_PATH };
+}
+async function check(name, fn) {
+  try { const detail = await fn(); checks.push({ name, verdict: 'PASS', ...detail }); }
+  catch (error) { checks.push({ name, verdict: 'FAIL', error: error.stack }); process.exitCode = 1; }
+}
+(async () => {
+  if (process.argv.includes('--baseline')) {
+    await check('original HTTP500 falsely approves and sends both emails', async () => {
+      const f = fixture('baseline', 'http500');
+      const result = await f.trade.approve(f.app.id);
+      assert.equal(result.ok, true);
+      assert.equal(f.persisted().status, 'approved');
+      assert.equal(f.mails.length, 2);
+      return { result, persisted: f.persisted(), calls: f.calls, emails: f.mails, store: f.store };
+    });
+  } else {
+    for (const fault of ['http500', 'mf500', 'read500', 'graphql_top', 'graphql_user', 'mf_user', 'malformed', 'empty', 'missing_tag', 'wrong_id', 'missing_customer', 'throw']) {
+      await check(fault + ' fails closed', async () => {
+        const f = fixture(fault, fault), result = await f.trade.approve(f.app.id);
+        assert.equal(result.ok, false);
+        assert.equal(f.persisted().status, 'pending');
+        assert.equal(f.mails.length, 0);
+        assert.equal(f.reload().get(f.app.id).status, 'pending');
+        return { result, calls: f.calls, store: f.store };
+      });
+    }
+    await check('verified entitlement then persisted approval and reload', async () => {
+      const f = fixture('happy'), result = await f.trade.approve(f.app.id);
+      assert.equal(result.ok, true);
+      assert.equal(f.persisted().status, 'approved');
+      assert.equal(f.reload().get(f.app.id).status, 'approved');
+      assert.match(f.state.customer.tags, /existing/);
+      assert.match(f.state.customer.tags, /trade_approved/);
+      assert.equal(f.mails.length, 2);
+      assert.ok(f.persisted().approval_progress.entitlement_verified_at);
+      await f.reload().approve(f.app.id);
+      assert.equal(f.mails.length, 2);
+      return { result, persisted: f.persisted(), calls: f.calls, emails: f.mails, store: f.store };
+    });
+    await check('failed mutation can retry after reload', async () => {
+      const f = fixture('retry', 'mf500');
+      assert.equal((await f.trade.approve(f.app.id)).ok, false);
+      assert.equal(f.mails.length, 0);
+      f.state.fault = null;
+      assert.equal((await f.reload().approve(f.app.id)).ok, true);
+      assert.equal(f.mails.length, 2);
+      return { persisted: f.persisted(), store: f.store };
+    });
+    await check('email partial success survives restart without duplicate rep send', async () => {
+      const f = fixture('mail-retry', 'mail503');
+      assert.equal((await f.trade.approve(f.app.id)).ok, false);
+      assert.equal(f.persisted().status, 'pending');
+      assert.equal(f.persisted().approval_progress.emails.rep.state, 'sent');
+      f.state.fault = null;
+      assert.equal((await f.reload().approve(f.app.id)).ok, true);
+      assert.equal(f.mails.filter(x => x === 'trade-rep-notify').length, 1);
+      assert.equal(f.mails.filter(x => x === 'trade-approved').length, 2);
+      return { persisted: f.persisted(), emails: f.mails, store: f.store };
+    });
+    await check('ambiguous email delivery blocks resend across restart', async () => {
+      const f = fixture('ambiguous-mail', 'mail_throw');
+      assert.equal((await f.trade.approve(f.app.id)).ok, false);
+      f.state.fault = null;
+      const result = await f.reload().approve(f.app.id);
+      assert.equal(result.ok, false);
+      assert.match(result.error, /delivery_unknown/);
+      assert.equal(f.mails.length, 2);
+      return { result, persisted: f.persisted(), store: f.store };
+    });
+    await check('final checkpoint failure stays pending and retry skips completed emails', async () => {
+      const f = fixture('checkpoint-failure', 'final_checkpoint');
+      const first = await f.trade.approve(f.app.id);
+      assert.equal(first.ok, false);
+      assert.equal(f.persisted().status, 'pending');
+      assert.equal(f.persisted().decision, null);
+      assert.equal(f.mails.length, 2);
+      assert.equal((await f.reload().approve(f.app.id)).ok, true);
+      assert.equal(f.mails.length, 2);
+      return { first, persisted: f.persisted(), store: f.store };
+    });
+    await check('same-process concurrent duplicate does not duplicate emails', async () => {
+      const f = fixture('concurrent');
+      const results = await Promise.all([f.trade.approve(f.app.id), f.trade.approve(f.app.id)]);
+      assert.equal(results.filter(r => r.ok).length, 1);
+      assert.equal(f.mails.length, 2);
+      return { results, store: f.store };
+    });
+    await check('intake during approval await survives checkpoint rewrites', async () => {
+      const f = fixture('intake-race');
+      const original = f.shopify.addTags;
+      let newer;
+      f.shopify.addTags = async (...args) => {
+        newer = f.trade.apply({ email: 'newer@example.invalid', business_name: 'Newer fixture' });
+        return original(...args);
+      };
+      assert.equal((await f.trade.approve(f.app.id)).ok, true);
+      assert.equal(f.reload().get(newer.id).status, 'pending');
+      assert.equal(f.reload().readAll().length, 2);
+      return { store: f.store, preservedApplication: newer.id };
+    });
+    await check('dry run reports simulated pending without external side effects', async () => {
+      const f = fixture('dryrun', null, { DRY_RUN: true });
+      const result = await f.trade.approve(f.app.id);
+      assert.equal(result.ok, false); // Existing one-click caller must not say approved.
+      assert.equal(result.simulated, true);
+      assert.equal(result.dryRun, true);
+      assert.equal(f.persisted().status, 'pending');
+      assert.equal(f.mails.length, 0);
+      assert.equal(f.calls.length, 0);
+      return { result, store: f.store };
+    });
+    await check('live missing token cannot be treated as approval', async () => {
+      const f = fixture('no-token', null, { SHOPIFY_FULFILLMENT_TOKEN: '' });
+      assert.equal((await f.trade.approve(f.app.id)).ok, false);
+      assert.equal(f.persisted().status, 'pending');
+      assert.equal(f.mails.length, 0);
+      assert.equal(f.calls.length, 0);
+      return { store: f.store };
+    });
+  }
+  const report = { timestamp: new Date().toISOString(), repo, environment: 'Real trade and Shopify modules in VM; real retained temp JSONL; config entirely stubbed; fetch replaced; require allowlisted; no external network', retainedRoot: root, sourceSha256: crypto.createHash('sha256').update(fs.readFileSync(path.join(repo, 'lib/trade.js'))).digest('hex'), checks };
+  const output = process.env.TRADE_TEST_REPORT || path.join(root, 'report.json');
+  fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
+  console.log(JSON.stringify({ report: output, checks: checks.map(({ name, verdict, error }) => ({ name, verdict, error })) }, null, 2));
+})();
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index de9ef1e..aad2b1a 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,89 +1,1191 @@
 {
-  "intent": "Full production signup journey for retail/returning and trade applicants",
-  "riskTier": "R4",
-  "environment": "production canary",
-  "buildIdentity": {
-    "signupServiceCommit": "aa1c2a5",
-    "shopifyFunctionTestCommit": "ebdcac3d",
-    "liveSourceParity": true
+  "task_id": "TK11285-cycle0645-owner",
+  "ticket": "TK-11285",
+  "correlation_id": "M-02033",
+  "intent": "An approval is recorded only after successful Shopify writes, readback of trade_approved on the intended customer, and acknowledged emails; retries preserve completed email receipts.",
+  "risk_tier": "R3 local external-integration simulation",
+  "timestamp": "2026-09-05T06:57:09.440567+00:00",
+  "environment": "Real trade and Shopify modules in VM; real retained temp JSONL; config entirely stubbed; fetch replaced; require allowlisted; no external network",
+  "build": {
+    "base_commit": "8a877d4ed47ea559582c0944a702e60a8a6d927a",
+    "branch": "fix/tk11285-approval-verification",
+    "source_sha256": {
+      "lib/trade.js": "e0a1278b4b065380b2457efa3387a10670cfb1b98f60c3a1c01f578498aa0e46",
+      "lib/shopify.js": "4cc0226a689e899a70bd5c1e133aab2806afcc8cebf023ec1c277a51cd03b6af",
+      "scripts/selftest.js": "6e5a18941d55d3851a0f988af51f7968c25621636fd535c7d6ed861ac860fe8e",
+      "scripts/trade-approval-test.js": "e3033478fc376ec64c39c3f6aa9ec26f66142e3c178f113d84ecd03f0dd785bf"
+    }
+  },
+  "preconditions": {
+    "main_checkout": "/Users/macstudio3/Projects/dw-signup-fulfillment",
+    "isolated_worktree": "/private/tmp/tk11285-owner.URtJU0/worktree",
+    "main_clean_at_start": true,
+    "cost_mode": "ZERO_COST_REQUIRED",
+    "DTD_ZERO_COST": "1",
+    "real_config_loaded": false,
+    "real_external_io": false
+  },
+  "baseline": {
+    "command": "node scripts/trade-approval-test.js --baseline",
+    "verdict": "PASS",
+    "meaning": "Reproduced original defect before editing production modules: HTTP500 returned/persisted approved and sent both fixture emails.",
+    "report": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-9oeZ25/report.json",
+    "checks": [
+      {
+        "name": "original HTTP500 falsely approves and sends both emails",
+        "verdict": "PASS",
+        "result": {
+          "ok": true,
+          "id": "TRADE-20260905-a05d4f",
+          "status": "approved",
+          "rep": {
+            "id": "fixture-rep",
+            "name": "Fixture Rep",
+            "email": "rep@example.invalid"
+          },
+          "steps": [
+            {
+              "step": "assign_rep",
+              "rep": {
+                "id": "fixture-rep",
+                "name": "Fixture Rep",
+                "email": "rep@example.invalid"
+              }
+            },
+            {
+              "step": "resolve_customer",
+              "customer": 123,
+              "via": "application"
+            },
+            {
+              "step": "tag_trade_approved",
+              "customer": 123,
+              "result": {
+                "status": 500,
+                "ok": false
+              }
+            },
+            {
+              "step": "set_metafield",
+              "customer": 123,
+              "result": {
+                "status": 201,
+                "ok": true
+              }
+            },
+            {
+              "step": "email_rep",
+              "to": "rep@example.invalid",
+              "subject": "Fixture rep",
+              "dryRun": false
+            },
+            {
+              "step": "email_applicant",
+              "to": "fixture@example.invalid",
+              "subject": "Fixture approval",
+              "dryRun": false
+            }
+          ]
+        },
+        "persisted": {
+          "id": "TRADE-20260905-a05d4f",
+          "email": "fixture@example.invalid",
+          "business_name": "Fixture",
+          "resale_cert": "",
+          "phone": "",
+          "extra": {
+            "shopify_customer_id": 123
+          },
+          "shopify_customer_id": 123,
+          "status": "approved",
+          "created_at": "2026-09-05T06:53:21.547Z",
+          "decided_at": "2026-09-05T06:53:21.551Z",
+          "decision": "approved",
+          "assigned_rep": {
+            "id": "fixture-rep",
+            "name": "Fixture Rep",
+            "email": "rep@example.invalid"
+          }
+        },
+        "calls": [
+          "GET",
+          "PUT",
+          "POST"
+        ],
+        "emails": [
+          "trade-rep-notify",
+          "trade-approved"
+        ],
+        "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-9oeZ25/baseline/data/trade-applications.jsonl"
+      }
+    ]
+  },
+  "commands": [
+    {
+      "command": "node scripts/trade-approval-test.js",
+      "verdict": "PASS",
+      "report": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/report.json",
+      "passed": 21
+    },
+    {
+      "command": "node --check lib/trade.js; node --check lib/shopify.js; node --check scripts/selftest.js",
+      "verdict": "PASS"
+    },
+    {
+      "command": "git diff --check",
+      "verdict": "PASS"
+    }
+  ],
+  "boundaries": {
+    "entry": "Actual trade.apply() persists a pending application in retained temp JSONL; actual approve() executes with real Shopify module under a transport adapter.",
+    "api": "Fetch adapter handles actual Shopify HTTP method, URL, JSON envelope and readback; returns injected transport/API/schema failures.",
+    "data": "Actual fs writes and newly loaded trade module read back status/progress; one-shot persistence failure tested.",
+    "side_effects": "Injected email service receipts; failed entitlement sends none; completed rep receipt survives reload without duplicate send.",
+    "retry": "Mutation failure/retry; email503 retry; ambiguous delivery is held; concurrent same-process calls; intake during await; final checkpoint failure/retry.",
+    "consumer_contract": "Approval errors return ok:false/error and pending; existing GET/POST consumers therefore do not report approved for errors or dry-run."
   },
-  "timestamp": "2026-08-31T08:42:00.304Z",
   "checks": [
     {
-      "name": "storefront entry",
+      "name": "http500 fails closed",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-fa5896",
+        "status": "pending",
+        "error": "shopify_http_error",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 500,
+              "ok": false
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/http500/data/trade-applications.jsonl"
+    },
+    {
+      "name": "mf500 fails closed",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-dbdf8e",
+        "status": "pending",
+        "error": "shopify_http_error",
+        "failedStep": "set_metafield",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 500,
+              "ok": false
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT",
+        "POST"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/mf500/data/trade-applications.jsonl"
+    },
+    {
+      "name": "read500 fails closed",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-3b532b",
+        "status": "pending",
+        "error": "shopify_http_error",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 500,
+              "ok": false
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/read500/data/trade-applications.jsonl"
+    },
+    {
+      "name": "graphql_top fails closed",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-ddddaf",
+        "status": "pending",
+        "error": "shopify_response_error",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/graphql_top/data/trade-applications.jsonl"
+    },
+    {
+      "name": "graphql_user fails closed",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-dd9b08",
+        "status": "pending",
+        "error": "shopify_response_error",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/graphql_user/data/trade-applications.jsonl"
+    },
+    {
+      "name": "mf_user fails closed",
       "verdict": "PASS",
-      "detail": "Trade form, samples banner, and daily DIG room rendered"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-ac124e",
+        "status": "pending",
+        "error": "shopify_response_error",
+        "failedStep": "set_metafield",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT",
+        "POST"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/mf_user/data/trade-applications.jsonl"
     },
     {
-      "name": "returning client",
+      "name": "malformed fails closed",
       "verdict": "PASS",
-      "detail": "Branded modal opens and hands off to Shopify customer authentication with return URL"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-7931ac",
+        "status": "pending",
+        "error": "shopify_response_error",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/malformed/data/trade-applications.jsonl"
     },
     {
-      "name": "client validation",
+      "name": "empty fails closed",
       "verdict": "PASS",
-      "detail": "Empty required form is blocked before any network submission"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-d43a2a",
+        "status": "pending",
+        "error": "invalid_tag_response",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/empty/data/trade-applications.jsonl"
     },
     {
-      "name": "SMS declined",
+      "name": "missing_tag fails closed",
       "verdict": "PASS",
-      "detail": "Application succeeds with optional SMS box unchecked and records disclosure evidence"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-6be3c9",
+        "status": "pending",
+        "error": "entitlement_not_verified",
+        "failedStep": "verify_entitlement",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 201,
+              "ok": true
+            }
+          },
+          {
+            "step": "verify_entitlement",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT",
+        "POST",
+        "GET"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/missing_tag/data/trade-applications.jsonl"
     },
     {
-      "name": "SMS accepted",
+      "name": "wrong_id fails closed",
       "verdict": "PASS",
-      "detail": "Explicit unchecked-by-default opt-in records affirmative consent, STOP/HELP, and DNC notice"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-5107dd",
+        "status": "pending",
+        "error": "entitlement_not_verified",
+        "failedStep": "verify_entitlement",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 201,
+              "ok": true
+            }
+          },
+          {
+            "step": "verify_entitlement",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT",
+        "POST",
+        "GET"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/wrong_id/data/trade-applications.jsonl"
     },
     {
-      "name": "API negative path",
+      "name": "missing_customer fails closed",
       "verdict": "PASS",
-      "detail": "Missing email returns HTTP 400"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-09a84a",
+        "status": "pending",
+        "error": "entitlement_not_verified",
+        "failedStep": "verify_entitlement",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 201,
+              "ok": true
+            }
+          },
+          {
+            "step": "verify_entitlement",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT",
+        "POST",
+        "GET"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/missing_customer/data/trade-applications.jsonl"
     },
     {
-      "name": "production application",
+      "name": "throw fails closed",
       "verdict": "PASS",
-      "detail": "Reused previously submitted live canary TRADE-20260831-4f173e; no duplicate message sent"
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-704eed",
+        "status": "pending",
+        "error": "approval_exception",
+        "failedStep": "tag_trade_approved",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          }
+        ]
+      },
+      "calls": [
+        "GET",
+        "PUT"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/throw/data/trade-applications.jsonl"
     },
     {
-      "name": "review queue boundary",
+      "name": "verified entitlement then persisted approval and reload",
       "verdict": "PASS",
-      "detail": "Production admin credential correctly rejected locally; independent SSH verifier found the same pending ID, marker, and SMS evidence in the live ledger"
+      "result": {
+        "ok": true,
+        "id": "TRADE-20260905-abc417",
+        "status": "approved",
+        "rep": {
+          "id": "fixture-rep",
+          "name": "Fixture Rep",
+          "email": "rep@example.invalid"
+        },
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 201,
+              "ok": true
+            }
+          },
+          {
+            "step": "verify_entitlement",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "email_rep",
+            "to": "rep@example.invalid",
+            "subject": "Fixture rep",
+            "dryRun": false
+          },
+          {
+            "step": "email_applicant",
+            "to": "fixture@example.invalid",
+            "subject": "Fixture approval",
+            "dryRun": false
+          }
+        ]
+      },
+      "persisted": {
+        "id": "TRADE-20260905-abc417",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": "123",
+        "status": "approved",
+        "created_at": "2026-09-05T06:56:33.051Z",
+        "decided_at": "2026-09-05T06:56:33.054Z",
+        "decision": "approved",
+        "assigned_rep": {
+          "id": "fixture-rep",
+          "name": "Fixture Rep",
+          "email": "rep@example.invalid"
+        },
+        "approval_progress": {
+          "customer_id": "123",
+          "emails": {
+            "rep": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.053Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.054Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T06:56:33.053Z"
+        },
+        "approval_error": null
+      },
+      "calls": [
+        "GET",
+        "PUT",
+        "POST",
+        "GET"
+      ],
+      "emails": [
+        "trade-rep-notify",
+        "trade-approved"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/happy/data/trade-applications.jsonl"
     },
     {
-      "name": "approval security",
+      "name": "failed mutation can retry after reload",
       "verdict": "PASS",
-      "detail": "Review queue requires auth and forged approval token is rejected"
+      "persisted": {
+        "id": "TRADE-20260905-a87b67",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": "123",
+        "status": "approved",
+        "created_at": "2026-09-05T06:56:33.062Z",
+        "decided_at": "2026-09-05T06:56:33.067Z",
+        "decision": "approved",
+        "assigned_rep": {
+          "id": "fixture-rep",
+          "name": "Fixture Rep",
+          "email": "rep@example.invalid"
+        },
+        "approval_progress": {
+          "customer_id": "123",
+          "emails": {
+            "rep": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.066Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.067Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T06:56:33.066Z"
+        },
+        "approval_error": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/retry/data/trade-applications.jsonl"
+    },
+    {
+      "name": "email partial success survives restart without duplicate rep send",
+      "verdict": "PASS",
+      "persisted": {
+        "id": "TRADE-20260905-da8c89",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": "123",
+        "status": "approved",
+        "created_at": "2026-09-05T06:56:33.071Z",
+        "decided_at": "2026-09-05T06:56:33.077Z",
+        "decision": "approved",
+        "assigned_rep": {
+          "id": "fixture-rep",
+          "name": "Fixture Rep",
+          "email": "rep@example.invalid"
+        },
+        "approval_progress": {
+          "customer_id": "123",
+          "emails": {
+            "rep": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.072Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.076Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T06:56:33.075Z"
+        },
+        "approval_error": null
+      },
+      "emails": [
+        "trade-rep-notify",
+        "trade-approved",
+        "trade-approved"
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/mail-retry/data/trade-applications.jsonl"
+    },
+    {
+      "name": "ambiguous email delivery blocks resend across restart",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-d6afa1",
+        "status": "pending",
+        "error": "email_delivery_unknown",
+        "failedStep": "email_applicant",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 201,
+              "ok": true
+            }
+          },
+          {
+            "step": "verify_entitlement",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "email_rep",
+            "skipped": true,
+            "reason": "already_sent",
+            "sent_at": "2026-09-05T06:56:33.082Z"
+          }
+        ]
+      },
+      "persisted": {
+        "id": "TRADE-20260905-d6afa1",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": "123",
+        "status": "pending",
+        "created_at": "2026-09-05T06:56:33.080Z",
+        "decided_at": null,
+        "decision": null,
+        "assigned_rep": {
+          "id": "fixture-rep",
+          "name": "Fixture Rep",
+          "email": "rep@example.invalid"
+        },
+        "approval_progress": {
+          "customer_id": "123",
+          "emails": {
+            "rep": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.082Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sending",
+              "attempted_at": "2026-09-05T06:56:33.082Z"
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T06:56:33.085Z"
+        },
+        "approval_error": {
+          "error": "email_delivery_unknown",
+          "step": "email_applicant",
+          "at": "2026-09-05T06:56:33.085Z"
+        }
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/ambiguous-mail/data/trade-applications.jsonl"
+    },
+    {
+      "name": "final checkpoint failure stays pending and retry skips completed emails",
+      "verdict": "PASS",
+      "first": {
+        "ok": false,
+        "id": "TRADE-20260905-f0fd1b",
+        "status": "pending",
+        "error": "approval_exception",
+        "failedStep": "email_applicant",
+        "steps": [
+          {
+            "step": "assign_rep",
+            "rep": {
+              "id": "fixture-rep",
+              "name": "Fixture Rep",
+              "email": "rep@example.invalid"
+            }
+          },
+          {
+            "step": "resolve_customer",
+            "customer": "123",
+            "via": "application"
+          },
+          {
+            "step": "tag_trade_approved",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "set_metafield",
+            "customer": "123",
+            "result": {
+              "status": 201,
+              "ok": true
+            }
+          },
+          {
+            "step": "verify_entitlement",
+            "customer": "123",
+            "result": {
+              "status": 200,
+              "ok": true
+            }
+          },
+          {
+            "step": "email_rep",
+            "to": "rep@example.invalid",
+            "subject": "Fixture rep",
+            "dryRun": false
+          },
+          {
+            "step": "email_applicant",
+            "to": "fixture@example.invalid",
+            "subject": "Fixture approval",
+            "dryRun": false
+          }
+        ]
+      },
+      "persisted": {
+        "id": "TRADE-20260905-f0fd1b",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": "123",
+        "status": "approved",
+        "created_at": "2026-09-05T06:56:33.089Z",
+        "decided_at": "2026-09-05T06:56:33.094Z",
+        "decision": "approved",
+        "assigned_rep": {
+          "id": "fixture-rep",
+          "name": "Fixture Rep",
+          "email": "rep@example.invalid"
+        },
+        "approval_progress": {
+          "customer_id": "123",
+          "emails": {
+            "rep": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.091Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T06:56:33.092Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T06:56:33.094Z"
+        },
+        "approval_error": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/checkpoint-failure/data/trade-applications.jsonl"
+    },
+    {
+      "name": "same-process concurrent duplicate does not duplicate emails",
+      "verdict": "PASS",
+      "results": [
+        {
+          "ok": true,
+          "id": "TRADE-20260905-5454d0",
+          "status": "approved",
+          "rep": {
+            "id": "fixture-rep",
+            "name": "Fixture Rep",
+            "email": "rep@example.invalid"
+          },
+          "steps": [
+            {
+              "step": "assign_rep",
+              "rep": {
+                "id": "fixture-rep",
+                "name": "Fixture Rep",
+                "email": "rep@example.invalid"
+              }
+            },
+            {
+              "step": "resolve_customer",
+              "customer": "123",
+              "via": "application"
+            },
+            {
+              "step": "tag_trade_approved",
+              "customer": "123",
+              "result": {
+                "status": 200,
+                "ok": true
+              }
+            },
+            {
+              "step": "set_metafield",
+              "customer": "123",
+              "result": {
+                "status": 201,
+                "ok": true
+              }
+            },
+            {
+              "step": "verify_entitlement",
+              "customer": "123",
+              "result": {
+                "status": 200,
+                "ok": true
+              }
+            },
+            {
+              "step": "email_rep",
+              "to": "rep@example.invalid",
+              "subject": "Fixture rep",
+              "dryRun": false
+            },
+            {
+              "step": "email_applicant",
+              "to": "fixture@example.invalid",
+              "subject": "Fixture approval",
+              "dryRun": false
+            }
+          ]
+        },
+        {
+          "ok": false,
+          "error": "approval_in_progress",
+          "id": "TRADE-20260905-5454d0"
+        }
+      ],
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/concurrent/data/trade-applications.jsonl"
+    },
+    {
+      "name": "intake during approval await survives checkpoint rewrites",
+      "verdict": "PASS",
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/intake-race/data/trade-applications.jsonl",
+      "preservedApplication": "TRADE-20260905-5af8fd"
+    },
+    {
+      "name": "dry run reports simulated pending without external side effects",
+      "verdict": "PASS",
+      "result": {
+        "ok": false,
+        "id": "TRADE-20260905-9b01cc",
+        "status": "pending",
+        "dryRun": true,
+        "simulated": true,
+        "error": "dry_run_simulated",
+        "message": "Approval simulation only; no entitlement or email was changed.",
+        "steps": []
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/dryrun/data/trade-applications.jsonl"
+    },
+    {
+      "name": "live missing token cannot be treated as approval",
+      "verdict": "PASS",
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/no-token/data/trade-applications.jsonl"
     }
   ],
-  "artifacts": [
-    "/Users/macstudio3/Projects/dw-signup-fulfillment/verification/artifacts/signup-2026-08-31T08-42-00-304Z.png"
+  "limitations": [
+    "No live Shopify/customer/email operation or deployment performed; those require explicit deployment approval and a separate controlled live verification.",
+    "Single-process in-flight guard only; multiple processes sharing this JSONL store remain unsupported.",
+    "Crash or ambiguous email delivery leaves sending/unknown receipt for manual reconciliation; exactly-once delivery requires provider idempotency outside this increment.",
+    "Legacy full npm selftest was not executed because it imports real config and mutates runtime data; only approval assertions updated and syntax checked. Focused test has no real config or network capability.",
+    "The existing whole-file store is not redesigned; disk corruption/multi-process coordination are outside this scoped increment."
   ],
-  "productionValidation": {
-    "consecutivePasses": 5,
-    "timestamp": "2026-08-31T08:43:00-07:00",
-    "verdict": "PASS",
-    "assertionsPerPass": [
-      "live service health reports DRY_RUN=false",
-      "invalid orders-paid webhook HMAC returns 401",
-      "valid signed no-op webhook returns 200",
-      "orders-paid webhook subscription exists",
-      "DW Free Samples automatic Function discount is ACTIVE",
-      "discount Function ID matches deployed function",
-      "product-discount combination remains enabled",
-      "2018 legacy customer fixture remains intact",
-      "legacy retail fixture does not carry trade_approved",
-      "storefront exposes returning-account and retail/trade sample copy"
-    ],
-    "passes": [1, 2, 3, 4, 5]
-  },
-  "sourceParity": {
-    "verdict": "PASS",
-    "detail": "SHA-256 hashes match between local and live Kamatera for server.js, config, trade approval, sample ledger, OAuth helper, and production validator."
-  },
-  "retainedState": {
-    "applicationId": "TRADE-20260831-4f173e",
-    "status": "pending",
-    "reason": "Retained as a labeled production E2E canary; intentionally not approved or rejected to avoid Shopify customer mutation or another outbound email."
-  },
-  "verdict": "PASS"
+  "retained_test_state": [
+    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-9oeZ25",
+    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY"
+  ],
+  "rollback": "Local branch/worktree retained; main checkout untouched. No merge, service restart, external writes, or deployment.",
+  "verdict": "PASS for isolated local acceptance; parent independent API verification and Cody acceptance remain with accountable finalizer /root"
 }

← 8a877d4 Record independent verification of live designer signup chan  ·  back to Dw Signup Fulfillment  ·  Serialize trade rejection with approval and preserve concurr 116fc35 →