← 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
A gmc-dwla-peryard-primary.js
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 →