[object Object]

← back to Dw Signup Fulfillment

retail: rewire webhook to sample-locked shared function code (DTD 5/5 path B) — matches approved memo §2, never discounts a roll; selftest now exercises the wired path + fail-loud unset-code guard (still DRY_RUN)

d0059b8df754fe8970c8dec5bf43920c4a08e080 · 2026-07-28 10:04:50 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d0059b8df754fe8970c8dec5bf43920c4a08e080
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 10:04:50 2026 -0700

    retail: rewire webhook to sample-locked shared function code (DTD 5/5 path B) — matches approved memo §2, never discounts a roll; selftest now exercises the wired path + fail-loud unset-code guard (still DRY_RUN)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/selftest.js | 38 +++++++++++++++++++++++++-------------
 server.js           | 25 ++++++++++++++-----------
 2 files changed, 39 insertions(+), 24 deletions(-)

diff --git a/scripts/selftest.js b/scripts/selftest.js
index 31cb65b..653c2dc 100644
--- a/scripts/selftest.js
+++ b/scripts/selftest.js
@@ -53,7 +53,8 @@ function restore() {
 
 const config = require('../lib/config');
 const webhook = require('../lib/webhook');
-const giftcard = require('../lib/giftcard');
+const retailCode = require('../lib/retail-code'); // WIRED retail path (DTD path B)
+const giftcard = require('../lib/giftcard');      // alternate (not wired)
 const trade = require('../lib/trade');
 const reps = require('../lib/reps');
 
@@ -69,33 +70,44 @@ async function main() {
   if (!config.DRY_RUN) { fail('DRY_RUN is OFF — refusing to run selftest that would make live writes'); return; }
 
   // ---------------------------------------------------------------------------
-  hr('(a) customers/create webhook — VALID HMAC → gift-card email path');
+  hr('(a) customers/create webhook — VALID HMAC → WIRED sample-locked shared-code path');
   const fakeCustomer = { id: 8675309, email: 'newshopper@example.com', first_name: 'Dana', created_at: new Date().toISOString() };
   const raw = Buffer.from(JSON.stringify(fakeCustomer), 'utf8');
   const goodHmac = webhook.sign(raw, TEST_SECRET);
   console.log('  computed X-Shopify-Hmac-Sha256 = ' + goodHmac);
   if (webhook.verify(raw, goodHmac)) ok('HMAC verify ACCEPTED the valid signature'); else fail('valid HMAC was rejected');
 
-  console.log('  --- retail gift-card issuance (what it WOULD do) ---');
+  console.log('  --- WIRED retail issuance (shared sample-locked code, what it WOULD do) ---');
+  const rcResult = await retailCode.issueRetailCode(fakeCustomer);
+  console.log('  result: ' + JSON.stringify(rcResult, null, 2));
+  if (rcResult.path === 'shared_code') ok('retail path is shared_code (sample-locked function discount — never touches a roll)'); else fail('retail path is not shared_code: ' + rcResult.path);
+  if (rcResult.code === TEST_SHARED_CODE) ok('emits the configured RETAIL_SHARED_CODE (' + rcResult.code + ')'); else fail('unexpected code: ' + rcResult.code);
+  if (rcResult.warn === null) ok('no WARN when RETAIL_SHARED_CODE is set'); else fail('unexpected warn: ' + rcResult.warn);
+  if (rcResult.email && rcResult.email.dryRun && !rcResult.email.skipped) ok('WOULD email the code to the customer (dry-run, not skipped)'); else fail('code email not dry-run / was skipped');
+
+  console.log('  --- fail-loud guard: unset RETAIL_SHARED_CODE must WARN + skip send ---');
+  const savedCode = process.env.RETAIL_SHARED_CODE; delete process.env.RETAIL_SHARED_CODE;
+  delete require.cache[require.resolve('../lib/config')]; delete require.cache[require.resolve('../lib/retail-code')];
+  const retailCodeUnset = require('../lib/retail-code');
+  const rcUnset = await retailCodeUnset.issueRetailCode(fakeCustomer);
+  if (rcUnset.warn && rcUnset.email.skipped) ok('unset code → WARN + email SKIPPED (never emails a wrong/blank code)'); else fail('unset code did not warn+skip');
+  if (savedCode != null) process.env.RETAIL_SHARED_CODE = savedCode;
+  delete require.cache[require.resolve('../lib/config')]; delete require.cache[require.resolve('../lib/retail-code')];
+
+  console.log('  --- alternate (NOT wired) gift-card path still functions for reference ---');
   const gcResult = await giftcard.issueRetailGiftCode(fakeCustomer);
-  console.log('  result: ' + JSON.stringify(gcResult, null, 2));
-  if (gcResult.path === 'gift_card') ok('retail path is gift_card (unique code emailed per signup)'); else fail('retail path is not gift_card');
-  if (gcResult.value === 12.75) ok('gift value = 3 × $4.25 = $12.75'); else fail('unexpected gift value: ' + gcResult.value);
-  const would = gcResult.shopifyCall && gcResult.shopifyCall.WOULD;
-  if (would && /gift_cards/.test(would)) ok('WOULD POST gift_cards (' + would + ')'); else fail('did not record a WOULD gift_cards call');
-  if (gcResult.email && gcResult.email.dryRun) ok('WOULD email the gift code to the customer (dry-run, no real send)'); else fail('gift email not dry-run');
-  if (gcResult.email && gcResult.email.subject) ok('email has a subject: ' + gcResult.email.subject); else fail('no email subject');
+  if (gcResult.path === 'gift_card' && gcResult.value === 12.75) ok('alternate gift_card path OK ($12.75) — kept for reference only'); else fail('alternate gift-card path broken');
 
   // ---------------------------------------------------------------------------
   hr('(b) customers/create webhook — INVALID HMAC → rejected');
   if (!webhook.verify(raw, 'this-is-not-the-right-signature')) ok('HMAC verify REJECTED a bad signature'); else fail('bad HMAC was accepted');
 
   // ---------------------------------------------------------------------------
-  hr('(a2) gift email clearly offers the 3 free samples');
-  const gtpl = require('../lib/email').retailGiftEmail({ firstName: 'Dana', code: 'DEMO-CODE-1234', value: 12.75, count: 3 });
+  hr('(a2) WIRED code email clearly offers the 3 free samples + includes the code');
+  const gtpl = require('../lib/email').retailCodeEmail({ firstName: 'Dana', code: 'DEMO-CODE-1234', count: 3 });
   const blob = (gtpl.subject || '') + ' ' + (gtpl.html || '');
   if (/\b3\b/.test(blob) && /sample/i.test(blob)) ok('email references "3" and "sample"'); else fail('email does not clearly offer 3 free samples');
-  if (/DEMO-CODE-1234/.test(blob)) ok('email includes the gift code'); else fail('email missing the code');
+  if (/DEMO-CODE-1234/.test(blob)) ok('email includes the sample code'); else fail('email missing the code');
 
   // ---------------------------------------------------------------------------
   hr('(c) trade application → moderated approve');
diff --git a/server.js b/server.js
index 8550ee7..4509f47 100644
--- a/server.js
+++ b/server.js
@@ -11,8 +11,8 @@
 const express = require('express');
 const config = require('./lib/config');
 const webhook = require('./lib/webhook');
-const retailCode = require('./lib/retail-code');       // WIRED default: function-backed unique sample code
-const giftcard = require('./lib/giftcard');            // alternate (not wired)
+const retailCode = require('./lib/retail-code');       // WIRED default: sample-locked shared function code (memo §2, DTD path B)
+const giftcard = require('./lib/giftcard');            // alternate (not wired — spendable on rolls)
 const giftcodeDiscount = require('./lib/giftcode-discount'); // alternate (not wired)
 const trade = require('./lib/trade');
 const reps = require('./lib/reps');
@@ -31,7 +31,7 @@ app.get('/', (_req, res) => {
   <style>body{font:15px/1.6 -apple-system,system-ui,sans-serif;margin:40px;color:#1a1a1a;background:#faf9f7}code{background:#eee;padding:1px 5px;border-radius:4px}.p{display:inline-block;padding:2px 8px;border-radius:4px;background:${config.DRY_RUN ? '#fde68a' : '#bbf7d0'};font-size:12px}</style>
   </head><body>
   <h1>DW Signup Fulfillment <span class="p">DRY_RUN: ${config.DRY_RUN ? 'ON' : 'OFF (LIVE)'}</span></h1>
-  <p>Service is running. It emails new customers a 3-free-samples gift code and handles trade applications.</p>
+  <p>Service is running. It emails new customers the sample-locked code for 3 free samples and handles trade applications.</p>
   <ul>
     <li><code>GET /healthz</code> — liveness (open)</li>
     <li><code>POST /webhooks/customers/create</code> — Shopify webhook (HMAC-verified)</li>
@@ -60,9 +60,11 @@ app.post('/webhooks/customers/create',
     res.status(200).json({ ok: true, received: true });
     try {
       console.log(`[webhook] customers/create id=${customer.id} email=${customer.email}`);
-      // WIRED retail default: email the new customer a unique gift-card code for 3 free samples.
-      const result = await giftcard.issueRetailGiftCode(customer);
-      console.log('[webhook] retail gift-card result:', JSON.stringify(result));
+      // WIRED retail path (DTD 2026-07-28, 5/5 → path B): email the new customer the
+      // SAMPLE-LOCKED shared "DW Free Samples" code (function-backed, never discounts a
+      // roll). Matches approved memo §2. Gift-card path retained as alternate only.
+      const result = await retailCode.issueRetailCode(customer);
+      console.log('[webhook] retail shared-code result:', JSON.stringify(result));
     } catch (e) {
       console.error('[webhook] fulfillment error:', e.message);
     }
@@ -129,16 +131,17 @@ app.post('/admin/trade/:id/reject', adminAuth, async (req, res) => {
 app.get('/reps/next', adminAuth, (_req, res) => res.json({ assigned: reps.houseAccount() }));
 
 // Manual retail trigger (admin) — handy for go-live smoke test without a real
-// webhook. Default is the WIRED gift-card path. ALTERNATES for comparison only:
-// ?mode=sharedcode (function shared code) and ?mode=discount (collection code).
+// webhook. Default mirrors the WIRED sample-locked shared-code path (DTD path B).
+// ALTERNATES for comparison only: ?mode=giftcard (stored value) and ?mode=discount
+// (collection code).
 app.post('/admin/retail/issue', adminAuth, async (req, res) => {
   const customer = req.body || {};
   if (!customer.email) return res.status(400).json({ ok: false, error: 'email required' });
-  let mode = req.query.mode || 'giftcard';
+  let mode = req.query.mode || 'sharedcode';
   let result;
-  if (mode === 'sharedcode') result = await retailCode.issueRetailCode(customer);          // alternate
+  if (mode === 'giftcard') result = await giftcard.issueRetailGiftCode(customer);            // alternate (spendable on rolls)
   else if (mode === 'discount') result = await giftcodeDiscount.issueRetailDiscountCode(customer); // alternate
-  else { mode = 'giftcard'; result = await giftcard.issueRetailGiftCode(customer); }        // WIRED default
+  else { mode = 'sharedcode'; result = await retailCode.issueRetailCode(customer); }          // WIRED default
   res.json({ ok: true, mode, result });
 });
 

← 80fab35 go-live prep: CORS for cross-origin /trade/apply + .deploy.c  ·  back to Dw Signup Fulfillment  ·  harden per contrarian gate: resolve trade customer-id by ema 379738e →