[object Object]

← back to Dw Signup Fulfillment

Serialize trade rejection with approval and preserve concurrent intake

116fc358f6bea4c8e36f36c53e880cf465349bd3 · 2026-09-05 00:04:46 -0700 · Steve Abrams

Files touched

Diff

commit 116fc358f6bea4c8e36f36c53e880cf465349bd3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Sep 5 00:04:46 2026 -0700

    Serialize trade rejection with approval and preserve concurrent intake
---
 lib/trade.js                   |  35 ++--
 scripts/trade-approval-test.js |  81 ++++++++-
 verification/e2e-proof.json    | 371 ++++++++++++++++++++++++++++++++---------
 3 files changed, 389 insertions(+), 98 deletions(-)

diff --git a/lib/trade.js b/lib/trade.js
index 6134e09..4afa8aa 100644
--- a/lib/trade.js
+++ b/lib/trade.js
@@ -155,9 +155,10 @@ function get(id) {
   return readAll().find(a => a.id === id) || null;
 }
 
-// Approval is serialized per application inside this service process. Completed
+// Approve and reject share one per-application lock inside this service process.
+// This does not coordinate multiple processes writing the same JSONL. Completed
 // email receipts are checkpointed; uncertain delivery needs manual reconciliation.
-const approvalsInFlight = new Set();
+const decisionsInFlight = new Set();
 function customerKey(value) {
   const match = String(value || '').match(/^(?:gid:\/\/shopify\/Customer\/)?([1-9]\d*)$/);
   return match ? match[1] : null;
@@ -189,7 +190,7 @@ function checkpointApproval(app) {
 }
 
 async function approve(id) {
-  if (approvalsInFlight.has(id)) return { ok: false, error: 'approval_in_progress', id };
+  if (decisionsInFlight.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}` };
@@ -199,7 +200,7 @@ async function approve(id) {
     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);
+  decisionsInFlight.add(id);
   const steps = [];
   let phase = 'resolve_customer';
   const fail = error => {
@@ -290,26 +291,30 @@ async function approve(id) {
     try { return fail('approval_exception'); }
     catch (_) { return { ok: false, id, error: 'approval_checkpoint_failed', failedStep: phase, steps }; }
   } finally {
-    approvalsInFlight.delete(id);
+    decisionsInFlight.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 (decisionsInFlight.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}` };
 
-  const tpl = email.tradeRejectedEmail();
-  const mail = await email.sendEmail({ to: app.email, subject: tpl.subject, html: tpl.html, source: 'trade-rejected' });
+  decisionsInFlight.add(id);
+  try {
+    const tpl = email.tradeRejectedEmail();
+    const mail = await email.sendEmail({ to: app.email, subject: tpl.subject, html: tpl.html, source: 'trade-rejected' });
 
-  app.status = 'rejected';
-  app.decision = 'rejected';
-  app.decided_at = new Date().toISOString();
-  rewriteAll(rows);
+    app.status = 'rejected';
+    app.decision = 'rejected';
+    app.decided_at = new Date().toISOString();
+    checkpointApproval(app);
 
-  return { ok: true, id, status: 'rejected', email: { to: app.email, subject: tpl.subject, dryRun: mail.dryRun || false } };
+    return { ok: true, id, status: 'rejected', email: { to: app.email, subject: tpl.subject, dryRun: mail.dryRun || false } };
+  } finally {
+    decisionsInFlight.delete(id);
+  }
 }
 
 function summarizeShopify(r) {
diff --git a/scripts/trade-approval-test.js b/scripts/trade-approval-test.js
index 3cfd295..0d5c703 100644
--- a/scripts/trade-approval-test.js
+++ b/scripts/trade-approval-test.js
@@ -46,6 +46,7 @@ function fixture(name, fault = null, settings = {}) {
   const email = {
     repNotifyEmail: () => ({ subject: 'Fixture rep', html: 'Fixture' }),
     tradeApprovedEmail: () => ({ subject: 'Fixture approval', html: 'Fixture' }),
+    tradeRejectedEmail: () => ({ subject: 'Fixture rejection', html: 'Fixture' }),
     async sendEmail(payload) {
       mails.push(payload.source);
       if (state.fault === 'mail503' && payload.source === 'trade-approved') return { ok: false, status: 503 };
@@ -74,7 +75,7 @@ function fixture(name, fault = null, settings = {}) {
   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 };
+  return { email, 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 }); }
@@ -175,6 +176,84 @@ async function check(name, fn) {
       assert.equal(f.reload().readAll().length, 2);
       return { store: f.store, preservedApplication: newer.id };
     });
+    function holdMail(f, source) {
+      let release, entered;
+      const ready = new Promise(resolve => { entered = resolve; });
+      const held = new Promise(resolve => { release = resolve; });
+      const original = f.email.sendEmail;
+      f.email.sendEmail = async payload => {
+        if (payload.source === source) { entered(); await held; }
+        return original(payload);
+      };
+      return { ready, release };
+    }
+    await check('reject first blocks approval until rejection completes', async () => {
+      const f = fixture('reject-first'), gate = holdMail(f, 'trade-rejected');
+      const rejecting = f.trade.reject(f.app.id);
+      await gate.ready;
+      const approval = await f.trade.approve(f.app.id);
+      assert.equal(approval.ok, false);
+      assert.equal(f.calls.length, 0);
+      gate.release();
+      assert.equal((await rejecting).ok, true);
+      assert.equal(f.persisted().status, 'rejected');
+      assert.deepEqual(f.mails, ['trade-rejected']);
+      return { approval, persisted: f.persisted(), store: f.store };
+    });
+    await check('approve first blocks rejection and preserves approval receipts', async () => {
+      const f = fixture('approve-first'), gate = holdMail(f, 'trade-rep-notify');
+      const approving = f.trade.approve(f.app.id);
+      await gate.ready;
+      const rejection = await f.trade.reject(f.app.id);
+      assert.equal(rejection.ok, false);
+      gate.release();
+      assert.equal((await approving).ok, true);
+      assert.equal(f.persisted().status, 'approved');
+      assert.equal(f.persisted().approval_progress.emails.applicant.state, 'sent');
+      assert.equal(f.mails.includes('trade-rejected'), false);
+      return { rejection, persisted: f.persisted(), store: f.store };
+    });
+    await check('concurrent rejection attempts send only one rejection', async () => {
+      const f = fixture('reject-reject'), gate = holdMail(f, 'trade-rejected');
+      const first = f.trade.reject(f.app.id);
+      await gate.ready;
+      const duplicate = await f.trade.reject(f.app.id);
+      assert.equal(duplicate.ok, false);
+      gate.release();
+      assert.equal((await first).ok, true);
+      assert.deepEqual(f.mails, ['trade-rejected']);
+      return { duplicate, persisted: f.persisted(), store: f.store };
+    });
+    await check('intake during rejection survives final persistence', async () => {
+      const f = fixture('reject-intake'), gate = holdMail(f, 'trade-rejected');
+      const rejecting = f.trade.reject(f.app.id);
+      await gate.ready;
+      const newer = f.trade.apply({ email: 'newer@example.invalid' });
+      gate.release();
+      assert.equal((await rejecting).ok, true);
+      assert.equal(f.reload().get(newer.id).status, 'pending');
+      assert.equal(f.reload().get(f.app.id).status, 'rejected');
+      assert.equal(f.reload().readAll().length, 2);
+      return { newerId: newer.id, store: f.store };
+    });
+    for (const failure of ['template', 'send']) {
+      await check('rejection ' + failure + ' exception releases shared decision lock', async () => {
+        const f = fixture('reject-fail-' + failure);
+        if (failure === 'template') f.email.tradeRejectedEmail = () => { throw Error('fixture rejection template failed'); };
+        else {
+          const original = f.email.sendEmail;
+          f.email.sendEmail = async payload => {
+            if (payload.source === 'trade-rejected') throw Error('fixture rejection send failed');
+            return original(payload);
+          };
+        }
+        await assert.rejects(f.trade.reject(f.app.id), /fixture rejection/);
+        assert.equal(f.persisted().status, 'pending');
+        assert.equal((await f.trade.approve(f.app.id)).ok, true);
+        assert.equal(f.persisted().status, 'approved');
+        return { persisted: f.persisted(), store: f.store };
+      });
+    }
     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);
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index aad2b1a..8ac18f5 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,20 +1,21 @@
 {
   "task_id": "TK11285-cycle0645-owner",
   "ticket": "TK-11285",
-  "correlation_id": "M-02033",
+  "correlation_id": "M02037",
   "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",
+  "timestamp": "2026-09-05T07:04:45.802600+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/trade.js": "4861f25f8a74472dd18578d0c4866f54482dc360c988f0fb8944ece18aa884fd",
       "lib/shopify.js": "4cc0226a689e899a70bd5c1e133aab2806afcc8cebf023ec1c277a51cd03b6af",
       "scripts/selftest.js": "6e5a18941d55d3851a0f988af51f7968c25621636fd535c7d6ed861ac860fe8e",
-      "scripts/trade-approval-test.js": "e3033478fc376ec64c39c3f6aa9ec26f66142e3c178f113d84ecd03f0dd785bf"
-    }
+      "scripts/trade-approval-test.js": "2637ff164cfcd5159a42a6d41e60e7c73c14d78011ace97b433d8d14988317a6"
+    },
+    "previous_increment": "d291ec9e62ff125cecb4a04420a00f6d1fcd9045"
   },
   "preconditions": {
     "main_checkout": "/Users/macstudio3/Projects/dw-signup-fulfillment",
@@ -23,7 +24,9 @@
     "cost_mode": "ZERO_COST_REQUIRED",
     "DTD_ZERO_COST": "1",
     "real_config_loaded": false,
-    "real_external_io": false
+    "real_external_io": false,
+    "resumed_child_DTD_ZERO_COST": null,
+    "resumed_guard_verification": "ZERO_COST_REQUIRED file independently verified; installed panel.sh and post-decision-codex.sh force DTD_ZERO_COST=1 from canonical file even when child variable absent. Parent clarified continuation; no environment/control change or provider call."
   },
   "baseline": {
     "command": "node scripts/trade-approval-test.js --baseline",
@@ -124,8 +127,8 @@
     {
       "command": "node scripts/trade-approval-test.js",
       "verdict": "PASS",
-      "report": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/report.json",
-      "passed": 21
+      "report": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/report.json",
+      "passed": 27
     },
     {
       "command": "node --check lib/trade.js; node --check lib/shopify.js; node --check scripts/selftest.js",
@@ -134,6 +137,11 @@
     {
       "command": "git diff --check",
       "verdict": "PASS"
+    },
+    {
+      "command": "node <root-evidence>/parent-approval-probe.js /private/tmp/tk11285-owner.URtJU0/worktree <root-evidence>/owner-race-after.json --race",
+      "verdict": "PASS",
+      "result": "Approval blocked during held rejection; rejection completes once; newer application survives."
     }
   ],
   "boundaries": {
@@ -142,7 +150,8 @@
     "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."
+    "consumer_contract": "Approval errors return ok:false/error and pending; existing GET/POST consumers therefore do not report approved for errors or dry-run.",
+    "shared_decision_lock": "Approve and reject acquire the same per-application lock synchronously before first await; finally releases for success and exceptions. Rejection checkpoints its row against freshly read data."
   },
   "checks": [
     {
@@ -150,7 +159,7 @@
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-fa5896",
+        "id": "TRADE-20260905-1e1812",
         "status": "pending",
         "error": "shopify_http_error",
         "failedStep": "tag_trade_approved",
@@ -182,14 +191,14 @@
         "GET",
         "PUT"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/http500/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/http500/data/trade-applications.jsonl"
     },
     {
       "name": "mf500 fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-dbdf8e",
+        "id": "TRADE-20260905-ec292b",
         "status": "pending",
         "error": "shopify_http_error",
         "failedStep": "set_metafield",
@@ -230,14 +239,14 @@
         "PUT",
         "POST"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/mf500/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/mf500/data/trade-applications.jsonl"
     },
     {
       "name": "read500 fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-3b532b",
+        "id": "TRADE-20260905-f8fb45",
         "status": "pending",
         "error": "shopify_http_error",
         "failedStep": "tag_trade_approved",
@@ -268,14 +277,14 @@
       "calls": [
         "GET"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/read500/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/read500/data/trade-applications.jsonl"
     },
     {
       "name": "graphql_top fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-ddddaf",
+        "id": "TRADE-20260905-489ed7",
         "status": "pending",
         "error": "shopify_response_error",
         "failedStep": "tag_trade_approved",
@@ -307,14 +316,14 @@
         "GET",
         "PUT"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/graphql_top/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/graphql_top/data/trade-applications.jsonl"
     },
     {
       "name": "graphql_user fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-dd9b08",
+        "id": "TRADE-20260905-f1ebd9",
         "status": "pending",
         "error": "shopify_response_error",
         "failedStep": "tag_trade_approved",
@@ -346,14 +355,14 @@
         "GET",
         "PUT"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/graphql_user/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/graphql_user/data/trade-applications.jsonl"
     },
     {
       "name": "mf_user fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-ac124e",
+        "id": "TRADE-20260905-ac0496",
         "status": "pending",
         "error": "shopify_response_error",
         "failedStep": "set_metafield",
@@ -394,14 +403,14 @@
         "PUT",
         "POST"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/mf_user/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/mf_user/data/trade-applications.jsonl"
     },
     {
       "name": "malformed fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-7931ac",
+        "id": "TRADE-20260905-dbec19",
         "status": "pending",
         "error": "shopify_response_error",
         "failedStep": "tag_trade_approved",
@@ -433,14 +442,14 @@
         "GET",
         "PUT"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/malformed/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/malformed/data/trade-applications.jsonl"
     },
     {
       "name": "empty fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-d43a2a",
+        "id": "TRADE-20260905-7120e4",
         "status": "pending",
         "error": "invalid_tag_response",
         "failedStep": "tag_trade_approved",
@@ -472,14 +481,14 @@
         "GET",
         "PUT"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/empty/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/empty/data/trade-applications.jsonl"
     },
     {
       "name": "missing_tag fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-6be3c9",
+        "id": "TRADE-20260905-b52496",
         "status": "pending",
         "error": "entitlement_not_verified",
         "failedStep": "verify_entitlement",
@@ -529,14 +538,14 @@
         "POST",
         "GET"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/missing_tag/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/missing_tag/data/trade-applications.jsonl"
     },
     {
       "name": "wrong_id fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-5107dd",
+        "id": "TRADE-20260905-e8c2aa",
         "status": "pending",
         "error": "entitlement_not_verified",
         "failedStep": "verify_entitlement",
@@ -586,14 +595,14 @@
         "POST",
         "GET"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/wrong_id/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/wrong_id/data/trade-applications.jsonl"
     },
     {
       "name": "missing_customer fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-09a84a",
+        "id": "TRADE-20260905-97924a",
         "status": "pending",
         "error": "entitlement_not_verified",
         "failedStep": "verify_entitlement",
@@ -643,14 +652,14 @@
         "POST",
         "GET"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/missing_customer/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/missing_customer/data/trade-applications.jsonl"
     },
     {
       "name": "throw fails closed",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-704eed",
+        "id": "TRADE-20260905-b8535c",
         "status": "pending",
         "error": "approval_exception",
         "failedStep": "tag_trade_approved",
@@ -674,14 +683,14 @@
         "GET",
         "PUT"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/throw/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/throw/data/trade-applications.jsonl"
     },
     {
       "name": "verified entitlement then persisted approval and reload",
       "verdict": "PASS",
       "result": {
         "ok": true,
-        "id": "TRADE-20260905-abc417",
+        "id": "TRADE-20260905-8e7c72",
         "status": "approved",
         "rep": {
           "id": "fixture-rep",
@@ -741,7 +750,7 @@
         ]
       },
       "persisted": {
-        "id": "TRADE-20260905-abc417",
+        "id": "TRADE-20260905-8e7c72",
         "email": "fixture@example.invalid",
         "business_name": "Fixture",
         "resale_cert": "",
@@ -751,8 +760,8 @@
         },
         "shopify_customer_id": "123",
         "status": "approved",
-        "created_at": "2026-09-05T06:56:33.051Z",
-        "decided_at": "2026-09-05T06:56:33.054Z",
+        "created_at": "2026-09-05T07:04:15.770Z",
+        "decided_at": "2026-09-05T07:04:15.773Z",
         "decision": "approved",
         "assigned_rep": {
           "id": "fixture-rep",
@@ -764,16 +773,16 @@
           "emails": {
             "rep": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.053Z",
+              "sent_at": "2026-09-05T07:04:15.773Z",
               "status": 200
             },
             "applicant": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.054Z",
+              "sent_at": "2026-09-05T07:04:15.773Z",
               "status": 200
             }
           },
-          "entitlement_verified_at": "2026-09-05T06:56:33.053Z"
+          "entitlement_verified_at": "2026-09-05T07:04:15.772Z"
         },
         "approval_error": null
       },
@@ -787,13 +796,13 @@
         "trade-rep-notify",
         "trade-approved"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/happy/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/happy/data/trade-applications.jsonl"
     },
     {
       "name": "failed mutation can retry after reload",
       "verdict": "PASS",
       "persisted": {
-        "id": "TRADE-20260905-a87b67",
+        "id": "TRADE-20260905-736047",
         "email": "fixture@example.invalid",
         "business_name": "Fixture",
         "resale_cert": "",
@@ -803,8 +812,8 @@
         },
         "shopify_customer_id": "123",
         "status": "approved",
-        "created_at": "2026-09-05T06:56:33.062Z",
-        "decided_at": "2026-09-05T06:56:33.067Z",
+        "created_at": "2026-09-05T07:04:15.783Z",
+        "decided_at": "2026-09-05T07:04:15.789Z",
         "decision": "approved",
         "assigned_rep": {
           "id": "fixture-rep",
@@ -816,26 +825,26 @@
           "emails": {
             "rep": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.066Z",
+              "sent_at": "2026-09-05T07:04:15.788Z",
               "status": 200
             },
             "applicant": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.067Z",
+              "sent_at": "2026-09-05T07:04:15.789Z",
               "status": 200
             }
           },
