From f1166815a6fc94b7d3f4c4cb6637469987012583 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 6 Jun 2024 14:22:10 -0600 Subject: [PATCH 1/3] fix(platforms/bigcommerce/addtocart): updating function to work with attributes in new groovy script --- .../bigcommerce/groovy/ss_variants.groovy | 114 +++++++++++++----- .../bigcommerce/src/addToCart.test.ts | 99 +++++++-------- .../bigcommerce/src/addToCart.ts | 75 ++++++------ 3 files changed, 162 insertions(+), 126 deletions(-) diff --git a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy index 9342475ce..27d79ea56 100644 --- a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy +++ b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy @@ -1,4 +1,4 @@ -/* +/* Snap variants script for BigCommerce Generates a JSON string with format: @@ -13,55 +13,107 @@ ] Each object (variant) in the array represents a variation of the product and each of the `options` should reflect that configuration. - When using this in Snap any properties found in `mappings.core` and `attributes` will be "masked" when the variant is selected. + When using this in Snap any properties found in `mappings.core` and `attributes` will be "masked" via result.display when the variant is selected. See Snap documentation for more details. */ import groovy.json.JsonOutput import groovy.json.JsonSlurper -import org.apache.commons.lang.StringUtils def slurper = new JsonSlurper() -def ss_variants = [] -if (Objects.nonNull(doc?.child_sku_options) && !StringUtils.isEmpty(doc?.child_sku_options)) { +/* map variants_json -> variants structure and put into array */ +def variant_array = [] + +// variant data to put into core fields +def core_fields_mapping = [ + uid: "product_id", + price: "price", + msrp: "retail_price", + sku: "child_sku", + imageUrl: 'image_url', + thumbnailImageUrl: 'image_url', +] + +// attributes outside of the options +def attributes_fields_mapping = [ + quantity: "inventory_level", // property must be named "quantity" for proper functionality +] + +def sku_options_by_id = [:]; +/* + sku_options_by_id = { + [id: string]: option[], // all options in this array have the same sku_option.id + } +*/ + +if (doc?.child_sku_options && Objects.nonNull(doc?.child_sku_options)) { def sku_options = slurper.parseText(doc.child_sku_options as String) - if(Objects.nonNull(sku_options) && !(sku_options as List).isEmpty()){ + + // build out map of sku_options_by_id options - options are grouped together by sku_option.id + if(Objects.nonNull(sku_options) && !(sku_options as List).isEmpty()) { sku_options.each { sku_option -> - def sku = [:] - def mappings = [:] - def core = [:] - def attributes = [:] - def option_data = [:] - def options = [:] - - core.put("imageUrl" , sku_option?.image_url) - core.put("url", doc.url) - core.put("uid" ,sku_option.child_sku) - mappings.put("core", core) - sku.put("mappings",mappings) - - if(Objects.nonNull(sku_option?.inventory_level)){ - attributes.put("available", sku_option?.inventory_level > 0) - } + sku_options_by_id[sku_option.id] = sku_options_by_id[sku_option.id] ?: []; + sku_options_by_id[sku_option.id].push(sku_option); + } + } - if(Objects.nonNull(sku_option?.option) && !StringUtils.isEmpty(sku_option?.option) && Objects.nonNull(sku_option?.value) && !StringUtils.isEmpty(sku_option?.value)){ - attributes.put("title", sku_option?.option + " / " + sku_option?.value) + // use sku_options_by_id map to poppulate variant_array + sku_options_by_id.each { id, options -> + def variant_object = [:] + variant_object.mappings = [:] + variant_object.mappings.core = [:] + variant_object.attributes = [:] + variant_object.options = [:] + // convert into a variant object + /* + { + "mappings": { + "core": { ... } + }, + "attributes": { + ... + } } - sku.put("attributes",attributes) + */ - option_data.put("value", sku_option?.value) + // loop through each option_array + options.each { option -> + /* populate core mappings */ + core_fields_mapping.each { core_field_name, variant_field_name -> + if (option[variant_field_name] && Objects.nonNull(option[variant_field_name])) { + variant_object.mappings.core[core_field_name] = option[variant_field_name] + } + } - if(Objects.nonNull(sku_option?.option)){ - options.put(sku_option?.option, option_data) + /* populate attributes */ + attributes_fields_mapping.each { attribute_field_name, variant_field_name -> + if (option[variant_field_name] && Objects.nonNull(option[variant_field_name])) { + variant_object.attributes[attribute_field_name] = option[variant_field_name] + } } - sku.put("options",options) - ss_variants.add(sku) + // determine availability + if (option.inventory_level > 0 && !option.purchasing_disabled) { + variant_object.attributes.available = true + } else { + variant_object.attributes.available = false + } + + /* populate options */ + if (option.option && option.value && option.option_id && option.option_value_id) { + variant_object.options[option.option] = [ + value: option.value, + optionId: option.option_value_id, + attributeId: option.option_id, + ] + } } + + variant_array.push(variant_object); } } -index.put("ss_variants", JsonOutput.toJson(ss_variants)) \ No newline at end of file +index.ss_variants = JsonOutput.toJson(variant_array) \ No newline at end of file diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts index 86f8f9b0d..cd1fe957e 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts @@ -88,15 +88,13 @@ describe('addToCart', () => { const item = results[0] as Product; addToCart([item]); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -112,16 +110,13 @@ describe('addToCart', () => { addToCart([item]); - const obj = { - product_id: item.id, - quantity: 4, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -134,23 +129,20 @@ describe('addToCart', () => { it('can use alternate id column', () => { const config = { - idFieldName: 'mappings.core.url', + idFieldName: 'mappings.core.sku', }; const item = results[0] as Product; addToCart([item], config); - const obj = { - product_id: item.mappings.core?.url, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', `${item.mappings.core?.sku}`); + formData.append('qty[]', `${item.quantity}`); const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -164,15 +156,13 @@ describe('addToCart', () => { addToCart([item]); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -193,15 +183,13 @@ describe('addToCart', () => { addToCart([item], config); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -223,15 +211,13 @@ describe('addToCart', () => { addToCart([item], config); - const obj = { - product_id: item.id, - quantity: item.quantity, - action: 'add', - }; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; @@ -249,15 +235,14 @@ describe('addToCart', () => { addToCart(items); for (let i = 0; i < items.length; i++) { - const obj = { - product_id: items[i].id, - quantity: items[i].quantity, - action: 'add', - }; + const item = items[i]; + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', item.id); + formData.append('qty[]', `${item.quantity}`); + const params = { - body: JSON.stringify(obj), - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: formData, method: 'POST', }; diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.ts b/packages/snap-platforms/bigcommerce/src/addToCart.ts index 47c96767c..275c7a897 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.ts @@ -8,10 +8,7 @@ type BigCommerceAddToCartConfig = { type LineItem = { product_id: string; quantity: number; -}; - -type FormData = { - line_items: LineItem[]; + attributes: { attributeId?: string; optionId?: string }[]; }; export const addToCart = async (items: Product[], config?: BigCommerceAddToCartConfig) => { @@ -20,9 +17,7 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC return; } - const formData: FormData = { - line_items: [], - }; + const lineItems: LineItem[] = []; items.map((item) => { let id = item?.display?.mappings?.core?.uid; @@ -44,19 +39,32 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC } if (id && item.quantity) { - const obj = { + const productDetails: LineItem = { product_id: id, quantity: item.quantity, + attributes: [], }; - formData.line_items.push(obj); + const options = item.variants?.active?.options; + if (options) { + Object.keys(options).forEach((option) => { + const attributeId = options[option].attributeId; + const optionId = options[option].optionId; + + if (attributeId && optionId) { + productDetails.attributes.push({ attributeId, optionId }); + } + }); + } + + lineItems.push(productDetails); } }); // first check how many products we are adding - if (formData.line_items.length) { - for (let i = 0; i < formData.line_items.length; i++) { - await addSingleProductv1(formData.line_items[i]); + if (lineItems.length) { + for (let i = 0; i < lineItems.length; i++) { + await addSingleProductv1(lineItems[i]); } } @@ -72,35 +80,26 @@ const addSingleProductv1 = async (item: LineItem) => { return; } - const endpoint = { - route: `/remote/v1/cart/add`, - method: 'POST', - accept: 'application/json', - content: 'application/json', - success: 200, - }; - try { - const payload = JSON.stringify({ - ...item, - action: 'add', + const formData = new FormData(); + formData.append('action', 'add'); + formData.append('product_id', `${item.product_id}`); + formData.append('qty[]', `${item.quantity}`); + item.attributes.forEach((attribute) => { + formData.append(`attribute[${attribute.attributeId}]`, `${attribute.optionId}`); + }); + + const response = await fetch('/remote/v1/cart/add', { + method: 'POST', + body: formData, }); - const init: RequestInit = { - method: endpoint.method, - credentials: 'same-origin', - headers: { - // note: no authorization - Accept: endpoint.accept, - 'Content-Type': endpoint.content, - }, - body: payload, - }; - - const response = await fetch(endpoint.route, init); - - if (response.status !== endpoint.success) { - throw new Error(`Error: addToCart responded with ${response.status}, ${response}`); + const data = await response.json(); + + if (response.status !== 200 || data?.data?.error) { + throw new Error(`Error: addToCart responded with: ${response.status}, ${data?.data?.error || response}`); + } else { + return data; } } catch (err) { console.error(err); From b832e660a853cbd9a9bdb54a26568e035270963a Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 7 Jun 2024 08:48:56 -0600 Subject: [PATCH 2/3] refactor(platforms/bigcommerce): updated addToCart function to use storefront API and updated test --- .../bigcommerce/groovy/ss_variants.groovy | 4 +- .../bigcommerce/src/addToCart.test.ts | 439 +++++++++++++----- .../bigcommerce/src/addToCart.ts | 98 ++-- .../src/MockData/meta/tfdz6e/meta.json | 148 ++++++ .../src/MockData/search/tfdz6e/variants.json | 356 ++++++++++++++ .../src/Search/Stores/SearchResultStore.ts | 1 + 6 files changed, 892 insertions(+), 154 deletions(-) create mode 100644 packages/snap-shared/src/MockData/meta/tfdz6e/meta.json create mode 100644 packages/snap-shared/src/MockData/search/tfdz6e/variants.json diff --git a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy index 27d79ea56..e83e52b18 100644 --- a/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy +++ b/packages/snap-platforms/bigcommerce/groovy/ss_variants.groovy @@ -106,8 +106,8 @@ if (doc?.child_sku_options && Objects.nonNull(doc?.child_sku_options)) { if (option.option && option.value && option.option_id && option.option_value_id) { variant_object.options[option.option] = [ value: option.value, - optionId: option.option_value_id, - attributeId: option.option_id, + optionValue: option.option_value_id, + optionId: option.option_id, ] } } diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts index cd1fe957e..155be1f9b 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.test.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.test.ts @@ -1,6 +1,5 @@ import 'whatwg-fetch'; import { addToCart } from './addToCart'; -import { Product } from '@searchspring/snap-store-mobx'; import { MockClient } from '@searchspring/snap-shared'; import { SearchStore } from '@searchspring/snap-store-mobx'; import { UrlManager, QueryStringTranslator, reactLinker } from '@searchspring/snap-url-manager'; @@ -10,9 +9,15 @@ import { Logger } from '@searchspring/snap-logger'; import { Tracker } from '@searchspring/snap-tracker'; import { SearchController } from '@searchspring/snap-controller'; +import type { Product, SearchResultStore, SearchStoreConfig } from '@searchspring/snap-store-mobx'; + +const HEADERS = { 'Content-Type': 'application/json', Accept: 'application/json' }; +const MOCK_CART_ID = '123456789'; const ORIGIN = 'http://localhost'; -const ADD_ROUTE = '/remote/v1/cart/add'; -const CART_ROUTE = '/cart.php'; +const CART_ROUTE = '/api/storefront/carts'; +const CART_EXISTS_ROUTE = `/api/storefront/carts/${MOCK_CART_ID}/items`; +const REDIRECT_ROUTE = '/cart.php'; +const MOCK_ADDED_RESPONSE = { id: MOCK_CART_ID }; const wait = (time = 1) => { return new Promise((resolve) => { @@ -36,15 +41,17 @@ const searchConfigDefault = { }, settings: {}, }; -let results: any; -let controller: any; + +let results: SearchResultStore; +let controller: SearchController; let errMock: any; +let fetchMock: any; -// @ts-ignore -const fetchMock = jest.spyOn(global, 'fetch').mockImplementation(() => Promise.resolve({ json: () => Promise.resolve([]), ok: true, status: 200 })); +const client = new MockClient(globals, {}); +// TODO: need to use variant data from BigCommerce const controllerServices: any = { - client: new MockClient(globals, {}), + client, store: new SearchStore(searchConfig, services), urlManager, eventManager: new EventManager(), @@ -63,6 +70,18 @@ describe('addToCart', () => { results = controller.store.results; errMock = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // @ts-ignore + fetchMock = jest.spyOn(global, 'fetch').mockImplementation((url) => { + let response: any = []; + if (url == CART_ROUTE) { + response = [{ id: MOCK_CART_ID }]; + } else if (url == CART_EXISTS_ROUTE) { + response = MOCK_ADDED_RESPONSE; + } + + return Promise.resolve({ json: () => Promise.resolve(response), ok: true, status: 200 }); + }); }); beforeEach(() => { @@ -77,180 +96,368 @@ describe('addToCart', () => { }); it('requires product(s) to be passed', () => { - // @ts-ignore + // @ts-ignore - adding with no params addToCart(); expect(fetchMock).not.toHaveBeenCalled(); expect(errMock).toHaveBeenCalledWith('Error: no products to add'); }); - it('adds data passed', () => { - const item = results[0] as Product; - addToCart([item]); - - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); - - const params = { - body: formData, - method: 'POST', + it('will log an error when it cannot find a custom id', async () => { + const config = { + idFieldName: 'mappings.dne.nope', }; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + const item = results[0] as Product; + + await addToCart([item], config); - fetchMock.mockClear(); + expect(errMock).toHaveBeenCalledWith(`Error: couldnt find column in item data. please verify 'idFieldName' in the config.`); + expect(fetchMock).not.toHaveBeenCalled(); }); - it('can add multiple quantities', () => { + it('will redirect by default', async () => { const item = results[0] as Product; - item.quantity = 4; + await addToCart([item]); + await wait(10); - addToCart([item]); + expect(window.location.href).toEqual(REDIRECT_ROUTE); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + fetchMock.mockClear(); + }); - const params = { - body: formData, - method: 'POST', + it('will not redirect if config is false', async () => { + const item = results[0] as Product; + const config = { + redirect: false, }; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + await addToCart([item], config); + await wait(10); - fetchMock.mockClear(); + expect(window.location.href).toEqual(ORIGIN); - item.quantity = 1; + fetchMock.mockClear(); }); - it('can use alternate id column', () => { + it('can use a custom redirect', async () => { const config = { - idFieldName: 'mappings.core.sku', + redirect: 'https://redirect.localhost', }; const item = results[0] as Product; - addToCart([item], config); + await addToCart([item], config); + await wait(10); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', `${item.mappings.core?.sku}`); - formData.append('qty[]', `${item.quantity}`); + expect(window.location.href).toEqual(config.redirect); - const params = { - body: formData, - method: 'POST', - }; + fetchMock.mockClear(); + }); - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + it('will return the API response after adding', async () => { + const item = results[0] as Product; + + const response = await addToCart([item]); + + expect(response).toStrictEqual(MOCK_ADDED_RESPONSE); fetchMock.mockClear(); }); - it('will redirect by default', async () => { - const item = results[0] as Product; + describe('when a cart exists', () => { + it('can add a single simple product', async () => { + const item = results[0] as Product; - addToCart([item]); + await addToCart([item]); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + const getParams = { + headers: HEADERS, + method: 'GET', + }; - const params = { - body: formData, - method: 'POST', - }; + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + const postBody = { + lineItems: [ + { + product_id: item.id, + quantity: item.quantity, + }, + ], + }; - await wait(10); + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; - expect(window.location.href).toEqual(CART_ROUTE); + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); - fetchMock.mockClear(); - }); + fetchMock.mockClear(); + }); - it('will not redirect if config is false', async () => { - const item = results[0] as Product; - const config = { - redirect: false, - }; + it('can add a single product with options', async () => { + client.mockData.updateConfig({ siteId: 'tfdz6e', search: 'variants' }); + const optionSearchConfig: SearchStoreConfig = { + ...searchConfig, + settings: { + redirects: { + singleResult: false, + }, + variants: { + field: 'ss_variants', + }, + }, + }; + const optionController = new SearchController(optionSearchConfig, controllerServices); - addToCart([item], config); + await optionController.search(); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + const results = optionController.store.results; - const params = { - body: formData, - method: 'POST', - }; + const item = results[0] as Product; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + await addToCart([item]); - await wait(10); + const getParams = { + headers: HEADERS, + method: 'GET', + }; - expect(window.location.href).toEqual(ORIGIN); + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: [ + { + product_id: item.display.mappings.core?.uid, + quantity: item.quantity, + optionSelections: [ + { + optionId: 570, + optionValue: 2900, + }, + ], + }, + ], + }; - fetchMock.mockClear(); - }); + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; - it('can use a custom redirect', async () => { - const config = { - redirect: 'https://redirect.localhost', - }; + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); - const item = results[0] as Product; + fetchMock.mockClear(); + }); - addToCart([item], config); + it('can add multiple items', async () => { + const items = results.slice(0, 3) as Product[]; + items.forEach((item) => item.quantity++); - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + await addToCart(items); - const params = { - body: formData, - method: 'POST', - }; + const getParams = { + headers: HEADERS, + method: 'GET', + }; - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); - await wait(10); + const postBody = { + lineItems: items.map((item) => ({ + product_id: item.id, + quantity: item.quantity, + })), + }; - expect(window.location.href).toEqual(config.redirect); + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; - fetchMock.mockClear(); + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); + + it('can use alternate id column', async () => { + const config = { + idFieldName: 'mappings.core.sku', + }; + + const item = results[0] as Product; + + await addToCart([item], config); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: [ + { + product_id: item.mappings.core?.sku, + quantity: item.quantity, + }, + ], + }; + + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; + + expect(fetchMock).toHaveBeenLastCalledWith(CART_EXISTS_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); }); - it('can add multiple items', async () => { - const items = results.slice(0, 3) as Product[]; - addToCart(items); + describe('when NO cart exists', () => { + beforeAll(() => { + // @ts-ignore + fetchMock = jest.spyOn(global, 'fetch').mockImplementation((url) => { + let response: any = []; + if (url == CART_EXISTS_ROUTE) { + response = MOCK_ADDED_RESPONSE; + } + + return Promise.resolve({ json: () => Promise.resolve(response), ok: true, status: 200 }); + }); + }); + + it('can add a single simple product', async () => { + const item = results[0] as Product; + + await addToCart([item]); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', item.id); - formData.append('qty[]', `${item.quantity}`); + const postBody = { + lineItems: [ + { + product_id: item.id, + quantity: item.quantity, + }, + ], + }; - const params = { - body: formData, + const postParams = { + headers: HEADERS, method: 'POST', + body: JSON.stringify(postBody), }; - await wait(10); + expect(fetchMock).toHaveBeenLastCalledWith(CART_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); + + it('can add a single product with options', async () => { + client.mockData.updateConfig({ siteId: 'tfdz6e', search: 'variants' }); + const optionSearchConfig: SearchStoreConfig = { + ...searchConfig, + settings: { + redirects: { + singleResult: false, + }, + variants: { + field: 'ss_variants', + }, + }, + }; + const optionController = new SearchController(optionSearchConfig, controllerServices); - expect(fetchMock).toHaveBeenCalledWith(ADD_ROUTE, params); - } + await optionController.search(); - fetchMock.mockClear(); + const results = optionController.store.results; + + const item = results[0] as Product; + + await addToCart([item]); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: [ + { + product_id: item.display.mappings.core?.uid, + quantity: item.quantity, + optionSelections: [ + { + optionId: 570, + optionValue: 2900, + }, + ], + }, + ], + }; + + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; + + expect(fetchMock).toHaveBeenLastCalledWith(CART_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); + + it('can add multiple items', async () => { + const items = results.slice(0, 3) as Product[]; + await addToCart(items); + + const getParams = { + headers: HEADERS, + method: 'GET', + }; + + expect(fetchMock).toHaveBeenCalledWith(CART_ROUTE, getParams); + + const postBody = { + lineItems: items.map((item) => ({ + product_id: item.id, + quantity: item.quantity, + })), + }; + + const postParams = { + headers: HEADERS, + method: 'POST', + body: JSON.stringify(postBody), + }; + + expect(fetchMock).toHaveBeenLastCalledWith(CART_ROUTE, postParams); + expect(fetchMock).toHaveBeenCalledTimes(2); + + fetchMock.mockClear(); + }); }); }); diff --git a/packages/snap-platforms/bigcommerce/src/addToCart.ts b/packages/snap-platforms/bigcommerce/src/addToCart.ts index 275c7a897..59ff2d177 100644 --- a/packages/snap-platforms/bigcommerce/src/addToCart.ts +++ b/packages/snap-platforms/bigcommerce/src/addToCart.ts @@ -8,7 +8,7 @@ type BigCommerceAddToCartConfig = { type LineItem = { product_id: string; quantity: number; - attributes: { attributeId?: string; optionId?: string }[]; + optionSelections?: { optionId?: string; optionValue?: string }[]; }; export const addToCart = async (items: Product[], config?: BigCommerceAddToCartConfig) => { @@ -26,13 +26,16 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC if (config?.idFieldName) { let level: any = item; config.idFieldName.split('.').map((field) => { - if (level[field]) { + if (level && level[field]) { level = level[field]; } else { - console.error('Error: couldnt find column in item data. please check your idFieldName is correct in the config.'); + console.error(`Error: couldnt find column in item data. please verify 'idFieldName' in the config.`); + level = undefined; + id = undefined; return; } }); + if (level && level !== item) { id = level; } @@ -42,17 +45,17 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC const productDetails: LineItem = { product_id: id, quantity: item.quantity, - attributes: [], }; const options = item.variants?.active?.options; if (options) { + productDetails.optionSelections = []; Object.keys(options).forEach((option) => { - const attributeId = options[option].attributeId; const optionId = options[option].optionId; + const optionValue = options[option].optionValue; - if (attributeId && optionId) { - productDetails.attributes.push({ attributeId, optionId }); + if (optionId && optionValue) { + productDetails.optionSelections?.push({ optionId, optionValue }); } }); } @@ -61,47 +64,70 @@ export const addToCart = async (items: Product[], config?: BigCommerceAddToCartC } }); - // first check how many products we are adding if (lineItems.length) { - for (let i = 0; i < lineItems.length; i++) { - await addSingleProductv1(lineItems[i]); + const addToCartResponse = await addLineItemsToCart(lineItems); + + // do redirect (or not) + if (config?.redirect !== false) { + setTimeout(() => (window.location.href = typeof config?.redirect == 'string' ? config?.redirect : '/cart.php')); } - } - // do redirect (or not) - if (config?.redirect !== false) { - setTimeout(() => (window.location.href = typeof config?.redirect == 'string' ? config?.redirect : '/cart.php')); + return addToCartResponse; } }; -const addSingleProductv1 = async (item: LineItem) => { - if (!item) { - console.error('Error: no product to add'); - return; - } - +async function addLineItemsToCart(lineItems: LineItem[]): Promise { try { - const formData = new FormData(); - formData.append('action', 'add'); - formData.append('product_id', `${item.product_id}`); - formData.append('qty[]', `${item.quantity}`); - item.attributes.forEach((attribute) => { - formData.append(`attribute[${attribute.attributeId}]`, `${attribute.optionId}`); - }); + const cartId = await getExistingCartId(); + + // if existing cartId use it, otherwise create new cart with items + let addToCartUrl = '/api/storefront/carts'; + if (cartId) { + addToCartUrl = `/api/storefront/carts/${cartId}/items`; + } - const response = await fetch('/remote/v1/cart/add', { + const body = JSON.stringify({ lineItems }); + + const response = await fetch(addToCartUrl, { method: 'POST', - body: formData, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, }); - const data = await response.json(); + if (response.status !== 200) { + throw new Error(`API rejected addToCart: ${response.status}`); + } - if (response.status !== 200 || data?.data?.error) { - throw new Error(`Error: addToCart responded with: ${response.status}, ${data?.data?.error || response}`); - } else { - return data; + const responseData = await response.json(); + + if (responseData?.id) { + // cart Id should exist now. + return responseData; } } catch (err) { - console.error(err); + console.error(`Error: could not add to cart.`, err); } -}; +} + +async function getExistingCartId(): Promise { + try { + const response = await fetch('/api/storefront/carts', { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + + const responseData = await response.json(); + + if (Array.isArray(responseData) && responseData.length) { + return responseData[0].id; + } + } catch (err) { + // error... + } +} diff --git a/packages/snap-shared/src/MockData/meta/tfdz6e/meta.json b/packages/snap-shared/src/MockData/meta/tfdz6e/meta.json new file mode 100644 index 000000000..7069718fb --- /dev/null +++ b/packages/snap-shared/src/MockData/meta/tfdz6e/meta.json @@ -0,0 +1,148 @@ +{ + "facets": { + "categories_hierarchy": { + "multiple": "single", + "display": "hierarchy", + "label": "Category", + "collapsed": true, + "hierarchyDelimiter": ">" + }, + "custom_color": { + "multiple": "or", + "display": "palette", + "label": "Color", + "collapsed": true + }, + "custom_depth": { + "multiple": "or", + "display": "list", + "label": "Depth", + "collapsed": true + }, + "custom_diameter": { + "multiple": "or", + "display": "list", + "label": "Diameter", + "collapsed": true + }, + "custom_head_diameter": { + "multiple": "or", + "display": "list", + "label": "Head Diameter", + "collapsed": true + }, + "custom_height": { + "multiple": "or", + "display": "list", + "label": "Height", + "collapsed": true + }, + "custom_hole_size": { + "multiple": "or", + "display": "list", + "label": "Hole Size", + "collapsed": true + }, + "custom_material": { + "multiple": "or", + "display": "list", + "label": "Type of Wood", + "collapsed": true + }, + "custom_shape": { + "multiple": "or", + "display": "list", + "label": "Shape", + "collapsed": true + }, + "custom_tenon_diameter": { + "multiple": "or", + "display": "list", + "label": "Tenon Diameter", + "collapsed": true + }, + "custom_tenon_length": { + "multiple": "or", + "display": "list", + "label": "Tenon Length", + "collapsed": true + }, + "custom_type_of_metal": { + "multiple": "or", + "display": "list", + "label": "Type of Metal", + "collapsed": true + }, + "result_depth": { + "multiple": "or", + "display": "list", + "label": "Thickness", + "collapsed": true + }, + "result_height": { + "multiple": "or", + "display": "list", + "label": "Length", + "collapsed": true + }, + "result_width": { + "multiple": "or", + "display": "list", + "label": "Width", + "collapsed": true + } + }, + "sortOptions": [ + { + "type": "relevance", + "field": "relevance", + "direction": "desc", + "label": "Best Match" + }, + { + "type": "field", + "field": "total_sold", + "direction": "desc", + "label": "Most Popular" + }, + { + "type": "field", + "field": "ss_days_since_created", + "direction": "asc", + "label": "Newest" + }, + { + "type": "field", + "field": "calculated_price", + "direction": "asc", + "label": "Price: Low to High" + }, + { + "type": "field", + "field": "calculated_price", + "direction": "desc", + "label": "Price: High to Low" + }, + { + "type": "field", + "field": "ss_diameter_inches", + "direction": "asc", + "label": "Diameter: Low to High" + }, + { + "type": "field", + "field": "ss_diameter_inches", + "direction": "desc", + "label": "Diameter: High to Low" + }, + { + "type": "field", + "field": "rating_average", + "direction": "desc", + "label": "Highest Rated" + } + ], + "pagination": { + "defaultPageSize": 72 + } +} \ No newline at end of file diff --git a/packages/snap-shared/src/MockData/search/tfdz6e/variants.json b/packages/snap-shared/src/MockData/search/tfdz6e/variants.json new file mode 100644 index 000000000..fbdef38f3 --- /dev/null +++ b/packages/snap-shared/src/MockData/search/tfdz6e/variants.json @@ -0,0 +1,356 @@ +{ + "pagination": { + "totalResults": 1, + "page": 1, + "pageSize": 30, + "totalPages": 1 + }, + "results": [ + { + "id": "4007", + "mappings": { + "core": { + "uid": "4007", + "sku": "LS-EEBP", + "name": "Easter Egg with Boho Etched Pattern", + "url": "/easter-egg-with-boho-etched-pattern/", + "price": 0, + "msrp": 0, + "imageUrl": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/27880/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44621.1.jpg", + "thumbnailImageUrl": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/27880/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44621.1.jpg", + "ratingCount": "0", + "brand": "Woodpeckers Crafts", + "popularity": "279" + } + }, + "attributes": { + "availability": "available", + "categories": [ + "Plywood and Wood Cutouts", + "Wooden Seasonal Cutouts", + "Wood Spring & Easter Cutouts", + "Shop By Season", + "Easter and Spring", + "Laser Cutouts" + ], + "categories_hierarchy": [ + "Plywood and Wood Cutouts", + "Plywood and Wood Cutouts>Wooden Seasonal Cutouts", + "Plywood and Wood Cutouts>Wooden Seasonal Cutouts>Wood Spring & Easter Cutouts", + "Shop By Season", + "Shop By Season>Easter and Spring", + "Plywood and Wood Cutouts>Laser Cutouts" + ], + "cdn_images": "28082,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__24263,jpg|27929,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44790,jpg|28010,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__22446,jpg|27877,woodpeckers-crafts-easter-egg-with-boho-etched-pattern__70969,jpg", + "child_sku_options": "[{\"option_value_id\":2900,\"value\":\"5 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8732,\"product_id\":4007,\"child_sku\":\"LS-EEBP-5\",\"price\":0.68,\"calculated_price\":0.68,\"retail_price\":null,\"width\":3.7175,\"height\":5,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2901,\"value\":\"6 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8733,\"product_id\":4007,\"child_sku\":\"LS-EEBP-6\",\"price\":1.05,\"calculated_price\":1.05,\"retail_price\":null,\"width\":4.461,\"height\":6,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2902,\"value\":\"7 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8734,\"product_id\":4007,\"child_sku\":\"LS-EEBP-7\",\"price\":1.28,\"calculated_price\":1.28,\"retail_price\":null,\"width\":5.2045,\"height\":7,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2903,\"value\":\"8 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8735,\"product_id\":4007,\"child_sku\":\"LS-EEBP-8\",\"price\":1.79,\"calculated_price\":1.79,\"retail_price\":null,\"width\":5.948,\"height\":8,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2904,\"value\":\"9 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8736,\"product_id\":4007,\"child_sku\":\"LS-EEBP-9\",\"price\":2.35,\"calculated_price\":2.35,\"retail_price\":null,\"width\":6.6915,\"height\":9,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2905,\"value\":\"10 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8737,\"product_id\":4007,\"child_sku\":\"LS-EEBP-10\",\"price\":3.23,\"calculated_price\":3.23,\"retail_price\":null,\"width\":7.435,\"height\":10,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2906,\"value\":\"12 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8738,\"product_id\":4007,\"child_sku\":\"LS-EEBP-12\",\"price\":4.71,\"calculated_price\":4.71,\"retail_price\":null,\"width\":8.922,\"height\":12,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2907,\"value\":\"14 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8739,\"product_id\":4007,\"child_sku\":\"LS-EEBP-14\",\"price\":5.65,\"calculated_price\":5.65,\"retail_price\":null,\"width\":10.409,\"height\":14,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2908,\"value\":\"16 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8740,\"product_id\":4007,\"child_sku\":\"LS-EEBP-16\",\"price\":9.41,\"calculated_price\":9.41,\"retail_price\":null,\"width\":11.896,\"height\":16,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999},{\"option_value_id\":2909,\"value\":\"18 Inch\",\"option_id\":570,\"option\":\"Size\",\"product_option_id\":570,\"id\":8741,\"product_id\":4007,\"child_sku\":\"LS-EEBP-18\",\"price\":9.41,\"calculated_price\":9.41,\"retail_price\":null,\"width\":13.383,\"height\":18,\"depth\":0.125,\"purchasing_disabled\":false,\"image_url\":\"\",\"cost_price\":0,\"upc\":\"\",\"mpn\":\"\",\"inventory_level\":99999}]", + "child_skus": [ + "LS-EEBP-5", + "LS-EEBP-6", + "LS-EEBP-7", + "LS-EEBP-8", + "LS-EEBP-9", + "LS-EEBP-10", + "LS-EEBP-12", + "LS-EEBP-14", + "LS-EEBP-16", + "LS-EEBP-18" + ], + "depth": "0", + "height": "0", + "id": "8f3b460ca891ef8375d35152b79d19fd", + "images": "k/383/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__24263.jpg|r/555/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44790.jpg|j/898/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__22446.jpg|p/674/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__70969.jpg", + "intellisuggestData": "eJwqSUupMktlYEhNLC5JLVJITU9XSMrPyGfwCdZ1dXUKYDBkMGQwYDBkSC_KTAEEAAD__zAJDEQ", + "intellisuggestSignature": "9b2f31f2e39929f18576d83d9d1fb072ca4928a4761e74d09ff91d0388dcbdfb", + "inventory_level": "999990", + "inventory_tracking": "sku", + "is_featured": "false", + "is_free_shipping": "false", + "is_visible": "true", + "map_price": "0", + "option_set_id": "529", + "product_type_unigram": "egg", + "rating_count": "0", + "result_depth": [ + "1/8\"" + ], + "result_height": [ + "5\"", + "6\"", + "7\"", + "8\"", + "9\"", + "10\"", + "12\"", + "14\"", + "16\"", + "18\"" + ], + "result_width": [ + "3-11/16\"", + "4-7/16\"", + "5-3/16\"", + "5-15/16\"", + "6-11/16\"", + "7-7/16\"", + "8-15/16\"", + "10-7/16\"", + "11-7/8\"", + "13-3/8\"" + ], + "reviews_count": "0", + "reviews_rating_sum": "0", + "ss_days_since_created": "129", + "ss_filter_depth": [ + "0.125" + ], + "ss_filter_height": [ + "5", + "6", + "7", + "8", + "9", + "10", + "12", + "14", + "16", + "18" + ], + "ss_filter_width": [ + "3.7175", + "4.461", + "5.2045", + "5.948", + "6.6915", + "7.435", + "8.922", + "10.409", + "11.896", + "13.383" + ], + "ss_hover_image": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/28082/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__24263.1.jpg", + "ss_image": "https://cdn11.bigcommerce.com/s-6d1tnboxyx/images/stencil/500x659/products/4007/27880/woodpeckers-crafts-easter-egg-with-boho-etched-pattern__44621.1.jpg", + "ss_in_stock": "1", + "ss_price_range": [ + "0.68", + "9.41" + ], + "ss_variant_depth": [ + "0.125" + ], + "ss_variant_height": [ + "5", + "6", + "7", + "8", + "9", + "10", + "12", + "14", + "16", + "18" + ], + "ss_variant_width": [ + "3.7175", + "4.461", + "5.2045", + "5.948", + "6.6915", + "7.435", + "8.922", + "10.409", + "11.896", + "13.383" + ], + "ss_variants": "[{\"mappings\":{\"core\":{\"uid\":4007,\"price\":0.68,\"sku\":\"LS-EEBP-5\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"5 Inch\",\"optionValue\":2900,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":1.05,\"sku\":\"LS-EEBP-6\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"6 Inch\",\"optionValue\":2901,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":1.28,\"sku\":\"LS-EEBP-7\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"7 Inch\",\"optionValue\":2902,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":1.79,\"sku\":\"LS-EEBP-8\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"8 Inch\",\"optionValue\":2903,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":2.35,\"sku\":\"LS-EEBP-9\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"9 Inch\",\"optionValue\":2904,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":3.23,\"sku\":\"LS-EEBP-10\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"10 Inch\",\"optionValue\":2905,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":4.71,\"sku\":\"LS-EEBP-12\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"12 Inch\",\"optionValue\":2906,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":5.65,\"sku\":\"LS-EEBP-14\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"14 Inch\",\"optionValue\":2907,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":9.41,\"sku\":\"LS-EEBP-16\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"16 Inch\",\"optionValue\":2908,\"optionId\":570}}},{\"mappings\":{\"core\":{\"uid\":4007,\"price\":9.41,\"sku\":\"LS-EEBP-18\"}},\"attributes\":{\"quantity\":99999,\"available\":true},\"options\":{\"Size\":{\"value\":\"18 Inch\",\"optionValue\":2909,\"optionId\":570}}}]", + "ss_visibility": "1", + "total_sold": "279", + "width": "0" + }, + "children": [] + } + ], + "filters": [], + "facets": [ + { + "field": "result_height", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "5\"", + "label": "5\"", + "count": 1 + }, + { + "filtered": false, + "value": "6\"", + "label": "6\"", + "count": 1 + }, + { + "filtered": false, + "value": "7\"", + "label": "7\"", + "count": 1 + }, + { + "filtered": false, + "value": "8\"", + "label": "8\"", + "count": 1 + }, + { + "filtered": false, + "value": "9\"", + "label": "9\"", + "count": 1 + }, + { + "filtered": false, + "value": "10\"", + "label": "10\"", + "count": 1 + }, + { + "filtered": false, + "value": "12\"", + "label": "12\"", + "count": 1 + }, + { + "filtered": false, + "value": "14\"", + "label": "14\"", + "count": 1 + }, + { + "filtered": false, + "value": "16\"", + "label": "16\"", + "count": 1 + }, + { + "filtered": false, + "value": "18\"", + "label": "18\"", + "count": 1 + } + ] + }, + { + "field": "result_width", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "3-11/16\"", + "label": "3-11/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "4-7/16\"", + "label": "4-7/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "5-3/16\"", + "label": "5-3/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "5-15/16\"", + "label": "5-15/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "6-11/16\"", + "label": "6-11/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "7-7/16\"", + "label": "7-7/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "8-15/16\"", + "label": "8-15/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "10-7/16\"", + "label": "10-7/16\"", + "count": 1 + }, + { + "filtered": false, + "value": "11-7/8\"", + "label": "11-7/8\"", + "count": 1 + }, + { + "filtered": false, + "value": "13-3/8\"", + "label": "13-3/8\"", + "count": 1 + } + ] + }, + { + "field": "result_depth", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "1/8\"", + "label": "1/8\"", + "count": 1 + } + ] + }, + { + "field": "categories_hierarchy", + "type": "value", + "filtered": false, + "values": [ + { + "filtered": false, + "value": "Plywood and Wood Cutouts", + "label": "Plywood and Wood Cutouts", + "count": 1 + }, + { + "filtered": false, + "value": "Shop By Season", + "label": "Shop By Season", + "count": 1 + } + ] + } + ], + "sorting": [], + "merchandising": { + "redirect": "", + "content": {}, + "campaigns": [ + { + "id": "163014", + "title": "Global Boost Rules (Cutouts and 1-3/4\" Cube and Coffins and 12x12x1/4 plywood)", + "type": "global" + } + ] + }, + "search": { + "query": "easter egg boho" + } +} \ No newline at end of file diff --git a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts index 66e6cb42d..6a393975d 100644 --- a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts +++ b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts @@ -130,6 +130,7 @@ export type VariantDataOptions = Record< backgroundImageUrl?: string; attributeId?: string; optionId?: string; + optionValue?: string; } >; From 09fd97ea43d235ed75eef439b676abf1e4f65e17 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 7 Jun 2024 10:15:51 -0600 Subject: [PATCH 3/3] fix(store-mobx/searchresultstore): normalizing variant price data to ensure they are numbers --- .../src/Search/Stores/SearchResultStore.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts index 6a393975d..e0593d229 100644 --- a/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts +++ b/packages/snap-store-mobx/src/Search/Stores/SearchResultStore.ts @@ -323,6 +323,16 @@ export class Variants { // create variants objects this.data = variantData .filter((variant) => variant.attributes.available !== false) + .map((variant) => { + // normalize price fields ensuring they are numbers + if (variant.mappings.core?.price) { + variant.mappings.core.price = Number(variant.mappings.core?.price); + } + if (variant.mappings.core?.msrp) { + variant.mappings.core.msrp = Number(variant.mappings.core?.msrp); + } + return variant; + }) .map((variant) => { Object.keys(variant.options).forEach((variantOption) => { if (!options.includes(variantOption)) {