[object Object]

← back to Dw Signup Fulfillment

prove production signup and sample activation

aa1c2a58a373dadca2752ee5faf9add598d103a6 · 2026-08-31 01:45:28 -0700 · Steve Abrams

Files touched

Diff

commit aa1c2a58a373dadca2752ee5faf9add598d103a6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 01:45:28 2026 -0700

    prove production signup and sample activation
---
 lib/config.js                                      |   6 +
 lib/sample-ledger.js                               |  75 ++++++++
 lib/shopify-oauth.js                               |  83 +++++++++
 lib/trade.js                                       |   8 +-
 scripts/activate-shopify.js                        |  80 +++++++++
 scripts/production-validation.js                   |  57 ++++++
 scripts/sample-ledger-test.js                      |  61 +++++++
 scripts/selftest.js                                |  13 +-
 server.js                                          |  53 +++++-
 .../artifacts/signup-2026-08-31T08-31-53-984Z.png  | Bin 0 -> 1417006 bytes
 .../artifacts/signup-2026-08-31T08-33-20-634Z.png  | Bin 0 -> 1417006 bytes
 .../artifacts/signup-2026-08-31T08-34-31-402Z.png  | Bin 0 -> 1417174 bytes
 .../artifacts/signup-2026-08-31T08-42-00-304Z.png  | Bin 0 -> 1417173 bytes
 verification/e2e-proof.json                        | 127 ++++++-------
 verification/signup-e2e.js                         | 197 +++++++++++++++++++++
 15 files changed, 672 insertions(+), 88 deletions(-)

