← back to Designer Wallcoverings

mailers/cc-api/build-china-seas-campaign.js

232 lines

'use strict';

/**
 * build-china-seas-campaign.js — Designer Wallcoverings
 *
 * Loads ../china-seas-launch.html, constructs the exact Constant Contact v3
 * create-campaign payload, and either:
 *   --dry-run  (DEFAULT): prints the full payload + runs a validation checklist.
 *              NEVER hits the network.
 *   --confirm           : would create the campaign — STILL double-gated by
 *              CC_LIVE === '1' inside cc-client. Without CC_LIVE it NO-OPs.
 *
 * The validation checklist is EXPECTED to FAIL today on:
 *   - "all image src absolute https"  (the 6 vendor logos point at a path that
 *     isn't a confirmed-live https asset yet — they must be hosted first)
 *   - possibly "physical address non-placeholder"
 * Those FAILs are correct — they are the remaining prerequisites, not bugs.
 */

const fs = require('fs');
const path = require('path');
const cc = require('./cc-client');

const MAILER = path.join(__dirname, '..', 'china-seas-launch.html');

const argv = process.argv.slice(2);
const CONFIRM = argv.includes('--confirm');
const DRY_RUN = argv.includes('--dry-run') || !CONFIRM; // default dry-run

// --- Campaign definition (edit creds/address at send-prep, not the HTML) ----
const CAMPAIGN = {
  name: `China Seas Launch — ${new Date().toISOString().slice(0, 10)}`,
  subject: 'China Seas — The Quadrille House, now at Designer Wallcoverings',
  preheader: 'Hand-printed lattice, ikat & chinoiserie — wallcovering and matching fabric.',
  fromName: 'Designer Wallcoverings',
  fromEmail: 'steve@designerwallcoverings.com', // must be a CC-verified from address
  replyTo: 'steve@designerwallcoverings.com',
  // CAN-SPAM physical address — confirm/replace at send-prep. Flagged as
  // placeholder-suspect if it still matches the mailer's commented placeholder.
  physicalAddress: {
    address_line1: '15442 Ventura Bl',
    city: 'Sherman Oaks',
    state_code: 'CA',
    postal_code: '91403',
    country_code: 'US',
  },
};

// Heuristic: this exact string is the placeholder baked into the mailer comment.
const PLACEHOLDER_ADDRESS_MARKER = '15442 Ventura Boulevard #102';

function loadHtml() {
  if (!fs.existsSync(MAILER)) {
    throw new Error(`mailer not found: ${MAILER}`);
  }
  return fs.readFileSync(MAILER, 'utf8');
}

