[object Object]

← back to Dw Signup Fulfillment

retail: wire automatic gift-card (unique code emailed per signup) as default; selftest green

a8ed621128691f95db5b30ac18d5642472a5e509 · 2026-07-28 09:34:02 -0700 · Steve

Files touched

Diff

commit a8ed621128691f95db5b30ac18d5642472a5e509
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 28 09:34:02 2026 -0700

    retail: wire automatic gift-card (unique code emailed per signup) as default; selftest green
---
 lib/giftcard.js     | 14 ++++++++------
 scripts/selftest.js | 46 ++++++++++++++++------------------------------
 server.js           | 17 +++++++++--------
 3 files changed, 33 insertions(+), 44 deletions(-)

diff --git a/lib/giftcard.js b/lib/giftcard.js
index 1ec79d1..368415e 100644
--- a/lib/giftcard.js
+++ b/lib/giftcard.js
@@ -1,11 +1,13 @@
 'use strict';
-// ALTERNATE — NOT WIRED. The retail default is lib/retail-code.js (function-backed
-// unique single-use sample code). This gift-card path is kept for reference only
-// (reachable via POST /admin/retail/issue?mode=giftcard). It is NOT safe as the
-// default: a gift-card balance can be spent on ANY line item (including full rolls),
-// not just sample swatches.
+// WIRED retail default (Steve's decision, TK-10006). On customers/create the service
+// emails the new customer a UNIQUE Shopify gift-card code for their 3 free samples.
+// Chosen because the function-backed discount is app-scoped (our token belongs to a
+// different app, so it can't mint per-customer function codes), and the admin
+// shared-code path wasn't surfacing cleanly. The gift card is fully automated with
+// the token we have, unique + single-use per signup, and bounded to $12.75 (so the
+// roll-leak exposure is capped at the card balance).
 //
-// (original notes) create a Shopify GIFT CARD worth
+// create a Shopify GIFT CARD worth
 // FREE_SAMPLE_COUNT × SAMPLE_PRICE (default 3 × $4.25 = $12.75), then email the
 // customer the code with a friendly "here are your 3 free samples" template.
 //
diff --git a/scripts/selftest.js b/scripts/selftest.js
index fff9bc1..31cb65b 100644
--- a/scripts/selftest.js
+++ b/scripts/selftest.js
@@ -53,7 +53,7 @@ function restore() {
 
 const config = require('../lib/config');
 const webhook = require('../lib/webhook');
-const retailCode = require('../lib/retail-code');
+const giftcard = require('../lib/giftcard');
 const trade = require('../lib/trade');
 const reps = require('../lib/reps');
 
@@ -69,47 +69,33 @@ 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 → shared-code email path');
+  hr('(a) customers/create webhook — VALID HMAC → gift-card email 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 shared-code issuance (what it WOULD do) ---');
-  const codeResult = await retailCode.issueRetailCode(fakeCustomer);
-  console.log('  result: ' + JSON.stringify(codeResult, null, 2));
-  if (codeResult.path === 'shared_code') ok('retail path is shared_code (email the admin-created master code)');
-  else fail('retail path is not shared_code');
-  if (codeResult.code === TEST_SHARED_CODE) ok('emits the configured shared code: ' + codeResult.code);
-  else fail('shared code mismatch: ' + codeResult.code);
-  if (!codeResult.warn) ok('RETAIL_SHARED_CODE set → no unset-code WARN'); else fail('unexpected WARN with code set: ' + codeResult.warn);
-  if (codeResult.email && codeResult.email.dryRun && !codeResult.email.skipped) ok('WOULD email the shared code to customer (dry-run, no real send)');
-  else fail('code email not dry-run / was skipped');
-  if (codeResult.email && codeResult.email.subject) ok('email has a subject: ' + codeResult.email.subject); else fail('no email subject');
+  console.log('  --- retail gift-card issuance (what it WOULD do) ---');
+  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');
 
   // ---------------------------------------------------------------------------
   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) retail-code with RETAIL_SHARED_CODE UNSET → skips send + WARNs');
-  const savedCode = process.env.RETAIL_SHARED_CODE;
-  process.env.RETAIL_SHARED_CODE = '';
-  delete require.cache[require.resolve('../lib/config')];
-  delete require.cache[require.resolve('../lib/shopify')];
-  delete require.cache[require.resolve('../lib/email')];
-  delete require.cache[require.resolve('../lib/retail-code')];
-  const retailCodeNoCode = require('../lib/retail-code');
-  const noCodeResult = await retailCodeNoCode.issueRetailCode(fakeCustomer);
-  if (noCodeResult.path === 'shared_code' && noCodeResult.warn && noCodeResult.email.skipped) ok('unset shared code → send skipped, WARN raised: ' + noCodeResult.warn);
-  else fail('expected a skip+WARN when RETAIL_SHARED_CODE is unset');
-  // restore env + module cache so the rest of the suite uses the pinned code.
-  process.env.RETAIL_SHARED_CODE = savedCode;
-  delete require.cache[require.resolve('../lib/config')];
-  delete require.cache[require.resolve('../lib/shopify')];
-  delete require.cache[require.resolve('../lib/email')];
-  delete require.cache[require.resolve('../lib/retail-code')];
+  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 });
+  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');
 
   // ---------------------------------------------------------------------------
   hr('(c) trade application → moderated approve');
diff --git a/server.js b/server.js
index aa2b144..821d61b 100644
--- a/server.js
+++ b/server.js
@@ -41,8 +41,9 @@ 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}`);
-      const result = await retailCode.issueRetailCode(customer);
-      console.log('[webhook] retail function-code result:', JSON.stringify(result));
+      // 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));
     } catch (e) {
       console.error('[webhook] fulfillment error:', e.message);
     }
@@ -91,17 +92,17 @@ app.post('/admin/trade/:id/reject', adminAuth, async (req, res) => {
 // --- Introspection helpers ---
 app.get('/reps/next', adminAuth, (_req, res) => res.json({ assigned: reps.houseAccount() }));
 
-// Manual retail-code trigger (admin) — handy for go-live smoke test without a
-// real webhook. Default is the WIRED function-backed code path. The two ALTERNATES
-// are reachable only for comparison: ?mode=giftcard and ?mode=discount.
+// 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).
 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 || 'code';
+  let mode = req.query.mode || 'giftcard';
   let result;
-  if (mode === 'giftcard') result = await giftcard.issueRetailGiftCode(customer);         // alternate
+  if (mode === 'sharedcode') result = await retailCode.issueRetailCode(customer);          // alternate
   else if (mode === 'discount') result = await giftcodeDiscount.issueRetailDiscountCode(customer); // alternate
-  else { mode = 'code'; result = await retailCode.issueRetailCode(customer); }             // WIRED default
+  else { mode = 'giftcard'; result = await giftcard.issueRetailGiftCode(customer); }        // WIRED default
   res.json({ ok: true, mode, result });
 });
 

← 471c945 retail: shared admin master-code model (function discounts a  ·  back to Dw Signup Fulfillment  ·  5x: add open root status page + favicon 204 (clean base URL, 73e3ebf →