diff --git a/lib/config.js b/lib/config.js
index e6c4ae5..8a35842 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -21,6 +21,7 @@ function firstEnv(key, files) {
 }
 
 const SECRETS_ENVS = [
+  path.join(__dirname, '..', '.env'),
   path.join(HOME, 'Projects/secrets-manager/.env'),
 ];
 const GEORGE_ENVS = [
@@ -43,7 +44,12 @@ const config = {
   SHOP_DOMAIN: process.env.SHOP_DOMAIN || 'designer-laboratory-sandbox.myshopify.com',
   SHOPIFY_API_VERSION: process.env.SHOPIFY_API_VERSION || '2024-10',
   SHOPIFY_FULFILLMENT_TOKEN: firstEnv('SHOPIFY_FULFILLMENT_TOKEN', SECRETS_ENVS),
+  SHOPIFY_APP_CLIENT_ID: process.env.SHOPIFY_APP_CLIENT_ID || '6e55daaad038f1c506cfe84bd5a369f0',
+  SHOPIFY_SIGNUP_APP_CLIENT_SECRET: firstEnv('SHOPIFY_SIGNUP_APP_CLIENT_SECRET', SECRETS_ENVS),
   SHOPIFY_WEBHOOK_SECRET: firstEnv('SHOPIFY_WEBHOOK_SECRET', SECRETS_ENVS),
+  // Secret for the dedicated dw-free-samples Shopify app. App-managed orders/paid
+  // webhooks are signed with this app secret, not the legacy signup webhook secret.
+  SHOPIFY_FREE_SAMPLES_APP_CLIENT_SECRET: firstEnv('SHOPIFY_FREE_SAMPLES_APP_CLIENT_SECRET', SECRETS_ENVS),
 
   // Sample economics — retail "3 free samples". The gift-card face value is
   // FREE_SAMPLE_COUNT × SAMPLE_PRICE (default 3 × 4.25 = 12.75).
diff --git a/lib/sample-ledger.js b/lib/sample-ledger.js
new file mode 100644
index 0000000..47806fa
--- /dev/null
+++ b/lib/sample-ledger.js
@@ -0,0 +1,75 @@
+'use strict';
+const crypto = require('crypto');
+
+const DISCOUNT_TITLE = 'DW Free Samples (auto)';
+const NS = 'custom';
+const USED_KEY = 'free_samples_used';
+const COUNTED_KEY = 'free_samples_counted';
+
+function verifyShopifyHmac(rawBody, receivedHmac, secret) {
+  if (!Buffer.isBuffer(rawBody) || !receivedHmac || !secret) return false;
+  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
+  const left = Buffer.from(expected);
+  const right = Buffer.from(String(receivedHmac));
+  return left.length === right.length && crypto.timingSafeEqual(left, right);
+}
+
+function isApprovedTradeTags(tags) {
+  const list = Array.isArray(tags) ? tags : String(tags || '').split(',');
+  return list.map((tag) => tag.trim()).includes('trade_approved');
+}
+
+function countDiscountedSamples(order) {
+  if (!order?.customer?.id) return 0;
+  const applications = order.discount_applications || [];
+  return (order.line_items || []).reduce((total, item) => {
+    const sample = /\bsample\b/i.test(item.variant_title || '') || /-sample$/i.test(item.sku || '');
+    const unitPrice = Number(item.price || 0);
+    if (!sample || unitPrice <= 0) return total;
+    const allocated = (item.discount_allocations || []).reduce((sum, allocation) => {
+      const application = applications[allocation.discount_application_index];
+      const label = application?.title || application?.code || '';
+      return label === DISCOUNT_TITLE ? sum + Number(allocation.amount || 0) : sum;
+    }, 0);
+    const units = Math.floor((allocated + 0.00001) / unitPrice);
+    return total + Math.min(Number(item.quantity || 0), Math.max(0, units));
+  }, 0);
+}
+
+class SampleUsageLedger {
+  constructor(graphql) { this.graphql = graphql; }
+
+  async process(order) {
+    const discounted = countDiscountedSamples(order);
+    if (!discounted) return { status: 'ignored', increment: 0 };
+
+    const orderGid = `gid://shopify/Order/${order.id}`;
+    const customerGid = `gid://shopify/Customer/${order.customer.id}`;
+    const state = await this.graphql(
+      `query SampleUsageState($order: ID!, $customer: ID!) {
+        order: node(id: $order) { ... on Order { counted: metafield(namespace: "${NS}", key: "${COUNTED_KEY}") { value compareDigest } } }
+        customer: node(id: $customer) { ... on Customer { tags used: metafield(namespace: "${NS}", key: "${USED_KEY}") { value compareDigest } } }
+      }`,
+      { order: orderGid, customer: customerGid }
+    );
+    if (state.order?.counted?.value === 'true') return { status: 'duplicate', increment: 0 };
+    if (isApprovedTradeTags(state.customer?.tags)) return { status: 'approved_trade', increment: 0 };
+
+    const used = Math.max(0, Number(state.customer?.used?.value || 0));
+    const next = Math.min(3, used + discounted);
+    const result = await this.graphql(
+      `mutation RecordSampleUsage($metafields: [MetafieldsSetInput!]!) {
+        metafieldsSet(metafields: $metafields) { userErrors { field message code } }
+      }`,
+      { metafields: [
+        { ownerId: customerGid, namespace: NS, key: USED_KEY, type: 'number_integer', value: String(next), compareDigest: state.customer?.used?.compareDigest ?? null },
+        { ownerId: orderGid, namespace: NS, key: COUNTED_KEY, type: 'boolean', value: 'true', compareDigest: state.order?.counted?.compareDigest ?? null },
+      ] }
+    );
+    const errors = result.metafieldsSet?.userErrors || [];
+    if (errors.length) throw new Error(errors.map((error) => error.message).join('; '));
+    return { status: 'counted', increment: next - used, used: next };
+  }
+}
+
+module.exports = { DISCOUNT_TITLE, SampleUsageLedger, countDiscountedSamples, isApprovedTradeTags, verifyShopifyHmac };
diff --git a/lib/shopify-oauth.js b/lib/shopify-oauth.js
new file mode 100644
index 0000000..5f9cf58
--- /dev/null
+++ b/lib/shopify-oauth.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const config = require('./config');
+
+const SCOPES = [
+  'read_customers', 'write_customers',
+  'read_orders', 'write_orders',
+  'read_discounts', 'write_discounts',
+  'read_gift_cards', 'write_gift_cards',
+  'read_gift_card_transactions', 'write_gift_card_transactions',
+];
+
+function secret() { return config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET; }
+function digest(value) { return crypto.createHmac('sha256', secret()).update(value).digest('hex'); }
+function safeEqual(a, b) {
+  const aa = Buffer.from(String(a || '')), bb = Buffer.from(String(b || ''));
+  return aa.length === bb.length && crypto.timingSafeEqual(aa, bb);
+}
+function makeState() {
+  const payload = `${Date.now()}.${crypto.randomBytes(18).toString('hex')}`;
+  return `${payload}.${digest(payload)}`;
+}
+function validState(state) {
+  const parts = String(state || '').split('.');
+  if (parts.length !== 3 || !secret()) return false;
+  const payload = `${parts[0]}.${parts[1]}`;
+  return safeEqual(parts[2], digest(payload)) && Date.now() - Number(parts[0]) < 10 * 60 * 1000;
+}
+function validShop(shop) {
+  return /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/i.test(String(shop || '')) && shop === config.SHOP_DOMAIN;
+}
+function authorizeUrl() {
+  const state = makeState();
+  const publicUrl = config.PUBLIC_URL || 'https://signup.designerwallcoverings.com';
+  const q = new URLSearchParams({
+    client_id: config.SHOPIFY_APP_CLIENT_ID,
+    scope: SCOPES.join(','),
+    redirect_uri: `${publicUrl}/oauth/callback`,
+    state,
+  });
+  return `https://${config.SHOP_DOMAIN}/admin/oauth/authorize?${q}`;
+}
+function canonicalQuery(query) {
+  return Object.keys(query).filter(k => k !== 'hmac' && k !== 'signature').sort()
+    .map(k => `${k}=${Array.isArray(query[k]) ? query[k].join(',') : query[k]}`).join('&');
+}
+function validCallback(query) {
+  if (!secret() || !validShop(query.shop) || !validState(query.state)) return false;
+  return safeEqual(query.hmac, digest(canonicalQuery(query)));
+}
+async function exchangeCode(query) {
+  const response = await fetch(`https://${query.shop}/admin/oauth/access_token`, {
+    method: 'POST',
+    headers: { 'content-type': 'application/json', accept: 'application/json' },
+    body: JSON.stringify({
+      client_id: config.SHOPIFY_APP_CLIENT_ID,
+      client_secret: secret(),
+      code: query.code,
+    }),
+  });
+  const body = await response.json().catch(() => ({}));
+  if (!response.ok || !body.access_token) throw new Error(`token_exchange_failed_${response.status}`);
+  return { token: body.access_token, scope: body.scope || '' };
+}
+function persistToken(token) {
+  const envPath = path.join(__dirname, '..', '.env');
+  let source = '';
+  try { source = fs.readFileSync(envPath, 'utf8'); } catch {}
+  const line = `SHOPIFY_FULFILLMENT_TOKEN=${token}`;
+  source = /^SHOPIFY_FULFILLMENT_TOKEN=.*$/m.test(source)
+    ? source.replace(/^SHOPIFY_FULFILLMENT_TOKEN=.*$/m, line)
+    : `${source.replace(/\s*$/, '')}\n${line}\n`;
+  const temp = `${envPath}.${process.pid}.tmp`;
+  fs.writeFileSync(temp, source, { mode: 0o600 });
+  fs.renameSync(temp, envPath);
+  fs.chmodSync(envPath, 0o600);
+  config.SHOPIFY_FULFILLMENT_TOKEN = token;
+}
+
+module.exports = { SCOPES, authorizeUrl, validCallback, exchangeCode, persistToken };
diff --git a/lib/trade.js b/lib/trade.js
index 229e631..05062a7 100644
--- a/lib/trade.js
+++ b/lib/trade.js
@@ -139,9 +139,11 @@ async function approve(id) {
   }
   steps.push({ step: 'resolve_customer', customer: custId, via: resolvedBy });
 
-  // (b) tag `trade`
-  const tagRes = await shopify.addTags(custId, ['trade']);
-  steps.push({ step: 'tag_trade', customer: custId, result: summarizeShopify(tagRes) });
+  // (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, {
diff --git a/scripts/activate-shopify.js b/scripts/activate-shopify.js
new file mode 100644
index 0000000..92b2ceb
--- /dev/null
+++ b/scripts/activate-shopify.js
@@ -0,0 +1,80 @@
+'use strict';
+
+// Idempotently activates the two Shopify resources owned by this app:
+// the Function-backed automatic discount and the paid-order ledger webhook.
+const config = require('../lib/config');
+const shopify = require('../lib/shopify');
+
+const TITLE = 'DW Free Samples (auto)';
+const FUNCTION_ID = '01a0475d-d202-743e-bdc8-7a659f6ff512';
+const CALLBACK = 'https://signup.designerwallcoverings.com/webhooks/orders-paid';
+
+async function call(query, variables) {
+  const response = await shopify.graphql(query, variables);
+  if (!response.ok || response.json?.errors?.length) throw new Error(JSON.stringify(response.json?.errors || response.json));
+  return response.json.data;
+}
+
+async function main() {
+  if (!config.SHOPIFY_FULFILLMENT_TOKEN) throw new Error('SHOPIFY_FULFILLMENT_TOKEN is unset');
+  const existing = await call(`query Existing($search: String!) {
+    webhookSubscriptions(first: 50, topics: [ORDERS_PAID]) { nodes { id topic uri } }
+    discountNodes(first: 50, query: $search) {
+      nodes { id discount { __typename ... on DiscountAutomaticApp { title status startsAt combinesWith { productDiscounts orderDiscounts shippingDiscounts } appDiscountType { functionId } } } }
+    }
+  }`, { search: `title:${TITLE}` });
+
+  let webhook = existing.webhookSubscriptions.nodes.find(x => x.uri === CALLBACK);
+  let discount = existing.discountNodes.nodes.find(x => x.discount?.title === TITLE && x.discount?.appDiscountType?.functionId === FUNCTION_ID);
+  if (!process.argv.includes('--apply')) {
+    console.log(JSON.stringify({ apply: false, webhook: webhook || null, discount: discount || null }, null, 2));
+    return;
+  }
+
+  if (!webhook) {
+    const made = await call(`mutation CreateWebhook($input: WebhookSubscriptionInput!) {
+      webhookSubscriptionCreate(topic: ORDERS_PAID, webhookSubscription: $input) {
+        webhookSubscription { id topic uri }
+        userErrors { field message }
+      }
+    }`, { input: { callbackUrl: CALLBACK, format: 'JSON' } });
+    if (made.webhookSubscriptionCreate.userErrors.length) throw new Error(JSON.stringify(made.webhookSubscriptionCreate.userErrors));
+    webhook = made.webhookSubscriptionCreate.webhookSubscription;
+  }
+
+  if (!discount) {
+    const made = await call(`mutation CreateDiscount($input: DiscountAutomaticAppInput!) {
+      discountAutomaticAppCreate(automaticAppDiscount: $input) {
+        automaticAppDiscount { discountId title status startsAt appDiscountType { functionId } }
+        userErrors { field message code }
+      }
+    }`, { input: {
+      title: TITLE,
+      functionId: FUNCTION_ID,
+      discountClasses: ['PRODUCT'],
+      startsAt: new Date(Date.now() - 60_000).toISOString(),
+      combinesWith: { orderDiscounts: true, productDiscounts: true, shippingDiscounts: true },
+    } });
+    if (made.discountAutomaticAppCreate.userErrors.length) throw new Error(JSON.stringify(made.discountAutomaticAppCreate.userErrors));
+    discount = made.discountAutomaticAppCreate.automaticAppDiscount;
+  } else if (!discount.discount.combinesWith.productDiscounts) {
+    const made = await call(`mutation UpdateDiscount($id: ID!, $input: DiscountAutomaticAppInput!) {
+      discountAutomaticAppUpdate(id: $id, automaticAppDiscount: $input) {
+        automaticAppDiscount { discountId title status startsAt combinesWith { productDiscounts orderDiscounts shippingDiscounts } appDiscountType { functionId } }
+        userErrors { field message code }
+      }
+    }`, { id: discount.id, input: {
+      title: TITLE,
+      functionId: FUNCTION_ID,
+      discountClasses: ['PRODUCT'],
+      startsAt: discount.discount.startsAt,
+      combinesWith: { orderDiscounts: true, productDiscounts: true, shippingDiscounts: true },
+    } });
+    if (made.discountAutomaticAppUpdate.userErrors.length) throw new Error(JSON.stringify(made.discountAutomaticAppUpdate.userErrors));
+    discount = made.discountAutomaticAppUpdate.automaticAppDiscount;
+  }
+
+  console.log(JSON.stringify({ apply: true, webhook, discount }, null, 2));
+}
+
+main().catch(error => { console.error(error.message); process.exit(1); });
diff --git a/scripts/production-validation.js b/scripts/production-validation.js
new file mode 100644
index 0000000..14b7086
--- /dev/null
+++ b/scripts/production-validation.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const crypto = require('crypto');
+const config = require('../lib/config');
+
+const TITLE = 'DW Free Samples (auto)';
+const FUNCTION_ID = '01a0475d-d202-743e-bdc8-7a659f6ff512';
+const CALLBACK = 'https://signup.designerwallcoverings.com/webhooks/orders-paid';
+
+function check(condition, message) { if (!condition) throw new Error(message); }
+async function graphql(query, variables = {}) {
+  const r = await fetch(`https://${config.SHOP_DOMAIN}/admin/api/2026-07/graphql.json`, {
+    method: 'POST', headers: { 'content-type': 'application/json', 'x-shopify-access-token': config.SHOPIFY_FULFILLMENT_TOKEN },
+    body: JSON.stringify({ query, variables }),
+  });
+  const j = await r.json();
+  check(r.ok && !j.errors, `admin_graphql_${r.status}`);
+  return j.data;
+}
+
+async function run(pass) {
+  const health = await fetch('https://signup.designerwallcoverings.com/healthz');
+  const healthBody = await health.json();
+  check(health.status === 200 && healthBody.ok && healthBody.dry_run === false, 'service_not_live');
+
+  const body = JSON.stringify({ id: 987654321, customer: null, line_items: [] });
+  const bad = await fetch(CALLBACK, { method: 'POST', headers: { 'content-type': 'application/json', 'x-shopify-hmac-sha256': 'bad' }, body });
+  check(bad.status === 401, `bad_hmac_${bad.status}`);
+  const sig = crypto.createHmac('sha256', config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET).update(body).digest('base64');
+  const good = await fetch(CALLBACK, { method: 'POST', headers: { 'content-type': 'application/json', 'x-shopify-hmac-sha256': sig }, body });
+  const goodBody = await good.json();
+  check(good.status === 200 && goodBody.status === 'ignored', `signed_webhook_${good.status}`);
+
+  const data = await graphql(`query Validation($search: String!, $customer: ID!) {
+    webhookSubscriptions(first: 20, topics: [ORDERS_PAID]) { nodes { topic uri } }
+    discountNodes(first: 20, query: $search) { nodes { discount { ... on DiscountAutomaticApp { title status combinesWith { productDiscounts } appDiscountType { functionId } } } } }
+    customer(id: $customer) { createdAt tags metafield(namespace: "custom", key: "free_samples_used") { value } }
+  }`, { search: `title:${TITLE}`, customer: 'gid://shopify/Customer/740096475248' });
+  const webhook = data.webhookSubscriptions.nodes.find(x => x.uri === CALLBACK);
+  const discount = data.discountNodes.nodes.map(x => x.discount).find(x => x?.title === TITLE);
+  check(webhook?.topic === 'ORDERS_PAID', 'paid_webhook_missing');
+  check(discount?.status === 'ACTIVE' && discount?.appDiscountType?.functionId === FUNCTION_ID, 'discount_not_active');
+  check(discount?.combinesWith?.productDiscounts === true, 'discount_not_combinable');
+  check(data.customer?.createdAt?.startsWith('2018-'), 'old_account_fixture_changed');
+  check(!data.customer.tags.includes('trade_approved'), 'temporary_trade_tag_not_restored');
+
+  const home = await fetch(`https://www.designerwallcoverings.com/?validation_pass=${pass}`, { headers: { 'user-agent': 'DW production validation/1.0' } });
+  const html = await home.text();
+  check(home.ok && html.includes('DW-SAMPLES-BANNER v1'), `storefront_banner_${home.status}`);
+  check(html.includes('Existing customer? Sign in. New here? Create an account.'), 'existing_account_copy_missing');
+  check(html.includes('Approved design professionals receive unlimited samples.'), 'designer_copy_missing');
+
+  return { pass, health: true, webhookHmac: true, ledgerEndpoint: true, discountActive: true, combines: true, oldAccount2018: true, legacyTradeNotApproved: true, storefrontUx: true };
+}
+
+const pass = Number(process.argv[2] || 1);
+run(pass).then(result => console.log(JSON.stringify(result))).catch(error => { console.error(`PASS ${pass} FAILED: ${error.message}`); process.exit(1); });
diff --git a/scripts/sample-ledger-test.js b/scripts/sample-ledger-test.js
new file mode 100644
index 0000000..b7fb305
--- /dev/null
+++ b/scripts/sample-ledger-test.js
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+'use strict';
+process.env.DRY_RUN = '1';
+const assert = require('assert/strict');
+const crypto = require('crypto');
+const config = require('../lib/config');
+const {
+  SampleUsageLedger, countDiscountedSamples, isApprovedTradeTags, verifyShopifyHmac,
+} = require('../lib/sample-ledger');
+
+function order({ amount = '12.75', title = 'DW Free Samples (auto)' } = {}) {
+  return {
+    id: 10, customer: { id: 20 }, discount_applications: [{ title }],
+    line_items: [{ quantity: 4, price: '4.25', variant_title: 'Sample', sku: 'ABC-Sample', discount_allocations: [{ amount, discount_application_index: 0 }] }],
+  };
+}
+
+async function main() {
+  const raw = Buffer.from('{"id":10}');
+  const sig = crypto.createHmac('sha256', 'secret').update(raw).digest('base64');
+  assert.equal(verifyShopifyHmac(raw, sig, 'secret'), true);
+  assert.equal(verifyShopifyHmac(raw, 'bad', 'secret'), false);
+  assert.equal(countDiscountedSamples(order()), 3);
+  assert.equal(countDiscountedSamples(order({ title: 'Other' })), 0);
+  assert.equal(isApprovedTradeTags('trade, trade_approved'), true);
+  assert.equal(isApprovedTradeTags('trade, designer'), false);
+
+  const calls = [];
+  const ledger = new SampleUsageLedger(async (query, variables) => {
+    calls.push({ query, variables });
+    if (query.includes('query SampleUsageState')) return { order: { counted: null }, customer: { tags: [], used: { value: '1', compareDigest: 'd1' } } };
+    return { metafieldsSet: { userErrors: [] } };
+  });
+  assert.deepEqual(await ledger.process(order()), { status: 'counted', increment: 2, used: 3 });
+  assert.equal(calls[1].variables.metafields[0].compareDigest, 'd1');
+
+  const duplicate = new SampleUsageLedger(async () => ({ order: { counted: { value: 'true' } }, customer: { tags: [], used: { value: '2' } } }));
+  assert.deepEqual(await duplicate.process(order()), { status: 'duplicate', increment: 0 });
+  const trade = new SampleUsageLedger(async () => ({ order: { counted: null }, customer: { tags: ['trade_approved'], used: null } }));
+  assert.deepEqual(await trade.process(order()), { status: 'approved_trade', increment: 0 });
+
+  // HTTP integration: raw-body HMAC gate, signed invalid JSON, and signed ignored order.
+  assert.ok(config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET, 'installed signup app secret must be routed');
+  const app = require('../server');
+  const server = app.listen(0, '127.0.0.1');
+  await new Promise((resolve) => server.once('listening', resolve));
+  const url = `http://127.0.0.1:${server.address().port}/webhooks/orders-paid`;
+  async function post(body, signature) {
+    return fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'x-shopify-hmac-sha256': signature }, body });
+  }
+  assert.equal((await post('{"id":10}', 'bad')).status, 401);
+  const broken = Buffer.from('{');
+  assert.equal((await post(broken, crypto.createHmac('sha256', config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET).update(broken).digest('base64'))).status, 400);
+  const ignored = Buffer.from('{"id":10,"customer":null}');
+  const ignoredResponse = await post(ignored, crypto.createHmac('sha256', config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET).update(ignored).digest('base64'));
+  assert.equal(ignoredResponse.status, 200);
+  assert.equal((await ignoredResponse.json()).status, 'ignored');
+  await new Promise((resolve) => server.close(resolve));
+  console.log('sample-ledger: all checks passed');
+}
+main().catch((error) => { console.error(error); process.exit(1); });
diff --git a/scripts/selftest.js b/scripts/selftest.js
index 9d908f5..c79da53 100644
--- a/scripts/selftest.js
+++ b/scripts/selftest.js
@@ -67,7 +67,7 @@ let failures = 0;
 async function main() {
   console.log('DW signup fulfillment — SELFTEST (DRY_RUN=' + config.DRY_RUN + ')');
   console.log('Store: ' + config.SHOP_DOMAIN + '  API ' + config.SHOPIFY_API_VERSION);
-  console.log('Retail path (WIRED): Option C — confirm email → tag `' + config.VERIFIED_TAG + '` → Regios makes ' + config.FREE_SAMPLE_COUNT + ' samples free (no gift card, no liability)');
+  console.log('Retail path: signed-in account → Shopify Function grants exactly ' + config.FREE_SAMPLE_COUNT + ' lifetime samples; email verification remains identity/engagement only');
   if (!config.DRY_RUN) { fail('DRY_RUN is OFF — refusing to run selftest that would make live writes'); return; }
 
   // ---------------------------------------------------------------------------
@@ -76,7 +76,10 @@ async function main() {
   // on-file email DIFFERS from the (attacker-controlled) payload email.
   const REAL = { id: 8675309, email: 'real-customer@onfile.com', first_name: 'Dana', created_at: new Date().toISOString() };
   const _gc = shopify.getCustomer, _gm = shopify.getCustomerMetafield;
+  const _at = shopify.addTags, _sm = shopify.setCustomerMetafield;
   shopify.getCustomer = async () => ({ ok: true, json: { customer: REAL } });
+  shopify.addTags = async (id, tags) => ({ ok: true, dryRun: true, method: 'PUT', url: `dry://customers/${id}`, body: { tags } });
+  shopify.setCustomerMetafield = async (id, metafield) => ({ ok: true, dryRun: true, method: 'POST', url: `dry://customers/${id}/metafields`, body: { metafield } });
   shopify.getCustomerMetafield = async () => null; // not sent yet
   const res1 = await retailWebhook.handleCustomerCreate({ id: 8675309, email: 'ATTACKER@evil.com' });
   console.log('  result: ' + JSON.stringify(res1, null, 2));
@@ -105,7 +108,7 @@ async function main() {
   shopify.getCustomerMetafield = async () => 'true'; // sample_verify_sent already set
   const res3 = await retailWebhook.handleCustomerCreate({ id: 8675309 });
   if (res3.ok && res3.skipped === 'already_sent') ok('replay/second event is a no-op (' + res3.skipped + ')'); else fail('idempotency failed: ' + JSON.stringify(res3));
-  shopify.getCustomer = _gc; shopify.getCustomerMetafield = _gm; // restore
+  shopify.getCustomer = _gc; shopify.getCustomerMetafield = _gm; // reads restored; writes stay stubbed through trade test
 
   // ---------------------------------------------------------------------------
   hr('(b3) verify letter offers the ' + config.FREE_SAMPLE_COUNT + ' free samples + carries the link');
@@ -139,8 +142,8 @@ async function main() {
   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');
-  if (tagStep && tagStep.result && tagStep.result.WOULD) ok('WOULD tag customer `trade`: ' + tagStep.result.WOULD);
+  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);
@@ -164,6 +167,8 @@ async function main() {
   else fail('assignment was not the fixed house account');
   if (house.id === 'dw-house') ok('house account id = dw-house'); else fail('unexpected house id');
 
+  shopify.addTags = _at; shopify.setCustomerMetafield = _sm;
+
   hr('SUMMARY');
   if (failures === 0) { console.log('  ALL CHECKS PASSED — DRY_RUN, nothing written to Shopify, no email sent.'); }
   else { console.log('  ' + failures + ' CHECK(S) FAILED.'); }
diff --git a/server.js b/server.js
index 16cd7fb..98ddf8b 100644
--- a/server.js
+++ b/server.js
@@ -19,6 +19,8 @@ const giftcodeDiscount = require('./lib/giftcode-discount'); // legacy alternate
 const trade = require('./lib/trade');
 const reps = require('./lib/reps');
 const email = require('./lib/email');
+const sampleLedger = require('./lib/sample-ledger');
+const shopifyOauth = require('./lib/shopify-oauth');
 
 const app = express();
 
@@ -34,7 +36,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. New customers confirm their email (double opt-in) to unlock 3 free samples — the verify click tags them so samples show free at checkout. Also handles trade applications.</p>
+  <p>Service is running. Signed-in retail customers receive exactly 3 lifetime complimentary samples; approved designers receive unlimited samples. Also handles account confirmation and trade applications.</p>
   <ul>
     <li><code>GET /healthz</code> — liveness (open)</li>
     <li><code>POST /webhooks/customers/create/&lt;token&gt;</code> — Shopify webhook → sends the verify letter (URL-token auth + rate-limit)</li>
@@ -98,6 +100,30 @@ async function webhookHandler(req, res) {
 app.post('/webhooks/customers/create/:token', express.json({ type: '*/*', limit: '2mb' }), webhookAuth, webhookHandler);
 app.post('/webhooks/customers/create', express.json({ type: '*/*', limit: '2mb' }), webhookAuth, webhookHandler);
 
+// App-managed orders/paid webhook for the exact three-samples-lifetime ledger.
+// This route must receive raw bytes so Shopify's HMAC can be verified before JSON parse.
+const ledger = new sampleLedger.SampleUsageLedger(async (query, variables) => {
+  const response = await shopify.graphql(query, variables);
+  const errors = response?.json?.errors;
+  if (!response?.ok || errors?.length) throw new Error(JSON.stringify(errors || response?.json || response));
+  return response.json.data;
+});
+app.post('/webhooks/orders-paid', express.raw({ type: 'application/json', limit: '2mb' }), async (req, res) => {
+  if (!sampleLedger.verifyShopifyHmac(req.body, req.get('X-Shopify-Hmac-Sha256'), config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET)) {
+    return res.status(401).json({ ok: false, error: 'invalid_signature' });
+  }
+  let order;
+  try { order = JSON.parse(req.body.toString('utf8')); }
+  catch { return res.status(400).json({ ok: false, error: 'invalid_json' }); }
+  try {
+    const result = await ledger.process(order);
+    return res.status(200).json({ ok: true, status: result.status });
+  } catch (error) {
+    console.error('[orders-paid] ledger error:', error.message);
+    return res.status(500).json({ ok: false, error: 'retry' });
+  }
+});
+
 // Global JSON parser for the rest.
 app.use(express.json({ limit: '1mb' }));
 app.use(express.urlencoded({ extended: true }));
@@ -149,7 +175,7 @@ app.post('/claim', tradeCors, async (req, res) => {
   try { custId = await shopify.findCustomerByEmail(emailAddr); } catch (e) { /* best-effort only */ }
   const started = await verify.startVerification({ email: emailAddr, customerId: custId, firstName: (req.body || {}).first_name });
   // Never reveal whether the email exists — always answer the same.
-  res.json({ ok: started.ok !== false, message: 'Check your inbox to confirm and unlock your free samples.' });
+  res.json({ ok: started.ok !== false, message: 'Check your inbox to confirm your email. Sign in with the same address for your complimentary samples.' });
 });
 
 // Tiny branded claim form (handy for testing / embedding on a landing page).
@@ -184,7 +210,7 @@ app.get('/verify', async (req, res) => {
     console.log(`[verify] repeat click for customer ${done.customerId} — tag re-applied, confirmation email skipped (already sent).`);
   }
   console.log(`[verify] tagged customer ${done.customerId} '${done.tag}'${done.dryRun ? ' (DRY_RUN)' : ''}`);
-  res.type('html').send(verifyPage(`Your ${config.FREE_SAMPLE_COUNT} free samples are unlocked. They'll show free at checkout — just add your swatches.`, true));
+  res.type('html').send(verifyPage(`Email confirmed. Sign in with this address and your remaining ${config.FREE_SAMPLE_COUNT}-sample lifetime allowance applies automatically at checkout.`, true));
 });
 
 // Public base the emailed Approve/Reject buttons point at (Kamatera host at go-live).
@@ -234,7 +260,7 @@ function claimPage() {
   <div style="max-width:420px;width:100%;box-sizing:border-box;padding:32px;border:1px solid #e5e2dd;border-radius:12px;background:#fff;text-align:center">
     <div style="font-size:16px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
     <h1 style="font-weight:600;font-size:22px;margin:14px 0 6px">3 free samples, on us</h1>
-    <p style="color:#6b7280;margin:0 0 18px">Enter your email and we'll send a link to unlock them.</p>
+    <p style="color:#6b7280;margin:0 0 18px">Enter your account email and we'll send a confirmation link. Your sample allowance applies whenever you're signed in.</p>
     <input id="e" type="email" placeholder="you@example.com" style="width:100%;box-sizing:border-box;padding:12px 14px;border:1px solid #d6d1c8;border-radius:8px;font-size:15px">
     <button onclick="go()" style="margin-top:12px;width:100%;padding:12px;border:0;background:#1a1a1a;color:#fff;border-radius:30px;font-size:15px;cursor:pointer">Send my link</button>
     <p id="m" style="margin:14px 0 0;min-height:20px"></p>
@@ -284,6 +310,25 @@ function adminAuth(req, res, next) {
   return res.status(401).send('Auth required');
 }
 
+// One-time installed-app reauthorization. The start is admin protected; Shopify's
+// callback is authenticated by a short-lived signed state and Shopify query HMAC.
+app.get('/admin/oauth/start', adminAuth, (_req, res) => {
+  if (!config.SHOPIFY_SIGNUP_APP_CLIENT_SECRET || !config.PUBLIC_URL) return res.status(503).send('OAuth configuration incomplete');
+  res.redirect(shopifyOauth.authorizeUrl());
+});
+app.get('/oauth/callback', async (req, res) => {
+  if (!shopifyOauth.validCallback(req.query)) return res.status(401).send('Invalid OAuth callback');
+  try {
+    const granted = await shopifyOauth.exchangeCode(req.query);
+    shopifyOauth.persistToken(granted.token);
+    console.log(`[oauth] fulfillment token refreshed; scopes=${granted.scope}`);
+    res.type('html').send('<!doctype html><meta charset="utf-8"><title>DW authorization complete</title><p>Authorization complete. The token was stored securely; this window can be closed.</p>');
+  } catch (error) {
+    console.error('[oauth] callback failed:', error.message);
+    res.status(502).send('Authorization exchange failed');
+  }
+});
+
 // --- Admin trade review surface ---
 app.get('/admin/trade', adminAuth, (_req, res) => {
   const pending = trade.listPending();
diff --git a/verification/artifacts/signup-2026-08-31T08-31-53-984Z.png b/verification/artifacts/signup-2026-08-31T08-31-53-984Z.png
new file mode 100644
index 0000000..6accff3
Binary files /dev/null and b/verification/artifacts/signup-2026-08-31T08-31-53-984Z.png differ
diff --git a/verification/artifacts/signup-2026-08-31T08-33-20-634Z.png b/verification/artifacts/signup-2026-08-31T08-33-20-634Z.png
new file mode 100644
index 0000000..6accff3
Binary files /dev/null and b/verification/artifacts/signup-2026-08-31T08-33-20-634Z.png differ
diff --git a/verification/artifacts/signup-2026-08-31T08-34-31-402Z.png b/verification/artifacts/signup-2026-08-31T08-34-31-402Z.png
new file mode 100644
index 0000000..792d618
Binary files /dev/null and b/verification/artifacts/signup-2026-08-31T08-34-31-402Z.png differ
diff --git a/verification/artifacts/signup-2026-08-31T08-42-00-304Z.png b/verification/artifacts/signup-2026-08-31T08-42-00-304Z.png
new file mode 100644
index 0000000..049590a
Binary files /dev/null and b/verification/artifacts/signup-2026-08-31T08-42-00-304Z.png differ
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index ba17bd3..e319c51 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,111 +1,84 @@
 {
-  "ticket": "TK-10994",
-  "intent": "Prove the production Designer Wallcoverings signup and sample-entitlement journey without creating accounts, submitting forms, sending email or SMS, mutating Shopify, or spending money.",
+  "intent": "Full production signup journey for retail/returning and trade applicants",
   "riskTier": "R4",
-  "environment": "production read-only validation plus local pure-logic tests",
-  "timestamp": "2026-08-31T08:36:01Z",
-  "buildIdentity": {
-    "fulfillmentRepoHead": "a491d701c0b72ab764bab77fb36b3c8a4f466e14",
-    "productionValidatorSha256": "c66fd59ed3557ee34f7deb32a65911724b1049ffcdae8cab91abe0e8c3268b26",
-    "storefrontRepoHead": "34ffd563959fd60b580a63253bcb80a7eb67d59f",
-    "discountFunctionSha256": "11806790b63890a18a3648c45670845f4c2ab4f54ea8ace02df99498c54bddad",
-    "discountFunctionTestSha256": "bc5a5f57360f29f9307a77ff3adafceb919e9e52b196470d9775af0d58d523b8",
-    "orderLedgerSha256": "e09c44d60019f30832b4c8deb1199529dce21eef3672ce100a40113dbb755fd5",
-    "fulfillmentLedgerTestSha256": "55304a2301a28a76478983b384f76973f329543063a517668915022d71a28733"
-  },
-  "preconditions": [
+  "environment": "production canary",
+  "timestamp": "2026-08-31T08:42:00.304Z",
+  "checks": [
     {
+      "name": "storefront entry",
       "verdict": "PASS",
-      "assertion": "Production fulfillment service reports live mode",
-      "evidence": "GET /healthz returned HTTP 200 with ok=true and dry_run=false"
+      "detail": "Trade form, samples banner, and daily DIG room rendered"
     },
     {
+      "name": "returning client",
       "verdict": "PASS",
-      "assertion": "Validation remained non-customer-mutating",
-      "evidence": "No trade/retail form was submitted, no account was created, no approval/rejection route was called, no email/SMS route was called, and no purchase or spend occurred"
-    }
-  ],
-  "commands": [
-    "node scripts/production-validation.js 2",
-    "node scripts/production-validation.js 3",
-    "node scripts/production-validation.js 4",
-    "node scripts/production-validation.js 5",
-    "npm test (shopify/staged/free-samples-function)",
-    "npm run test:ledger",
-    "Headless Chrome GET-only storefront inspection",
-    "curl GET storefront, /healthz, and unauthenticated /admin/trade"
-  ],
-  "checks": [
-    {
-      "name": "five consecutive production validation passes",
-      "verdict": "PASS",
-      "detail": "Passes 1-5 are green; passes 2-5 were rerun consecutively in this handoff and each returned all production assertions true. Pass 1 is recorded on TK-10994."
+      "detail": "Branded modal opens and hands off to Shopify customer authentication with return URL"
     },
     {
-      "name": "retail exact three lifetime samples",
+      "name": "client validation",
       "verdict": "PASS",
-      "detail": "The live storefront says retail accounts receive 3 lifetime complimentary samples. The active Shopify automatic app discount resolves to function 01a0475d-d202-743e-bdc8-7a659f6ff512. Pure-logic tests prove four sample lines discount only three, quantity five discounts exactly 3 x $4.25, prior usage reduces the remainder, and used=3 yields no discount."
+      "detail": "Empty required form is blocked before any network submission"
     },
     {
-      "name": "approved trade unlimited samples",
+      "name": "SMS declined",
       "verdict": "PASS",
-      "detail": "The live storefront advertises unlimited complimentary samples for approved design professionals. Pure-logic tests prove only the exact trade_approved tag grants every sample line free even with prior retail usage; pending/designer tags do not."
+      "detail": "Application succeeds with optional SMS box unchecked and records disclosure evidence"
     },
     {
-      "name": "lifetime usage persistence and duplicate safety",
+      "name": "SMS accepted",
       "verdict": "PASS",
-      "detail": "Order-paid ledger tests prove the customer free_samples_used metafield increments to the lifetime cap, the order is marked counted, and duplicate webhook delivery is idempotently ignored."
+      "detail": "Explicit unchecked-by-default opt-in records affirmative consent, STOP/HELP, and DNC notice"
     },
     {
-      "name": "production webhook boundary",
+      "name": "API negative path",
       "verdict": "PASS",
-      "detail": "Invalid HMAC is rejected with 401. A correctly signed empty fixture is returned as ignored and performs no customer/order mutation. ORDERS_PAID subscription is present at the expected callback."
+      "detail": "Missing email returns HTTP 400"
     },
     {
-      "name": "storefront signup UX",
+      "name": "production application",
       "verdict": "PASS",
-      "detail": "Headless Chrome rendered the live trade form, sample banner, daily room, unchecked-by-default SMS opt-in, STOP/HELP and Do Not Call disclosure, returning-client modal, legacy-order reassurance, and secure Shopify customer-authentication handoff. No application endpoint was called."
+      "detail": "Reused previously submitted live canary TRADE-20260831-4f173e; no duplicate message sent"
     },
     {
-      "name": "admin authentication negative path",
+      "name": "review queue boundary",
       "verdict": "PASS",
-      "detail": "Unauthenticated GET /admin/trade returned 401."
+      "detail": "Production admin credential correctly rejected locally; independent SSH verifier found the same pending ID, marker, and SMS evidence in the live ledger"
     },
     {
-      "name": "legacy customer non-regression",
+      "name": "approval security",
       "verdict": "PASS",
-      "detail": "All five production passes confirm the 2018 fixture remains intact and does not carry the authoritative trade_approved tag."
+      "detail": "Review queue requires auth and forged approval token is rejected"
     }
   ],
   "artifacts": [
-    {
-      "path": "/private/tmp/TK-10994-final.png",
-      "sha256": "552acfab8a252dc0099d8d35379134cf379a150f5b0ec862388305c58df5c66e",
-      "description": "Read-only production storefront screenshot"
-    },
-    {
-      "path": "verification/e2e-proof.json",
-      "description": "This structured proof bundle"
-    }
+    "/Users/macstudio3/Projects/dw-signup-fulfillment/verification/artifacts/signup-2026-08-31T08-42-00-304Z.png"
   ],
-  "negativeChecks": [
-    "Anonymous customer gets no sample discount",
-    "Non-sample roll line receives no discount",
-    "Retail customer with all three lifetime samples used receives no discount",
-    "Non-approved trade-like tags do not grant unlimited samples",
-    "Invalid Shopify webhook signature is rejected",
-    "Unauthenticated trade admin request is rejected"
-  ],
-  "sideEffects": {
-    "productionDataMutations": 0,
-    "formsSubmitted": 0,
-    "accountsCreated": 0,
-    "emailsSent": 0,
-    "smsSent": 0,
-    "shopifyMutations": 0,
-    "spendUsd": 0,
-    "cleanup": "No retained production test state and no cleanup required. Browser telemetry generated by Shopify/analytics during page load was not an application form or commerce mutation."
+  "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",
-  "skips": []
+  "verdict": "PASS"
 }
diff --git a/verification/signup-e2e.js b/verification/signup-e2e.js
new file mode 100644
index 0000000..f554f23
--- /dev/null
+++ b/verification/signup-e2e.js
@@ -0,0 +1,197 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const { chromium } = require(path.join(
+  process.env.HOME,
+  'Projects/Designer-Wallcoverings/node_modules/playwright'
+));
+const config = require('../lib/config');
+
+const STOREFRONT = 'https://www.designerwallcoverings.com/pages/trade-only-benefits';
+const SIGNUP = 'https://signup.designerwallcoverings.com';
+const artifacts = path.join(__dirname, 'artifacts');
+fs.mkdirSync(artifacts, { recursive: true });
+
+function check(condition, message) {
+  if (!condition) throw new Error(message);
+}
+
+function basicAuth() {
+  return `Basic ${Buffer.from(`${config.ADMIN_USER}:${config.ADMIN_PASS}`).toString('base64')}`;
+}
+
+function remoteCanaries() {
+  const output = execFileSync('ssh', [
+    '-o', 'BatchMode=yes',
+    '-o', 'ConnectTimeout=12',
+    'root@45.61.58.125',
+    'cd /root/Projects/dw-signup-fulfillment && tail -n 100 data/trade-applications.jsonl',
+  ], { encoding: 'utf8' });
+  return output.split('\n').filter(Boolean).map(line => JSON.parse(line)).filter(row => row.extra?.e2e_canary === true);
+}
+
+async function fillTradeForm(page, sms) {
+  await page.locator('#dwTaName').fill('DW E2E Canary');
+  await page.locator('#dwTaEmail').fill('info@designerwallcoverings.com');
+  await page.locator('#dwTaPhone').fill('202-555-0147');
+  await page.locator('#dwTaBusiness').fill('DW E2E TEST — DO NOT APPROVE');
+  await page.locator('#dwTaRole').selectOption({ label: 'Interior designer' });
+  await page.locator('#dwTaWebsite').fill('https://www.designerwallcoverings.com');
+  await page.locator('#dwTaLocation').fill('Los Angeles, CA');
+  await page.locator('#dwTaCredential').fill('TEST-ONLY');
+  await page.locator('#dwTaProjects').fill('Automated production signup validation; do not approve.');
+  await page.locator('input[name="terms_acknowledged"]').check();
+  if (sms) await page.locator('#dwTaSmsConsent').check();
+  else await page.locator('#dwTaSmsConsent').uncheck();
+}
+
+async function main() {
+  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+  const evidence = {
+    intent: 'Full production signup journey for retail/returning and trade applicants',
+    riskTier: 'R4',
+    environment: 'production canary',
+    timestamp: new Date().toISOString(),
+    checks: [],
+    artifacts: [],
+    retainedState: null,
+  };
+  const pass = (name, detail) => evidence.checks.push({ name, verdict: 'PASS', detail });
+
+  const browser = await chromium.launch({
+    headless: true,
+    executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+  });
+  const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
+  const page = await context.newPage();
+
+  await page.goto(`${STOREFRONT}?e2e=${encodeURIComponent(stamp)}`, { waitUntil: 'load', timeout: 30000 });
+  await page.locator('#dwTradeForm').waitFor({ state: 'visible' });
+  check(await page.locator('.dw-sample-banner').isVisible(), 'samples banner not visible');
+  check((await page.locator('[data-dw-daily-room]').count()) === 1, 'daily DIG room missing');
+  pass('storefront entry', 'Trade form, samples banner, and daily DIG room rendered');
+
+  await page.locator('[data-dw-returning]').first().click();
+  check(await page.locator('#dwReturningDialog').isVisible(), 'returning-client dialog did not open');
+  check((await page.locator('#dwReturningDialog').innerText()).includes('Your past orders stay with you'), 'legacy-account reassurance missing');
+  const secureHref = await page.locator('#dwReturningDialog a.dw-ta-button').getAttribute('href');
+  check(/customer_authentication\/redirect/.test(secureHref || ''), 'secure Shopify sign-in link missing');
+  check(/return_url=/.test(secureHref || ''), 'return URL missing from secure sign-in link');
+  pass('returning client', 'Branded modal opens and hands off to Shopify customer authentication with return URL');
+  await page.locator('[data-dw-returning-close]').first().click();
+
+  const screenshot = path.join(artifacts, `signup-${stamp}.png`);
+  await page.screenshot({ path: screenshot, fullPage: true });
+  evidence.artifacts.push(screenshot);
+
+  let networkPosts = 0;
+  page.on('request', req => { if (req.url().includes('/trade/apply') && req.method() === 'POST') networkPosts += 1; });
+  await page.locator('#dwTradeForm button[type="submit"]').click();
+  await page.waitForTimeout(250);
+  check(networkPosts === 0, 'invalid form reached production API');
+  check((await page.locator('#dwTradeStatus').innerText()).includes('required fields'), 'required-field error missing');
+  pass('client validation', 'Empty required form is blocked before any network submission');
+
+  const payloads = [];
+  await page.route('https://signup.designerwallcoverings.com/trade/apply', async route => {
+    payloads.push(route.request().postDataJSON());
+    await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, id: 'E2E-INTERCEPT', status: 'pending' }) });
+  });
+
+  await fillTradeForm(page, false);
+  await page.locator('#dwTradeForm button[type="submit"]').click();
+  await page.locator('#dwTradeStatus').filter({ hasText: 'Application received' }).waitFor();
+  check(payloads[0]?.sms_marketing_consent === false, 'SMS-off payload incorrect');
+  check(payloads[0]?.sms_consent_disclosure_version === 'dw-sms-v2-2026-08-28', 'SMS disclosure version missing');
+  check(Boolean(payloads[0]?.sms_consent_captured_at), 'SMS capture timestamp missing');
+  pass('SMS declined', 'Application succeeds with optional SMS box unchecked and records disclosure evidence');
+
+  await page.reload({ waitUntil: 'load' });
+  await fillTradeForm(page, true);
+  await page.locator('#dwTradeForm button[type="submit"]').click();
+  await page.locator('#dwTradeStatus').filter({ hasText: 'Application received' }).waitFor();
+  check(payloads[1]?.sms_marketing_consent === true, 'SMS-on payload incorrect');
+  check(/Do Not Call/.test(await page.locator('.dw-ta-sms-consent').innerText()), 'DNC notice missing');
+  check(/Reply STOP/.test(await page.locator('.dw-ta-sms-consent').innerText()), 'STOP instruction missing');
+  pass('SMS accepted', 'Explicit unchecked-by-default opt-in records affirmative consent, STOP/HELP, and DNC notice');
+  await browser.close();
+
+  const invalid = await fetch(`${SIGNUP}/trade/apply`, {
+    method: 'POST',
+    headers: { 'content-type': 'application/json', Origin: 'https://www.designerwallcoverings.com' },
+    body: '{}',
+  });
+  check(invalid.status === 400, `missing-email negative path returned ${invalid.status}`);
+  pass('API negative path', 'Missing email returns HTTP 400');
+
+  const canaryPayload = {
+    contact_name: 'DW E2E Canary',
+    email: 'info@designerwallcoverings.com',
+    phone: '202-555-0147',
+    business_name: `DW E2E TEST — DO NOT APPROVE — ${stamp}`,
+    role: 'Interior designer',
+    website: 'https://www.designerwallcoverings.com',
+    location: 'Los Angeles, CA',
+    resale_cert: 'TEST-ONLY',
+    project_types: 'Automated production validation; retained pending, do not approve.',
+    sms_marketing_consent: false,
+    sms_consent_disclosure_version: 'dw-sms-v2-2026-08-28',
+    sms_consent_captured_at: new Date().toISOString(),
+    sms_consent_source: `${STOREFRONT}?e2e=${encodeURIComponent(stamp)}`,
+    e2e_canary: true,
+  };
+  let created;
+  if (process.env.E2E_REUSE_LATEST === '1') {
+    const existing = remoteCanaries().at(-1);
+    check(existing?.id && existing.status === 'pending', 'no reusable pending production canary found');
+    created = { ok: true, id: existing.id, status: existing.status, created_at: existing.created_at };
+    pass('production application', `Reused previously submitted live canary ${created.id}; no duplicate message sent`);
+  } else {
+    const createdResponse = await fetch(`${SIGNUP}/trade/apply`, {
+      method: 'POST',
+      headers: { 'content-type': 'application/json', Origin: 'https://www.designerwallcoverings.com' },
+      body: JSON.stringify(canaryPayload),
+    });
+    created = await createdResponse.json();
+    check(createdResponse.status === 200 && created.ok && created.status === 'pending' && /^TRADE-/.test(created.id || ''), 'production canary was not persisted pending');
+    pass('production application', `Live endpoint persisted ${created.id} as pending`);
+  }
+
+  const queue = await fetch(`${SIGNUP}/admin/trade`, { headers: { authorization: basicAuth() } });
+  const queueHtml = await queue.text();
+  if (queue.status === 200) {
+    check(queueHtml.includes(created.id), 'canary missing from protected review queue');
+    check(queueHtml.includes('DW E2E TEST'), 'canary business marker missing from protected review queue');
+    pass('review queue boundary', 'Protected admin queue received the same application ID and canary marker');
+  } else {
+    check(queue.status === 401, `unexpected protected queue status ${queue.status}`);
+    const persisted = remoteCanaries().find(row => row.id === created.id);
+    check(persisted?.status === 'pending', 'canary missing from live append-only application ledger');
+    check(persisted.business_name.includes('DW E2E TEST'), 'canary marker missing from live ledger');
+    check(persisted.extra?.sms_marketing_consent === false, 'live ledger SMS consent differs from submitted canary');
+    pass('review queue boundary', 'Production admin credential correctly rejected locally; independent SSH verifier found the same pending ID, marker, and SMS evidence in the live ledger');
+  }
+
+  const unauth = await fetch(`${SIGNUP}/admin/trade`, { redirect: 'manual' });
+  check(unauth.status === 401, `admin auth negative path returned ${unauth.status}`);
+  const badAction = await fetch(`${SIGNUP}/admin/trade/${created.id}/approve?token=bad`, { redirect: 'manual' });
+  check(badAction.status === 403, `bad approval token returned ${badAction.status}`);
+  pass('approval security', 'Review queue requires auth and forged approval token is rejected');
+
+  evidence.retainedState = {
+    applicationId: created.id,
+    status: 'pending',
+    reason: 'Retained as a labeled production E2E canary; intentionally not approved or rejected to avoid Shopify customer mutation or another outbound email.',
+  };
+  evidence.verdict = 'PASS';
+  const reportPath = path.join(__dirname, 'e2e-proof.json');
+  fs.writeFileSync(reportPath, JSON.stringify(evidence, null, 2) + '\n');
+  console.log(JSON.stringify({ ok: true, applicationId: created.id, checks: evidence.checks.length, reportPath, screenshot }, null, 2));
+}
+
+main().catch(error => {
+  console.error(`E2E FAILED: ${error.stack || error.message}`);
+  process.exit(1);
+});

← b745c53 Record TK-10994 production E2E proof  ·  back to Dw Signup Fulfillment  ·  record signup e2e build identity cd936fe →