// Extract every <img src="..."> for the absolute-https check.
function extractImageSrcs(html) {
  const srcs = [];
  const re = /<img\b[^>]*?\bsrc\s*=\s*(["'])(.*?)\1/gi;
  let m;
  while ((m = re.exec(html)) !== null) srcs.push(m[2]);
  return srcs;
}

function isAbsoluteHttps(url) {
  return /^https:\/\//i.test(url);
}

// Vendor logos that aren't hosted at a confirmed-live public https URL yet.
// Per the mailer's own comment, these point at a DW path that returns 301
// (not a confirmed asset). Flag any logo src on that un-hosted path.
function unhostedLogoSrcs(srcs) {
  return srcs.filter((s) => /\/vendor-logos\//i.test(s));
}

// Customer-visible "Wallpaper" surfaces only: alt text + rendered text between
// tags. We deliberately DON'T flag the word inside third-party S3 image URLs
// (e.g. Quadrille's `Wallpaper_Images/` bucket path — not ours to rename), an
// existing live collection slug (`/collections/schumacher-wallpaper`), or HTML
// comments — the DW brand rule bans the word in COPY/TITLES/CAPTIONS/ALT, not
// in upstream URLs we don't control.
function visibleWallpaperHits(html) {
  const hits = [];
  // strip HTML comments first
  const noComments = html.replace(/<!--[\s\S]*?-->/g, '');
  // alt attributes
  const altRe = /\balt\s*=\s*(["'])(.*?)\1/gi;
  let m;
  while ((m = altRe.exec(noComments)) !== null) {
    if (/\bwallpapers?\b/i.test(m[2])) hits.push(`alt: "${m[2]}"`);
  }
  // visible text nodes (between > and <), with tags removed
  const textOnly = noComments.replace(/<[^>]+>/g, ' ');
  const textHits = textOnly.match(/\bwallpapers?\b/gi) || [];
  for (const t of textHits) hits.push(`visible text: "${t}"`);
  return hits;
}

function runChecklist(html) {
  const results = [];
  const add = (name, pass, detail) => results.push({ name, pass, detail });

  add('subject set', !!CAMPAIGN.subject && CAMPAIGN.subject.trim().length > 0, CAMPAIGN.subject);
  add('from name + email set',
    !!CAMPAIGN.fromName && !!CAMPAIGN.fromEmail && CAMPAIGN.fromEmail.includes('@'),
    `${CAMPAIGN.fromName} <${CAMPAIGN.fromEmail}>`);
  add('reply-to set',
    !!CAMPAIGN.replyTo && CAMPAIGN.replyTo.includes('@'),
    CAMPAIGN.replyTo);

  // physical address present + non-placeholder
  const addr = CAMPAIGN.physicalAddress || {};
  const addrComplete = !!(addr.address_line1 && addr.city && addr.state_code && addr.postal_code && addr.country_code);
  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 the mailer placeholder marker — confirm the real DW postal address at send-prep'
      : 'confirmed non-placeholder');

  // html non-empty
  add('html content non-empty', !!html && html.length > 200, `${html.length} bytes`);

  // banned word "Wallpaper" must not appear in CUSTOMER-VISIBLE copy/alt
  // (DW brand rule; this is the DW main mailer). Upstream URL paths / live
  // collection slugs / HTML comments are intentionally NOT flagged.
  const bannedHits = visibleWallpaperHits(html);
  add('no banned word "Wallpaper" (visible copy + alt)', bannedHits.length === 0,
    bannedHits.length ? `found in visible surfaces:\n      - ${bannedHits.join('\n      - ')}` : 'clean (alt + visible text)');

  // all image src absolute https
  const srcs = extractImageSrcs(html);
  const nonHttps = srcs.filter((s) => !isAbsoluteHttps(s));
  add('all image src absolute https', nonHttps.length === 0,
    nonHttps.length ? `${nonHttps.length} of ${srcs.length} not absolute-https:\n      - ${nonHttps.join('\n      - ')}`
                    : `${srcs.length}/${srcs.length} absolute-https`);

  // logos hosted at a confirmed-live public URL (the real send-prep blocker)
  const unhosted = unhostedLogoSrcs(srcs);
  add('vendor logos hosted at public https (not the un-hosted /vendor-logos/ path)',
    unhosted.length === 0,
    unhosted.length
      ? `${unhosted.length} logo(s) still on the un-hosted DW /vendor-logos/ path (returns 301; host on Shopify Files CDN first):\n      - ${unhosted.join('\n      - ')}`
      : 'all logos on a confirmed-live host');

  return results;
}

function buildPayload(html) {
  return {
    name: CAMPAIGN.name,
    email_campaign_activities: [
      {
        format_type: 5,
        from_name: CAMPAIGN.fromName,
        from_email: CAMPAIGN.fromEmail,
        reply_to_email: CAMPAIGN.replyTo,
        subject: CAMPAIGN.subject,
        preheader: CAMPAIGN.preheader,
        html_content: html,
        physical_address_in_footer: CAMPAIGN.physicalAddress,
      },
    ],
  };
}

async function main() {
  const html = loadHtml();

  console.log('='.repeat(72));
  console.log('China Seas → Constant Contact v3 campaign builder');
  console.log(`mode: ${CONFIRM ? '--confirm' : '--dry-run (default)'}   CC_LIVE=${process.env.CC_LIVE === '1' ? '1' : '(unset)'}`);
  console.log('='.repeat(72));

  // --- payload (always printed in dry-run; html truncated for readability) ---
  const payload = buildPayload(html);
  const printable = JSON.parse(JSON.stringify(payload));
  printable.email_campaign_activities[0].html_content =
    `<<${html.length} bytes of HTML — printed truncated>>\n` + html.slice(0, 400) + '\n…';
  console.log('\n--- CREATE-CAMPAIGN PAYLOAD (html_content truncated) ---');
  console.log(JSON.stringify(printable, null, 2));

  // --- validation checklist ---
  console.log('\n--- VALIDATION CHECKLIST ---');
  const results = runChecklist(html);
  let failed = 0;
  for (const r of results) {
    const mark = r.pass ? 'PASS' : 'FAIL';
    if (!r.pass) failed++;
    console.log(`  [${mark}] ${r.name}`);
    if (r.detail) console.log(`         ${r.detail}`);
  }
  console.log(`\n  ${results.length - failed}/${results.length} checks passed, ${failed} failed.`);

  if (DRY_RUN) {
    console.log('\nDRY-RUN: no network call made. (default)');
    if (failed > 0) {
      console.log('Validation has FAILs — these are the remaining send-prep prerequisites. See README + readiness memo.');
    }
    process.exitCode = failed > 0 ? 2 : 0;
    return;
  }

  // --- --confirm path: still double-gated inside cc-client by CC_LIVE ---
  if (failed > 0) {
    console.log('\nREFUSING to create campaign: validation has FAILs. Fix the prerequisites first.');
    process.exitCode = 2;
    return;
  }
  console.log('\n--confirm passed + validation clean → attempting create (NO-OPs unless CC_LIVE=1):');
  const activityId = await cc.createCustomCodeCampaign({
    name: CAMPAIGN.name,
    subject: CAMPAIGN.subject,
    preheader: CAMPAIGN.preheader,
    fromEmail: CAMPAIGN.fromEmail,
    fromName: CAMPAIGN.fromName,
    replyTo: CAMPAIGN.replyTo,
    htmlContent: html,
    physicalAddress: CAMPAIGN.physicalAddress,
    confirm: true,
  });
  console.log('result:', activityId);
}

main().catch((e) => {
  console.error('[build-china-seas] ERROR:', e.message);
  process.exitCode = 1;
});