[object Object]

← back to AbramsEgo

fix(affiliate): refund parens → negative, full-digest+occ dedup (no blank-id collision), header-only CJ CSV — close-review fixes (TK-10132)

60f4236b9b365db87a6d8679449bb2139977f822 · 2026-08-03 08:35:04 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 60f4236b9b365db87a6d8679449bb2139977f822
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Mon Aug 3 08:35:04 2026 -0700

    fix(affiliate): refund parens → negative, full-digest+occ dedup (no blank-id collision), header-only CJ CSV — close-review fixes (TK-10132)
---
 package.json                             |  2 +-
 scripts/import-affiliate-commissions.mjs | 18 +++++++++++++++---
 scripts/pull-cj-commissions.mjs          |  6 +++++-
 3 files changed, 21 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 723d05c..b4b120b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "abramsego",
-  "version": "0.1.2",
+  "version": "0.1.3",
   "private": true,
   "description": "AbramsEgo — self-funding AI-agent command center for Steve's fleet",
   "main": "server.js",
diff --git a/scripts/import-affiliate-commissions.mjs b/scripts/import-affiliate-commissions.mjs
index 2d64b34..ac7bc0b 100644
--- a/scripts/import-affiliate-commissions.mjs
+++ b/scripts/import-affiliate-commissions.mjs
@@ -100,7 +100,13 @@ function resolveCol(headers, override, candidates) {
   return -1;
 }
 
-function parseAmount(s) { const n = Number(String(s).replace(/[^0-9.\-]/g, '')); return isFinite(n) ? n : NaN; }
+function parseAmount(s) {
+  const str = String(s).trim();
+  const isNeg = /^\(.*\)$/.test(str); // (12.50) = refund/reversal in CJ/PartnerStack exports
+  const n = Number(str.replace(/[(),]/g, '').replace(/[^0-9.\-]/g, ''));
+  if (!isFinite(n)) return NaN;
+  return isNeg ? -Math.abs(n) : n;
+}
 function parseDate(s) {
   const d = new Date(String(s).trim());
   if (isNaN(d)) return null;
@@ -108,7 +114,9 @@ function parseDate(s) {
   if (!/\d{1,2}:\d{2}/.test(String(s))) d.setUTCHours(12, 0, 0, 0);
   return d.toISOString();
 }
-const rowHash = (o) => crypto.createHash('sha256').update([o.program, o.ts.slice(0, 10), o.amount.toFixed(2), o.extId, o.desc].join('|')).digest('hex').slice(0, 16);
+// full 64-char digest (no truncation → no birthday collisions); `occ` disambiguates
+// genuinely-distinct rows that share program/date/amount/desc and have a blank extId
+const rowHash = (o, occ) => crypto.createHash('sha256').update([o.program, o.ts.slice(0, 10), o.amount.toFixed(2), o.extId, o.desc, occ].join('|')).digest('hex');
 
 // ── load ──────────────────────────────────────────────────────────────────
 const rows = parseCsv(fs.readFileSync(csvPath, 'utf8')).filter((r) => r.some((c) => (c || '').trim() !== ''));
@@ -127,6 +135,7 @@ if (cDate < 0 || cAmt < 0) {
 
 const imported = fs.existsSync(IMPORTED) ? JSON.parse(fs.readFileSync(IMPORTED, 'utf8')) : { hashes: [] };
 const seen = new Set(imported.hashes);
+const fileOcc = new Map(); // per-file occurrence counter for blank-extId rows
 
 const toBook = [], skipped = [], bad = [];
 for (const r of rows.slice(1)) {
@@ -135,7 +144,10 @@ for (const r of rows.slice(1)) {
   const desc = (cDesc >= 0 ? r[cDesc] : '').trim();
   const extId = (cId >= 0 ? r[cId] : '').trim();
   const rec = { program: program || network, ts, amount: Math.round(amount * 100) / 100, desc, extId };
-  rec.hash = rowHash(rec);
+  const baseKey = [rec.program, rec.ts.slice(0, 10), rec.amount.toFixed(2), rec.extId, rec.desc].join('|');
+  const occ = rec.extId ? 0 : (fileOcc.get(baseKey) || 0);
+  if (!rec.extId) fileOcc.set(baseKey, occ + 1);
+  rec.hash = rowHash(rec, occ);
   if (seen.has(rec.hash)) { skipped.push(rec); continue; }
   seen.add(rec.hash); toBook.push(rec);
 }
diff --git a/scripts/pull-cj-commissions.mjs b/scripts/pull-cj-commissions.mjs
index a42158c..e1416d1 100644
--- a/scripts/pull-cj-commissions.mjs
+++ b/scripts/pull-cj-commissions.mjs
@@ -54,7 +54,11 @@ while (cursor < end) {
   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); }
+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; };

← 839ac1d auto-save: 2026-08-03T08:22:05 (1 files) — data/local-fleet.  ·  back to AbramsEgo  ·  auto-save: 2026-08-03T08:52:19 (3 files) — data/affiliate-cl ca7c066 →