-          "entitlement_verified_at": "2026-09-05T06:56:33.066Z"
+          "entitlement_verified_at": "2026-09-05T07:04:15.787Z"
         },
         "approval_error": null
       },
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/retry/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/retry/data/trade-applications.jsonl"
     },
     {
       "name": "email partial success survives restart without duplicate rep send",
       "verdict": "PASS",
       "persisted": {
-        "id": "TRADE-20260905-da8c89",
+        "id": "TRADE-20260905-3985f4",
         "email": "fixture@example.invalid",
         "business_name": "Fixture",
         "resale_cert": "",
@@ -845,8 +854,8 @@
         },
         "shopify_customer_id": "123",
         "status": "approved",
-        "created_at": "2026-09-05T06:56:33.071Z",
-        "decided_at": "2026-09-05T06:56:33.077Z",
+        "created_at": "2026-09-05T07:04:15.792Z",
+        "decided_at": "2026-09-05T07:04:15.800Z",
         "decision": "approved",
         "assigned_rep": {
           "id": "fixture-rep",
@@ -858,16 +867,16 @@
           "emails": {
             "rep": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.072Z",
+              "sent_at": "2026-09-05T07:04:15.794Z",
               "status": 200
             },
             "applicant": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.076Z",
+              "sent_at": "2026-09-05T07:04:15.800Z",
               "status": 200
             }
           },
