← back to Gmc Titlefix
apply-full.mjs
72 lines
// Push the FULL 10,352-row FRESH price overrides onto the supplemental datasource.
// Mirrors apply-canary.mjs exactly, but: (1) reads the FULL file, (2) has a REAL
// guard (refuses to fire without --apply --yes-i-am-steve; bare run = dry-run summary),
// (3) re-validates the cleanliness invariant at runtime and ABORTS if any row could
// re-leak <=$4.26 or is missing a field.
// Reversible: same offerId override; removing the supplement reverts to $4.25.
// Run ONLY after the 400-row canary passes the 24-48h verify gate (approved>=95%).
import fs from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { token, MERCHANT } = require('./_auth.js');
// CURRENCY BUG FIX 2026-09-11 (TK-11450). These writers honour row.feedLabel (which can be 'CA')
// but hardcoded currencyCode:'USD' (literally). Result, measured live: all 1,077 Merchant Center CA offers are
// DW-written, 996 correctly CAD and 81 carrying a CA-market AMOUNT stamped USD. Google requires the
// offer currency to match an available shipping service's currency; the only CA service is CAD, so
// every one of those 81 is disapproved — 81 of 81, 100%, versus 1.3% for the CAD ones.
// Verified before fixing: 77 of the 81 feed amounts match the product's Shopify CA contextual price
// and ZERO match its USD base price, so the amounts were always CAD and only the label was wrong.
// FAILS CLOSED: an unknown feedLabel throws rather than silently defaulting to USD, because
// defaulting to USD is precisely what caused this.
const FEED_CURRENCY = { US: 'USD', CA: 'CAD', GB: 'GBP' };
function currencyForFeed(feedLabel) {
const c = FEED_CURRENCY[String(feedLabel || '').toUpperCase()];
if (!c) throw new Error(`refusing to write: no currency mapping for feedLabel "${feedLabel}" (add it to FEED_CURRENCY rather than defaulting to USD)`);
return c;
}
const DS = 'accounts/146735262/dataSources/10693978453';
const FULL = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-full.json';
const sleep = ms => new Promise(r=>setTimeout(r,ms));
const args = new Set(process.argv.slice(2));
const ARMED = args.has('--apply') && args.has('--yes-i-am-steve');
const list = JSON.parse(fs.readFileSync(FULL,'utf8')).overrides;
// --- Pre-flight: re-validate the cleanliness invariant. Abort rather than re-leak. ---
const leak = list.filter(r => !(r.realPrice > 4.26));
const nonUS = list.filter(r => r.feedLabel !== 'US');
const missing = list.filter(r => !r.offerId || !r.contentLanguage || !r.feedLabel);
const prices = list.map(r => r.realPrice);
console.log(`FULL override file: ${list.length} rows | realPrice $${Math.min(...prices)}..$${Math.max(...prices)}`);
console.log(` rows <=$4.26 (LEAK): ${leak.length} | non-US: ${nonUS.length} | missing-field: ${missing.length}`);
if (leak.length || nonUS.length || missing.length) {
console.error('ABORT: file failed the cleanliness invariant — refusing to push (would re-leak / malformed).');
process.exit(1);
}
if (!ARMED) {
console.log('\nDRY-RUN (not armed). File is clean and ready.');
console.log('To fire the live push of all ' + list.length + ' overrides, run:');
console.log(' node ~/Projects/gmc-titlefix/apply-full.mjs --apply --yes-i-am-steve');
process.exit(0);
}
// --- Armed: fire the live push (identical mechanics to apply-canary.mjs) ---
let tok = await token(), tokAt = Date.now(), ok=0, fail=0; const fails=[];
console.log(`\nARMED — pushing ${list.length} FULL overrides -> ${DS}`);
for (let i=0;i<list.length;i++){
if (Date.now()-tokAt > 50*60*1000){ tok=await token(); tokAt=Date.now(); }
const row=list[i];
const url=`https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
const body={ offerId:row.offerId, contentLanguage:row.contentLanguage, feedLabel:row.feedLabel, productAttributes:{ price:{ amountMicros:String(Math.round(row.realPrice*1e6)), currencyCode:currencyForFeed(row.feedLabel) } } };
const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
if (r.ok) ok++; else { fail++; const t=await r.text(); if(fails.length<12) fails.push(`${row.offerId} ${r.status} ${t.slice(0,110)}`); if(r.status===429) await sleep(3000); }
if (i%50===0) process.stdout.write(` ${i}/${list.length} ok ${ok} fail ${fail}\n`);
}
console.log(`\nFULL DONE: ok ${ok} / fail ${fail} of ${list.length}`);
if (fails.length){ console.log('--- first failures ---'); fails.forEach(f=>console.log(' '+f)); }
fs.writeFileSync('/Users/macstudio3/.claude/yolo-queue/gmc-full-apply-result.json', JSON.stringify({ when:'today', ds:DS, pushed:list.length, ok, fail },null,2));