← back to Gmc Titlefix

build-fresh-canary.mjs

81 lines

// READ-ONLY: build a FRESH $4.25→roll price override list by joining live MC offers
// to TODAY's live roll prices (out/active-roll-and-sample.csv, scanned today), via pid
// extracted from offerId shopify_US_<pid>_<vid>. Guarantees pushed price == today's landing.
// Writes full list + a ~400 systematic-sample canary. NO writes to Google.
import fs from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { token, MERCHANT } = require('./_auth.js');
const mc = require('./_mc-read-v1.js'); // Merchant API v1 read shim (Content API v2.1 sunset 2026-08-18)

const CSV = '/Users/macstudio3/.claude/skills/google-merchant-agent/out/active-roll-and-sample.csv';
const OUT_FULL = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-full.json';
const OUT_CANARY = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-canary.json';
const CANARY_N = 400;

// pid -> today's roll price (maxVariantPrice), only where roll>4.26
function loadFreshRoll() {
  const lines = fs.readFileSync(CSV, 'utf8').split('\n');
  const m = new Map();
  for (let i = 1; i < lines.length; i++) {
    if (!lines[i]) continue;
    // naive split is unsafe (quoted commas in title/vendor); but pid(0), rollPrice(8), maxPrice(10)
    // are numeric and BEFORE... no — title/vendor are quoted & contain commas. Parse robustly:
    const f = parseCsv(lines[i]);
    const pid = f[0], roll = parseFloat(f[8]);
    if (pid && !isNaN(roll) && roll > 4.26) m.set(pid, roll);
  }
  return m;
}
function parseCsv(line){ const out=[];let cur='',q=false;for(let i=0;i<line.length;i++){const c=line[i];
  if(q){ if(c==='"'){ if(line[i+1]==='"'){cur+='"';i++;} else q=false; } else cur+=c; }
  else { if(c===','){out.push(cur);cur='';} else if(c==='"')q=true; else cur+=c; } } out.push(cur); return out; }

async function listOffers() {
  const offers = [];
  // Merchant API v1 paginated list via shim (replaces v2.1 products?maxResults=250 loop).
  // feedLabel/contentLanguage live in the raw v1 body (_v1), not the v2.1-compat top level.
  await mc.listProducts(async (p) => {
    offers.push({ offerId: p.offerId, price: parseFloat(p.price?.value || '0'), feedLabel: p._v1?.feedLabel || 'US', contentLanguage: p._v1?.contentLanguage || 'en', title: (p.title || '').slice(0, 45) });
    if (offers.length % 10000 === 0) process.stderr.write(`  ...${offers.length} offers read\n`);
  });
  return offers;
}

const roll = loadFreshRoll();
console.log(`Fresh roll prices (pid→roll>4.26): ${roll.size}`);
const offers = await listOffers();
console.log(`Live MC offers read: ${offers.length}`);

const RE = /^shopify_[A-Z]+_(\d+)_\d+$/;
let leak=0, joined=0, sampleOnlyOrNoRoll=0, legacyBareVid=0, alreadyReal=0;
const overrides=[];
for (const o of offers) {
  if (o.price > 4.26) { alreadyReal++; continue; }
  leak++;
  const mm = o.offerId.match(RE);
  if (!mm) { legacyBareVid++; continue; }          // bare-vid CA/GB legacy — defer to full rollout
  const pid = mm[1];
  const rp = roll.get(pid);
  if (rp === undefined) { sampleOnlyOrNoRoll++; continue; } // sample-only or no fresh roll → leave $4.25
  joined++;
  overrides.push({ offerId:o.offerId, contentLanguage:o.contentLanguage, feedLabel:o.feedLabel, pid, currentPrice:o.price, realPrice:rp, title:o.title });
}
fs.writeFileSync(OUT_FULL, JSON.stringify({ generated_at:'today-live', total_offers:offers.length, leak_offers:leak, override_rows_us_pidjoin:overrides.length, deferred_legacy_bare_vid:legacyBareVid, left_at_425_sampleonly_or_noroll:sampleOnlyOrNoRoll, already_real:alreadyReal, overrides }, null, 2));

// systematic sample for representativeness across the (vendor-clustered) list
const step = Math.max(1, Math.floor(overrides.length / CANARY_N));
const canary = []; for (let i=0; i<overrides.length && canary.length<CANARY_N; i+=step) canary.push(overrides[i]);
fs.writeFileSync(OUT_CANARY, JSON.stringify({ generated_at:'today-live', datasource:'accounts/146735262/dataSources/10693978453', count:canary.length, sampled_every_nth:step, overrides:canary }, null, 2));

console.log(`\n=== FRESH OVERRIDE BUILD (read-only) ===`);
console.log(`  leak offers (<=$4.26):        ${leak}`);
console.log(`  US pid-join override rows:    ${overrides.length}`);
console.log(`  deferred legacy bare-vid:     ${legacyBareVid}  (CA/GB — need vid->pid, full rollout)`);
console.log(`  left at $4.25 (sampleonly):   ${sampleOnlyOrNoRoll}`);
console.log(`  canary rows (~${CANARY_N}):           ${canary.length}  every ${step}th`);
console.log(`  full  -> ${OUT_FULL}`);
console.log(`  canary-> ${OUT_CANARY}`);
console.log('--- canary sample ---');
canary.slice(0,10).forEach(o=>console.log(`  ${o.offerId}  $${o.currentPrice} -> $${o.realPrice}  ${o.title}`));