← back to AbramsEgo
affiliate: add CJ commissions API puller (paged 30d windows) -> importer CSV; verified auth, 0 commissions as of 2026-08-03 (TK-10132)
7c1d82cf27d07b025dcd1de5335d424eb47e9fb9 · 2026-08-03 07:30:18 -0700 · steve@designerwallcoverings.com
Files touched
A scripts/pull-cj-commissions.mjs
Diff
commit 7c1d82cf27d07b025dcd1de5335d424eb47e9fb9
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Mon Aug 3 07:30:18 2026 -0700
affiliate: add CJ commissions API puller (paged 30d windows) -> importer CSV; verified auth, 0 commissions as of 2026-08-03 (TK-10132)
---
scripts/pull-cj-commissions.mjs | 79 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/scripts/pull-cj-commissions.mjs b/scripts/pull-cj-commissions.mjs
new file mode 100644
index 0000000..a42158c
--- /dev/null
+++ b/scripts/pull-cj-commissions.mjs
@@ -0,0 +1,79 @@
+#!/usr/bin/env node
+// pull-cj-commissions.mjs — pull publisher commissions from CJ (Commission Junction)
+// and emit an importer-ready CSV, so the loop is: CJ API → CSV → import-affiliate-commissions.mjs.
+//
+// Reads CJ_TOKEN (personal access token) + CJ_COMPANY_ID (publisher CID) from the
+// ENVIRONMENT only — never prints, logs, or writes the token anywhere.
+//
+// USAGE:
+// CJ_TOKEN=… CJ_COMPANY_ID=… node scripts/pull-cj-commissions.mjs [--since 2026-01-01] [--before 2026-08-04] [--out cj.csv]
+// (default window: Jan 1 this-year → tomorrow UTC; default: CSV to stdout)
+
+const TOKEN = process.env.CJ_TOKEN;
+const CID = process.env.CJ_COMPANY_ID;
+if (!TOKEN || !CID) {
+ console.error('✗ CJ_TOKEN and CJ_COMPANY_ID must be set in the environment.');
+ process.exit(2);
+}
+
+const arg = (n, d) => { const i = process.argv.indexOf(n); return i >= 0 ? process.argv[i + 1] : d; };
+const since = `${arg('--since', '2026-01-01')}T00:00:00Z`;
+const before = `${arg('--before', new Date(Date.now() + 864e5).toISOString().slice(0, 10))}T00:00:00Z`;
+const outFile = arg('--out', null);
+
+// CJ caps each query at 31 days, so page the range in ≤30-day windows.
+const q = (s, b) => `{
+ publisherCommissions(forPublishers:["${CID}"], sincePostingDate:"${s}", beforePostingDate:"${b}") {
+ count
+ payloadComplete
+ records { advertiserName postingDate pubCommissionAmountUsd saleAmountUsd actionStatus actionType commissionId orderId }
+ }
+}`;
+
+async function queryWindow(s, b) {
+ const res = await fetch('https://commissions.api.cj.com/query', {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: q(s, b) }),
+ }).catch((e) => { console.error('✗ network error:', e.message); process.exit(1); });
+ const text = await res.text();
+ if (!res.ok) { console.error(`✗ CJ API HTTP ${res.status} (${s.slice(0,10)}→${b.slice(0,10)}):`, text.slice(0, 300)); process.exit(1); }
+ let json; try { json = JSON.parse(text); } catch { console.error('✗ non-JSON:', text.slice(0, 300)); process.exit(1); }
+ if (json.errors) { console.error('✗ CJ GraphQL errors:', JSON.stringify(json.errors)); process.exit(1); }
+ return (json.data && json.data.publisherCommissions) || { count: 0, records: [] };
+}
+
+const DAY = 864e5, WIN = 30 * DAY;
+const records = [];
+let cursor = Date.parse(since); const end = Date.parse(before);
+while (cursor < end) {
+ const wEnd = Math.min(cursor + WIN, end);
+ const pc = await queryWindow(new Date(cursor).toISOString(), new Date(wEnd).toISOString());
+ records.push(...(pc.records || []));
+ console.error(` ${new Date(cursor).toISOString().slice(0,10)} → ${new Date(wEnd).toISOString().slice(0,10)}: ${(pc.records||[]).length} record(s)`);
+ cursor = wEnd;
+}
+console.error(`CJ commissions ${since.slice(0,10)} → ${before.slice(0,10)}: ${records.length} record(s) total`);
+if (!records.length) { console.error('(no commission records in window — nothing to book)'); if (outFile) {} process.exit(0); }
+
+// emit CSV the importer's cj preset reads (Event Date / Advertiser Name / Publisher Commission / Commission ID)
+const esc = (v) => { const s = String(v ?? ''); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; };
+const header = 'Event Date,Advertiser Name,Publisher Commission,Action Status,Commission ID,Order ID';
+const lines = records.map((r) => [
+ (r.postingDate || '').slice(0, 10),
+ esc(r.advertiserName),
+ r.pubCommissionAmountUsd ?? '0',
+ esc(r.actionStatus),
+ esc(r.commissionId),
+ esc(r.orderId),
+].join(','));
+const csv = [header, ...lines].join('\n') + '\n';
+
+if (outFile) { const fs = await import('fs'); fs.writeFileSync(outFile, csv); console.error(`✓ wrote ${records.length} row(s) → ${outFile}`); }
+else process.stdout.write(csv);
+
+// quick totals to stderr (visible even when CSV goes to stdout)
+const byStatus = {};
+let total = 0;
+for (const r of records) { const a = Number(r.pubCommissionAmountUsd) || 0; total += a; byStatus[r.actionStatus] = (byStatus[r.actionStatus] || 0) + a; }
+console.error(`total pub commission: $${total.toFixed(2)} ${JSON.stringify(byStatus)}`);
← 123353f affiliate commissions: add real-date-aware, idempotent CSV i
·
back to AbramsEgo
·
affiliate registry: write verified 2026-08-03 terms (cookies 859eea6 →