← back to Gmc Titlefix

step-e-verify-retry.js

70 lines

// DTD-B (2026-06-18, 3/3): page the live processed feed, find which of our 29,672
// offers did NOT flip to a "Sample:" title (transient-500 fails + reprocess-lag),
// and re-push ONLY those to the canonical supplemental DS. Doubles as the verify pass.
//
// READ phase is read-only (products.list paging). WRITE phase re-pushes only the
// residual un-flipped offers (same body shape as step-d) — within the approved
// "consolidate + scale all" scope. Idempotent.
//
// Usage: node step-e-verify-retry.js [dataSourceName]   (default DS = 10677010255)
const {token,MERCHANT}=require('./_auth');
const fs=require('fs');
const DS=process.argv[2]||'accounts/146735262/dataSources/10677010255';
const list=require('./gmc_titlefix_list.json');
const want=new Map();                 // offerId -> proposedTitle (must start "Sample:")
for(const r of list) want.set(r.offerId, r.proposedTitle);
const sleep=ms=>new Promise(r=>setTimeout(r,ms));

(async()=>{
  let tok=await token(), tokAt=Date.now();
  const reTok=async()=>{ if(Date.now()-tokAt>50*60*1000){ tok=await token(); tokAt=Date.now(); } };

  // ---- READ: page the whole processed feed, record effective title for our offers ----
  const seen=new Map();                // offerId -> effective title (for offers in `want`)
  let pageToken='', pages=0, scanned=0;
  do{
    await reTok();
    const u=new URL(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products`);
    u.searchParams.set('maxResults','250');
    if(pageToken) u.searchParams.set('pageToken',pageToken);
    const r=await fetch(u,{headers:{Authorization:'Bearer '+tok}});
    if(r.status===429||r.status>=500){ await sleep(2500); continue; }
    const j=await r.json();
    for(const p of (j.resources||[])){
      scanned++;
      const off=p.offerId;
      if(want.has(off)) seen.set(off, p.title||'');
    }
    pageToken=j.nextPageToken||'';
    if(++pages%25===0) console.log(`  paged ${pages} (scanned ${scanned}, matched ${seen.size}/${want.size})`);
  }while(pageToken);
  console.log(`READ done: ${pages} pages, scanned ${scanned}, matched ${seen.size}/${want.size} of our offers`);

  // ---- DIFF: which of our offers are NOT yet "Sample:"-prefixed (or absent from feed) ----
  const residual=[];
  for(const r of list){
    const t=seen.get(r.offerId);
    if(t===undefined || !/^Sample:/.test(t)) residual.push(r);
  }
  const absent=residual.filter(r=>!seen.has(r.offerId)).length;
  console.log(`DIFF: ${residual.length} offers still un-flipped (${absent} absent from feed, ${residual.length-absent} present-but-not-Sample)`);
  fs.writeFileSync('/tmp/gmc-titlefix-residual.json', JSON.stringify(residual.map(r=>r.offerId),null,2));

  if(!residual.length){ console.log('✅ ALL 29,672 flipped — nothing to retry.'); return; }

  // ---- WRITE: re-push only the residual (with one 500-retry each) ----
  let ok=0, fail=0;
  for(let i=0;i<residual.length;i++){
    await reTok();
    const row=residual[i];
    const url=`https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
    const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US', productAttributes:{ title: row.proposedTitle } };
    let r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
    if(r.status===429||r.status>=500){ await sleep(3000); r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)}); }
    if(r.ok) ok++; else { fail++; if(fail<=15) console.error('RETRY-FAIL',row.offerId,r.status,(await r.text()).slice(0,100)); }
    if(i%200===0) console.log(`  retry ${i}/${residual.length} | ok ${ok} fail ${fail}`);
  }
  console.log(`RETRY DONE: re-pushed ${residual.length} | ok ${ok} fail ${fail}`);
  console.log('Re-run this script after a few min to confirm residual → 0 (allow reprocess lag).');
})();