diff --git a/docs/INTEGRATION_RECOMMENDATIONS.md b/docs/INTEGRATION_RECOMMENDATIONS.md index 5c5697ccc..c44a28133 100644 --- a/docs/INTEGRATION_RECOMMENDATIONS.md +++ b/docs/INTEGRATION_RECOMMENDATIONS.md @@ -15,13 +15,14 @@ Context variables may be applied to individual recommendation profiles similar t | Option | Value | Page | Description | |---|---|:---:|---| -| product | current product sku | product detail page | used to identify the current product being viewed | +| products | array of SKU strings | product detail page | SKU value(s) to identify the current product(s) being viewed | | cart | array (or function that returns an array) of current cart skus | all | optional method of setting cart contents | | options.siteId | global siteId overwrite | all | optional global siteId overwrite | | options.categories | array of category path strings | all | optional category identifiers used in category trending recommendation profiles | | options.brands | array of brand strings | all | optional brand identifiers used in brand trending recommendation profiles | | options.branch | template branch overwrite | all | optional branch overwrite for recommendations template (advanced usage) | | options.filters | array of filters | all | optional recommendation filters | +| options.blockedItems | array of strings | all | SKU values to identify which products to exclude from the response | | options.batched | boolean (default: `true`)| all | only applies to recommendation context, optional disable profile from being batched in a single request, can also be set globally [via config](https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Recommendation) | | options.order | number | all | optional order number for recommendation params to be added to the batched request. Profiles that do not specify an order will be placed at the end, in the occurrence they appear in the DOM. | options.limit | number (default: 20, max: 20) | all | optional maximum number of results to display, can also be set globally [via config globals](https://github.com/searchspring/snap/tree/main/packages/snap-controller/src/Recommendation) | @@ -35,7 +36,7 @@ A typical "similar" profile that would display products similar to the product p ```html ``` diff --git a/packages/snap-client/README.md b/packages/snap-client/README.md index 72bcb7ef5..fcc5fe52c 100644 --- a/packages/snap-client/README.md +++ b/packages/snap-client/README.md @@ -257,7 +257,7 @@ const client = new Client(globals, clientConfig); const results = await client.recommend({ tag: 'similar', siteId: 'abc123', - product: 'product123', + products: ['product123'], shopper: 'snapdev', }); ``` diff --git a/packages/snap-client/src/Client/apis/Recommend.test.ts b/packages/snap-client/src/Client/apis/Recommend.test.ts index fd90e8b5f..3e81d752a 100644 --- a/packages/snap-client/src/Client/apis/Recommend.test.ts +++ b/packages/snap-client/src/Client/apis/Recommend.test.ts @@ -3,6 +3,8 @@ import { ApiConfiguration } from './Abstract'; import { RecommendAPI } from './Recommend'; import { MockData } from '@searchspring/snap-shared'; +import type { RecommendRequestModel } from '../../types'; + const mockData = new MockData(); const wait = (time?: number) => { @@ -134,7 +136,7 @@ describe('Recommend Api', () => { headers: {}, }; - const batchParams = { + const batchParams: Partial = { siteId: '8uyt2m', lastViewed: [ 'marnie-runner-2-7x10', @@ -153,7 +155,7 @@ describe('Recommend Api', () => { 'dayna-4x6', 'moema-4x6', ], - product: ['marnie-runner-2-7x10'], + product: 'marnie-runner-2-7x10', }; it('batchRecommendations batches as expected', async () => { @@ -418,6 +420,46 @@ describe('Recommend Api', () => { requestMock.mockReset(); }); + it('batchRecommendations will combine `product` and `products` params into `products` when used together', async () => { + const api = new RecommendAPI(new ApiConfiguration({})); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + const requestURL = + 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend?tags=crossSell&limits=20&products=some_sku&products=some_sku2&products=marnie-runner-2-7x10&siteId=8uyt2m&lastViewed=marnie-runner-2-7x10&lastViewed=ruby-runner-2-7x10&lastViewed=abbie-runner-2-7x10&lastViewed=riley-4x6&lastViewed=joely-5x8&lastViewed=helena-4x6&lastViewed=kwame-4x6&lastViewed=sadie-4x6&lastViewed=candice-runner-2-7x10&lastViewed=esmeray-4x6&lastViewed=camilla-230x160&lastViewed=candice-4x6&lastViewed=sahara-4x6&lastViewed=dayna-4x6&lastViewed=moema-4x6'; + + // @ts-ignore + api.batchRecommendations({ tags: ['crossSell'], products: ['some_sku', 'some_sku2'], ...batchParams }); + + //add delay for paramBatch.timeout + await wait(250); + + expect(requestMock).toHaveBeenCalledWith(requestURL, GETParams); + requestMock.mockReset(); + }); + + it('batchRecommendations will utilize the `blockedItems` parameter when provided', async () => { + const api = new RecommendAPI(new ApiConfiguration({})); + + const requestMock = jest + .spyOn(global.window, 'fetch') + .mockImplementation(() => Promise.resolve({ status: 200, json: () => Promise.resolve(mockData.recommend()) } as Response)); + + const requestURL = + 'https://8uyt2m.a.searchspring.io/boost/8uyt2m/recommend?tags=undefined&limits=20&blockedItems=blocked_sku1&blockedItems=blocked_sku2&siteId=8uyt2m&lastViewed=marnie-runner-2-7x10&lastViewed=ruby-runner-2-7x10&lastViewed=abbie-runner-2-7x10&lastViewed=riley-4x6&lastViewed=joely-5x8&lastViewed=helena-4x6&lastViewed=kwame-4x6&lastViewed=sadie-4x6&lastViewed=candice-runner-2-7x10&lastViewed=esmeray-4x6&lastViewed=camilla-230x160&lastViewed=candice-4x6&lastViewed=sahara-4x6&lastViewed=dayna-4x6&lastViewed=moema-4x6&product=marnie-runner-2-7x10'; + + // @ts-ignore + api.batchRecommendations({ blockedItems: ['blocked_sku1', 'blocked_sku2'], ...batchParams }); + + //add delay for paramBatch.timeout + await wait(250); + + expect(requestMock).toHaveBeenCalledWith(requestURL, GETParams); + requestMock.mockReset(); + }); + it('batchRecommendations handles POST requests', async () => { const api = new RecommendAPI(new ApiConfiguration({})); diff --git a/packages/snap-client/src/Client/apis/Recommend.ts b/packages/snap-client/src/Client/apis/Recommend.ts index 9ca8063a0..bd86a279f 100644 --- a/packages/snap-client/src/Client/apis/Recommend.ts +++ b/packages/snap-client/src/Client/apis/Recommend.ts @@ -106,6 +106,13 @@ export class RecommendAPI extends API { batch.request.limits = (batch.request.limits as number[]).concat(limits); batch.request = { ...batch.request, ...otherParams }; + + // combine product data if both 'product' and 'products' are used + if (batch.request.product && Array.isArray(batch.request.products)) { + batch.request.products = batch.request.products.concat(batch.request.product); + + delete batch.request.product; + } }); try { diff --git a/packages/snap-client/src/index.ts b/packages/snap-client/src/index.ts index 2bea6c11b..c44802876 100644 --- a/packages/snap-client/src/index.ts +++ b/packages/snap-client/src/index.ts @@ -1,3 +1,3 @@ export * from './Client/Client'; -export { ClientGlobals, ClientConfig, TrendingResponseModel, RecommendCombinedResponseModel } from './types'; +export { ClientGlobals, ClientConfig, TrendingResponseModel, RecommendCombinedRequestModel, RecommendCombinedResponseModel } from './types'; diff --git a/packages/snap-client/src/types.ts b/packages/snap-client/src/types.ts index d885c248a..432312f26 100644 --- a/packages/snap-client/src/types.ts +++ b/packages/snap-client/src/types.ts @@ -99,6 +99,7 @@ export type RecommendRequestModel = { tags: string[]; siteId: string; product?: string; + products?: string[]; shopper?: string; categories?: string[]; brands?: string[]; @@ -109,6 +110,7 @@ export type RecommendRequestModel = { limits?: number | number[]; order?: number; filters?: RecommendationRequestFilterModel[]; + blockedItems?: string[]; }; export type GetRecommendRequestModel = Omit & { @@ -164,6 +166,7 @@ export type RecommendCombinedRequestModel = { tag: string; siteId: string; product?: string; + products?: string[]; shopper?: string; categories?: string[]; brands?: string[]; @@ -172,6 +175,7 @@ export type RecommendCombinedRequestModel = { test?: boolean; branch?: string; filters?: RecommendationRequestFilterModel[]; + blockedItems?: string[]; }; export type RecommendationRequestFilterModel = RecommendationRequestRangeFilterModel | RecommendationRequestValueFilterModel; diff --git a/packages/snap-controller/src/Autocomplete/AutocompleteController.ts b/packages/snap-controller/src/Autocomplete/AutocompleteController.ts index 394b3b03a..19fe14e9e 100644 --- a/packages/snap-controller/src/Autocomplete/AutocompleteController.ts +++ b/packages/snap-controller/src/Autocomplete/AutocompleteController.ts @@ -33,7 +33,7 @@ const defaultConfig: AutocompleteControllerConfig = { }, redirects: { merchandising: true, - singleResult: true, + singleResult: false, }, }, }; diff --git a/packages/snap-controller/src/Autocomplete/README.md b/packages/snap-controller/src/Autocomplete/README.md index 0db2e217d..211958b4f 100644 --- a/packages/snap-controller/src/Autocomplete/README.md +++ b/packages/snap-controller/src/Autocomplete/README.md @@ -20,7 +20,7 @@ The `AutocompleteController` is used when making queries to the API `autocomplet | settings.history.limit | when set, historical (previously searched) queries will be fetched and made available in the history store | ➖ | | | settings.history.showResults | if history limit is set and there is no input, the first term results will be displayed | false | | | settings.redirects.merchandising | boolean to disable merchandising redirects when ac form is submitted | true | | -| settings.redirects.singleResult | enable redirect to product detail page if search yields 1 result count | true | | +| settings.redirects.singleResult | enable redirect to product detail page if search yields 1 result count | false | |
diff --git a/packages/snap-controller/src/Recommendation/RecommendationController.ts b/packages/snap-controller/src/Recommendation/RecommendationController.ts index b22e85f68..ea041194a 100644 --- a/packages/snap-controller/src/Recommendation/RecommendationController.ts +++ b/packages/snap-controller/src/Recommendation/RecommendationController.ts @@ -8,6 +8,7 @@ import { ControllerTypes } from '../types'; import type { ProductViewEvent } from '@searchspring/snap-tracker'; import type { RecommendationStore } from '@searchspring/snap-store-mobx'; import type { Next } from '@searchspring/snap-event-manager'; +import type { RecommendCombinedRequestModel } from '@searchspring/snap-client'; import type { RecommendationControllerConfig, BeforeSearchObj, AfterStoreObj, ControllerServices, ContextVariables } from '../types'; type RecommendationTrackMethods = { @@ -21,19 +22,6 @@ type RecommendationTrackMethods = { render: (results?: Product[]) => BeaconEvent | undefined; }; -type RecommendCombinedRequestModel = { - tag: string; - siteId: string; - product?: string; - shopper?: string; - categories?: string[]; - brands?: string[]; - cart?: string[]; - lastViewed?: string[]; - test?: boolean; - branch?: string; -}; - const defaultConfig: RecommendationControllerConfig = { id: 'recommend', tag: '', @@ -288,7 +276,7 @@ export class RecommendationController extends AbstractController { })(); get params(): RecommendCombinedRequestModel { - const params = { + const params: RecommendCombinedRequestModel = { tag: this.config.tag, batched: this.config.batched, branch: this.config.branch || 'production', diff --git a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx index 67dff88ac..426d5a25d 100644 --- a/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx +++ b/packages/snap-preact/src/Instantiators/RecommendationInstantiator.tsx @@ -124,12 +124,12 @@ export class RecommendationInstantiator { const contextGlobals: any = {}; const elemContext = getContext( - ['shopperId', 'shopper', 'product', 'seed', 'cart', 'options', 'profile', 'custom'], + ['shopperId', 'shopper', 'product', 'products', 'seed', 'cart', 'options', 'profile', 'custom'], elem as HTMLScriptElement ); const context: ContextVariables = deepmerge(this.context, elemContext); - const { shopper, shopperId, product, seed, cart, options } = context; + const { shopper, shopperId, product, products, seed, cart, options } = context; /* type instantiatorContext = { @@ -138,6 +138,7 @@ export class RecommendationInstantiator { }; shopperId?: string; product?: string; + products?: string[]; seed?: string; cart?: string[] | () => string[]; options?: { @@ -146,6 +147,7 @@ export class RecommendationInstantiator { batched?: boolean; realtime?: boolean; categories?: string[]; + blockedItems?: string[]; brands?: string[]; limit?: number; } @@ -158,6 +160,11 @@ export class RecommendationInstantiator { if (product || seed) { contextGlobals.product = product || seed; } + if (products) { + contextGlobals.products = products; + } + + // options if (options?.branch) { contextGlobals.branch = options.branch; } @@ -176,6 +183,10 @@ export class RecommendationInstantiator { if (options?.limit && Number.isInteger(Number(options?.limit))) { contextGlobals.limits = Number(options?.limit); } + if (options?.blockedItems && Array.isArray(options.blockedItems)) { + contextGlobals.blockedItems = options.blockedItems; + } + let cartContents; if (typeof cart === 'function') { try {