-          "entitlement_verified_at": "2026-09-05T06:56:33.075Z"
+          "entitlement_verified_at": "2026-09-05T07:04:15.799Z"
         },
         "approval_error": null
       },
@@ -876,14 +885,14 @@
         "trade-approved",
         "trade-approved"
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/mail-retry/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/mail-retry/data/trade-applications.jsonl"
     },
     {
       "name": "ambiguous email delivery blocks resend across restart",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-d6afa1",
+        "id": "TRADE-20260905-7b3979",
         "status": "pending",
         "error": "email_delivery_unknown",
         "failedStep": "email_applicant",
@@ -929,12 +938,12 @@
             "step": "email_rep",
             "skipped": true,
             "reason": "already_sent",
-            "sent_at": "2026-09-05T06:56:33.082Z"
+            "sent_at": "2026-09-05T07:04:15.806Z"
           }
         ]
       },
       "persisted": {
-        "id": "TRADE-20260905-d6afa1",
+        "id": "TRADE-20260905-7b3979",
         "email": "fixture@example.invalid",
         "business_name": "Fixture",
         "resale_cert": "",
@@ -944,7 +953,7 @@
         },
         "shopify_customer_id": "123",
         "status": "pending",
-        "created_at": "2026-09-05T06:56:33.080Z",
+        "created_at": "2026-09-05T07:04:15.804Z",
         "decided_at": null,
         "decision": null,
         "assigned_rep": {
@@ -957,30 +966,30 @@
           "emails": {
             "rep": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.082Z",
+              "sent_at": "2026-09-05T07:04:15.806Z",
               "status": 200
             },
             "applicant": {
               "state": "sending",
-              "attempted_at": "2026-09-05T06:56:33.082Z"
+              "attempted_at": "2026-09-05T07:04:15.807Z"
             }
           },
