[object Object]

← back to Gmc Titlefix

auto-save: 2026-07-17T19:47:29 (1 files) — gmc-dwla-peryard-primary.js

2abd4aff8f74f597bbcbe74d2c51eddf0b74337b · 2026-07-17 19:47:37 -0700 · Steve Abrams

Files touched

Diff

commit 2abd4aff8f74f597bbcbe74d2c51eddf0b74337b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 17 19:47:37 2026 -0700

    auto-save: 2026-07-17T19:47:29 (1 files) — gmc-dwla-peryard-primary.js
---
 gmc-dwla-peryard-primary.js | 207 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 207 insertions(+)

diff --git a/gmc-dwla-peryard-primary.js b/gmc-dwla-peryard-primary.js
new file mode 100644
index 0000000..375e58e
--- /dev/null
+++ b/gmc-dwla-peryard-primary.js
@@ -0,0 +1,207 @@
+// GMC DWLA PER-YARD PRIMARY-OFFER DEPLOY (Steve-APPROVED 2026-07-16).
+//
+// PROBLEM: The Justin David / "Los Angeles Fabrics" (Phillipe Romano, DWLA-) line
+// is present on LIVE Merchant Center 146735262 ONLY as $4.25 "Sample" offers
+// (offerIds shopify_US_<pid>_<vid> from the Shopify Google&YouTube channel).
+// ZERO per-yard offers exist. A supplemental/update feed CANNOT create a missing
+// offer, so we create the per-yard offers as NEW PRIMARY offers.
+//
+// STRATEGY (DTD verdict B, 2026-07-16): a DEDICATED NEW primary datasource
+// "DW LA Fabrics Per-Yard" holds all per-yard productInputs, isolated from the
+// Shopify-channel offers. FULLY REVERSIBLE: delete the datasource → every
+// per-yard offer vanishes. FEED-ONLY: never touches Shopify variant/price/weight.
+//
+// Offer content comes from the controlled feed's DWLA-*-Yard rows (correct
+// per-yard price, link, image, title). shipping_weight is ADDED here (feed-only):
+//   Commercial Fabric / Drapery  -> 1 lb
+//   Commercial Wallcovering      -> 4 lb
+// offerId = the feed row id (DWLA-<sku>-Yard) — greppable, distinct from the
+// shopify_US_* sample offers.
+//
+//   node gmc-dwla-peryard-primary.js                    # DRY-RUN: build the per-yard offer list from the feed, no writes
+//   node gmc-dwla-peryard-primary.js --create-ds        # GATED: create the dedicated primary datasource (prints its name/id)
+//   node gmc-dwla-peryard-primary.js --pilot 3 --ds <DS> # GATED: insert the first N per-yard offers as a pilot
+//   node gmc-dwla-peryard-primary.js --scale --ds <DS>   # GATED: insert ALL remaining per-yard offers
+//   node gmc-dwla-peryard-primary.js --verify --ds <DS> [--pilot 3]  # read back inserted offers from MC
+//   node gmc-dwla-peryard-primary.js --delete-ds <DS>    # REVERT: delete the whole datasource (all per-yard offers vanish)
+//
+// Cost: $0 (Google Merchant API + local feed read; no per-call charge).
+const { token, MERCHANT } = require('./_auth.js');
+const fs = require('fs');
+
+const FEED_TSV = '/Users/macstudio3/Projects/designerwallcoverings/today-viewer/data/google-merchant-feed.tsv';
+const OFFERS_JSON = '/Users/macstudio3/.claude/yolo-queue/gmc-dwla-peryard-offers.json';
+const LOG = '/Users/macstudio3/.claude/yolo-queue/gmc-dwla-peryard-deploy-log.json';
+
+const CATEGORY = 'Home & Garden > Decor > Wallpaper'; // Google taxonomy (same as the rest of the feed)
+const WEIGHT_FABRIC = 1;         // Commercial Fabric / Drapery  ≈ 1 lb (per Steve 2026-07-16)
+const WEIGHT_WALLCOVERING = 4;   // Commercial Wallcovering       ≈ 4 lb
+
+const args = process.argv.slice(2);
+const has = f => args.includes(f);
+const val = (f, d) => { const i = args.indexOf(f); return (i !== -1 && args[i + 1] && !args[i + 1].startsWith('--')) ? args[i + 1] : d; };
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// ---- 1) Build the per-yard offer list from the controlled feed ---------------
+function buildOffers() {
+  const lines = fs.readFileSync(FEED_TSV, 'utf8').split('\n');
+  const cols = lines[0].split('\t');
+  const ix = n => cols.indexOf(n);
+  const c = {
+    id: ix('id'), title: ix('title'), description: ix('description'), link: ix('link'),
+    image: ix('image_link'), addl: ix('additional_image_link'), avail: ix('availability'),
+    price: ix('price'), brand: ix('brand'), mpn: ix('mpn'), ptype: ix('product_type'),
+  };
+  const offers = [];
+  for (let i = 1; i < lines.length; i++) {
+    if (!lines[i]) continue;
+    const f = lines[i].split('\t');
+    const id = f[c.id];
+    if (!/^DWLA-.*-Yard$/.test(id)) continue;                     // per-yard DWLA rows only
+    const priceNum = parseFloat((f[c.price] || '').split(' ')[0]);
+    if (!(priceNum > 4.26)) continue;                             // never advertise the $4.25 sample as per-yard
+    const ptype = f[c.ptype] || '';
+    const weight = /wallcovering/i.test(ptype) ? WEIGHT_WALLCOVERING : WEIGHT_FABRIC;
+    offers.push({
+      offerId: id,
+      title: f[c.title] || '',
+      description: f[c.description] || f[c.title] || '',
+      link: f[c.link] || '',
+      imageLink: f[c.image] || '',
+      additionalImageLinks: (f[c.addl] || '').split(',').filter(Boolean),
+      availability: f[c.avail] || 'in_stock',
+      priceAmount: priceNum,
+      brand: f[c.brand] || 'Los Angeles Fabrics',
+      mpn: f[c.mpn] || id,
+      productType: ptype || 'Commercial Fabric',
+      shippingWeightLb: weight,
+    });
+  }
+  return offers;
+}
+
+// ---- 2) Create the dedicated primary datasource ------------------------------
+async function createDataSource() {
+  const tok = await token();
+  const url = `https://merchantapi.googleapis.com/datasources/v1/accounts/${MERCHANT}/dataSources`;
+  const body = {
+    displayName: 'DW LA Fabrics Per-Yard',
+    primaryProductDataSource: { contentLanguage: 'en', feedLabel: 'US', countries: ['US'] },
+  };
+  const r = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+  const j = await r.json();
+  if (r.status >= 200 && r.status < 300 && j.name) {
+    console.log('DATASOURCE CREATED:\n' + j.name + '\n  id: ' + j.dataSourceId);
+    appendLog({ action: 'create-ds', datasource: j.name, id: j.dataSourceId });
+    return j.name;
+  }
+  console.error('create-ds FAILED', r.status, JSON.stringify(j.error || j).slice(0, 300));
+  process.exit(1);
+}
+
+// ---- 3) Insert per-yard offers (pilot or scale) ------------------------------
+async function insertOffers(ds, offers, label) {
+  let tok = await token(), tokAt = Date.now(), ok = 0, fail = 0;
+  const echoes = [];
+  console.log(`[${label}] inserting ${offers.length} per-yard offers → ${ds}`);
+  for (let i = 0; i < offers.length; i++) {
+    if (Date.now() - tokAt > 50 * 60 * 1000) { tok = await token(); tokAt = Date.now(); }
+    const o = offers[i];
+    const url = `https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(ds)}`;
+    const productAttributes = {
+      title: o.title,
+      description: o.description,
+      link: o.link,
+      imageLink: o.imageLink,
+      additionalImageLinks: o.additionalImageLinks,
+      availability: o.availability,
+      condition: 'new',
+      price: { amountMicros: String(Math.round(o.priceAmount * 1e6)), currencyCode: 'USD' },
+      brand: o.brand,
+      mpn: o.mpn,
+      productTypes: [o.productType],
+      googleProductCategory: CATEGORY,
+      shippingWeight: { value: o.shippingWeightLb, unit: 'lb' },
+    };
+    const body = { offerId: o.offerId, contentLanguage: 'en', feedLabel: 'US', productAttributes };
+    const r = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+    const txt = await r.text(); let j = {}; try { j = JSON.parse(txt); } catch (e) {}
+    if (r.ok && !j.error) {
+      ok++;
+      if (echoes.length < 5) echoes.push({ offerId: o.offerId, name: j.name, price: o.priceAmount, weight: o.shippingWeightLb });
+    } else {
+      fail++;
+      if (fail <= 10) console.error('FAIL', o.offerId, r.status, txt.slice(0, 220));
+    }
+    if (i % 250 === 0 && i) console.log(`  ${i}/${offers.length} | ok ${ok} fail ${fail}`);
+    if (r.status === 429) await sleep(3000);
+  }
+  console.log(`[${label}] DONE: ok ${ok} fail ${fail} of ${offers.length}`);
+  if (echoes.length) console.log('  API echoes:\n' + echoes.map(e => `    ${e.offerId} → ${e.name} ($${e.price}, ${e.weight}lb)`).join('\n'));
+  appendLog({ action: label, datasource: ds, attempted: offers.length, ok, fail, echoes });
+  return { ok, fail, echoes };
+}
+
+// ---- 4) Verify offers are ingested (read back from MC) -----------------------
+async function verify(ds, sample) {
+  const tok = await token(); const H = { Authorization: 'Bearer ' + tok };
+  console.log(`Verifying ${sample.length} per-yard offers on live MC (Content API products.get)...`);
+  for (const o of sample) {
+    const pid = `online:en:US:${o.offerId}`;
+    const r = await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(pid)}`, { headers: H });
+    const j = await r.json();
+    if (j.error) { console.log(`  ${o.offerId}  NOT-FOUND-YET (${j.error.code}) — expected within Google's ~24h merge`); continue; }
+    console.log(`  ${o.offerId}  price=${j.price?.value} ${j.price?.currency} | title="${(j.title || '').slice(0, 45)}" | shippingWeight=${JSON.stringify(j.shippingWeight || null)}`);
+  }
+}
+
+// ---- 5) REVERT: delete the datasource ----------------------------------------
+async function deleteDataSource(ds) {
+  const tok = await token();
+  const url = `https://merchantapi.googleapis.com/datasources/v1/accounts/${MERCHANT}/dataSources/${ds.split('/').pop()}`;
+  const r = await fetch(url, { method: 'DELETE', headers: { Authorization: 'Bearer ' + tok } });
+  console.log(r.ok ? `✓ DELETED datasource ${ds} — all per-yard offers under it are removed.` : `DELETE failed ${r.status} ${(await r.text()).slice(0, 200)}`);
+  appendLog({ action: 'delete-ds', datasource: ds, http: r.status });
+}
+
+function appendLog(entry) {
+  let log = [];
+  try { log = JSON.parse(fs.readFileSync(LOG, 'utf8')); } catch (e) {}
+  log.push({ at: new Date().toISOString(), ...entry });
+  fs.writeFileSync(LOG, JSON.stringify(log, null, 2));
+}
+
+(async () => {
+  const offers = buildOffers();
+  fs.writeFileSync(OFFERS_JSON, JSON.stringify({ generated_at: new Date().toISOString(), count: offers.length, offers }, null, 2));
+
+  if (has('--create-ds')) return createDataSource();
+
+  const ds = val('--ds', null);
+  if (has('--delete-ds')) return deleteDataSource(val('--delete-ds', args[args.indexOf('--delete-ds') + 1]));
+
+  if (has('--pilot')) {
+    if (!ds) { console.error('need --ds <datasource>'); process.exit(1); }
+    const n = parseInt(val('--pilot', '3'), 10);
+    return void await insertOffers(ds, offers.slice(0, n), `pilot-${n}`);
+  }
+  if (has('--scale')) {
+    if (!ds) { console.error('need --ds <datasource>'); process.exit(1); }
+    return void await insertOffers(ds, offers, 'scale');
+  }
+  if (has('--verify')) {
+    if (!ds) { console.error('need --ds <datasource>'); process.exit(1); }
+    const n = has('--pilot') ? parseInt(val('--pilot', '3'), 10) : 3;
+    return verify(ds, offers.slice(0, n));
+  }
+
+  // DRY-RUN
+  const wall = offers.filter(o => o.shippingWeightLb === WEIGHT_WALLCOVERING).length;
+  const fab = offers.length - wall;
+  console.log(`DWLA per-yard offers built from feed: ${offers.length}`);
+  console.log(`  shipping_weight tiers: ${fab} @ 1lb (fabric/drapery), ${wall} @ 4lb (commercial wallcovering)`);
+  console.log(`  price range: $${Math.min(...offers.map(o => o.priceAmount)).toFixed(2)} – $${Math.max(...offers.map(o => o.priceAmount)).toFixed(2)}`);
+  console.log(`Wrote ${OFFERS_JSON}`);
+  console.log('--- first 3 (pilot candidates) ---');
+  offers.slice(0, 3).forEach(o => console.log(`  ${o.offerId}  $${o.priceAmount}  ${o.shippingWeightLb}lb  "${o.title.slice(0, 45)}"`));
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });

← 93e51cb weight-override: DEFAULT_DS so --apply needs no long arg (te  ·  back to Gmc Titlefix  ·  auto-save: 2026-07-26T17:14:46 (1 files) — gmc-price-overrid c10e177 →