[object Object]

← back to Designer Wallcoverings

Harden PJ mailer draft safety and mobile layout

96a93b907fabadbef2ce66c1fb3aa35087aeb24a · 2026-08-30 09:39:30 -0700 · Steve

Files touched

Diff

commit 96a93b907fabadbef2ce66c1fb3aa35087aeb24a
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Aug 30 09:39:30 2026 -0700

    Harden PJ mailer draft safety and mobile layout
---
 mailers/cc-api/build-phillip-jeffries-campaign.js |  8 +++--
 mailers/cc-api/cc-client-gate.test.js             | 41 +++++++++++++++++++++++
 mailers/cc-api/cc-client.js                       | 10 ++++--
 mailers/phillip-jeffries-fall-2026-launch.html    |  8 ++---
 4 files changed, 59 insertions(+), 8 deletions(-)

diff --git a/mailers/cc-api/build-phillip-jeffries-campaign.js b/mailers/cc-api/build-phillip-jeffries-campaign.js
index 1eb27314..6b27f9c9 100644
--- a/mailers/cc-api/build-phillip-jeffries-campaign.js
+++ b/mailers/cc-api/build-phillip-jeffries-campaign.js
@@ -26,12 +26,12 @@ const DRY_RUN = argv.includes('--dry-run') || !CONFIRM; // default dry-run
 const CAMPAIGN = {
   name: `Phillip Jeffries — Fall 2026 Collection — ${new Date().toISOString().slice(0, 10)}`,
   subject: 'Phillip Jeffries Fall 2026 — fifty years in the making',
-  preheader: 'A celebration of artisanal history: Idyllic, Cirque de Soho, Coco Weave & Plush Pillars — the new Phillip Jeffries Fall 2026 Collection, available to the trade at Designer Wallcoverings. Shop it online or see it in person at our Sherman Oaks showroom.',
+  preheader: 'A celebration of artisanal history: Idyllic, Cirque de Soho, Coco Weave & Plush Pillars — the new Phillip Jeffries Fall 2026 Collection. Explore it online or experience Phillip Jeffries materials at our Sherman Oaks showroom.',
   fromName: 'Designer Wallcoverings',
   fromEmail: 'steve@designerwallcoverings.com',
   replyTo: 'steve@designerwallcoverings.com',
   physicalAddress: {
-    address_line1: '15442 Ventura Bl',
+    address_line1: '15442 Ventura Blvd',
     city: 'Sherman Oaks',
     state_code: 'CA',
     postal_code: '91403',
@@ -40,6 +40,7 @@ const CAMPAIGN = {
 };
 
 const PLACEHOLDER_ADDRESS_MARKER = '15442 Ventura Boulevard #102';
+const PUBLISHED_SHOWROOM_ADDRESS = '15442 Ventura Blvd';
 
 function loadHtml() {
   if (!fs.existsSync(MAILER)) throw new Error(`mailer not found: ${MAILER}`);
@@ -73,9 +74,12 @@ function runChecklist(html) {
   const addrIsPlaceholder = (addr.address_line1 || '').includes(PLACEHOLDER_ADDRESS_MARKER);
   add('physical address complete', addrComplete, JSON.stringify(addr));
   add('physical address non-placeholder', addrComplete && !addrIsPlaceholder, addrIsPlaceholder ? 'matches placeholder marker' : 'confirmed non-placeholder');
+  add('physical address matches published showroom address', addr.address_line1 === PUBLISHED_SHOWROOM_ADDRESS, addr.address_line1 || '(missing)');
   add('html content non-empty', !!html && html.length > 200, `${html.length} bytes`);
   const bannedHits = visibleWallpaperHits(html);
   add('no banned word "Wallpaper" (visible copy + alt)', bannedHits.length === 0, bannedHits.length ? `found:\n      - ${bannedHits.join('\n      - ')}` : 'clean (alt + visible text)');
+  const inventoryClaims = `${CAMPAIGN.preheader}\n${html}`.match(/\bstocked to the trade\b|\bevery one is waiting\b|\bmemo sample of any\b/gi) || [];
+  add('no unverified inventory/blanket-availability claims', inventoryClaims.length === 0, inventoryClaims.length ? inventoryClaims.join(', ') : 'clean');
   const srcs = extractImageSrcs(html);
   const nonHttps = srcs.filter((s) => !isAbsoluteHttps(s));
   add('all image src absolute https', nonHttps.length === 0, nonHttps.length ? `${nonHttps.length}/${srcs.length} not absolute-https` : `${srcs.length}/${srcs.length} absolute-https`);
diff --git a/mailers/cc-api/cc-client-gate.test.js b/mailers/cc-api/cc-client-gate.test.js
new file mode 100644
index 00000000..1ffe9c50
--- /dev/null
+++ b/mailers/cc-api/cc-client-gate.test.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { execFileSync } = require('node:child_process');
+const path = require('node:path');
+
+const clientPath = path.join(__dirname, 'cc-client.js');
+
+function probe(envChanges, confirm) {
+  const env = { ...process.env, ...envChanges };
+  for (const [key, value] of Object.entries(env)) {
+    if (value === undefined) delete env[key];
+  }
+  const script = `
+    const cc = require(${JSON.stringify(clientPath)});
+    cc.createCustomCodeCampaign({
+      name: 'gate-test', subject: 'gate-test', fromEmail: 'test@example.com',
+      fromName: 'Gate Test', replyTo: 'test@example.com', htmlContent: '<p>test</p>',
+      physicalAddress: { address_line1: '1 Test St', city: 'Test', state_code: 'CA', postal_code: '00000', country_code: 'US' },
+      confirm: ${JSON.stringify(confirm)}
+    }).then((result) => process.stdout.write('RESULT=' + result));
+  `;
+  return execFileSync(process.execPath, ['-e', script], { env, encoding: 'utf8' });
+}
+
+test('gitignored .env cannot arm a live write when caller leaves CC_LIVE unset', () => {
+  const output = probe({ CC_LIVE: undefined }, true);
+  assert.match(output, /REFUSING live campaign create/);
+  assert.match(output, /CC_LIVE!=1 \(env gate closed\)/);
+  assert.match(output, /NO-OP POST https:\/\/api\.cc\.email\/v3\/emails/);
+  assert.doesNotMatch(output, /token refresh|LIVE POST/);
+});
+
+test('explicit CC_LIVE still requires the independent confirm gate', () => {
+  const output = probe({ CC_LIVE: '1' }, false);
+  assert.match(output, /REFUSING live campaign create/);
+  assert.match(output, /--confirm not passed/);
+  assert.match(output, /NO-OP POST https:\/\/api\.cc\.email\/v3\/emails/);
+  assert.doesNotMatch(output, /token refresh|LIVE POST/);
+});
diff --git a/mailers/cc-api/cc-client.js b/mailers/cc-api/cc-client.js
index efad14cc..d647878d 100644
--- a/mailers/cc-api/cc-client.js
+++ b/mailers/cc-api/cc-client.js
@@ -26,15 +26,21 @@
 
 const fs = require('fs');
 const path = require('path');
+// Arming must come from the invoking process, never from the credential file.
+// Capture it before dotenv loads so a persisted CC_LIVE=1 cannot silently turn
+// a local `--confirm` review into a live API request.
+const LIVE_ARMED_BY_CALLER = process.env.CC_LIVE === '1';
 // Auto-load local gitignored .env (CC_CLIENT_ID/SECRET/REFRESH_TOKEN) so runs
-// don't need the creds exported by hand. Inline env still wins (override:false).
+// don't need the creds exported by hand. CC_LIVE is intentionally excluded
+// from this convenience: it must be explicit for every live invocation.
 try { require('dotenv').config({ path: path.join(__dirname, '.env') }); } catch { /* dotenv optional */ }
+if (!LIVE_ARMED_BY_CALLER) delete process.env.CC_LIVE;
 
 const TOKEN_URL = 'https://authz.constantcontact.com/oauth2/default/v1/token';
 const API_BASE = 'https://api.cc.email/v3';
 const TOKEN_CACHE = path.join(__dirname, '.cc-token-cache.json'); // gitignored
 
-const LIVE = process.env.CC_LIVE === '1';
+const LIVE = LIVE_ARMED_BY_CALLER;
 
 function log(...a) { console.log('[cc-client]', ...a); }
 
diff --git a/mailers/phillip-jeffries-fall-2026-launch.html b/mailers/phillip-jeffries-fall-2026-launch.html
index 91794ccc..4ef46e96 100644
--- a/mailers/phillip-jeffries-fall-2026-launch.html
+++ b/mailers/phillip-jeffries-fall-2026-launch.html
@@ -14,7 +14,7 @@
   img { -ms-interpolation-mode: bicubic; border: 0; outline: none; text-decoration: none; display: block; }
   a { color: #1a1a1a; text-decoration: none; }
   .wrap { width: 100%; background: #f4f1ec; padding: 0; }
-  .container { max-width: 620px; margin: 0 auto; background: #ffffff; }
+  .container { width: 100% !important; max-width: 620px; margin: 0 auto; background: #ffffff; }
   .hero-img { width: 100%; height: auto; display: block; }
   .thumb-img { width: 100%; height: auto; display: block; }
   .h1 { font-size: 30px; line-height: 36px; letter-spacing: 0.5px; font-weight: 400; margin: 0; color: #1a1a1a; }
@@ -75,7 +75,7 @@
       <p style="font-family: Helvetica, Arial, sans-serif; font-size: 11px; letter-spacing: 3px; text-transform: uppercase; color: #7a6030; margin: 0;">Phillip Jeffries · Fall 2026 Collection</p>
       <p style="font-family: Georgia, 'Times New Roman', serif; font-size: 28px; line-height: 34px; letter-spacing: 1px; color: #1a1a1a; text-align: center; margin: 14px 0 0;">Fifty Years in the Making<br><span style="font-size:15px; letter-spacing:2px; color:#5a544c;">a celebration of artisanal history</span></p>
       <p class="lede" style="font-family: Georgia, 'Times New Roman', serif; font-size: 15px; line-height: 22px; color: #5a544c; margin: 18px 32px 0; max-width: 480px;">
-        Half a century of the handmade, distilled into one collection &mdash; <em>Journey</em>. Phillip Jeffries marks fifty years with <em>Idyllic</em>, <em>Cirque de Soho</em>, <em>Coco Weave</em> and <em>Plush Pillars</em> &mdash; artisanal grounds, painterly landscapes and richly woven texture that marry Japanese and European craft. Every pattern is available to the trade at Designer Wallcoverings, and every one is waiting to be touched in our Sherman Oaks showroom.
+        Half a century of the handmade, distilled into one collection &mdash; <em>Journey</em>. Phillip Jeffries marks fifty years with <em>Idyllic</em>, <em>Cirque de Soho</em>, <em>Coco Weave</em> and <em>Plush Pillars</em> &mdash; artisanal grounds, painterly landscapes and richly woven texture that marry Japanese and European craft. Explore the collection online, or visit our Sherman Oaks showroom to experience Phillip Jeffries materials in person.
       </p>
       <p style="margin: 28px 0 0;">
         <a href="https://www.designerwallcoverings.com/collections/phillip-jeffries?utm_source=email&utm_medium=mailer&utm_campaign=phillip-jeffries-fall-2026" target="_blank" style="display:inline-block; padding:13px 34px; background:transparent; color:#1a1a1a !important; font-family: Helvetica, Arial, sans-serif; font-size:11px; letter-spacing:3px; text-transform:uppercase; text-decoration:none; border-radius:2px; border:1px solid #1a1a1a;">Shop the Collection</a>
@@ -133,7 +133,7 @@
     <!-- ============ THE FULL ASSORTMENT — editorial line + CTA (no fabricated product URL) ============ -->
     <tr><td style="padding: 22px 36px 8px;" align="center">
       <p style="font-family: Georgia, serif; font-size: 14px; line-height: 22px; color: #5a544c; margin: 0; max-width: 460px;">
-        The full <em>Journey</em> assortment &mdash; embroidered raffia, grasscloth mosaics, woven naturals, dimensional texture and an ethereal landscape mural &mdash; is available to the trade at Designer Wallcoverings.
+        Explore the full <em>Journey</em> assortment &mdash; embroidered raffia, grasscloth mosaics, woven naturals, dimensional texture and an ethereal landscape mural &mdash; through Designer Wallcoverings.
       </p>
       <p style="margin: 22px 0 0;">
         <a href="https://www.designerwallcoverings.com/collections/phillip-jeffries?utm_source=email&utm_medium=mailer&utm_campaign=phillip-jeffries-fall-2026" target="_blank" style="display:inline-block; padding:12px 30px; background:transparent; color:#1a1a1a !important; font-family: Helvetica, Arial, sans-serif; font-size:10px; letter-spacing:3px; text-transform:uppercase; text-decoration:none; border-radius:2px; border:1px solid #1a1a1a;">See Every Pattern</a>
@@ -155,7 +155,7 @@
     <!-- ============ CLOSING CTA — memo samples front-loaded ============ -->
     <tr><td align="center" style="padding: 44px 36px 24px; background:#ffffff;">
       <p style="font-family: Georgia, serif; font-size: 14px; line-height: 22px; color: #5a544c; margin: 0;">
-        Prefer to specify from your desk? Request a <em>memo sample</em> of any Phillip Jeffries pattern &mdash; or shop the whole Fall 2026 Collection at trade pricing.
+        Prefer to specify from your desk? Request a <em>memo sample</em> for a Phillip Jeffries pattern &mdash; or explore the Fall 2026 Collection at trade pricing.
       </p>
       <p style="margin: 24px 0 0;">
         <a href="https://www.designerwallcoverings.com/collections/phillip-jeffries?utm_source=email&utm_medium=mailer&utm_campaign=phillip-jeffries-fall-2026" target="_blank" style="display:inline-block; padding:13px 34px; background:transparent; color:#1a1a1a !important; font-family: Helvetica, Arial, sans-serif; font-size:11px; letter-spacing:3px; text-transform:uppercase; text-decoration:none; border-radius:2px; border:1px solid #1a1a1a;">Shop the Collection</a>

← 652e9641 PJ Fall 2026 mailer: editorial precision fixes from Cody gat  ·  back to Designer Wallcoverings  ·  Record TK-10832 Codex comparison evidence 8d8a8c63 →