-          "entitlement_verified_at": "2026-09-05T06:56:33.085Z"
+          "entitlement_verified_at": "2026-09-05T07:04:15.811Z"
         },
         "approval_error": {
           "error": "email_delivery_unknown",
           "step": "email_applicant",
-          "at": "2026-09-05T06:56:33.085Z"
+          "at": "2026-09-05T07:04:15.812Z"
         }
       },
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/ambiguous-mail/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/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",
+        "id": "TRADE-20260905-2587ec",
         "status": "pending",
         "error": "approval_exception",
         "failedStep": "email_applicant",
@@ -1037,7 +1046,7 @@
         ]
       },
       "persisted": {
-        "id": "TRADE-20260905-f0fd1b",
+        "id": "TRADE-20260905-2587ec",
         "email": "fixture@example.invalid",
         "business_name": "Fixture",
         "resale_cert": "",
@@ -1047,8 +1056,8 @@
         },
         "shopify_customer_id": "123",
         "status": "approved",
-        "created_at": "2026-09-05T06:56:33.089Z",
-        "decided_at": "2026-09-05T06:56:33.094Z",
+        "created_at": "2026-09-05T07:04:15.816Z",
+        "decided_at": "2026-09-05T07:04:15.823Z",
         "decision": "approved",
         "assigned_rep": {
           "id": "fixture-rep",
@@ -1060,20 +1069,20 @@
           "emails": {
             "rep": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.091Z",
+              "sent_at": "2026-09-05T07:04:15.818Z",
               "status": 200
             },
             "applicant": {
               "state": "sent",
-              "sent_at": "2026-09-05T06:56:33.092Z",
+              "sent_at": "2026-09-05T07:04:15.819Z",
               "status": 200
             }
           },
