[object Object]

← back to Dw Signup Fulfillment

rep assignment: fixed DW House Account (not round-robin) per Steve

d786b75e8866bf21d6b8142c0b41c5f3634874a1 · 2026-07-28 07:52:15 -0700 · Steve

Files touched

Diff

commit d786b75e8866bf21d6b8142c0b41c5f3634874a1
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 28 07:52:15 2026 -0700

    rep assignment: fixed DW House Account (not round-robin) per Steve
---
 lib/reps.js         | 88 +++++++++++------------------------------------------
 lib/trade.js        |  6 ++--
 scripts/selftest.js | 27 ++++++----------
 server.js           |  2 +-
 4 files changed, 32 insertions(+), 91 deletions(-)

diff --git a/lib/reps.js b/lib/reps.js
index fdad843..05a88f8 100644
--- a/lib/reps.js
+++ b/lib/reps.js
@@ -1,72 +1,20 @@
 'use strict';
-// Rep round-robin with a persisted rotation cursor.
+// Designer / trade signups are ALL assigned to a single DW House Account.
+// (Steve directive 2026-07-28 — NOT round-robin across outside reps.)
 //
-// The roster (data/reps.json) is the seeded designersalesreps roster: each rep
-// has at least { name, email }. This roster uses { name, agency, ... } and does
-// NOT carry an explicit email, so we DERIVE a contact email deterministically
-// from the agency domain in contact_url (adapting to the roster we were given,
-// per the brief). At go-live Steve can drop a real `email` field per rep into
-// data/reps.json and it will be used verbatim.
-const fs = require('fs');
-const path = require('path');
-
-const DATA_DIR = path.join(__dirname, '..', 'data');
-const REPS_PATH = path.join(DATA_DIR, 'reps.json');
-const CURSOR_PATH = path.join(DATA_DIR, 'rep-cursor.json');
-
-function loadReps() {
-  const raw = JSON.parse(fs.readFileSync(REPS_PATH, 'utf8'));
-  return raw.map((r, i) => ({
-    id: r.id != null ? r.id : i + 1,
-    name: r.name || `Rep ${i + 1}`,
-    email: r.email || deriveEmail(r),
-    agency: r.agency || null,
-    city: r.city || null,
-  }));
-}
-
-// Derive a plausible sales@<domain> from contact_url when no email is present.
-// Clearly a placeholder — at go-live the real per-rep email should be added to
-// reps.json. Never invents a domain: falls back to a routable internal address.
-function deriveEmail(r) {
-  try {
-    if (r.contact_url) {
-      const host = new URL(r.contact_url).hostname.replace(/^www\./, '');
-      return `sales@${host}`;
-    }
-  } catch { /* fall through */ }
-  return 'reps@designerwallcoverings.com';
-}
-
-function readCursor() {
-  try { return JSON.parse(fs.readFileSync(CURSOR_PATH, 'utf8')); }
-  catch { return { index: -1, lastRepId: null, assignments: 0 }; }
-}
-
-function writeCursor(c) {
-  fs.mkdirSync(DATA_DIR, { recursive: true });
-  fs.writeFileSync(CURSOR_PATH, JSON.stringify(c, null, 2));
-}
-
-// Advance the round-robin and return the next rep. Persists the cursor so the
-// rotation survives restarts. This is the "least-loaded via round-robin" the
-// brief asks for — with an even roster, round-robin IS even load distribution.
-function nextRep() {
-  const reps = loadReps();
-  if (!reps.length) throw new Error('no reps in roster');
-  const cur = readCursor();
-  const index = (cur.index + 1) % reps.length;
-  const rep = reps[index];
-  writeCursor({ index, lastRepId: rep.id, assignments: (cur.assignments || 0) + 1, updated_at: new Date().toISOString() });
-  return rep;
-}
-
-// Peek without advancing (for previews / dry inspection).
-function peekNext() {
-  const reps = loadReps();
-  const cur = readCursor();
-  const index = (cur.index + 1) % reps.length;
-  return reps[index];
-}
-
-module.exports = { loadReps, nextRep, peekNext, readCursor, deriveEmail, REPS_PATH, CURSOR_PATH };
+// The house-account identity is env-configurable so the notify destination can
+// be pointed wherever DW wants; it defaults to the office inbox George reads.
+// data/reps.json (the outside-rep roster) is retained in the repo for reference
+// but is intentionally NOT used for assignment.
+const HOUSE_ACCOUNT = {
+  id: 'dw-house',
+  name: process.env.HOUSE_ACCOUNT_NAME || 'DW House Account',
+  email: (process.env.HOUSE_ACCOUNT_EMAIL || 'info@designerwallcoverings.com').trim().toLowerCase(),
+};
+
+function houseAccount() { return { ...HOUSE_ACCOUNT }; }
+
+// Canonical assignment for a new trade customer: always the DW House Account.
+function assignRep() { return houseAccount(); }
+
+module.exports = { houseAccount, assignRep, HOUSE_ACCOUNT };
diff --git a/lib/trade.js b/lib/trade.js
index eb851d8..90f8e80 100644
--- a/lib/trade.js
+++ b/lib/trade.js
@@ -6,7 +6,7 @@
 // full-file rewrite is simplest and safe).
 //
 // APPROVE performs, all DRY_RUN-safe:
-//   (a) pick the least-loaded rep via round-robin (lib/reps.js persisted cursor)
+//   (a) assign to the single DW House Account (lib/reps.js — fixed assignment)
 //   (b) tag the customer `trade` via Admin API (tagsAdd — appends, no clobber)
 //   (c) set customer metafield custom.assigned_rep
 //   (d) notify the assigned rep by email (George)
@@ -78,8 +78,8 @@ async function approve(id) {
 
   const steps = [];
 
-  // (a) least-loaded rep via round-robin
-  const rep = reps.nextRep();
+  // (a) assign to the DW House Account (fixed — not round-robin)
+  const rep = reps.assignRep();
   steps.push({ step: 'assign_rep', rep: { id: rep.id, name: rep.name, email: rep.email } });
 
   const custId = app.shopify_customer_id;
diff --git a/scripts/selftest.js b/scripts/selftest.js
index 860afac..4f2b8fc 100644
--- a/scripts/selftest.js
+++ b/scripts/selftest.js
@@ -118,23 +118,16 @@ async function main() {
   else fail('application still pending after approve');
 
   // ---------------------------------------------------------------------------
-  hr('(d) round-robin across the full rep roster');
-  const roster = reps.loadReps();
-  console.log('  roster size = ' + roster.length);
-  const seen = [];
-  for (let i = 0; i < roster.length + 2; i++) {
-    const r = reps.nextRep();
-    seen.push(r.name);
-  }
-  console.log('  rotation (' + seen.length + ' picks): ');
-  seen.forEach((n, i) => console.log('    ' + String(i + 1).padStart(2) + '. ' + n));
-  // The first roster.length picks should cover every distinct rep exactly once.
-  const firstCycle = new Set(seen.slice(0, roster.length));
-  if (firstCycle.size === roster.length) ok('first ' + roster.length + ' picks covered EVERY rep exactly once (even round-robin)');
-  else fail('round-robin did not cover all reps in one cycle (got ' + firstCycle.size + '/' + roster.length + ')');
-  // Wrap-around: pick N+1 should equal pick 1.
-  if (seen[roster.length] === seen[0]) ok('cursor wrapped around correctly (pick ' + (roster.length + 1) + ' === pick 1)');
-  else fail('cursor did not wrap');
+  hr('(d) fixed assignment to the DW House Account');
+  const picks = [];
+  for (let i = 0; i < 5; i++) picks.push(reps.assignRep());
+  const house = reps.houseAccount();
+  console.log('  house account = ' + house.name + ' <' + house.email + '>');
+  console.log('  5 consecutive assignments: ' + picks.map(p => p.name).join(', '));
+  // Every assignment must be the SAME single house account (no rotation).
+  if (picks.every(p => p.name === house.name && p.email === house.email)) ok('all assignments go to the single DW House Account (no round-robin)');
+  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');
 
   hr('SUMMARY');
   if (failures === 0) { console.log('  ALL CHECKS PASSED — DRY_RUN, nothing written to Shopify, no email sent.'); }
diff --git a/server.js b/server.js
index 340e62e..e50e4c0 100644
--- a/server.js
+++ b/server.js
@@ -88,7 +88,7 @@ app.post('/admin/trade/:id/reject', adminAuth, async (req, res) => {
 });
 
 // --- Introspection helpers ---
-app.get('/reps/next', adminAuth, (_req, res) => res.json({ next: reps.peekNext(), cursor: reps.readCursor() }));
+app.get('/reps/next', adminAuth, (_req, res) => res.json({ assigned: reps.houseAccount() }));
 
 // Manual retail-code trigger (admin) — handy for go-live smoke test without a
 // real webhook. Honors the ?mode=discount query to exercise the alternative path.

← bc2558d initial scaffold: DW signup fulfillment (retail gift-card +  ·  back to Dw Signup Fulfillment  ·  retail: function-backed unique sample codes (safe sample-loc c323e03 →