← back to Dw Signup Fulfillment
prove production signup and sample activation
aa1c2a58a373dadca2752ee5faf9add598d103a6 · 2026-08-31 01:45:28 -0700 · Steve Abrams
Files touched
M lib/config.jsA lib/sample-ledger.jsA lib/shopify-oauth.jsM lib/trade.jsA scripts/activate-shopify.jsA scripts/production-validation.jsA scripts/sample-ledger-test.jsM scripts/selftest.jsM server.jsA verification/artifacts/signup-2026-08-31T08-31-53-984Z.pngA verification/artifacts/signup-2026-08-31T08-33-20-634Z.pngA verification/artifacts/signup-2026-08-31T08-34-31-402Z.pngA verification/artifacts/signup-2026-08-31T08-42-00-304Z.pngM verification/e2e-proof.jsonA verification/signup-e2e.js
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/<token></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 →