-          "entitlement_verified_at": "2026-09-05T06:56:33.094Z"
+          "entitlement_verified_at": "2026-09-05T07:04:15.822Z"
         },
         "approval_error": null
       },
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/checkpoint-failure/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/checkpoint-failure/data/trade-applications.jsonl"
     },
     {
       "name": "same-process concurrent duplicate does not duplicate emails",
@@ -1081,7 +1090,7 @@
       "results": [
         {
           "ok": true,
-          "id": "TRADE-20260905-5454d0",
+          "id": "TRADE-20260905-6e7a55",
           "status": "approved",
           "rep": {
             "id": "fixture-rep",
@@ -1143,23 +1152,212 @@
         {
           "ok": false,
           "error": "approval_in_progress",
-          "id": "TRADE-20260905-5454d0"
+          "id": "TRADE-20260905-6e7a55"
         }
       ],
-      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY/concurrent/data/trade-applications.jsonl"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/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"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/intake-race/data/trade-applications.jsonl",
+      "preservedApplication": "TRADE-20260905-4568a3"
+    },
+    {
+      "name": "reject first blocks approval until rejection completes",
+      "verdict": "PASS",
+      "approval": {
+        "ok": false,
+        "error": "approval_in_progress",
+        "id": "TRADE-20260905-971cc0"
+      },
+      "persisted": {
+        "id": "TRADE-20260905-971cc0",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": 123,
+        "status": "rejected",
+        "created_at": "2026-09-05T07:04:15.845Z",
+        "decided_at": "2026-09-05T07:04:15.847Z",
+        "decision": "rejected",
+        "assigned_rep": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/reject-first/data/trade-applications.jsonl"
+    },
+    {
+      "name": "approve first blocks rejection and preserves approval receipts",
+      "verdict": "PASS",
+      "rejection": {
+        "ok": false,
+        "error": "approval_in_progress",
+        "id": "TRADE-20260905-fc216e"
+      },
+      "persisted": {
+        "id": "TRADE-20260905-fc216e",
+        "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-05T07:04:15.852Z",
+        "decided_at": "2026-09-05T07:04:15.856Z",
+        "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-05T07:04:15.854Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T07:04:15.855Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T07:04:15.854Z"
+        },
+        "approval_error": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/approve-first/data/trade-applications.jsonl"
+    },
+    {
+      "name": "concurrent rejection attempts send only one rejection",
+      "verdict": "PASS",
+      "duplicate": {
+        "ok": false,
+        "error": "approval_in_progress",
+        "id": "TRADE-20260905-7af14c"
+      },
+      "persisted": {
+        "id": "TRADE-20260905-7af14c",
+        "email": "fixture@example.invalid",
+        "business_name": "Fixture",
+        "resale_cert": "",
+        "phone": "",
+        "extra": {
+          "shopify_customer_id": 123
+        },
+        "shopify_customer_id": 123,
+        "status": "rejected",
+        "created_at": "2026-09-05T07:04:15.859Z",
+        "decided_at": "2026-09-05T07:04:15.860Z",
+        "decision": "rejected",
+        "assigned_rep": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/reject-reject/data/trade-applications.jsonl"
+    },
+    {
+      "name": "intake during rejection survives final persistence",
+      "verdict": "PASS",
+      "newerId": "TRADE-20260905-05fc42",
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/reject-intake/data/trade-applications.jsonl"
+    },
+    {
+      "name": "rejection template exception releases shared decision lock",
+      "verdict": "PASS",
+      "persisted": {
+        "id": "TRADE-20260905-e45a80",
+        "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-05T07:04:15.874Z",
+        "decided_at": "2026-09-05T07:04:15.879Z",
+        "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-05T07:04:15.877Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T07:04:15.878Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T07:04:15.877Z"
+        },
+        "approval_error": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/reject-fail-template/data/trade-applications.jsonl"
+    },
+    {
+      "name": "rejection send exception releases shared decision lock",
+      "verdict": "PASS",
+      "persisted": {
+        "id": "TRADE-20260905-091da2",
+        "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-05T07:04:15.883Z",
+        "decided_at": "2026-09-05T07:04:15.888Z",
+        "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-05T07:04:15.887Z",
+              "status": 200
+            },
+            "applicant": {
+              "state": "sent",
+              "sent_at": "2026-09-05T07:04:15.888Z",
+              "status": 200
+            }
+          },
+          "entitlement_verified_at": "2026-09-05T07:04:15.886Z"
+        },
+        "approval_error": null
+      },
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/reject-fail-send/data/trade-applications.jsonl"
     },
     {
       "name": "dry run reports simulated pending without external side effects",
       "verdict": "PASS",
       "result": {
         "ok": false,
-        "id": "TRADE-20260905-9b01cc",
+        "id": "TRADE-20260905-e0447d",
         "status": "pending",
         "dryRun": true,
         "simulated": true,
@@ -1167,12 +1365,12 @@
         "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"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/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"
+      "store": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq/no-token/data/trade-applications.jsonl"
     }
   ],
   "limitations": [
@@ -1184,8 +1382,17 @@
   ],
   "retained_test_state": [
     "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-9oeZ25",
-    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY"
+    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-Dh9eLY",
+    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/tk11285-flow-AKX4Rq"
   ],
   "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"
+  "verdict": "PASS for27 isolated local checks and owner rerun of parent race harness; final parent/Cody acceptance remains pending",
+  "increment2": {
+    "intent": "Fix parent/Cody independently reproduced reject-first decision race and rejection/intake lost write.",
+    "dtd": "/private/tmp/dtd-cycle11285-final.GWFqzJ/codex-debate.txt FINAL KEEP",
+    "before": "<root-evidence>/parent-race-before.json",
+    "after": "<root-evidence>/owner-race-after.json",
+    "scope": "Single service process only; no multi-process guarantee and no redesign of rejection email failure policy.",
+    "new_checks": 6
+  }
 }

← d291ec9 Verify trade entitlement before approval and preserve retry  ·  back to Dw Signup Fulfillment  ·  Retire obsolete theme commands that could undo designer sign dc1323f →