← 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
M lib/giftcard.jsM scripts/selftest.jsM server.js
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 →