Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy Loading for Prices #201

Open
wants to merge 42 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e4b9a79
feat: Add promotionalPrices attribute to Algolia product configuration
htuzel Aug 12, 2024
6dc76de
Logic changed for indexing promo end date and start date.
htuzel Aug 13, 2024
1371dba
feat: Remove unnecessary start and end date fields in promotionalPrices
htuzel Aug 26, 2024
91b3440
Improved logic
htuzel Sep 3, 2024
731dfb1
Lazy load and refactoring for promotional prices
htuzel Sep 16, 2024
f442ad6
linter errors
htuzel Sep 16, 2024
4d7c720
unit test mockups
htuzel Sep 17, 2024
53ab58e
small refactoring for name convention
htuzel Sep 17, 2024
822fced
small refactorings
htuzel Sep 17, 2024
caf44d7
promotions
htuzel Sep 17, 2024
268c995
Prevented duplicate price fetching and double update issue
htuzel Sep 18, 2024
c69bf86
removed console.log
htuzel Sep 18, 2024
6e8c05f
Some refactorings
htuzel Sep 19, 2024
9747efe
rulebased promotion logic removed
htuzel Sep 20, 2024
ea60034
Update cartridges/bm_algolia/cartridge/templates/resources/algolia.pr…
htuzel Sep 20, 2024
e370584
Update cartridges/int_algolia/cartridge/scripts/algolia/model/algolia…
htuzel Sep 20, 2024
42b4126
removed unnecessery imports
htuzel Sep 20, 2024
0e6eef7
som eimprovements
htuzel Sep 20, 2024
9919e16
linter fix
htuzel Sep 22, 2024
869aee8
Lazy load logic is improved
htuzel Sep 22, 2024
b0f9b8a
lazy load reverted
htuzel Sep 22, 2024
96d9519
lazy load remaining parts & refactoring
htuzel Sep 22, 2024
f97107f
Update cartridges/int_algolia/cartridge/scripts/algolia/model/algolia…
htuzel Sep 26, 2024
66018c9
CR fixes
htuzel Sep 26, 2024
3a6a018
Merge branch 'poc/promotional-prices' of https://github.com/algolia/a…
htuzel Sep 26, 2024
5cfe908
Update cartridges/int_algolia_sfra/cartridge/static/default/js/algoli…
htuzel Sep 26, 2024
79e8745
Merge branch 'develop' into poc/promotional-prices
htuzel Oct 23, 2024
475bdf8
Fixed linter errors and made some improvements
htuzel Oct 23, 2024
aa8b4b0
Updated code according to reviews
htuzel Oct 28, 2024
7220478
Revert "lazy load reverted"
htuzel Oct 31, 2024
362a97d
Revert "lazy load remaining parts & refactoring"
htuzel Oct 31, 2024
5753b66
Merge branch 'develop' into poc/promotional-prices
htuzel Oct 31, 2024
f49695b
Fixed typo for preference name
htuzel Oct 31, 2024
592363a
Removed console log
htuzel Oct 31, 2024
09eae82
Fixed Constant value
htuzel Oct 31, 2024
923799c
Removed unneccesery comment
htuzel Oct 31, 2024
0594fa7
Apply suggestions from code review
htuzel Nov 4, 2024
6bdb32d
Refactoring for reviews
htuzel Nov 4, 2024
8ac0263
Removed unnecessery file
htuzel Nov 4, 2024
1321415
Improved list price logic according to defaul behaviour of cartridge
htuzel Nov 5, 2024
6d7f657
Refactoring for getPriceHTML function
htuzel Nov 5, 2024
5bb400a
fixed linter errors
htuzel Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cartridges/bm_algolia/cartridge/controllers/AlgoliaBM.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function handleSettings() {
var algoliaEnable = ('Enable' in params) && (params.Enable.submitted === true);
var algoliaEnableContentSearch = ('EnableContentSearch' in params) && (params.EnableContentSearch.submitted === true);
var algoliaEnableRecommend = ('EnableRecommend' in params) && (params.EnableRecommend.submitted === true);
var algoliaEnablePricingLazyLoad = ('EnablePricingLazyLoad' in params) && (params.EnablePricingLazyLoad.submitted === true);
algoliaData.setPreference('Enable', algoliaEnable);
algoliaData.setPreference('ApplicationID', params.ApplicationID.value);
algoliaData.setSetOfStrings('AdditionalAttributes', params.AdditionalAttributes.value);
Expand All @@ -47,6 +48,7 @@ function handleSettings() {
algoliaData.setPreference('EnableSSR', params.EnableSSR.submitted);
algoliaData.setPreference('EnableContentSearch', algoliaEnableContentSearch);
algoliaData.setPreference('EnableRecommend', algoliaEnableRecommend);
algoliaData.setPreference('EnablePricingLazyLoad', algoliaEnablePricingLazyLoad);
} catch (error) {
Logger.error(error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@
</td>
</tr>

<iscomment> Algolia_EnablePricingLazyLoad </iscomment>
<tr>
<td class="table_detail w s" colspan="1">
<isprint value="${Resource.msg('algolia.label.enable', 'algolia', null)}" />
<isprint value="${Resource.msg('algolia.label.preference.enablePricingLazyLoad', 'algolia', null)}" />
<i class="fa fa-info-circle dw-nc-text-info info-hover"></i>
<div class="tooltip">${Resource.msg('algolia.label.preference.enablePricingLazyLoad.help', 'algolia', null)}</div>
</td>
<td class="table_detail w e s">
<input type="checkbox" ${pdict.algoliaData.getPreference('EnablePricingLazyLoad') ? "checked": ""} id="EnablePricingLazyLoad" name="EnablePricingLazyLoad" />
</td>
</tr>

<iscomment> Apply button </iscomment>
<tr>
<td class="w e s buttonspacing" align="right" colspan="2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ algolia.label.preference.insights=insights events
algolia.label.preference.enablessr=Enable server-side rendering
algolia.label.preference.enablecontentsearch=Enable content search
algolia.label.preference.enablerecommend=Recommend
algolia.label.preference.enablePricingLazyLoad=Lazy Loading for prices
algolia.label.preference.clientid=OCAPI Client ID
algolia.label.preference.clientpassword=OCAPI Client password
algolia.label.button.apply=Apply
Expand All @@ -41,6 +42,7 @@ algolia.label.preference.enableinsights.help=Send Algolia insights events from t
algolia.label.preference.enablessr.help=Server-side rendering for SFRA's CLP (category landing page) and search results pages. Improves SEO. Only triggered when the User-Agent advertise itself as a bot. May result in extra search requests.
algolia.label.preference.enablecontentsearch.help=Enable Content Search on the storefront
algolia.label.preference.enablerecommend.help=Enable Algolia Recommend on the SFRA storefront cartridge. See the documentation for additional setup steps
algolia.label.preference.enablePricingLazyLoad.help=When enabled, prices are fetched from SFCC when search results are displayed. Permits to display the most up-to-date promotions without having to index them.


# v2 job report table
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ const clientSideData = {
"searchApiKey": getPreference('SearchApiKey'),
"enableContentSearch": getPreference('EnableContentSearch'),
"enableRecommend": getPreference('EnableRecommend'),
"EnablePricingLazyLoad": getPreference('EnablePricingLazyLoad'),
"locale": request.getLocale(),
"currencyCode": request.getSession().getCurrency().getCurrencyCode(),
"currencySymbol": request.getSession().getCurrency().getSymbol(),
"productsIndex": calculateIndexName('products'),
"categoriesIndex": calculateIndexName('categories'),
"contentsIndex": calculateIndexName('contents'),
"recordModel": getPreference('RecordModel'),
"priceEndpoint": URLUtils.url('Algolia-Price').toString(),
"quickViewUrlBase": URLUtils.url('Product-ShowQuickView').toString(),
"strings": {
"placeholder": Resource.msg('label.header.searchwatermark', 'common', ''),
Expand Down Expand Up @@ -55,7 +57,8 @@ const clientSideData = {
"separator": Resource.msg('search.pricefilter.separator','algolia',null),
"submit": Resource.msg('search.pricefilter.submit','algolia',null),
},
"newArrivals": Resource.msg('panel.newarrivals','algolia',null)
"newArrivals": Resource.msg('panel.newarrivals','algolia',null),
"from": Resource.msg('label.from','algolia',null),
},
"noImages": {
"large": URLUtils.staticURL('/images/noimagelarge.png').toString(),
Expand Down
58 changes: 58 additions & 0 deletions cartridges/int_algolia_sfra/cartridge/controllers/Algolia.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

var server = require('server');

var cache = require('*/cartridge/scripts/middleware/cache');

server.get('Price', cache.applyShortPromotionSensitiveCache, function (req, res, next) {
var PromotionMgr = require('dw/campaign/PromotionMgr');
var ProductMgr = require('dw/catalog/ProductMgr');
var ProductFactory = require('*/cartridge/scripts/factories/product');

var params = req.querystring;
var productIds = params.pids;
var productIdsArr = productIds.split(',');

var productsArr = [];
for (var i = 0; i < productIdsArr.length; i++) {
var product = ProductFactory.get(
{
pid: productIdsArr[i]
}
);

//find the minimum price and the promotion
var minPrice = Number.MAX_VALUE;
var activePromotion = null;

var promotions = product.promotions || [];
var apiProduct = ProductMgr.getProduct(product.id);

for (var j = 0; j < promotions.length; j++) {
var promotion = promotions[j]
var apiPromotion = PromotionMgr.getPromotion(promotion.id);
Comment on lines +31 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure all this is needed? You are using the ProductFactory, which already sets the best active promotion in the sales price: https://github.com/SalesforceCommerceCloud/storefront-reference-architecture/blob/3a99a7990e70756eb21b775c73e7680a984be0d3/cartridges/app_storefront_base/cartridge/scripts/factories/price.js#L84

I've debugged and I confirm that I see the promotion price.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in this case, we don't know which promotion is applied for this price. There may be multiple promotions, and we need to know which promotion is causing this price and display call out message.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SFRA systematically uses the first promotion: https://github.com/SalesforceCommerceCloud/storefront-reference-architecture/blob/3a99a7990e70756eb21b775c73e7680a984be0d3/cartridges/app_storefront_base/cartridge/scripts/helpers/pricing.js#L46

So we actually know. But this is an good reason to compute it ourselves, as technically there could be a better price than the one present in the sales price.

var promotionPrice = apiPromotion.getPromotionalPrice(apiProduct);

if (promotionPrice.value && promotionPrice.value < minPrice ) {
minPrice = promotionPrice.value;
activePromotion = promotion;
}
}

if (activePromotion) {
product.activePromotion = {
price: minPrice,
promotion: activePromotion
};
}

productsArr.push(product);
}

res.json({
products : productsArr
});
next();
});

module.exports = server.exports();
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function enableInstantSearch(config) {
const contentSearchbarTab = document.querySelector('#content-search-bar-button');
const navbar = document.querySelector('.search-nav');
const activeCustomerPromotionsEl = document.querySelector('#algolia-activePromos');
const isPricingLazyLoad = algoliaData.EnablePricingLazyLoad;
activeCustomerPromotions = JSON.parse(activeCustomerPromotionsEl.dataset.promotions);

let displaySwatches = false;
Expand Down Expand Up @@ -242,16 +243,18 @@ function enableInstantSearch(config) {
showMoreText: algoliaData.strings.moreResults,
empty: '',
item(hit, { html, components }) {
const hiddenCalloutMsg = !hit.calloutMsg && 'd-none';
const shouldHideCallout = isPricingLazyLoad || !hit.calloutMsg;
const productId = algoliaData.recordModel === 'master-level' ? (hit.defaultVariantID ? hit.defaultVariantID : hit.objectID) : hit.objectID;
const callOutMsgClassname = `callout-msg-placeholder-${productId}`;
return html`
<div class="product"
data-pid="${hit.objectID}"
data-query-id="${hit.__queryID}"
data-index-name="${hit.__indexName}"
>
<div class="product-tile">
<small class="callout-msg ${hiddenCalloutMsg}">
${hit.calloutMsg }
<small class="callout-msg ${shouldHideCallout ? 'd-none' : ''} ${callOutMsgClassname}">
${!isPricingLazyLoad && hit.calloutMsg}
</small>
<div class="image-container">
<a href="${hit.url}">
Expand Down Expand Up @@ -282,7 +285,8 @@ function enableInstantSearch(config) {
</a>
</div>
<div class="price">
${html`
${isPricingLazyLoad && html`<span class="price-placeholder" data-product-id="${productId}"></span>`}
${!isPricingLazyLoad && html`
${ (hit.displayPrice < hit.price || (hit.promotionalPrice && hit.promotionalPrice < hit.price)) && html`
<span class="strike-through list">
<span class="value"> ${hit.currencySymbol} ${hit.price} </span>
Expand Down Expand Up @@ -515,6 +519,14 @@ function enableInstantSearch(config) {
search.start();

search.on('render', function () {
if (isPricingLazyLoad && search.status === 'idle') {
var items = search.renderState[algoliaData.productsIndex].infiniteHits.hits;
var productIDs = items.map((item) => algoliaData.recordModel === 'master-level' ? (item.defaultVariantID ? item.defaultVariantID : item.objectID) : item.objectID);
fetchPromoPrices(productIDs).then(() => {
updateAllProductPrices();
});
}

var emptyFacetSelector = '.ais-HierarchicalMenu--noRefinement';
$(emptyFacetSelector).each(function () {
$(this).parents().eq(2).hide();
Expand Down Expand Up @@ -623,6 +635,103 @@ function enableInstantSearch(config) {
}
}

// Add this at the top of the file, outside of any function
const fetchedPrices = new Map();

/**
* Fetches promotional prices for a list of product IDs
* @param {Array} productIDs - An array of product IDs.
* @returns {Promise} A promise that resolves when prices are fetched
*/
function fetchPromoPrices(productIDs) {
if (productIDs.length === 0) return Promise.resolve();

// Filter out already fetched product IDs
const unfetchedProductIDs = productIDs.filter(id => !fetchedPrices.has(id));

if (unfetchedProductIDs.length === 0) return Promise.resolve();

return $.ajax({
url: algoliaData.priceEndpoint,
type: 'GET',
data: {
pids: unfetchedProductIDs.toString(),
},
}).then(function(data) {
let products = data.products;
for (let product of products) {
fetchedPrices.set(product.id, product);
}
});
}

/**
* Updates the price display for all products on the page
*/
function updateAllProductPrices() {
const pricePlaceholders = document.querySelectorAll('.price-placeholder');

pricePlaceholders.forEach(placeholder => {
const productId = placeholder.getAttribute('data-product-id');
const product = fetchedPrices.get(productId);

if (product) {
let priceHtml = getPriceHtml(product);
placeholder.innerHTML = priceHtml;

if (product.activePromotion && product.activePromotion.price) {
const calloutMsg = document.querySelector(`.callout-msg-placeholder-${productId}`);
if (calloutMsg) {
calloutMsg.innerHTML = product.activePromotion.promotion.calloutMsg;
calloutMsg.classList.remove('d-none');
}
}
}
});
}

/**
* Generates HTML for displaying product prices, including promotional prices if applicable.
* @param {Object} product - The product object containing price information.
* @returns {string} HTML string representing the price display.
*/
function getPriceHtml(product) {
let priceObj = product.price;

const hasActivePromotion = product.activePromotion && product.activePromotion.price;
const hasListPrice = priceObj && priceObj.list && priceObj.list.value;
const hasSalesPrice = priceObj && priceObj.sales && priceObj.sales.value;

// Determine if the product is on sale
const isOnSale = hasActivePromotion || hasListPrice;

// Get the list price or fallback to sales price if list price is unavailable
const listPriceValue = hasListPrice
? priceObj.list.value
: hasSalesPrice
? priceObj.sales.value
: null;

// Get the sales price from active promotion or regular sales price
const salesPriceValue = hasActivePromotion
? product.activePromotion.price
: hasSalesPrice
? priceObj.sales.value
: null;

if (isOnSale) {
return `<span class="strike-through list">
<span class="value"> ${algoliaData.currencySymbol} ${listPriceValue} </span>
</span>
<span class="sales">
<span class="value"> ${algoliaData.currencySymbol} ${salesPriceValue} </span>
</span>`;
}

return `<span class="value"> ${algoliaData.currencySymbol} ${salesPriceValue} </span>`;
}


/**
* Build a product URL with Algolia query parameters
* @param {string} objectID objectID
Expand All @@ -645,7 +754,6 @@ function generateProductUrl({ objectID, productUrl, queryID, indexName }) {
* @return {number} The calculated sales price
*/
function calculateDisplayPrice(item) {

var promotions;
var calloutMsg = '';
var variant;
Expand Down
1 change: 0 additions & 1 deletion metadata/algolia/meta/custom-objecttype-definitions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,4 @@
</attribute-group>
</group-definitions>
</custom-type>

</metadata>
9 changes: 9 additions & 0 deletions metadata/algolia/meta/system-objecttype-extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ Setting this preference replaces the first two segments, the final index name be
<default-value>false</default-value>
</attribute-definition>

<attribute-definition attribute-id="Algolia_EnablePricingLazyLoad">
<display-name xml:lang="x-default">Enables Pricing Lazy Load</display-name>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<display-name xml:lang="x-default">Enables Pricing Lazy Load</display-name>
<display-name xml:lang="x-default">Enable prices lazy loading</display-name>

<description xml:lang="x-default">It fetches prices from SFCC not Algolia</description>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<description xml:lang="x-default">It fetches prices from SFCC not Algolia</description>
<description xml:lang="x-default">Fetch prices from SFCC after the search results are returned from Algolia</description>

<type>boolean</type>
<mandatory-flag>false</mandatory-flag>
<externally-managed-flag>false</externally-managed-flag>
</attribute-definition>

<attribute-definition attribute-id="Algolia_EnableSSR">
<display-name xml:lang="x-default">Enable SSR</display-name>
<description xml:lang="x-default">Enables server-side rendering for CLP search results. Helps with SEO as CLP pages are no longer rendered with empty containers that are to be filled by client-side code, but increases page load times a tiny bit (which can be countered with page caching).</description>
Expand Down Expand Up @@ -265,6 +273,7 @@ Setting this preference replaces the first two segments, the final index name be
<attribute attribute-id="Algolia_EnableSSR"/>
<attribute attribute-id="Algolia_EnableContentSearch"/>
<attribute attribute-id="Algolia_EnableRecommend"/>
<attribute attribute-id="Algolia_EnablePricingLazyLoad"/>
</attribute-group>
</group-definitions>

Expand Down