← back to Dw Signup Fulfillment
TK-11185: server-side find-or-create customer at trade-apply time (born linked, always approvable)
31cd4ddbe210a4f3d87a3e23ffa9f4c4fedbb32f · 2026-09-03 11:12:01 -0700 · Steve Abrams
Public trade applications carried no shopify_customer_id, so approve() hard-failed
cannot_resolve_customer → designers "filled it out and nothing happened." DW is on
NEW_CUSTOMER_ACCOUNTS (passwordless OTP — verified live 2026-09-03), so the account
cannot be carried through the register page; it is minted server-side via the Admin API.
- lib/shopify.js: createCustomer() (GraphQL customerCreate, DRY_RUN-safe, REST-numeric id
from GID tail, handles email-taken re-resolve + phone-format retry, deps seam for tests)
and findOrCreateCustomer() (find first, create only if none — linkage by resolved id).
- lib/trade.js: applyAndLink() find-or-creates + stamps shopify_customer_id onto the
persisted application, records link_status/link_error, and graceful-degrades (still
persists unlinked if create fails — never a black hole).
- server.js /trade/apply: awaits applyAndLink before firing the office notify, so a linked
app never yields an un-approvable review card; applicant still sees success on failure.
- shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid: staged snippet
replacing the "Application received" dead-end with two-step OTP account sign-in (/account);
logged-in path preserved. NOT deployed — live theme PUT is Steve-gated.
- selftest.js (c2): create+link, existing reuse, create-failure degrade, approve() resolving,
plus createCustomer taken/phone-retry branch coverage. Full suite green in DRY_RUN.
Contrarian panel: SHIP IT (4-1), dissent (untested error branches) addressed before commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru
Files touched
M lib/shopify.jsM lib/trade.jsM scripts/selftest.jsM server.jsA shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid
Diff
commit 31cd4ddbe210a4f3d87a3e23ffa9f4c4fedbb32f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 3 11:12:01 2026 -0700
TK-11185: server-side find-or-create customer at trade-apply time (born linked, always approvable)
Public trade applications carried no shopify_customer_id, so approve() hard-failed
cannot_resolve_customer → designers "filled it out and nothing happened." DW is on
NEW_CUSTOMER_ACCOUNTS (passwordless OTP — verified live 2026-09-03), so the account
cannot be carried through the register page; it is minted server-side via the Admin API.
- lib/shopify.js: createCustomer() (GraphQL customerCreate, DRY_RUN-safe, REST-numeric id
from GID tail, handles email-taken re-resolve + phone-format retry, deps seam for tests)
and findOrCreateCustomer() (find first, create only if none — linkage by resolved id).
- lib/trade.js: applyAndLink() find-or-creates + stamps shopify_customer_id onto the
persisted application, records link_status/link_error, and graceful-degrades (still
persists unlinked if create fails — never a black hole).
- server.js /trade/apply: awaits applyAndLink before firing the office notify, so a linked
app never yields an un-approvable review card; applicant still sees success on failure.
- shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid: staged snippet
replacing the "Application received" dead-end with two-step OTP account sign-in (/account);
logged-in path preserved. NOT deployed — live theme PUT is Steve-gated.
- selftest.js (c2): create+link, existing reuse, create-failure degrade, approve() resolving,
plus createCustomer taken/phone-retry branch coverage. Full suite green in DRY_RUN.
Contrarian panel: SHIP IT (4-1), dissent (untested error branches) addressed before commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X3co77k7JzTkAJRdemt6Ru
---
lib/shopify.js | 82 +++++++++++++++++++-
lib/trade.js | 52 ++++++++++++-
scripts/selftest.js | 68 +++++++++++++++++
server.js | 24 +++++-
.../snippets/dw-trade-apply.liquid | 89 ++++++++++++++++++++++
5 files changed, 309 insertions(+), 6 deletions(-)
diff --git a/lib/shopify.js b/lib/shopify.js
index 9863375..32c2d7d 100644
--- a/lib/shopify.js
+++ b/lib/shopify.js
@@ -124,6 +124,85 @@ async function findCustomerByEmail(emailAddr) {
return c && c.id ? c.id : null;
}
+// Create a Shopify customer via the GraphQL Admin API (customerCreate mutation).
+// DRY_RUN-safe: graphql() short-circuits mutations under DRY_RUN / no-token and
+// returns the synthetic envelope below, so a dry run "creates" a plausible id.
+//
+// DW runs NEW CUSTOMER ACCOUNTS (passwordless OTP, Shopify-hosted) — VERIFIED live
+// 2026-09-03: shop.customerAccountsV2.customerAccountsVersion = NEW_CUSTOMER_ACCOUNTS.
+// You therefore CANNOT carry a token through Shopify's register page into the
+// customers/create webhook; the account must be minted server-side here so a public
+// trade application (no store account) is immediately linkable + approvable.
+//
+// Returns a NUMERIC id (e.g. 600000123) to stay consistent with findCustomerByEmail()
+// + the REST addTags()/setCustomerMetafield() helpers, which all key on numeric ids.
+// The GraphQL customer.id is a GID (gid://shopify/Customer/123) — we return its tail.
+// `deps` is a testability seam ONLY (default = the module internals). createCustomer
+// calls the module-local graphql()/findCustomerByEmail() directly, which the selftest
+// can't monkeypatch through the exports; injecting them here lets the taken/phone-retry
+// branches be unit-tested without a live Shopify call. Production callers pass nothing.
+async function createCustomer(emailAddr, fields = {}, deps = {}) {
+ const gql = deps.graphql || graphql;
+ const findByEmail = deps.findCustomerByEmail || findCustomerByEmail;
+ const e = (emailAddr || '').trim().toLowerCase();
+ if (!e) return { ok: false, id: null, error: 'email_required' };
+
+ const input = { email: e };
+ if (fields.firstName) input.firstName = String(fields.firstName).trim();
+ if (fields.lastName) input.lastName = String(fields.lastName).trim();
+ if (fields.phone) input.phone = String(fields.phone).trim();
+
+ const query = `mutation createCust($input: CustomerInput!) {
+ customerCreate(input: $input) {
+ customer { id email }
+ userErrors { field message }
+ }
+ }`;
+ const synthetic = () => ({
+ customerCreate: {
+ customer: { id: 'gid://shopify/Customer/' + (600000000 + Math.floor(Math.random() * 1e6)), email: e },
+ userErrors: [],
+ },
+ });
+
+ const r = await gql(query, { input }, { synthetic });
+ const payload = r && r.json && r.json.data ? r.json.data.customerCreate : null;
+ const userErrors = (payload && payload.userErrors) || [];
+
+ // A phone/format userError shouldn't sink the whole create — retry once without the
+ // phone (email is the only field that actually matters for linkage/OTP login).
+ if (userErrors.length && input.phone && userErrors.some(u => /phone/i.test(u.field || '') || /phone/i.test(u.message || ''))) {
+ delete input.phone;
+ return createCustomer(e, { firstName: input.firstName, lastName: input.lastName }, deps);
+ }
+
+ // "Email has already been taken" — a customer already exists (race vs our pre-check,
+ // or a webhook created it meanwhile). Re-resolve by email and reuse that id.
+ if (userErrors.some(u => /taken|already/i.test(u.message || ''))) {
+ const existing = await findByEmail(e);
+ if (existing) return { ok: true, id: existing, created: false, via: 'existing_taken', dryRun: r.dryRun || false };
+ return { ok: false, id: null, error: 'email_taken_unresolvable', userErrors };
+ }
+
+ if (userErrors.length) return { ok: false, id: null, error: 'user_errors', userErrors };
+
+ const gid = payload && payload.customer && payload.customer.id ? String(payload.customer.id) : '';
+ const numericId = gid ? gid.split('/').pop() : null;
+ if (!numericId) return { ok: false, id: null, error: 'no_id_returned', raw: r && r.json };
+ return { ok: true, id: numericId, created: true, via: 'created', dryRun: r.dryRun || false };
+}
+
+// Resolve a customer id for an email, creating the account if none exists. Linkage is
+// ALWAYS by the resolved customer id — never by later email-guessing. Returns
+// { ok, id, created, via } or { ok:false, id:null, error } (caller degrades gracefully).
+async function findOrCreateCustomer(emailAddr, fields = {}) {
+ const e = (emailAddr || '').trim().toLowerCase();
+ if (!e) return { ok: false, id: null, error: 'email_required' };
+ const existing = await findCustomerByEmail(e);
+ if (existing) return { ok: true, id: existing, created: false, via: 'existing' };
+ return createCustomer(e, fields);
+}
+
// Append a tag to a customer without dropping existing tags.
async function addTags(customerId, newTags) {
const wanted = (Array.isArray(newTags) ? newTags : [newTags]).map(t => t.trim()).filter(Boolean);
@@ -204,6 +283,7 @@ async function graphql(query, variables, opts = {}) {
}
module.exports = {
- request, graphql, createGiftCard, disableGiftCard, updateCustomer, getCustomer, getCustomerMetafield, findCustomerByEmail, addTags,
+ request, graphql, createGiftCard, disableGiftCard, updateCustomer, getCustomer, getCustomerMetafield, findCustomerByEmail,
+ createCustomer, findOrCreateCustomer, addTags,
setCustomerMetafield, createWebhook, base,
};
diff --git a/lib/trade.js b/lib/trade.js
index 05062a7..a58d839 100644
--- a/lib/trade.js
+++ b/lib/trade.js
@@ -97,6 +97,56 @@ function apply(body) {
return app;
}
+// Apply AND link in one request. This is the go-forward intake path: a public trade
+// application carries NO shopify_customer_id, which used to leave approve() hard-failing
+// `cannot_resolve_customer` (TK-11185 — "I filled it out and nothing happened"). Here we
+// server-side find-or-create the customer FIRST (DW is on New Customer Accounts / OTP, so
+// the account can't be minted through the register page — it must be minted via Admin API),
+// stamp the resolved id onto the persisted application, and only then hand back so the
+// caller can notify the office. Approve() can never hit cannot_resolve_customer for a new
+// app that came through here.
+//
+// GRACEFUL DEGRADE (directive §3): if the Shopify create fails, we STILL persist the
+// application (never a black hole) with shopify_customer_id:null + a link_error, and the
+// applicant still sees success. A later pass can resolve those (they're queryable by
+// link_status:'unlinked'). Returns { app, linkage:{ ok, id, created, via, error } }.
+async function applyAndLink(body) {
+ const emailAddr = (body.email || '').trim().toLowerCase();
+ // Best-effort name for the customer record (New Customer Accounts shows it in-account).
+ const contact = body.contact_name || body.name || '';
+ const firstName = body.first_name || (contact ? String(contact).split(' ')[0] : '');
+ const lastName = body.last_name || (contact ? String(contact).split(' ').slice(1).join(' ') : '');
+
+ let linkage = { ok: false, id: null, created: false, via: null, error: null };
+ try {
+ const r = await shopify.findOrCreateCustomer(emailAddr, { firstName, lastName, phone: body.phone });
+ if (r && r.ok && r.id) {
+ linkage = { ok: true, id: r.id, created: !!r.created, via: r.via || null, error: null, dryRun: r.dryRun || false };
+ } else {
+ linkage.error = (r && (r.error || 'unknown')) || 'unknown';
+ if (r && r.userErrors) linkage.userErrors = r.userErrors;
+ }
+ } catch (e) {
+ linkage.error = 'exception:' + (e && e.message ? e.message : String(e));
+ }
+
+ // Stamp the resolved id (or null) BEFORE persisting so the record is born linked.
+ const app = apply({ ...body, email: emailAddr, shopify_customer_id: linkage.id || null });
+ // Annotate linkage on the persisted record so a later unlinked-recovery pass can find it.
+ // (apply() already appended; rewrite the single row with the annotation.)
+ const rows = readAll();
+ const rec = rows.find(a => a.id === app.id);
+ if (rec) {
+ rec.link_status = linkage.ok ? 'linked' : 'unlinked';
+ rec.link_via = linkage.via || null;
+ rec.link_created = linkage.created || false;
+ if (!linkage.ok) rec.link_error = linkage.error || 'unknown';
+ rewriteAll(rows);
+ Object.assign(app, { link_status: rec.link_status, link_via: rec.link_via, link_created: rec.link_created, link_error: rec.link_error });
+ }
+ return { app, linkage };
+}
+
function listPending() {
return readAll().filter(a => a.status === 'pending').sort((a, b) => a.created_at.localeCompare(b.created_at));
}
@@ -193,4 +243,4 @@ function summarizeShopify(r) {
return { status: r.status, ok: r.ok };
}
-module.exports = { apply, listPending, get, approve, reject, readAll, APPS_PATH, actionToken, verifyActionToken };
+module.exports = { apply, applyAndLink, listPending, get, approve, reject, readAll, APPS_PATH, actionToken, verifyActionToken };
diff --git a/scripts/selftest.js b/scripts/selftest.js
index c79da53..2782e87 100644
--- a/scripts/selftest.js
+++ b/scripts/selftest.js
@@ -155,6 +155,74 @@ async function main() {
if (trade.listPending().length === pendingBefore - 1) ok('application moved out of pending queue');
else fail('application still pending after approve');
+ // ---------------------------------------------------------------------------
+ // TK-11185 — server-side find-or-create at apply-time so every public application is
+ // born LINKED and approvable (the old public path left shopify_customer_id:null →
+ // approve() hard-failed cannot_resolve_customer → "I filled it out and nothing happened").
+ hr('(c2) TK-11185 — applyAndLink find-or-create + graceful degrade + approve resolves');
+
+ // createCustomer is DRY_RUN-safe (graphql mutation short-circuits) — no stub, no network.
+ const ccDry = await shopify.createCustomer('dry@run.test', { firstName: 'Dry', phone: '310-555-0000' });
+ if (ccDry.ok && ccDry.created && ccDry.dryRun && /^\d+$/.test(String(ccDry.id))) ok('createCustomer is DRY_RUN-safe → synthetic numeric id ' + ccDry.id + ' (no live write)');
+ else fail('createCustomer not DRY_RUN-safe: ' + JSON.stringify(ccDry));
+
+ // createCustomer error branches — inject graphql/findCustomerByEmail via the deps seam
+ // so the taken/phone-retry paths run WITHOUT a live call (the DRY_RUN happy path above
+ // never exercises these branches; createCustomer calls the module-internal fns directly).
+ // "Email has already been taken" → re-resolve by email and reuse that id.
+ const ccTaken = await shopify.createCustomer('taken@studio.com', {}, {
+ graphql: async () => ({ ok: true, json: { data: { customerCreate: { customer: null, userErrors: [{ field: 'email', message: 'Email has already been taken' }] } } } }),
+ findCustomerByEmail: async () => '55',
+ });
+ if (ccTaken.ok && ccTaken.id === '55' && ccTaken.created === false && ccTaken.via === 'existing_taken') ok('createCustomer: "email taken" → re-resolves + reuses existing id 55 (no dup)');
+ else fail('createCustomer taken-branch failed: ' + JSON.stringify(ccTaken));
+ // Phone-format userError → retry once WITHOUT phone, then succeed. First call returns a
+ // phone error, second call (no phone) returns a created customer.
+ let gqlCall = 0;
+ const ccPhone = await shopify.createCustomer('phone@studio.com', { firstName: 'Pat', phone: 'not-a-phone' }, {
+ graphql: async (q, vars) => {
+ gqlCall++;
+ if (gqlCall === 1 && vars.input.phone) return { ok: true, json: { data: { customerCreate: { customer: null, userErrors: [{ field: 'phone', message: 'Phone is invalid' }] } } } };
+ return { ok: true, json: { data: { customerCreate: { customer: { id: 'gid://shopify/Customer/900123', email: vars.input.email }, userErrors: [] } } } };
+ },
+ });
+ if (ccPhone.ok && ccPhone.id === '900123' && ccPhone.created === true && gqlCall === 2) ok('createCustomer: bad phone → retries once WITHOUT phone → created id 900123');
+ else fail('createCustomer phone-retry failed: ' + JSON.stringify(ccPhone) + ' calls=' + gqlCall);
+
+ // Stub the resolver per-scenario so linkage is deterministic + makes NO live call.
+ const _foc = shopify.findOrCreateCustomer;
+
+ // Scenario 1 — NEW account: no existing customer → create + stamp.
+ shopify.findOrCreateCustomer = async () => ({ ok: true, id: '700000001', created: true, via: 'created' });
+ const s1 = await trade.applyAndLink({ email: 'New@Studio.com', business_name: 'New Studio', contact_name: 'Nadia Newman' });
+ if (s1.linkage.ok && s1.app.shopify_customer_id === '700000001' && s1.app.link_status === 'linked' && s1.app.link_created === true) ok('new-account: created + stamped id 700000001, link_status=linked');
+ else fail('new-account link failed: ' + JSON.stringify(s1));
+
+ // Scenario 2 — EXISTING account: reuse the found id, do NOT create.
+ shopify.findOrCreateCustomer = async () => ({ ok: true, id: '42', created: false, via: 'existing' });
+ const s2 = await trade.applyAndLink({ email: 'Repeat@Studio.com', business_name: 'Repeat Studio' });
+ if (s2.linkage.ok && s2.app.shopify_customer_id === '42' && s2.app.link_created === false && s2.app.link_via === 'existing') ok('existing-account: reused id 42 by resolved customer id (not email-guessing), no create');
+ else fail('existing-account reuse failed: ' + JSON.stringify(s2));
+
+ // Scenario 3 — Shopify create FAILS → still persist (never a black hole), unlinked + link_error.
+ shopify.findOrCreateCustomer = async () => ({ ok: false, id: null, error: 'user_errors' });
+ const s3 = await trade.applyAndLink({ email: 'Fails@Studio.com', business_name: 'Fails Studio' });
+ const s3persisted = trade.get(s3.app.id);
+ if (s3persisted && s3.app.shopify_customer_id === null && s3.app.link_status === 'unlinked' && s3.app.link_error === 'user_errors') ok('create-failure: application STILL persisted (unlinked, link_error recorded) — applicant sees success, no black hole');
+ else fail('graceful degrade failed: ' + JSON.stringify(s3));
+
+ // Scenario 4 — a linked new app can now be APPROVED (no cannot_resolve_customer).
+ shopify.findOrCreateCustomer = async () => ({ ok: true, id: '800000009', created: true, via: 'created' });
+ const s4 = await trade.applyAndLink({ email: 'Approve@Me.com', business_name: 'Approve Me Co' });
+ shopify.getCustomer = async () => ({ ok: true, json: { customer: { id: 800000009, tags: 'trade' } } }); // addTags merge read
+ const s4ap = await trade.approve(s4.app.id);
+ shopify.getCustomer = _gc; // restore
+ const s4resolve = s4ap.steps && s4ap.steps.find(s => s.step === 'resolve_customer');
+ if (s4ap.ok && s4ap.status === 'approved' && s4ap.error !== 'cannot_resolve_customer' && s4resolve && s4resolve.via === 'application') ok('approve() resolves via the STAMPED id (via=application) — cannot_resolve_customer can never fire for a linked app');
+ else fail('approve did not resolve via stamped id: ' + JSON.stringify(s4ap));
+
+ shopify.findOrCreateCustomer = _foc; // restore
+
// ---------------------------------------------------------------------------
hr('(d) fixed assignment to the DW House Account');
const picks = [];
diff --git a/server.js b/server.js
index 207866c..c758e08 100644
--- a/server.js
+++ b/server.js
@@ -146,11 +146,27 @@ function tradeCors(req, res, next) {
app.options('/trade/apply', tradeCors);
// --- Trade application intake (public) ---
-app.post('/trade/apply', tradeCors, (req, res) => {
+app.post('/trade/apply', tradeCors, async (req, res) => {
const b = req.body || {};
if (!b.email) return res.status(400).json({ ok: false, error: 'email required' });
- const created = trade.apply(b);
- // Email the office inbox a review card with one-click Approve/Reject buttons.
+ // TK-11185: synchronously find-or-create the Shopify customer + stamp the id, so the
+ // application is born LINKED and approve() can never hard-fail cannot_resolve_customer.
+ // DW is on New Customer Accounts (OTP) so the account is minted server-side (Admin API),
+ // not carried through a register page. Degrades gracefully — if the create fails the app
+ // is still persisted (unlinked, dead-lettered by the log below), applicant still sees ok.
+ let created, linkage;
+ try {
+ ({ app: created, linkage } = await trade.applyAndLink(b));
+ } catch (e) {
+ console.error('[trade] applyAndLink error:', e.message);
+ created = trade.apply(b); // absolute backstop — never a black hole
+ linkage = { ok: false, error: 'route_exception:' + e.message };
+ }
+ if (!linkage.ok) {
+ console.error(`[trade] application ${created.id} (${created.email}) persisted UNLINKED (${linkage.error}) — review /admin/trade; resolvable in a later recovery pass.`);
+ }
+ // Email the office inbox a review card with one-click Approve/Reject buttons — fired ONLY
+ // after the link attempt resolved, so a linked app never yields an un-approvable card.
// Fire-and-forget so the applicant's response isn't blocked on George; DRY_RUN-safe.
notifyTradeApplication(created).catch(e => console.error('[trade] notify failed:', e.message));
// Also send the applicant the "about us + services" welcome letter (approved 2026-08-07).
@@ -161,7 +177,7 @@ app.post('/trade/apply', tradeCors, (req, res) => {
const r = await email.sendEmail({ to: created.email, subject, html, source: 'designer-welcome' });
if (r && r.ok === false) console.error(`[trade] designer-welcome send FAILED for ${created.email}: ${r.error || r.status}`);
})().catch(e => console.error('[trade] designer-welcome error:', e.message));
- res.json({ ok: true, id: created.id, status: created.status, created_at: created.created_at });
+ res.json({ ok: true, id: created.id, status: created.status, created_at: created.created_at, linked: !!linkage.ok });
});
// --- Retail sample claim (public, double opt-in) — email in, verify letter out. ---
diff --git a/shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid b/shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid
new file mode 100644
index 0000000..284c0b6
--- /dev/null
+++ b/shopify/staged/trade-account-ux-20260828/snippets/dw-trade-apply.liquid
@@ -0,0 +1,89 @@
+{%- comment -%}
+ dw-trade-apply.liquid — STAGED (TK-11185, 2026-09-03) — NOT YET DEPLOYED
+ ---------------------------------------------------------------------------
+ Designer-side, two-step in-flow trade application. The SERVER (signup.designer-
+ wallcoverings.com /trade/apply) now server-side find-or-creates the applicant's
+ Shopify customer at apply-time and stamps shopify_customer_id, so the account
+ EXISTS the moment the form succeeds. DW runs New Customer Accounts (passwordless
+ OTP) — VERIFIED live 2026-09-03 (shop.customerAccountsV2 = NEW_CUSTOMER_ACCOUNTS)
+ — so the old "Application received." dead-end is replaced by routing the applicant
+ into OTP account login for their now-linked account (Steve's two-step in-flow).
+
+ DEPLOY NOTE: the LIVE trade-apply UX currently lives inside the theme snippet
+ `snippets/dw-signin-modal.liquid` (the .then success handler at ~line 165 of the
+ 2026-08-06 backup). At deploy, replace THAT success handler's markup with the
+ success block below (the `renderSuccess()` HTML). This standalone snippet is the
+ canonical source-of-truth for the corrected flow; see the deploy memo.
+
+ Logged-in visitors already carry customer.id (Liquid) — keep that path: a signed-in
+ designer never sees the OTP button, they see "you're signed in, application received."
+{%- endcomment -%}
+
+<div id="dw-trade-apply" data-dw-trade-apply>
+ <form data-dwsm-trade novalidate>
+ <label>Business name<input type="text" name="business_name" autocomplete="organization" required></label>
+ <label>Work email<input type="email" name="email" autocomplete="email" required
+ {%- if customer %} value="{{ customer.email }}"{% endif -%}></label>
+ <label>Resale certificate #<input type="text" name="resale_cert"></label>
+ <button type="submit" data-dwsm-trade-submit>Apply for Trade Access</button>
+ </form>
+</div>
+
+<script>
+(function(){
+ var root = document.getElementById('dw-trade-apply');
+ if(!root) return;
+ var TRADE_APPLY_URL = 'https://signup.designerwallcoverings.com/trade/apply';
+ // New Customer Accounts (OTP) login. /account on the storefront redirects to the
+ // Shopify-hosted passwordless login; account URL verified: shopify.com/1541177456/account.
+ var ACCOUNT_LOGIN_URL = '/account';
+ {%- if customer %}var IS_LOGGED_IN = true;{% else %}var IS_LOGGED_IN = false;{% endif -%}
+ var form = root.querySelector('[data-dwsm-trade]');
+
+ // Success is a TWO-STEP hand-off, not a dead end: the account now exists (server minted
+ // it), so we route the applicant into OTP sign-in to finish. Logged-in designers already
+ // have a session — they just get the confirmation, no sign-in prompt.
+ function renderSuccess(email){
+ if (IS_LOGGED_IN) {
+ root.innerHTML =
+ '<div style="text-align:center;padding:12px 4px;color:#333;">' +
+ '<p style="margin:0 0 8px;font-weight:600;">Application received.</p>' +
+ '<p style="margin:0;color:#555;">You\'re signed in. We\'ll email you the moment your trade pricing is approved.</p>' +
+ '</div>';
+ return;
+ }
+ var e = (email||'').replace(/</g,'<');
+ root.innerHTML =
+ '<div style="text-align:center;padding:12px 4px;color:#333;">' +
+ '<p style="margin:0 0 8px;font-weight:600;">Application received — your account is set up.</p>' +
+ '<p style="margin:0 0 14px;color:#555;">Sign in with <strong>' + e + '</strong> to finish. ' +
+ 'We use a one-time code sent to your email — no password needed. ' +
+ 'You\'ll get trade pricing the moment we approve you.</p>' +
+ '<a href="' + ACCOUNT_LOGIN_URL + '" ' +
+ 'style="display:inline-block;padding:11px 22px;background:#111;color:#fff;text-decoration:none;' +
+ 'border-radius:2px;font:600 14px/1 system-ui,sans-serif;letter-spacing:.02em;">Sign in to your account</a>' +
+ '</div>';
+ }
+
+ root.addEventListener('click', function(e){
+ if(!e.target.closest('[data-dwsm-trade-submit]')) return;
+ e.preventDefault();
+ if(!form) return;
+ var body = {
+ business_name:(form.business_name.value||'').trim(),
+ email:(form.email.value||'').trim(),
+ resale_cert:(form.resale_cert.value||'').trim()
+ };
+ if(!body.email||!body.business_name){ alert('Business name and work email are required.'); return; }
+ var btn = e.target.closest('[data-dwsm-trade-submit]');
+ if(btn){ btn.disabled = true; btn.textContent = 'Submitting…'; }
+ fetch(TRADE_APPLY_URL,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
+ .then(function(r){return r.json();})
+ .then(function(){ renderSuccess(body.email); })
+ .catch(function(){
+ if(btn){ btn.disabled = false; btn.textContent = 'Apply for Trade Access'; }
+ alert('Sorry, something went wrong. Please try again.');
+ });
+ });
+})();
+</script>
← 48d0832 TK-11120: definitive checkout test — verified-sample caps at
·
back to Dw Signup Fulfillment
·
TK-11185: rate-limit public /trade/apply (per-IP) — close th eba3bdc →