← back to AbramsEgo

scripts/pull-cj-commissions.mjs

84 lines

#!/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) { const fs = await import('fs'); fs.writeFileSync(outFile, 'Event Date,Advertiser Name,Publisher Commission,Action Status,Commission ID,Order ID\n'); console.error(`✓ wrote header-only CSV → ${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)}`);