/*! * PHASE 6D — Nuva Consultations -> IS60 product bridge (read-only resolver) * * WHAT THIS FILE DOES, AND ONLY THIS: * NuvaProductCatalogAdmin already exposes getProductUrl(product) and * isProductPurchasable(productId), and NuvaProductRecommendationUI's * buildProductCardData() already calls exactly those two functions (fresh * off window.NuvaProductCatalogAdmin on every call) to decide a * recommended product's "View Product" link and availability. This file * wraps those two functions so that, when a Nuva catalog product carries a * sku, its link/availability reflect the REAL, live IS60 WooCommerce * record instead of only the Nuva-side catalog's own bookkeeping. * * WHAT THIS FILE DELIBERATELY DOES NOT DO: * - It does not touch the assessment/matching/recommendation engines, the * safety gate, NuvaCart, or NuvaCheckout. * - It does not decide whether a product is a good match for the * customer's skin -- that stays the matching engine's job. * - It never fabricates a product, price, or URL. A SKU that IS60 can't * resolve is simply left unresolved, and the existing catalog-side * status is used unchanged (fail-open for the rest of the consultation). * - It never sends assessment answers, concerns, or any personal data * anywhere -- the only thing sent to IS60 is a product SKU that this * page's own catalog already stored. * * Runs on the Quick Skin Match (free) and Advanced Skin Scan (advanced) * pages, which both load the same shared NuvaProductCatalogAdmin / * NuvaProductRecommendationUI modules -- one bridge, both paths (Phase 6D * Step 7). */ (function () { 'use strict'; var IS60_RESOLVE_ENDPOINT = 'https://is60.store/wp-json/is60/v1/resolve-products'; var BATCH_SIZE = 20; // matches the endpoint's own cap var REF_BY_SLUG = { 'quick-skin-match': 'nuva-quick-skin-match', 'ai-skin-scan': 'nuva-advanced-skin-scan' }; function currentRefTag() { var slug = (window.location.pathname || '').replace(/\/+$/, '').split('/').pop(); return REF_BY_SLUG[slug] || 'nuva-consultation'; } // sku -> resolved product object from IS60, or `false` once confirmed // unresolvable. Absence of a key means "not looked up (yet)". var resolved = Object.create(null); var pending = Object.create(null); function chunk(arr, size) { var out = []; for (var i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); return out; } // Looks up a batch of SKUs against IS60's public, read-only resolver. // Fails open: a network error or bad response just leaves those SKUs // unresolved, so callers fall back to the Nuva catalog's own status. function resolveSkus(skus) { var need = []; for (var i = 0; i < skus.length; i++) { var s = skus[i]; if (s && !(s in resolved) && !pending[s]) need.push(s); } if (!need.length) return Promise.resolve(); need.forEach(function (s) { pending[s] = true; }); var batches = chunk(need, BATCH_SIZE); return Promise.all( batches.map(function (batch) { var url = IS60_RESOLVE_ENDPOINT + '?skus=' + encodeURIComponent(batch.join(',')); return fetch(url) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (list) { var foundInBatch = Object.create(null); (list || []).forEach(function (item) { if (item && item.sku) { resolved[item.sku] = item; foundInBatch[item.sku] = true; } }); batch.forEach(function (s) { if (!foundInBatch[s]) resolved[s] = false; // confirmed: IS60 has no such published, purchasable SKU }); }) .catch(function () { // Network/CORS failure: leave these SKUs unresolved rather than // guessing. isProductPurchasable()/getProductUrl() fall back to // the existing Nuva catalog behavior below. }); }) ).then(function () { need.forEach(function (s) { delete pending[s]; }); }); } function withRefTag(url) { if (!url) return url; var sep = url.indexOf('?') === -1 ? '?' : '&'; // Opaque, non-sensitive context only -- never assessment content. return url + sep + 'utm_source=nuva-consultation&utm_medium=recommendation&nuva_ref=' + encodeURIComponent(currentRefTag()); } function patchCatalogAdmin(admin) { if (!admin || admin.__is60BridgeApplied) return; admin.__is60BridgeApplied = true; var originalGetProductUrl = admin.getProductUrl.bind(admin); var originalIsPurchasable = admin.isProductPurchasable.bind(admin); var originalGetProduct = typeof admin.getProduct === 'function' ? admin.getProduct.bind(admin) : null; admin.getProductUrl = function (product) { var sku = product && product.sku; var live = sku ? resolved[sku] : null; if (live && live.url && live.purchasable) { return withRefTag(live.url); } return originalGetProductUrl(product); }; admin.isProductPurchasable = function (productId) { var product = originalGetProduct ? originalGetProduct(productId) : null; var sku = product && product.sku; var live = sku ? resolved[sku] : null; if (live === false) return false; // IS60 confirmed: no such published/purchasable product if (live && typeof live.purchasable === 'boolean') return live.purchasable; return originalIsPurchasable(productId); // not yet resolved (or no sku) -- unchanged behavior }; } function prefetchFromCatalog(admin) { if (!admin || typeof admin.listProducts !== 'function') return; var products = admin.listProducts() || []; var skus = products .map(function (p) { return p && p.sku; }) .filter(Boolean); if (skus.length) resolveSkus(skus); } function init() { var admin = window.NuvaProductCatalogAdmin; if (!admin) return; // this page doesn't load the shared catalog module patchCatalogAdmin(admin); prefetchFromCatalog(admin); } init(); if (document.readyState !== 'complete') { window.addEventListener('load', init); // retry once in case the engine attaches after this script runs } // Event-ready for a future phase (Step 16) -- nothing is sent anywhere yet. window.NuvaIS60Bridge = { version: '1.0', resolveSkus: resolveSkus, getResolved: function (sku) { return Object.prototype.hasOwnProperty.call(resolved, sku) ? resolved[sku] : undefined; } }; })();