← back to Gmc Titlefix
build-mdc-sample-titles.mjs
146 lines
#!/usr/bin/env node
/**
* build-mdc-sample-titles.mjs — build the GOOGLE-facing "Sample —" title override list
* for the MDC showroom line (tag:Showroom). (TK-11307, DTD verdict B)
*
* WHY: MDC products carry a single $4.25 memo-sample variant (no roll). On Google the
* OFFER PRICE is a compliant $4.25, but the TITLE reads "<Pattern> Wallcovering | Phillipe
* Romano" — so Google sees a $4.25-priced wallcovering and flags a price-mismatch. Per
* Google's sample policy the title must BEGIN with "Sample". This builds a supplemental-feed
* title-override list that changes ONLY the GOOGLE title via a GMC supplemental data source
* — the on-site Shopify PDP title is NEVER touched.
*
* OFFER IDENTITY (fixed 2026-09-09 after a 404 diagnosis): MDC offers live on GMC under the
* LEGACY BARE-VARIANT-ID feed — offerId = the bare Shopify variant legacyResourceId (e.g.
* "44595192496179"), product resource name = "en~US~<offerId>" (NO "online~" prefix, NO
* "shopify_US_" compound). Not every ACTIVE tag:Showroom product is actually a live GMC
* product, so this builder pulls the REAL set of GMC offerIds and includes ONLY MDC offers
* that already exist on GMC — so the push can never 404 on a missing product.
*
* SCOPE — STRICTLY tag:Showroom, ACTIVE. Belt-and-suspenders: re-affirms hasShowroomTag per
* product, so a sellable Phillipe Romano product (Spazzolato etc.) is never included.
*
* READ-ONLY (Shopify GET + GMC products.list GET). Writes one local JSON. Nothing pushed.
* OUTPUT: data/mdc-sample-title-overrides.json (offerId, gmcName, productId, sku, currentTitle, proposedTitle)
* USAGE: node build-mdc-sample-titles.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
import { resolveSampleTitleDS } from './resolve-mdc-sample-source.mjs';
export { resolveSampleTitleDS };
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VER = '2024-10';
const GTITLE_MAX = 150; // Google title max length
/**
* The Google-facing sample title transform (exported for tests). Strips a trailing
* "| Phillipe Romano" vendor suffix, wraps as a memo sample, ALWAYS begins with "Sample".
* Truncation is end-only so the "Sample" prefix survives.
*/
export function mdcSampleTitle(title) {
const clean = String(title == null ? '' : title)
.replace(/\s*[|\-–—]\s*Phillipe\s+Romano\s*$/i, '')
.trim();
let t = `Sample — ${clean} — Memo Swatch`;
if (t.length > GTITLE_MAX) t = t.slice(0, GTITLE_MAX - 1).trimEnd() + '…';
return t;
}
/** Pick the memo-sample variant (sku ends -Sample), else the single/first variant. */
export function sampleVariant(variants) {
if (!variants || !variants.length) return null;
return variants.find(v => /-sample$/i.test(v.sku || '')) || variants[0];
}
/**
* Resolve the EXISTING, PRIMARY-LINKED "DW Sample Title Overrides" supplemental data source
* (never create a new one — that spawned the empty-duplicate 404 mess). Returns the data
* source RESOURCE NAME that is actually referenced by a primary feed's takeFromDataSources.
* (exported so the pusher reuses the identical resolution.)
*/
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
const { token, MERCHANT } = require('./_auth');
const { hasShowroomTag } = require(process.env.HOME + '/Projects/fix-live-board/config/showroom-vendor.cjs');
const TOKEN = (fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8')
.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
if (!TOKEN) { console.error('no Shopify token'); process.exit(1); }
const SEP = `https://${SHOP}/admin/api/${VER}/graphql.json`;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const sgql = async (query, variables) => {
for (let a = 0; a < 8; a++) {
let j;
try { const r = await fetch(SEP, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) }); j = await r.json(); }
catch { await sleep(1500 * (a + 1)); continue; }
if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } throw new Error(JSON.stringify(j.errors)); }
const t = j.extensions?.cost?.throttleStatus; if (t && t.currentlyAvailable < 400) await sleep(1200);
return j.data;
}
throw new Error('shopify retries');
};
(async () => {
const tok = await token();
const ds = await resolveSampleTitleDS(tok, { merchant: MERCHANT });
// 1. Real GMC offerId set (paginate products.list) — so we only override offers that EXIST.
process.stderr.write('pulling GMC offerId set…\n');
const gmcOffers = new Set();
let pt = null, gpages = 0;
do {
const url = `https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/products?pageSize=250` + (pt ? `&pageToken=${pt}` : '');
const r = await fetch(url, { headers: { Authorization: 'Bearer ' + tok } });
const j = await r.json();
for (const p of (j.products || [])) if (p.offerId) gmcOffers.add(String(p.offerId));
pt = j.nextPageToken; gpages++;
} while (pt && gpages < 400);
process.stderr.write(` GMC offers: ${gmcOffers.size} (pages ${gpages})\n`);
// 2. Enumerate ACTIVE tag:Showroom MDC products; offerId = bare sample-variant legacyId.
const Q = `query($cursor:String){
products(first:100, after:$cursor, query:"status:active tag:'Showroom'"){
pageInfo{ hasNextPage endCursor }
nodes{ legacyResourceId title tags variants(first:10){ nodes{ legacyResourceId sku } } }
}
}`;
const rows = []; let cursor = null, has = true, seen = 0, notag = 0, notOnGmc = 0;
while (has) {
const d = await sgql(Q, { cursor });
for (const p of d.products.nodes) {
seen++;
if (!hasShowroomTag(p.tags)) { notag++; continue; }
const v = sampleVariant(p.variants.nodes);
if (!v) continue;
const offerId = String(v.legacyResourceId); // BARE variant id — the legacy GMC feed identity
if (!gmcOffers.has(offerId)) { notOnGmc++; continue; } // not a live GMC product → skip (no 404)
rows.push({
offerId,
gmcName: `accounts/${MERCHANT}/products/en~US~${offerId}`,
productId: String(p.legacyResourceId),
sku: v.sku,
currentTitle: p.title,
proposedTitle: mdcSampleTitle(p.title),
});
}
has = d.products.pageInfo.hasNextPage; cursor = d.products.pageInfo.endCursor;
}
const DIR = path.join(__dirname, 'data'); fs.mkdirSync(DIR, { recursive: true });
const out = path.join(DIR, 'mdc-sample-title-overrides.json');
fs.writeFileSync(out, JSON.stringify(rows, null, 1));
console.log('MDC (tag:Showroom) Google sample-title override list');
console.log(` active tag:'Showroom' seen: ${seen} | skipped(no real tag): ${notag} | skipped(not a live GMC offer): ${notOnGmc}`);
console.log(` overrides written: ${rows.length}`);
if (rows[0]) { console.log(' sample:', rows[0].offerId, '|', rows[0].currentTitle, '->', rows[0].proposedTitle); }
console.log(' reuse datasource:', ds.name || ds.error, ds.linked ? '(linked to primary ✓)' : '');
if (ds.duplicates?.length) console.log(' EMPTY DUPLICATE title datasources to delete:', ds.duplicates.join(', '));
console.log(' wrote:', out);
console.log('\n Fire (Steve only): node push-mdc-sample-titles.mjs --apply --i-am-steve');
})();
}