diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index 330495f9..bcdc746c 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -2020,6 +2020,152 @@ public String recommendationsAsJSON(RecommendationsRequest req, UserInfo userInf } } + /** + * Queries the recommendations service to retrieve results for every pod on a page. + * + *

Beta: available to a small group of users and may change. Contact your Customer Success + * Manager to request access. + * + *

The top-level result id of the response identifies the page request and is not a tracking + * id: send each pod's own result id with that pod's tracking events. + * + * @param req the recommendation page request + * @param userInfo optional information about the user + * @return a recommendation page response + * @throws ConstructorException if the request is invalid. + */ + public RecommendationPageResponse recommendationPage( + RecommendationPageRequest req, UserInfo userInfo) throws ConstructorException { + try { + String json = recommendationPageAsJSON(req, userInfo); + return createRecommendationPageResponse(json); + } catch (Exception exception) { + throw new ConstructorException(exception); + } + } + + /** + * Queries the recommendations service to retrieve results for every pod on a page. + * + * @param req the recommendation page request + * @param userInfo optional information about the user + * @return a string of JSON + * @throws ConstructorException if the request is invalid. + */ + public String recommendationPageAsJSON(RecommendationPageRequest req, UserInfo userInfo) + throws ConstructorException { + try { + List paths = Arrays.asList("recommendations", "v1", "pages", req.getPageId()); + HttpUrl url = (userInfo == null) ? this.makeUrl(paths) : this.makeUrl(paths, userInfo); + HttpUrl.Builder builder = url.newBuilder(); + + if (StringUtils.isNotBlank(req.getSection())) { + builder.addQueryParameter("section", req.getSection()); + } + + if (req.getItemIds() != null) { + for (String itemId : req.getItemIds()) { + builder.addQueryParameter("item_id", itemId); + } + } + + if (StringUtils.isNotBlank(req.getVariationId())) { + if (req.getItemIds() == null || req.getItemIds().size() != 1) { + throw new IllegalArgumentException( + "variationId requires exactly one itemId to be specified"); + } + builder.addQueryParameter("variation_id", req.getVariationId()); + } + + if (StringUtils.isNotBlank(req.getTerm())) { + builder.addQueryParameter("term", req.getTerm()); + } + + addRecommendationPageOverridableParameters(builder, "", req); + + if (req.getPodOverrides() != null) { + for (Map.Entry override : + req.getPodOverrides().entrySet()) { + if (override.getValue() != null) { + addRecommendationPageOverridableParameters( + builder, + "pod_overrides[" + override.getKey() + "]", + override.getValue()); + } + } + } + + Request request = + this.makeUserRequestBuilder(userInfo).url(builder.build()).get().build(); + + Response response = clientWithRetry.newCall(request).execute(); + return getResponseBody(response); + } catch (Exception exception) { + throw new ConstructorException(exception); + } + } + + /** + * Adds the parameters that can be set per pod on a page request, in the same wire format as + * {@link #recommendationsAsJSON(RecommendationsRequest, UserInfo)}, nested under a bracket + * prefix (empty for page-wide values). + */ + private static void addRecommendationPageOverridableParameters( + HttpUrl.Builder builder, String prefix, RecommendationPagePodOverride params) { + if (params.getNumResults() != null) { + builder.addQueryParameter( + bracketName(prefix, "num_results"), String.valueOf(params.getNumResults())); + } + + if (params.getFacets() != null) { + for (Map.Entry> facet : params.getFacets().entrySet()) { + for (String facetValue : facet.getValue()) { + builder.addQueryParameter( + bracketName(prefix, "filters") + "[" + facet.getKey() + "]", + facetValue); + } + } + } + + if (params.getFilterMatchTypes() != null) { + for (Map.Entry matchType : params.getFilterMatchTypes().entrySet()) { + builder.addQueryParameter( + bracketName(prefix, "filter_match_types") + "[" + matchType.getKey() + "]", + matchType.getValue()); + } + } + + if (params.getVariationsMap() != null) { + builder.addQueryParameter( + bracketName(prefix, "variations_map"), + new Gson().toJson(params.getVariationsMap())); + } + + if (params.getPreFilterExpression() != null) { + builder.addQueryParameter( + bracketName(prefix, "pre_filter_expression"), params.getPreFilterExpression()); + } + + if (params.getFormatOptions() != null) { + for (Map.Entry formatOption : params.getFormatOptions().entrySet()) { + builder.addQueryParameter( + bracketName(prefix, "fmt_options") + "[" + formatOption.getKey() + "]", + formatOption.getValue()); + } + } + + if (params.getHiddenFields() != null) { + for (String hiddenField : params.getHiddenFields()) { + builder.addQueryParameter( + bracketName(prefix, "fmt_options") + "[hidden_fields]", hiddenField); + } + } + } + + private static String bracketName(String prefix, String name) { + return prefix.isEmpty() ? name : prefix + "[" + name + "]"; + } + /** * Makes a URL to issue the requests to. Note that the URL will automagically have the apiKey * embedded. @@ -2416,6 +2562,23 @@ protected static RecommendationsResponse createRecommendationsResponse(String st return new Gson().fromJson(transformed, RecommendationsResponse.class); } + /** + * Transforms a JSON string to a new JSON string for easy Gson parsing into a recommendation + * page response. + */ + protected static RecommendationPageResponse createRecommendationPageResponse(String string) { + JSONObject json = new JSONObject(string); + JSONArray pods = json.getJSONObject("response").getJSONArray("pods"); + for (int i = 0; i < pods.length(); i++) { + JSONObject podResponse = pods.getJSONObject(i).optJSONObject("response"); + if (podResponse != null && podResponse.optJSONArray("results") != null) { + moveMetadataOutOfResultData(podResponse.getJSONArray("results")); + } + } + String transformed = json.toString(); + return new Gson().fromJson(transformed, RecommendationPageResponse.class); + } + /** * Transforms a JSON string to a new JSON string for easy Gson parsing into an All Tasks * response. Using JSON objects to achieve this is considerably less error prone than attempting diff --git a/constructorio-client/src/main/java/io/constructor/client/RecommendationPagePodOverride.java b/constructorio-client/src/main/java/io/constructor/client/RecommendationPagePodOverride.java new file mode 100644 index 00000000..71b70bbd --- /dev/null +++ b/constructorio-client/src/main/java/io/constructor/client/RecommendationPagePodOverride.java @@ -0,0 +1,119 @@ +package io.constructor.client; + +import java.util.List; +import java.util.Map; + +/** + * Constructor.io Recommendation Page Pod Override. Values set here replace (are not merged with) + * the page-wide values for one pod. + */ +public class RecommendationPagePodOverride { + + private Integer numResults; + private Map> facets; + private Map filterMatchTypes; + private String preFilterExpression; + private VariationsMap variationsMap; + private Map formatOptions; + private List hiddenFields; + + /** + * @param numResults the number of results to return for this pod + */ + public void setNumResults(Integer numResults) { + this.numResults = numResults; + } + + /** + * @return the number of results + */ + public Integer getNumResults() { + return numResults; + } + + /** + * @param facets the filters for this pod. Replaces all page-wide filters for this pod. + */ + public void setFacets(Map> facets) { + this.facets = facets; + } + + /** + * @return the facets + */ + public Map> getFacets() { + return facets; + } + + /** + * @param filterMatchTypes whether results must match "all", "any" or "none" of each filter's + * values, keyed by filter name + */ + public void setFilterMatchTypes(Map filterMatchTypes) { + this.filterMatchTypes = filterMatchTypes; + } + + /** + * @return the filter match types + */ + public Map getFilterMatchTypes() { + return filterMatchTypes; + } + + /** + * @param preFilterExpression the faceting expression to scope this pod's results (JSON-encoded + * query string) + */ + public void setPreFilterExpression(String preFilterExpression) { + this.preFilterExpression = preFilterExpression; + } + + /** + * @return the prefilter expression + */ + public String getPreFilterExpression() { + return preFilterExpression; + } + + /** + * @param variationsMap the variationsMap for this pod + */ + public void setVariationsMap(VariationsMap variationsMap) { + this.variationsMap = variationsMap; + } + + /** + * @return the variations map + */ + public VariationsMap getVariationsMap() { + return variationsMap; + } + + /** + * @param formatOptions the format options for this pod + */ + public void setFormatOptions(Map formatOptions) { + this.formatOptions = formatOptions; + } + + /** + * @return the format options + */ + public Map getFormatOptions() { + return formatOptions; + } + + /** + * @param hiddenFields the hidden fields to return for this pod + */ + public void setHiddenFields(List hiddenFields) { + this.hiddenFields = hiddenFields; + } + + /** + * @return the hidden fields + */ + public List getHiddenFields() { + return hiddenFields; + } +} diff --git a/constructorio-client/src/main/java/io/constructor/client/RecommendationPageRequest.java b/constructorio-client/src/main/java/io/constructor/client/RecommendationPageRequest.java new file mode 100644 index 00000000..b8bcb147 --- /dev/null +++ b/constructorio-client/src/main/java/io/constructor/client/RecommendationPageRequest.java @@ -0,0 +1,119 @@ +package io.constructor.client; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Constructor.io Recommendation Page Request. Page-wide values apply to every pod on the page; use + * {@link #setPodOverrides(Map)} to give one pod its own values. + */ +public class RecommendationPageRequest extends RecommendationPagePodOverride { + + private String pageId; + private String term; + private List itemIds; + private String variationId; + private String section; + private Map podOverrides; + + /** + * Creates a recommendation page request + * + * @param pageId the page id to retrieve results for + */ + public RecommendationPageRequest(String pageId) throws IllegalArgumentException { + if (pageId == null) { + throw new IllegalArgumentException("pageId is a required parameter of type string"); + } + + this.pageId = pageId; + this.section = "Products"; + this.podOverrides = new LinkedHashMap(); + } + + /** + * @param pageId the page id to set + */ + public void setPageId(String pageId) { + this.pageId = pageId; + } + + /** + * @return the page id + */ + public String getPageId() { + return pageId; + } + + /** + * @param term the term to set (required for query recommendations pods) + */ + public void setTerm(String term) { + this.term = term; + } + + /** + * @return the term + */ + public String getTerm() { + return term; + } + + /** + * @param itemIds the item id's to set (required for item-based pods) + */ + public void setItemIds(List itemIds) { + this.itemIds = itemIds; + } + + /** + * @return the item id's + */ + public List getItemIds() { + return itemIds; + } + + /** + * @param variationId the variation id to set. Can be used with exactly one item id. + */ + public void setVariationId(String variationId) { + this.variationId = variationId; + } + + /** + * @return the variation id + */ + public String getVariationId() { + return variationId; + } + + /** + * @param section the section to set + */ + public void setSection(String section) { + this.section = section; + } + + /** + * @return the section + */ + public String getSection() { + return section; + } + + /** + * @param podOverrides per-pod values keyed by pod id. Each replaces (is not merged with) the + * page-wide value for that pod. + */ + public void setPodOverrides(Map podOverrides) { + this.podOverrides = podOverrides; + } + + /** + * @return the pod overrides + */ + public Map getPodOverrides() { + return podOverrides; + } +} diff --git a/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPagePod.java b/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPagePod.java new file mode 100644 index 00000000..83cdf703 --- /dev/null +++ b/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPagePod.java @@ -0,0 +1,64 @@ +package io.constructor.client.models; + +import com.google.gson.annotations.SerializedName; +import java.util.Map; + +/** One pod of a Constructor.io Recommendation Page Response */ +public class RecommendationPagePod { + + @SerializedName("pod_id") + private String podId; + + @SerializedName("request") + private Map request; + + @SerializedName("response") + private RecommendationsResponseInner response; + + @SerializedName("result_id") + private String resultId; + + /** + * @return the pod id + */ + public String getPodId() { + return podId; + } + + /** + * @return the pod's effective request: page-wide parameters with this pod's overrides applied + */ + public Map getRequest() { + return request; + } + + /** + * @return the pod's response, in the same shape as a single-pod recommendations response + */ + public RecommendationsResponseInner getResponse() { + return response; + } + + /** + * @return the pod's result id. Send this with the pod's recommendation view and click events. + */ + public String getResultId() { + return resultId; + } + + public void setPodId(String podId) { + this.podId = podId; + } + + public void setRequest(Map request) { + this.request = request; + } + + public void setResponse(RecommendationsResponseInner response) { + this.response = response; + } + + public void setResultId(String resultId) { + this.resultId = resultId; + } +} diff --git a/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPageResponse.java b/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPageResponse.java new file mode 100644 index 00000000..13b385c8 --- /dev/null +++ b/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPageResponse.java @@ -0,0 +1,55 @@ +package io.constructor.client.models; + +import com.google.gson.annotations.SerializedName; +import java.util.Map; + +/** + * Constructor.io Recommendation Page Response ... uses Gson/Reflection to load data in. + * + *

The top-level result id identifies the page request and is not a tracking id: send each pod's + * own {@link RecommendationPagePod#getResultId()} with that pod's tracking events. + */ +public class RecommendationPageResponse { + + @SerializedName("result_id") + private String resultId; + + @SerializedName("response") + private RecommendationPageResponseInner response; + + @SerializedName("request") + private Map request; + + /** + * @return the id of the page request. Not a tracking id. + */ + public String getResultId() { + return resultId; + } + + /** + * @return the response + */ + public RecommendationPageResponseInner getResponse() { + return response; + } + + /** + * @return the page request as understood by the server + */ + public Map getRequest() { + return request; + } + + public void setResultId(String resultId) { + this.resultId = resultId; + } + + public void setResponse(RecommendationPageResponseInner response) { + this.response = response; + } + + public void setRequest(Map request) { + this.request = request; + } +} diff --git a/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPageResponseInner.java b/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPageResponseInner.java new file mode 100644 index 00000000..d7ba77e2 --- /dev/null +++ b/constructorio-client/src/main/java/io/constructor/client/models/RecommendationPageResponseInner.java @@ -0,0 +1,64 @@ +package io.constructor.client.models; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** Constructor.io Recommendation Page Response Inner ... uses Gson/Reflection to load data in */ +public class RecommendationPageResponseInner { + + @SerializedName("page_id") + private String pageId; + + @SerializedName("display_name") + private String displayName; + + @SerializedName("page_type") + private String pageType; + + @SerializedName("pods") + private List pods; + + /** + * @return the page id + */ + public String getPageId() { + return pageId; + } + + /** + * @return the page display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * @return the page type + */ + public String getPageType() { + return pageType; + } + + /** + * @return the pods, in the page's configured order + */ + public List getPods() { + return pods; + } + + public void setPageId(String pageId) { + this.pageId = pageId; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public void setPageType(String pageType) { + this.pageType = pageType; + } + + public void setPods(List pods) { + this.pods = pods; + } +} diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIORecommendationPageTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIORecommendationPageTest.java new file mode 100644 index 00000000..40aae8bb --- /dev/null +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIORecommendationPageTest.java @@ -0,0 +1,227 @@ +package io.constructor.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import io.constructor.client.models.RecommendationPagePod; +import io.constructor.client.models.RecommendationPageResponse; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class ConstructorIORecommendationPageTest { + + private static final String MOCK_API_KEY = "key_mock_page_api"; + private static final String PAGE_RESULT_ID = "a1b2c3d4-0000-0000-0000-0000000000ff"; + private static MockWebServer mockServer; + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @BeforeClass + public static void setup() throws Exception { + mockServer = new MockWebServer(); + mockServer.start(); + } + + @AfterClass + public static void teardown() throws Exception { + mockServer.shutdown(); + } + + private ConstructorIO mockedConstructor() throws Exception { + String body = Utils.getTestResource("response.recommendation_page.pdp_b2c.json"); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(body)); + return new ConstructorIO("", MOCK_API_KEY, false, "127.0.0.1", mockServer.getPort()); + } + + @Test + public void recommendationPageShouldRequestPageEndpointWithSharedParameters() throws Exception { + ConstructorIO constructor = mockedConstructor(); + RecommendationPageRequest request = new RecommendationPageRequest("pdp_b2c"); + Map> facets = new HashMap>(); + facets.put("in_stock", Arrays.asList("true")); + Map filterMatchTypes = new HashMap(); + filterMatchTypes.put("color", "any"); + request.setItemIds(Arrays.asList("product-123")); + request.setNumResults(10); + request.setFacets(facets); + request.setFilterMatchTypes(filterMatchTypes); + UserInfo userInfo = new UserInfo(3, "c62a-2a09-faie"); + userInfo.setUserId("user-id"); + + constructor.recommendationPage(request, userInfo); + + HttpUrl url = mockServer.takeRequest().getRequestUrl(); + assertEquals("/recommendations/v1/pages/pdp_b2c", url.encodedPath()); + assertEquals(MOCK_API_KEY, url.queryParameter("key")); + assertEquals("c62a-2a09-faie", url.queryParameter("i")); + assertEquals("3", url.queryParameter("s")); + assertEquals("user-id", url.queryParameter("ui")); + assertEquals("Products", url.queryParameter("section")); + assertEquals("product-123", url.queryParameter("item_id")); + assertEquals("10", url.queryParameter("num_results")); + assertEquals("true", url.queryParameter("filters[in_stock]")); + assertEquals("any", url.queryParameter("filter_match_types[color]")); + for (String name : url.queryParameterNames()) { + assertTrue("no pod overrides were sent", !name.startsWith("pod_overrides")); + } + } + + @Test + public void recommendationPageShouldEncodePodOverridesInBracketNotation() throws Exception { + ConstructorIO constructor = mockedConstructor(); + RecommendationPageRequest request = new RecommendationPageRequest("pdp_b2c"); + request.setItemIds(Arrays.asList("product-123")); + request.setNumResults(10); + + RecommendationPagePodOverride similarItems = new RecommendationPagePodOverride(); + similarItems.setNumResults(0); + + RecommendationPagePodOverride completeTheLook = new RecommendationPagePodOverride(); + Map> facets = new LinkedHashMap>(); + facets.put("in_stock", Arrays.asList("true")); + facets.put("color", Arrays.asList("red", "blue")); + Map filterMatchTypes = new HashMap(); + filterMatchTypes.put("color", "all"); + Map formatOptions = new HashMap(); + formatOptions.put("groups_max_depth", "2"); + VariationsMap variationsMap = new VariationsMap(); + variationsMap.addGroupByRule("color", "data.color"); + completeTheLook.setNumResults(8); + completeTheLook.setFacets(facets); + completeTheLook.setFilterMatchTypes(filterMatchTypes); + completeTheLook.setPreFilterExpression( + "{\"or\":[{\"name\":\"brand\",\"value\":\"acme\"}]}"); + completeTheLook.setVariationsMap(variationsMap); + completeTheLook.setFormatOptions(formatOptions); + completeTheLook.setHiddenFields(Arrays.asList("inventory", "margin")); + + Map podOverrides = + new LinkedHashMap(); + podOverrides.put("similar_items", similarItems); + podOverrides.put("complete_the_look", completeTheLook); + request.setPodOverrides(podOverrides); + + constructor.recommendationPage(request, null); + + RecordedRequest recordedRequest = mockServer.takeRequest(); + HttpUrl url = recordedRequest.getRequestUrl(); + String prefix = "pod_overrides[complete_the_look]"; + assertEquals("10", url.queryParameter("num_results")); + assertEquals("0", url.queryParameter("pod_overrides[similar_items][num_results]")); + assertEquals("8", url.queryParameter(prefix + "[num_results]")); + assertEquals("true", url.queryParameter(prefix + "[filters][in_stock]")); + assertEquals( + Arrays.asList("red", "blue"), + url.queryParameterValues(prefix + "[filters][color]")); + assertEquals("all", url.queryParameter(prefix + "[filter_match_types][color]")); + assertEquals( + "{\"or\":[{\"name\":\"brand\",\"value\":\"acme\"}]}", + url.queryParameter(prefix + "[pre_filter_expression]")); + assertEquals( + new com.google.gson.Gson().toJson(variationsMap), + url.queryParameter(prefix + "[variations_map]")); + assertEquals("2", url.queryParameter(prefix + "[fmt_options][groups_max_depth]")); + assertEquals( + Arrays.asList("inventory", "margin"), + url.queryParameterValues(prefix + "[fmt_options][hidden_fields]")); + assertTrue( + "brackets are percent-encoded in the raw path", + recordedRequest + .getPath() + .contains("pod_overrides%5Bsimilar_items%5D%5Bnum_results%5D=0")); + } + + @Test + public void recommendationPageShouldReturnPerPodResultIds() throws Exception { + ConstructorIO constructor = mockedConstructor(); + RecommendationPageRequest request = new RecommendationPageRequest("pdp_b2c"); + request.setItemIds(Collections.singletonList("product-123")); + + RecommendationPageResponse response = constructor.recommendationPage(request, null); + mockServer.takeRequest(); + + List pods = response.getResponse().getPods(); + assertEquals(PAGE_RESULT_ID, response.getResultId()); + assertEquals("pdp_b2c", response.getResponse().getPageId()); + assertEquals("pdp", response.getResponse().getPageType()); + assertEquals(2, pods.size()); + assertEquals("similar_items", pods.get(0).getPodId()); + assertEquals("a1b2c3d4-0000-0000-0000-000000000001", pods.get(0).getResultId()); + assertEquals("similar_items", pods.get(0).getResponse().getPod().getId()); + assertEquals(1, pods.get(0).getResponse().getResults().size()); + assertEquals("Red Running Shoe", pods.get(0).getResponse().getResults().get(0).getValue()); + assertEquals( + 49.99, + pods.get(0).getResponse().getResults().get(0).getData().getMetadata().get("price")); + assertEquals("complete_the_look", pods.get(1).getPodId()); + assertEquals("a1b2c3d4-0000-0000-0000-000000000002", pods.get(1).getResultId()); + assertEquals(0, pods.get(1).getResponse().getResults().size()); + for (RecommendationPagePod pod : pods) { + assertNotEquals(PAGE_RESULT_ID, pod.getResultId()); + } + } + + @Test + public void recommendationPageRequestShouldRequirePageId() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("pageId is a required parameter of type string"); + new RecommendationPageRequest(null); + } + + @Test + public void recommendationPageShouldRejectVariationIdWithoutSingleItemId() throws Exception { + ConstructorIO constructor = + new ConstructorIO("", MOCK_API_KEY, false, "127.0.0.1", mockServer.getPort()); + RecommendationPageRequest request = new RecommendationPageRequest("pdp_b2c"); + request.setVariationId("v1"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("variationId requires exactly one itemId to be specified"); + constructor.recommendationPage(request, null); + } + + @Test + public void recommendationPageRequestDefaults() throws Exception { + RecommendationPageRequest request = new RecommendationPageRequest("pdp_b2c"); + + assertEquals("pdp_b2c", request.getPageId()); + assertEquals("Products", request.getSection()); + assertNull(request.getNumResults()); + assertTrue(request.getPodOverrides().isEmpty()); + } + + // The page endpoint is not yet enabled on the test index, and the test index + // has no page configured (`/recommendations/v1/pages/pdp_b2c` returns 404 there) + @Ignore("the page endpoint is not enabled and no page is configured on the test index") + @Test + public void recommendationPageShouldReturnPerPodResultIdsLive() throws Exception { + ConstructorIO constructor = + new ConstructorIO("", System.getenv("TEST_REQUEST_API_KEY"), true, null); + RecommendationPageRequest request = new RecommendationPageRequest("pdp_b2c"); + request.setItemIds(Collections.singletonList("power_drill")); + + RecommendationPageResponse response = constructor.recommendationPage(request, null); + + assertTrue(response.getResponse().getPods().size() > 0); + for (RecommendationPagePod pod : response.getResponse().getPods()) { + assertNotEquals(response.getResultId(), pod.getResultId()); + assertEquals(pod.getPodId(), pod.getResponse().getPod().getId()); + } + } +} diff --git a/constructorio-client/src/test/resources/response.recommendation_page.pdp_b2c.json b/constructorio-client/src/test/resources/response.recommendation_page.pdp_b2c.json new file mode 100644 index 00000000..13ee9229 --- /dev/null +++ b/constructorio-client/src/test/resources/response.recommendation_page.pdp_b2c.json @@ -0,0 +1,39 @@ +{ + "request": { "page_id": "pdp_b2c", "item_id": "product-123", "num_results": 10, "section": "Products" }, + "response": { + "page_id": "pdp_b2c", + "display_name": "PDP - B2C", + "page_type": "pdp", + "pods": [ + { + "pod_id": "similar_items", + "request": { "item_id": "product-123", "num_results": 12, "section": "Products", "pod_id": "similar_items" }, + "response": { + "results": [ + { + "data": { "id": "product-987", "variation_id": "product-987-red", "url": "https://example.com/987", "price": 49.99 }, + "value": "Red Running Shoe", + "is_slotted": false, + "labels": {}, + "strategy": { "id": "alternative_items" } + } + ], + "total_num_results": 1, + "pod": { "id": "similar_items", "display_name": "Similar Items" } + }, + "result_id": "a1b2c3d4-0000-0000-0000-000000000001" + }, + { + "pod_id": "complete_the_look", + "request": { "item_id": "product-123", "num_results": 8, "section": "Products", "pod_id": "complete_the_look" }, + "response": { + "results": [], + "total_num_results": 0, + "pod": { "id": "complete_the_look", "display_name": "Complete the Look" } + }, + "result_id": "a1b2c3d4-0000-0000-0000-000000000002" + } + ] + }, + "result_id": "a1b2c3d4-0000-0000-0000-0000000000ff" +}