Skip to content
/* Filtrous - contextual (per-customer) PDP price (2026-08-04, rev 2026-08-12). The V3 app renders prices from a static asset (catalog-index.json) baked at theme build time, so every visitor sees list price - B2B customers included, even though Shopify computes their contract price in Liquid on every request and checkout charges it correctly. sections/filtrous-ctx-price.liquid emits that request-time Liquid price as window.FV3_CTX_PRICE. This script swaps it into the PDP price element and, when the two differ, keeps the static index value visible struck through as the list price - so the contract saving is legible rather than invisible. 2026-08-12: Call-for-Quote SKUs (any SKU containing "OSK", plus EXK-BD-0096-PP) are never priced anywhere on the storefront - the app paints "Call for Quote" on their PDP and this script must leave it alone. Without the guard, signed-in company contacts saw their contract price swapped over the label. Same predicate as filtrous-v3.js / hero-nine. The struck value is read off the DOM because the app painted it from the static index, which IS list price. Liquid cannot supply it: for a company-location customer `variant.price` already resolves to the contract price, and compare_at_price means sale pricing, not B2B. That read happens ONCE per rendered node and is cached on the node itself (data-list-price). Re-deriving it from the DOM on re-entry would be wrong: after a swap the visible number is the contract price, so a second pass would strike the contract price and promote list - inverting the two. 2026-08-26: the "identical for logged-out visitors" assumption only holds while catalog-index.json is in step with Shopify. It was baked 2026-08-14, seven SKUs have been repriced upward since, and logged-out visitors were shown the stale lower number struck through against the current one - a price rise dressed as a saving (MBP-10-2070 read $136.50 / $165.00). Two guards now make that structurally impossible rather than a function of index freshness. The section only sets showWas for a signed-in company contact, and the strike below additionally requires the struck value to be strictly greater than the price being shown: a contract price is never above list, so a lower "list" is index drift, not a discount. The price SWAP is deliberately left ungated. The Liquid value is the one checkout charges, so swapping it in is what keeps a drifted PDP honest - disabling the section would put the stale index price back on the page. Scope: PDP only, and only when the rendered SKU matches the Liquid product - the app is a SPA-ish renderer, so we guard against it painting a different product than the one Liquid saw. Grid/search cards still show list price (known limitation, fix is a Liquid-rendered price endpoint). Revert: remove the Contextual price section from templates/product.json. */ (function () { if (window.__fctxInit) return; window.__fctxInit = true; var C = window.FV3_CTX_PRICE; if (!C || !C.sku || !C.price) return; /* Call for Quote - these SKUs never show a price, for any customer. */ var CFQ_SKU = String(C.sku).toUpperCase(); if (CFQ_SKU.indexOf('OSK') >= 0 || CFQ_SKU === 'EXK-BD-0096-PP') return; var SHOW_WAS = C.showWas !== false; var MUT = '#5c6f7e'; /* --mut */ /* money string -> integer cents, for comparing the struck value to C.raw */ function cents(v) { var n = parseFloat(String(v).replace(/[^0-9.]/g, '')); return isFinite(n) ? Math.round(n * 100) : null; } /* text of the price node with the unit suffix and any prior strike removed */ function bareText(el) { var probe = el.cloneNode(true); var ps = probe.querySelector('small'); if (ps && ps.parentNode) ps.parentNode.removeChild(ps); var pw = probe.querySelector('.pdp-was'); if (pw && pw.parentNode) pw.parentNode.removeChild(pw); return (probe.textContent || '').trim(); } function apply() { var app = document.getElementById('fv3-app'); if (!app) return; var part = app.querySelector('.fv3-buy .part'); if (!part) return; if (part.textContent.trim().toUpperCase() !== String(C.sku).toUpperCase()) return; var el = app.querySelector('.fv3-buy .pdp-price'); if (!el) return; if (el.getAttribute('data-ctx-price') === C.price) return; /* capture list price once per node - see header note on re-entry */ var listPrice = el.getAttribute('data-list-price'); if (listPrice === null) { listPrice = bareText(el); el.setAttribute('data-list-price', listPrice); } /* the unit suffix ("/ pack") is reused verbatim */ var small = el.querySelector('small'); el.textContent = ''; /* strike only a genuine saving - see header note on index drift */ var listC = cents(listPrice); var ctxC = typeof C.raw === 'number' ? C.raw : cents(C.price); var saving = listC !== null && ctxC !== null && listC > ctxC; if (SHOW_WAS && saving && listPrice !== C.price) { var was = document.createElement('s'); was.className = 'pdp-was'; was.setAttribute( 'style', 'margin-right:8px;font-size:15px;font-weight:500;color:' + MUT + ';' ); was.textContent = listPrice; el.appendChild(was); } el.appendChild(document.createTextNode(C.price + (small ? ' ' : ''))); if (small) el.appendChild(small); el.setAttribute('data-ctx-price', C.price); } function boot() { apply(); var mount = document.getElementById('fv3-app') || document.body; new MutationObserver(apply).observe(mount, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();
Liquid error (layout/theme line 757): Could not find asset snippets/whatsapp-button.liquid