[object Object]

← back to Designer Wallcoverings

auto-save: 2026-07-27T14:21:14 (1 files) — mailers/cc-api/import-fm-clients.js

4a9a4727158dde0d0be7bb4e9e195d9264a4ba0f · 2026-07-27 14:21:21 -0700 · Steve Abrams

Files touched

Diff

commit 4a9a4727158dde0d0be7bb4e9e195d9264a4ba0f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 14:21:21 2026 -0700

    auto-save: 2026-07-27T14:21:14 (1 files) — mailers/cc-api/import-fm-clients.js
---
 mailers/cc-api/import-fm-clients.js | 95 +++++++++++++++++++++++++++++++++++++
 1 file changed, 95 insertions(+)

diff --git a/mailers/cc-api/import-fm-clients.js b/mailers/cc-api/import-fm-clients.js
new file mode 100644
index 00000000..3f097f96
--- /dev/null
+++ b/mailers/cc-api/import-fm-clients.js
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+/**
+ * Import the segmented, hygiene-passed FileMaker client list into a new
+ * Constant Contact list. SAFE BY DEFAULT — dry-run (mock) unless BOTH
+ * CC_LIVE=1 and CONFIRM=1 are set. Reads the MAILABLE segment only.
+ *
+ *   Dry run (default):  node import-fm-clients.js
+ *   LIVE import:        CC_LIVE=1 CONFIRM=1 node import-fm-clients.js
+ *
+ * Steps (v3 API):
+ *   1. GET  /contact_lists         → reuse list if the name already exists
+ *   2. POST /contact_lists         → else create it, capture list_id
+ *   3. POST /activities/contacts_json_import  (batched)  permission = implied
+ *   4. report queued activity ids  (import runs async on CC's side)
+ */
+const fs = require('fs');
+const path = require('path');
+const cc = require('./cc-client.js');
+
+const MAILABLE = path.join(
+  process.env.HOME,
+  'Projects/marketing-command-center/data/cc-import-fm-clients-mailable.json'
+);
+const LIST_NAME = 'DW FileMaker Clients (existing relationship)';
+const BATCH = 500;
+const LIVE = process.env.CC_LIVE === '1' && process.env.CONFIRM === '1';
+const API = 'https://api.cc.email/v3';
+
+async function req(method, urlPath, body) {
+  if (!LIVE) {
+    console.log(`  [DRY-RUN] ${method} ${urlPath}` + (body ? ` (${JSON.stringify(body).length}b body)` : ''));
+    return { __mock: true };
+  }
+  const token = await cc.getAccessToken();
+  const res = await fetch(`${API}${urlPath}`, {
+    method,
+    headers: {
+      Authorization: `Bearer ${token}`,
+      Accept: 'application/json',
+      ...(body ? { 'Content-Type': 'application/json' } : {}),
+    },
+    ...(body ? { body: JSON.stringify(body) } : {}),
+  });
+  const txt = await res.text();
+  let j; try { j = txt ? JSON.parse(txt) : {}; } catch { j = { raw: txt }; }
+  if (!res.ok) throw new Error(`${method} ${urlPath} → ${res.status}: ${txt}`);
+  return j;
+}
+
+(async () => {
+  const data = JSON.parse(fs.readFileSync(MAILABLE, 'utf8'));
+  const contacts = data.import_data;
+  console.log(`Mode: ${LIVE ? 'LIVE ✅' : 'DRY-RUN (set CC_LIVE=1 CONFIRM=1 to write)'}`);
+  console.log(`Mailable contacts: ${contacts.length.toLocaleString()}`);
+
+  // 1/2 — find or create the list
+  let listId;
+  if (LIVE) {
+    const lr = await req('GET', '/contact_lists?limit=1000');
+    const existing = (lr.lists || []).find((l) => l.name === LIST_NAME);
+    if (existing) { listId = existing.list_id; console.log(`Reusing existing list ${listId}`); }
+  }
+  if (!listId) {
+    const cr = await req('POST', '/contact_lists', {
+      name: LIST_NAME,
+      favorite: false,
+      description: 'DW clients from FileMaker (existing relationship). Hygiene-passed + dead-domain filtered 2026-07-27.',
+    });
+    listId = LIVE ? cr.list_id : 'DRY-RUN-LIST-ID';
+    console.log(`Created list ${listId}`);
+  }
+
+  // 3 — batched json import, permission basis = implied (existing customer)
+  let queued = 0;
+  // CC v3 contacts_json_import schema uses `email` (not `email_address`)
+  const toImport = (r) => {
+    const o = { email: r.email_address || r.email };
+    if (r.company_name) o.company_name = r.company_name;
+    if (r.phone) o.phone = r.phone;
+    return o;
+  };
+  for (let i = 0; i < contacts.length; i += BATCH) {
+    const chunk = contacts.slice(i, i + BATCH).map(toImport);
+    const body = { import_data: chunk, list_ids: [listId] };
+    const r = await req('POST', '/activities/contacts_json_import', body);
+    queued += chunk.length;
+    if (i % (BATCH * 10) === 0 || i + BATCH >= contacts.length) {
+      console.log(`  queued ${queued.toLocaleString()}/${contacts.length.toLocaleString()}` +
+        (r.activity_id ? ` (activity ${r.activity_id})` : ''));
+    }
+  }
+  console.log(`\nDone. ${queued.toLocaleString()} contacts submitted to list ${listId}.`);
+  console.log('CC processes imports asynchronously — verify final counts in the CC UI or via GET /activities.');
+  if (!LIVE) console.log('\n(No data was written — this was a dry run.)');
+})().catch((e) => { console.error('IMPORT FAILED:', e.message); process.exit(1); });

← 02da14bc auto-save: 2026-07-27T11:20:11 (1 files) — shopify/scripts/c  ·  back to Designer Wallcoverings  ·  TK-17: add reversible Batch A (quotes tag) + Batch B (Hollyw 54898819 →