SFRA Server-side Javascript - Source: app_storefront_base/cartridge/scripts/cart/cartHelpers.js menu

SFRA / Server-side JS / Source: app_storefront_base/cartridge/scripts/cart/cartHelpers.js

  1. 'use strict';
  2. var ProductMgr = require('dw/catalog/ProductMgr');
  3. var Resource = require('dw/web/Resource');
  4. var Transaction = require('dw/system/Transaction');
  5. var URLUtils = require('dw/web/URLUtils');
  6. var collections = require('*/cartridge/scripts/util/collections');
  7. var ShippingHelpers = require('*/cartridge/scripts/checkout/shippingHelpers');
  8. var productHelper = require('*/cartridge/scripts/helpers/productHelpers');
  9. var arrayHelper = require('*/cartridge/scripts/util/array');
  10. var BONUS_PRODUCTS_PAGE_SIZE = 6;
  11. /**
  12. * Replaces Bundle master product items with their selected variants
  13. *
  14. * @param {dw.order.ProductLineItem} apiLineItem - Cart line item containing Bundle
  15. * @param {string[]} childProducts - List of bundle product item ID's with chosen product variant
  16. * ID's
  17. */
  18. function updateBundleProducts(apiLineItem, childProducts) {
  19. var bundle = apiLineItem.product;
  20. var bundleProducts = bundle.getBundledProducts();
  21. var bundlePids = collections.map(bundleProducts, function (product) { return product.ID; });
  22. var selectedProducts = childProducts.filter(function (product) {
  23. return bundlePids.indexOf(product.pid) === -1;
  24. });
  25. var bundleLineItems = apiLineItem.getBundledProductLineItems();
  26. selectedProducts.forEach(function (product) {
  27. var variant = ProductMgr.getProduct(product.pid);
  28. collections.forEach(bundleLineItems, function (item) {
  29. if (item.productID === variant.masterProduct.ID) {
  30. item.replaceProduct(variant);
  31. }
  32. });
  33. });
  34. }
  35. /**
  36. * @typedef urlObject
  37. * @type Object
  38. * @property {string} url - Option ID
  39. * @property {string} configureProductsUrl - url that will be used to get selected bonus products
  40. * @property {string} adToCartUrl - url to use to add products to the cart
  41. */
  42. /**
  43. * Gets the newly added bonus discount line item
  44. * @param {dw.order.Basket} currentBasket -
  45. * @param {dw.util.Collection} previousBonusDiscountLineItems - contains BonusDiscountLineItems
  46. * already processed
  47. * @param {Object} urlObject - Object with data to be used in the choice of bonus products modal
  48. * @param {string} pliUUID - the uuid of the qualifying product line item.
  49. * @return {Object} - either the object that represents data needed for the choice of
  50. * bonus products modal window or undefined
  51. */
  52. function getNewBonusDiscountLineItem(
  53. currentBasket,
  54. previousBonusDiscountLineItems,
  55. urlObject,
  56. pliUUID) {
  57. var bonusDiscountLineItems = currentBasket.getBonusDiscountLineItems();
  58. var newBonusDiscountLineItem;
  59. var result = {};
  60. newBonusDiscountLineItem = collections.find(bonusDiscountLineItems, function (item) {
  61. return !previousBonusDiscountLineItems.contains(item);
  62. });
  63. collections.forEach(bonusDiscountLineItems, function (item) {
  64. if (!previousBonusDiscountLineItems.contains(item)) {
  65. Transaction.wrap(function () {
  66. item.custom.bonusProductLineItemUUID = pliUUID; // eslint-disable-line no-param-reassign
  67. });
  68. }
  69. });
  70. if (newBonusDiscountLineItem) {
  71. result.bonusChoiceRuleBased = newBonusDiscountLineItem.bonusChoiceRuleBased;
  72. result.bonuspids = [];
  73. var iterBonusProducts = newBonusDiscountLineItem.bonusProducts.iterator();
  74. while (iterBonusProducts.hasNext()) {
  75. var newBProduct = iterBonusProducts.next();
  76. result.bonuspids.push(newBProduct.ID);
  77. }
  78. result.uuid = newBonusDiscountLineItem.UUID;
  79. result.pliUUID = pliUUID;
  80. result.maxBonusItems = newBonusDiscountLineItem.maxBonusItems;
  81. result.addToCartUrl = urlObject.addToCartUrl;
  82. result.showProductsUrl = urlObject.configureProductstUrl;
  83. result.showProductsUrlListBased = URLUtils.url('Product-ShowBonusProducts', 'DUUID', newBonusDiscountLineItem.UUID, 'pids', result.bonuspids.toString(), 'maxpids', newBonusDiscountLineItem.maxBonusItems).toString();
  84. result.showProductsUrlRuleBased = URLUtils.url('Product-ShowBonusProducts', 'DUUID', newBonusDiscountLineItem.UUID, 'pagesize', BONUS_PRODUCTS_PAGE_SIZE, 'pagestart', 0, 'maxpids', newBonusDiscountLineItem.maxBonusItems).toString();
  85. result.pageSize = BONUS_PRODUCTS_PAGE_SIZE;
  86. result.configureProductstUrl = URLUtils.url('Product-ShowBonusProducts', 'pids', result.bonuspids.toString(), 'maxpids', newBonusDiscountLineItem.maxBonusItems).toString();
  87. result.newBonusDiscountLineItem = newBonusDiscountLineItem;
  88. result.labels = {
  89. close: Resource.msg('link.choiceofbonus.close', 'product', null),
  90. selectprods: Resource.msgf('modal.header.selectproducts', 'product', null, null),
  91. maxprods: Resource.msgf('label.choiceofbonus.selectproducts', 'product', null, newBonusDiscountLineItem.maxBonusItems)
  92. };
  93. }
  94. return newBonusDiscountLineItem ? result : undefined;
  95. }
  96. /**
  97. * @typedef SelectedOption
  98. * @type Object
  99. * @property {string} optionId - Option ID
  100. * @property {string} selectedValueId - Selected option value ID
  101. */
  102. /**
  103. * Determines whether a product's current options are the same as those just selected
  104. *
  105. * @param {dw.util.Collection} existingOptions - Options currently associated with this product
  106. * @param {SelectedOption[]} selectedOptions - Product options just selected
  107. * @return {boolean} - Whether a product's current options are the same as those just selected
  108. */
  109. function hasSameOptions(existingOptions, selectedOptions) {
  110. var selected = {};
  111. for (var i = 0, j = selectedOptions.length; i < j; i++) {
  112. selected[selectedOptions[i].optionId] = selectedOptions[i].selectedValueId;
  113. }
  114. return collections.every(existingOptions, function (option) {
  115. return option.optionValueID === selected[option.optionID];
  116. });
  117. }
  118. /**
  119. * Determines whether provided Bundle items are in the list of submitted bundle item IDs
  120. *
  121. * @param {dw.util.Collection<dw.order.ProductLineItem>} productLineItems - Bundle item IDs
  122. * currently in the Cart
  123. * @param {string[]} childProducts - List of bundle items for the submitted Bundle under
  124. * consideration
  125. * @return {boolean} - Whether provided Bundle items are in the list of submitted bundle item IDs
  126. */
  127. function allBundleItemsSame(productLineItems, childProducts) {
  128. return collections.every(productLineItems, function (item) {
  129. return arrayHelper.find(childProducts, function (childProduct) {
  130. return item.productID === childProduct.pid;
  131. });
  132. });
  133. }
  134. /**
  135. * Adds a line item for this product to the Cart
  136. *
  137. * @param {dw.order.Basket} currentBasket -
  138. * @param {dw.catalog.Product} product -
  139. * @param {number} quantity - Quantity to add
  140. * @param {string[]} childProducts - the products' sub-products
  141. * @param {dw.catalog.ProductOptionModel} optionModel - the product's option model
  142. * @param {dw.order.Shipment} defaultShipment - the cart's default shipment method
  143. * @return {dw.order.ProductLineItem} - The added product line item
  144. */
  145. function addLineItem(
  146. currentBasket,
  147. product,
  148. quantity,
  149. childProducts,
  150. optionModel,
  151. defaultShipment
  152. ) {
  153. var productLineItem = currentBasket.createProductLineItem(
  154. product,
  155. optionModel,
  156. defaultShipment
  157. );
  158. if (product.bundle && childProducts.length) {
  159. updateBundleProducts(productLineItem, childProducts);
  160. }
  161. productLineItem.setQuantityValue(quantity);
  162. return productLineItem;
  163. }
  164. /**
  165. * Sets a flag to exclude the quantity for a product line item matching the provided UUID. When
  166. * updating a quantity for an already existing line item, we want to exclude the line item's
  167. * quantity and use the updated quantity instead.
  168. * @param {string} selectedUuid - Line item UUID to exclude
  169. * @param {string} itemUuid - Line item in-process to consider for exclusion
  170. * @return {boolean} - Whether to include the line item's quantity
  171. */
  172. function excludeUuid(selectedUuid, itemUuid) {
  173. return selectedUuid
  174. ? itemUuid !== selectedUuid
  175. : true;
  176. }
  177. /**
  178. * Calculate the quantities for any existing instance of a product, either as a single line item
  179. * with the same or different options, as well as inclusion in product bundles. Providing an
  180. * optional "uuid" parameter, typically when updating the quantity in the Cart, will exclude the
  181. * quantity for the matching line item, as the updated quantity will be used instead. "uuid" is not
  182. * used when adding a product to the Cart.
  183. *
  184. * @param {string} productId - ID of product to be added or updated
  185. * @param {dw.util.Collection<dw.order.ProductLineItem>} lineItems - Cart product line items
  186. * @param {string} [uuid] - When provided, excludes the quantity for the matching line item
  187. * @return {number} - Total quantity of all instances of requested product in the Cart and being
  188. * requested
  189. */
  190. function getQtyAlreadyInCart(productId, lineItems, uuid) {
  191. var qtyAlreadyInCart = 0;
  192. collections.forEach(lineItems, function (item) {
  193. if (item.bundledProductLineItems.length) {
  194. collections.forEach(item.bundledProductLineItems, function (bundleItem) {
  195. if (bundleItem.productID === productId && excludeUuid(uuid, bundleItem.UUID)) {
  196. qtyAlreadyInCart += bundleItem.quantityValue;
  197. }
  198. });
  199. } else if (item.productID === productId && excludeUuid(uuid, item.UUID)) {
  200. qtyAlreadyInCart += item.quantityValue;
  201. }
  202. });
  203. return qtyAlreadyInCart;
  204. }
  205. /**
  206. * Find all line items that contain the product specified. A product can appear in different line
  207. * items that have different option selections or in product bundles.
  208. *
  209. * @param {string} productId - Product ID to match
  210. * @param {dw.util.Collection<dw.order.ProductLineItem>} productLineItems - Collection of the Cart's
  211. * product line items
  212. * @return {Object} properties includes,
  213. * matchingProducts - collection of matching products
  214. * uuid - string value for the last product line item
  215. * @return {dw.order.ProductLineItem[]} - Filtered list of product line items matching productId
  216. */
  217. function getMatchingProducts(productId, productLineItems) {
  218. var matchingProducts = [];
  219. var uuid;
  220. collections.forEach(productLineItems, function (item) {
  221. if (item.productID === productId) {
  222. matchingProducts.push(item);
  223. uuid = item.UUID;
  224. }
  225. });
  226. return {
  227. matchingProducts: matchingProducts,
  228. uuid: uuid
  229. };
  230. }
  231. /**
  232. * Filter all the product line items matching productId and
  233. * has the same bundled items or options in the cart
  234. * @param {dw.catalog.Product} product - Product object
  235. * @param {string} productId - Product ID to match
  236. * @param {dw.util.Collection<dw.order.ProductLineItem>} productLineItems - Collection of the Cart's
  237. * product line items
  238. * @param {string[]} childProducts - the products' sub-products
  239. * @param {SelectedOption[]} options - product options
  240. * @return {dw.order.ProductLineItem[]} - Filtered all the product line item matching productId and
  241. * has the same bundled items or options
  242. */
  243. function getExistingProductLineItemsInCart(product, productId, productLineItems, childProducts, options) {
  244. var matchingProductsObj = getMatchingProducts(productId, productLineItems);
  245. var matchingProducts = matchingProductsObj.matchingProducts;
  246. var productLineItemsInCart = matchingProducts.filter(function (matchingProduct) {
  247. return product.bundle
  248. ? allBundleItemsSame(matchingProduct.bundledProductLineItems, childProducts)
  249. : hasSameOptions(matchingProduct.optionProductLineItems, options || []);
  250. });
  251. return productLineItemsInCart;
  252. }
  253. /**
  254. * Filter the product line item matching productId and
  255. * has the same bundled items or options in the cart
  256. * @param {dw.catalog.Product} product - Product object
  257. * @param {string} productId - Product ID to match
  258. * @param {dw.util.Collection<dw.order.ProductLineItem>} productLineItems - Collection of the Cart's
  259. * product line items
  260. * @param {string[]} childProducts - the products' sub-products
  261. * @param {SelectedOption[]} options - product options
  262. * @return {dw.order.ProductLineItem} - get the first product line item matching productId and
  263. * has the same bundled items or options
  264. */
  265. function getExistingProductLineItemInCart(product, productId, productLineItems, childProducts, options) {
  266. return getExistingProductLineItemsInCart(product, productId, productLineItems, childProducts, options)[0];
  267. }
  268. /**
  269. * Check if the bundled product can be added to the cart
  270. * @param {string[]} childProducts - the products' sub-products
  271. * @param {dw.util.Collection<dw.order.ProductLineItem>} productLineItems - Collection of the Cart's
  272. * product line items
  273. * @param {number} quantity - the number of products to the cart
  274. * @return {boolean} - return true if the bundled product can be added
  275. */
  276. function checkBundledProductCanBeAdded(childProducts, productLineItems, quantity) {
  277. var atsValueByChildPid = {};
  278. var totalQtyRequested = 0;
  279. var canBeAdded = false;
  280. childProducts.forEach(function (childProduct) {
  281. var apiChildProduct = ProductMgr.getProduct(childProduct.pid);
  282. atsValueByChildPid[childProduct.pid] =
  283. apiChildProduct.availabilityModel.inventoryRecord.ATS.value;
  284. });
  285. canBeAdded = childProducts.every(function (childProduct) {
  286. var bundleQuantity = quantity;
  287. var itemQuantity = bundleQuantity * childProduct.quantity;
  288. var childPid = childProduct.pid;
  289. totalQtyRequested = itemQuantity + getQtyAlreadyInCart(childPid, productLineItems);
  290. return totalQtyRequested <= atsValueByChildPid[childPid];
  291. });
  292. return canBeAdded;
  293. }
  294. /**
  295. * Adds a product to the cart. If the product is already in the cart it increases the quantity of
  296. * that product.
  297. * @param {dw.order.Basket} currentBasket - Current users's basket
  298. * @param {string} productId - the productId of the product being added to the cart
  299. * @param {number} quantity - the number of products to the cart
  300. * @param {string[]} childProducts - the products' sub-products
  301. * @param {SelectedOption[]} options - product options
  302. * @return {Object} returns an error object
  303. */
  304. function addProductToCart(currentBasket, productId, quantity, childProducts, options) {
  305. var availableToSell;
  306. var defaultShipment = currentBasket.defaultShipment;
  307. var perpetual;
  308. var product = ProductMgr.getProduct(productId);
  309. var productInCart;
  310. var productLineItems = currentBasket.productLineItems;
  311. var productQuantityInCart;
  312. var quantityToSet;
  313. var optionModel = productHelper.getCurrentOptionModel(product.optionModel, options);
  314. var result = {
  315. error: false,
  316. message: Resource.msg('text.alert.addedtobasket', 'product', null)
  317. };
  318. var totalQtyRequested = 0;
  319. var canBeAdded = false;
  320. if (product.bundle) {
  321. canBeAdded = checkBundledProductCanBeAdded(childProducts, productLineItems, quantity);
  322. } else {
  323. totalQtyRequested = quantity + getQtyAlreadyInCart(productId, productLineItems);
  324. perpetual = product.availabilityModel.inventoryRecord.perpetual;
  325. canBeAdded =
  326. (perpetual
  327. || totalQtyRequested <= product.availabilityModel.inventoryRecord.ATS.value);
  328. }
  329. if (!canBeAdded) {
  330. result.error = true;
  331. result.message = Resource.msgf(
  332. 'error.alert.selected.quantity.cannot.be.added.for',
  333. 'product',
  334. null,
  335. product.availabilityModel.inventoryRecord.ATS.value,
  336. product.name
  337. );
  338. return result;
  339. }
  340. productInCart = getExistingProductLineItemInCart(
  341. product, productId, productLineItems, childProducts, options);
  342. if (productInCart) {
  343. productQuantityInCart = productInCart.quantity.value;
  344. quantityToSet = quantity ? quantity + productQuantityInCart : productQuantityInCart + 1;
  345. availableToSell = productInCart.product.availabilityModel.inventoryRecord.ATS.value;
  346. if (availableToSell >= quantityToSet || perpetual) {
  347. productInCart.setQuantityValue(quantityToSet);
  348. result.uuid = productInCart.UUID;
  349. } else {
  350. result.error = true;
  351. result.message = availableToSell === productQuantityInCart
  352. ? Resource.msg('error.alert.max.quantity.in.cart', 'product', null)
  353. : Resource.msg('error.alert.selected.quantity.cannot.be.added', 'product', null);
  354. }
  355. } else {
  356. var productLineItem;
  357. productLineItem = addLineItem(
  358. currentBasket,
  359. product,
  360. quantity,
  361. childProducts,
  362. optionModel,
  363. defaultShipment
  364. );
  365. result.uuid = productLineItem.UUID;
  366. }
  367. return result;
  368. }
  369. /**
  370. * Loops through all Shipments and attempts to select a ShippingMethod, where absent
  371. * @param {dw.order.Basket} basket - the target Basket object
  372. */
  373. function ensureAllShipmentsHaveMethods(basket) {
  374. var shipments = basket.shipments;
  375. collections.forEach(shipments, function (shipment) {
  376. ShippingHelpers.ensureShipmentHasMethod(shipment);
  377. });
  378. }
  379. /**
  380. * return a link to enable reporting of add to cart events
  381. * @param {dw.order.Basket} currentBasket - the target Basket object
  382. * @param {boolean} resultError - the target Basket object
  383. * @return {string|boolean} returns a url or boolean value false
  384. */
  385. function getReportingUrlAddToCart(currentBasket, resultError) {
  386. if (currentBasket && currentBasket.allLineItems.length && !resultError) {
  387. return URLUtils.url('ReportingEvent-MiniCart').toString();
  388. }
  389. return false;
  390. }
  391. module.exports = {
  392. addLineItem: addLineItem,
  393. addProductToCart: addProductToCart,
  394. checkBundledProductCanBeAdded: checkBundledProductCanBeAdded,
  395. ensureAllShipmentsHaveMethods: ensureAllShipmentsHaveMethods,
  396. getQtyAlreadyInCart: getQtyAlreadyInCart,
  397. getNewBonusDiscountLineItem: getNewBonusDiscountLineItem,
  398. getExistingProductLineItemInCart: getExistingProductLineItemInCart,
  399. getExistingProductLineItemsInCart: getExistingProductLineItemsInCart,
  400. getMatchingProducts: getMatchingProducts,
  401. allBundleItemsSame: allBundleItemsSame,
  402. hasSameOptions: hasSameOptions,
  403. BONUS_PRODUCTS_PAGE_SIZE: BONUS_PRODUCTS_PAGE_SIZE,
  404. updateBundleProducts: updateBundleProducts,
  405. getReportingUrlAddToCart: getReportingUrlAddToCart
  406. };