diff --git a/modules/apps/commerce/commerce-product-api/bnd.bnd b/modules/apps/commerce/commerce-product-api/bnd.bnd index d4ae3633d42590..007982d4f058b7 100644 --- a/modules/apps/commerce/commerce-product-api/bnd.bnd +++ b/modules/apps/commerce/commerce-product-api/bnd.bnd @@ -1,6 +1,6 @@ Bundle-Name: Liferay Commerce Product API Bundle-SymbolicName: com.liferay.commerce.product.api -Bundle-Version: 88.2.1 +Bundle-Version: 88.3.0 Export-Package:\ com.liferay.commerce.product.availability,\ com.liferay.commerce.product.catalog,\ diff --git a/modules/apps/commerce/commerce-product-api/src/main/java/com/liferay/commerce/product/exception/CPDefinitionOptionRelException.java b/modules/apps/commerce/commerce-product-api/src/main/java/com/liferay/commerce/product/exception/CPDefinitionOptionRelException.java new file mode 100644 index 00000000000000..87e7bedf4cf909 --- /dev/null +++ b/modules/apps/commerce/commerce-product-api/src/main/java/com/liferay/commerce/product/exception/CPDefinitionOptionRelException.java @@ -0,0 +1,29 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ +package com.liferay.commerce.product.exception; + +import com.liferay.portal.kernel.exception.PortalException; + +/** + * @author Marco Leo + */ +public class CPDefinitionOptionRelException extends PortalException { + + public CPDefinitionOptionRelException() { + } + + public CPDefinitionOptionRelException(String msg) { + super(msg); + } + + public CPDefinitionOptionRelException(String msg, Throwable throwable) { + super(msg, throwable); + } + + public CPDefinitionOptionRelException(Throwable throwable) { + super(throwable); + } + +} \ No newline at end of file diff --git a/modules/apps/commerce/commerce-product-api/src/main/java/com/liferay/commerce/product/exception/CPInstanceOptionValueRelException.java b/modules/apps/commerce/commerce-product-api/src/main/java/com/liferay/commerce/product/exception/CPInstanceOptionValueRelException.java new file mode 100644 index 00000000000000..4eb2d1d4b94051 --- /dev/null +++ b/modules/apps/commerce/commerce-product-api/src/main/java/com/liferay/commerce/product/exception/CPInstanceOptionValueRelException.java @@ -0,0 +1,29 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ +package com.liferay.commerce.product.exception; + +import com.liferay.portal.kernel.exception.PortalException; + +/** + * @author Marco Leo + */ +public class CPInstanceOptionValueRelException extends PortalException { + + public CPInstanceOptionValueRelException() { + } + + public CPInstanceOptionValueRelException(String msg) { + super(msg); + } + + public CPInstanceOptionValueRelException(String msg, Throwable throwable) { + super(msg, throwable); + } + + public CPInstanceOptionValueRelException(Throwable throwable) { + super(throwable); + } + +} \ No newline at end of file diff --git a/modules/apps/commerce/commerce-product-api/src/main/resources/com/liferay/commerce/product/exception/packageinfo b/modules/apps/commerce/commerce-product-api/src/main/resources/com/liferay/commerce/product/exception/packageinfo index 2a0a9557d56a7c..b4c2656f2c5dff 100644 --- a/modules/apps/commerce/commerce-product-api/src/main/resources/com/liferay/commerce/product/exception/packageinfo +++ b/modules/apps/commerce/commerce-product-api/src/main/resources/com/liferay/commerce/product/exception/packageinfo @@ -1 +1 @@ -version 13.5.0 \ No newline at end of file +version 13.6.0 \ No newline at end of file diff --git a/modules/apps/commerce/commerce-product-content-web/src/main/resources/META-INF/resources/product_publisher/render/list/entry/view.jsp b/modules/apps/commerce/commerce-product-content-web/src/main/resources/META-INF/resources/product_publisher/render/list/entry/view.jsp index 83ec7c1c73c7f9..4cc1da13f8e967 100644 --- a/modules/apps/commerce/commerce-product-content-web/src/main/resources/META-INF/resources/product_publisher/render/list/entry/view.jsp +++ b/modules/apps/commerce/commerce-product-content-web/src/main/resources/META-INF/resources/product_publisher/render/list/entry/view.jsp @@ -15,6 +15,8 @@ CPContentHelper cpContentHelper = (CPContentHelper)request.getAttribute(CPConten CPCatalogEntry cpCatalogEntry = cpContentHelper.getCPCatalogEntry(request); boolean hasMultipleCPSkus = cpContentHelper.hasMultipleCPSkus(cpCatalogEntry); + +boolean hasOptions = cpContentHelper.hasCPDefinitionOptionRels(cpCatalogEntry.getCPDefinitionId()); %>
@@ -101,7 +103,7 @@ boolean hasMultipleCPSkus = cpContentHelper.hasMultipleCPSkus(cpCatalogEntry);
- +
CPDefinitionMetaKeywords CPDefinitionMetaTitle CPDefinitionNameDefaultLanguage + CPDefinitionOptionRel CPDefinitionOptionRelPriceType CPDefinitionOptionSKUContributor CPDefinitionOptionValueRelCPInstance @@ -1563,6 +1564,7 @@ CPInstanceJson CPInstanceMaxPriceValue CPInstanceMinPriceValue + CPInstanceOptionValueRel CPInstancePrice CPInstancePromoPrice CPInstanceReplacementCPInstanceUuid diff --git a/modules/apps/commerce/commerce-service/src/main/java/com/liferay/commerce/service/impl/CommerceOrderItemLocalServiceImpl.java b/modules/apps/commerce/commerce-service/src/main/java/com/liferay/commerce/service/impl/CommerceOrderItemLocalServiceImpl.java index c2983f56290bd4..1d3456ecbfb446 100644 --- a/modules/apps/commerce/commerce-service/src/main/java/com/liferay/commerce/service/impl/CommerceOrderItemLocalServiceImpl.java +++ b/modules/apps/commerce/commerce-service/src/main/java/com/liferay/commerce/service/impl/CommerceOrderItemLocalServiceImpl.java @@ -37,12 +37,18 @@ import com.liferay.commerce.price.CommerceProductPriceImpl; import com.liferay.commerce.price.CommerceProductPriceRequest; import com.liferay.commerce.product.constants.CPConstants; +import com.liferay.commerce.product.exception.CPDefinitionOptionRelException; +import com.liferay.commerce.product.exception.CPInstanceOptionValueRelException; import com.liferay.commerce.product.exception.NoSuchCPInstanceException; import com.liferay.commerce.product.exception.NoSuchCPInstanceUnitOfMeasureException; import com.liferay.commerce.product.model.CPDefinition; +import com.liferay.commerce.product.model.CPDefinitionOptionRel; +import com.liferay.commerce.product.model.CPDefinitionOptionValueRel; import com.liferay.commerce.product.model.CPInstance; import com.liferay.commerce.product.model.CPInstanceUnitOfMeasure; import com.liferay.commerce.product.model.CPMeasurementUnit; +import com.liferay.commerce.product.model.CPOption; +import com.liferay.commerce.product.model.CPOptionValue; import com.liferay.commerce.product.option.CommerceOptionValue; import com.liferay.commerce.product.option.CommerceOptionValueHelper; import com.liferay.commerce.product.service.CPDefinitionLocalService; @@ -50,6 +56,7 @@ import com.liferay.commerce.product.service.CPInstanceLocalService; import com.liferay.commerce.product.service.CPInstanceUnitOfMeasureLocalService; import com.liferay.commerce.product.service.CPMeasurementUnitLocalService; +import com.liferay.commerce.product.util.CPInstanceHelper; import com.liferay.commerce.product.util.CPJSONUtil; import com.liferay.commerce.service.CommerceOrderLocalService; import com.liferay.commerce.service.base.CommerceOrderItemLocalServiceBaseImpl; @@ -71,6 +78,7 @@ import com.liferay.portal.kernel.json.JSONException; import com.liferay.portal.kernel.json.JSONFactory; import com.liferay.portal.kernel.json.JSONObject; +import com.liferay.portal.kernel.json.JSONUtil; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.model.User; @@ -1382,6 +1390,8 @@ private CommerceOrderItem _createCommerceOrderItem( GetterUtil.getBoolean( serviceContext.getAttribute("validateOrder"), true)); + json = _validateJSON(cpDefinition, cpInstance, json); + long commerceOrderItemId = counterLocalService.increment(); CommerceOrderItem commerceOrderItem = @@ -2431,6 +2441,10 @@ private CommerceOrderItem _updateCommerceOrderItem( GetterUtil.getBoolean( serviceContext.getAttribute("validateOrder"), true)); + json = _validateJSON( + commerceOrderItem.getCPDefinition(), + commerceOrderItem.fetchCPInstance(), json); + _updateCommerceInventoryBookedQuantity( userId, commerceOrderItem, commerceOrderItem.getCommerceInventoryBookedQuantityId(), quantity, @@ -2468,6 +2482,10 @@ private CommerceOrderItem _updateCommerceOrderItem( GetterUtil.getBoolean( serviceContext.getAttribute("validateOrder"), true)); + json = _validateJSON( + commerceOrderItem.getCPDefinition(), + commerceOrderItem.fetchCPInstance(), json); + _updateCommerceInventoryBookedQuantity( userId, commerceOrderItem, commerceOrderItem.getCommerceInventoryBookedQuantityId(), quantity, @@ -2623,6 +2641,45 @@ private void _validate( } } + private String _validateJSON( + CPDefinition cpDefinition, CPInstance cpInstance, String json) + throws PortalException { + + String sanitizedJSON = json; + JSONArray optionJSONArray = CPJSONUtil.toJSONArray(json); + + Map> + cpDefinitionOptionRelKeysOptionValueRelKeysMap = + _cpDefinitionOptionRelLocalService. + getCPDefinitionOptionRelKeysCPDefinitionOptionValueRelKeys( + cpInstance.getCPInstanceId()); + + JSONArray optionRelJSONArray = CPJSONUtil.toJSONArray( + cpDefinitionOptionRelKeysOptionValueRelKeysMap); + + for (CPDefinitionOptionRel cpDefinitionOptionRel : + cpDefinition.getCPDefinitionOptionRels()) { + + if (cpDefinitionOptionRel.isSkuContributor()) { + _validateSkuContributorOption( + cpDefinitionOptionRel, cpInstance, optionJSONArray, + optionRelJSONArray); + + sanitizedJSON = optionJSONArray.toString(); + } + else if (cpDefinitionOptionRel.isRequired()) { + if (CPJSONUtil.isEmpty(json)) { + throw new CPDefinitionOptionRelException( + "Required option is missing"); + } + + _validateRequiredOption(cpDefinitionOptionRel, optionJSONArray); + } + } + + return sanitizedJSON; + } + private void _validateParentCommerceOrderId( CommerceOrderItem commerceOrderItem) throws PortalException { @@ -2637,6 +2694,170 @@ private void _validateParentCommerceOrderId( } } + private void _validateRequiredOption( + CPDefinitionOptionRel cpDefinitionOptionRel, + JSONArray optionJSONArray) + throws PortalException { + + CPOption cpOption = cpDefinitionOptionRel.getCPOption(); + + boolean containsRequiredOption = false; + + for (int i = 0; i < optionJSONArray.length(); i++) { + JSONObject jsonObject = optionJSONArray.getJSONObject(i); + + String key = jsonObject.getString("key"); + + if (Objects.equals(key, cpOption.getKey())) { + JSONArray valueJSONArray = jsonObject.getJSONArray("value"); + + if ((valueJSONArray == null) || + (valueJSONArray.length() == 0)) { + + throw new CPInstanceOptionValueRelException( + "Required option must have a value"); + } + + String optionTypeKey = cpOption.getCommerceOptionTypeKey(); + + if (optionTypeKey.matches("checkbox_multiple|radio|select")) { + boolean multipleSelect = false; + List stringList = new ArrayList<>(); + + if (Objects.equals(optionTypeKey, "checkbox_multiple")) { + multipleSelect = true; + stringList = JSONUtil.toStringList(valueJSONArray); + } + + List cpOptionValues = + cpOption.getCPOptionValues(); + + for (CPOptionValue cpOptionValue : cpOptionValues) { + if (multipleSelect) { + stringList.remove(cpOptionValue.getKey()); + + if (stringList.isEmpty()) { + containsRequiredOption = true; + + break; + } + } + else { + if (Objects.equals( + cpOptionValue.getKey(), + valueJSONArray.get(0))) { + + containsRequiredOption = true; + + break; + } + } + } + } + else if (Objects.equals(optionTypeKey, "checkbox")) { + String valueString = valueJSONArray.getString(0); + + if (Objects.equals(valueString, cpOption.getKey()) || + Objects.equals(valueString, "[]")) { + + containsRequiredOption = true; + } + } + else { + containsRequiredOption = true; + + break; + } + } + } + + if (!containsRequiredOption) { + throw new CPDefinitionOptionRelException( + "Required option is missing"); + } + } + + private void _validateSkuContributorOption( + CPDefinitionOptionRel cpDefinitionOptionRel, CPInstance cpInstance, + JSONArray optionJSONArray, JSONArray optionRelJSONArray) + throws PortalException { + + boolean jsonOptionExists = false; + + for (int i = 0; i < optionRelJSONArray.length(); i++) { + JSONObject instanceJSONObject = optionRelJSONArray.getJSONObject(i); + + if (Objects.equals( + instanceJSONObject.get("key"), + cpDefinitionOptionRel.getKey())) { + + for (int j = 0; j < optionJSONArray.length(); j++) { + JSONObject optionJSONObject = optionJSONArray.getJSONObject( + j); + + String key = optionJSONObject.getString("key"); + + if (Objects.equals(key, cpDefinitionOptionRel.getKey())) { + jsonOptionExists = true; + + optionJSONObject.put( + "skuOptionName", + instanceJSONObject.get("skuOptionName") + ).put( + "skuOptionValueNames", + instanceJSONObject.get("skuOptionValueNames") + ).put( + "value", instanceJSONObject.get("value") + ); + + break; + } + } + + if (!jsonOptionExists) { + JSONObject jsonObject = _jsonFactory.createJSONObject(); + + Map> + cpDefinitionOptionRelValueRelMap = + _cpInstanceHelper. + getCPInstanceCPDefinitionOptionRelsMap( + cpInstance.getCPInstanceId()); + + List + cpDefinitionOptionValueRelList = + cpDefinitionOptionRelValueRelMap.get( + cpDefinitionOptionRel); + + CPDefinitionOptionValueRel cpDefinitionOptionValueRel = + cpDefinitionOptionValueRelList.get(0); + + jsonObject.put( + "key", instanceJSONObject.get("key") + ).put( + "price", cpDefinitionOptionValueRel.getPrice() + ).put( + "priceType", cpDefinitionOptionRel.getPriceType() + ).put( + "quantity", cpDefinitionOptionValueRel.getQuantity() + ).put( + "skuOptionKey", instanceJSONObject.get("key") + ).put( + "skuOptionName", instanceJSONObject.get("skuOptionName") + ).put( + "skuOptionValueKey", instanceJSONObject.get("value") + ).put( + "skuOptionValueNames", + instanceJSONObject.get("skuOptionValueNames") + ).put( + "value", instanceJSONObject.get("value") + ); + + optionJSONArray.put(jsonObject); + } + } + } + } + private static final String[] _SELECTED_FIELD_NAMES = { Field.ENTRY_CLASS_PK, Field.COMPANY_ID, Field.UID }; @@ -2688,6 +2909,9 @@ private void _validateParentCommerceOrderId( private CPDefinitionOptionRelLocalService _cpDefinitionOptionRelLocalService; + @Reference + private CPInstanceHelper _cpInstanceHelper; + @Reference private CPInstanceLocalService _cpInstanceLocalService; diff --git a/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-impl/src/main/java/com/liferay/headless/commerce/admin/order/internal/jaxrs/exception/mapper/CPDefinitionOptionRelExceptionMapper.java b/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-impl/src/main/java/com/liferay/headless/commerce/admin/order/internal/jaxrs/exception/mapper/CPDefinitionOptionRelExceptionMapper.java new file mode 100644 index 00000000000000..73b090497b1eab --- /dev/null +++ b/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-impl/src/main/java/com/liferay/headless/commerce/admin/order/internal/jaxrs/exception/mapper/CPDefinitionOptionRelExceptionMapper.java @@ -0,0 +1,39 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.headless.commerce.admin.order.internal.jaxrs.exception.mapper; + +import com.liferay.commerce.product.exception.CPDefinitionOptionRelException; +import com.liferay.portal.vulcan.jaxrs.exception.mapper.BaseExceptionMapper; +import com.liferay.portal.vulcan.jaxrs.exception.mapper.Problem; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import org.osgi.service.component.annotations.Component; + +/** + * @author Lianne Louie + */ +@Component( + property = { + "osgi.jaxrs.application.select=(osgi.jaxrs.name=Liferay.Headless.Commerce.Admin.Order)", + "osgi.jaxrs.extension=true", + "osgi.jaxrs.name=Liferay.Headless.Commerce.Admin.Order.CPDefinitionOptionRelExceptionMapper" + }, + service = ExceptionMapper.class +) +public class CPDefinitionOptionRelExceptionMapper + extends BaseExceptionMapper { + + @Override + protected Problem getProblem( + CPDefinitionOptionRelException cpDefinitionOptionRelException) { + + return new Problem( + Response.Status.BAD_REQUEST, "The option is invalid"); + } + +} \ No newline at end of file diff --git a/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-impl/src/main/java/com/liferay/headless/commerce/admin/order/internal/jaxrs/exception/mapper/CPInstanceOptionValueRelExceptionMapper.java b/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-impl/src/main/java/com/liferay/headless/commerce/admin/order/internal/jaxrs/exception/mapper/CPInstanceOptionValueRelExceptionMapper.java new file mode 100644 index 00000000000000..8a0ffcc9dc6dcc --- /dev/null +++ b/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-impl/src/main/java/com/liferay/headless/commerce/admin/order/internal/jaxrs/exception/mapper/CPInstanceOptionValueRelExceptionMapper.java @@ -0,0 +1,39 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.headless.commerce.admin.order.internal.jaxrs.exception.mapper; + +import com.liferay.commerce.product.exception.CPInstanceOptionValueRelException; +import com.liferay.portal.vulcan.jaxrs.exception.mapper.BaseExceptionMapper; +import com.liferay.portal.vulcan.jaxrs.exception.mapper.Problem; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import org.osgi.service.component.annotations.Component; + +/** + * @author Lianne Louie + */ +@Component( + property = { + "osgi.jaxrs.application.select=(osgi.jaxrs.name=Liferay.Headless.Commerce.Admin.Order)", + "osgi.jaxrs.extension=true", + "osgi.jaxrs.name=Liferay.Headless.Commerce.Admin.Order.CPInstanceOptionValueRelExceptionMapper" + }, + service = ExceptionMapper.class +) +public class CPInstanceOptionValueRelExceptionMapper + extends BaseExceptionMapper { + + @Override + protected Problem getProblem( + CPInstanceOptionValueRelException cpInstanceOptionValueRelException) { + + return new Problem( + Response.Status.BAD_REQUEST, "The option value is invalid"); + } + +} \ No newline at end of file diff --git a/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-test/src/testIntegration/java/com/liferay/headless/commerce/admin/order/resource/v1_0/test/OrderItemResourceTest.java b/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-test/src/testIntegration/java/com/liferay/headless/commerce/admin/order/resource/v1_0/test/OrderItemResourceTest.java index 179565d48517be..11d59a825d2a09 100644 --- a/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-test/src/testIntegration/java/com/liferay/headless/commerce/admin/order/resource/v1_0/test/OrderItemResourceTest.java +++ b/modules/apps/commerce/headless/headless-commerce/headless-commerce-admin-order-test/src/testIntegration/java/com/liferay/headless/commerce/admin/order/resource/v1_0/test/OrderItemResourceTest.java @@ -15,11 +15,14 @@ import com.liferay.commerce.media.constants.CommerceMediaConstants; import com.liferay.commerce.model.CommerceOrder; import com.liferay.commerce.model.CommerceOrderItem; +import com.liferay.commerce.product.constants.CPConstants; import com.liferay.commerce.product.constants.CommerceChannelConstants; import com.liferay.commerce.product.model.CPDefinition; import com.liferay.commerce.product.model.CPInstance; +import com.liferay.commerce.product.model.CPOption; import com.liferay.commerce.product.model.CommerceCatalog; import com.liferay.commerce.product.model.CommerceChannel; +import com.liferay.commerce.product.service.CPOptionLocalService; import com.liferay.commerce.product.service.CommerceChannelLocalService; import com.liferay.commerce.product.test.util.CPTestUtil; import com.liferay.commerce.product.type.virtual.constants.VirtualCPTypeConstants; @@ -36,6 +39,7 @@ import com.liferay.headless.commerce.admin.order.client.dto.v1_0.OrderItem; import com.liferay.petra.string.StringBundler; import com.liferay.petra.string.StringPool; +import com.liferay.portal.kernel.json.JSONFactory; import com.liferay.portal.kernel.model.User; import com.liferay.portal.kernel.repository.model.FileEntry; import com.liferay.portal.kernel.service.ServiceContext; @@ -199,6 +203,15 @@ public void testPatchOrderItemByExternalReferenceCode() throws Exception { super.testPatchOrderItemByExternalReferenceCode(); } + @Override + @Test + public void testPostOrderIdOrderItem() throws Exception { + super.testPostOrderIdOrderItem(); + + _testPostOrderItemWithMissingRequiredOption(); + _testPostOrderItemWithSkuContributorOption(); + } + @Override protected String[] getAdditionalAssertFieldNames() { return new String[] {"quantity"}; @@ -394,6 +407,67 @@ private OrderItem _addCommerceOrderItem(OrderItem orderItem) }; } + private OrderItem _addProductWithMissingOptionJSON( + boolean required, boolean skuContributor) + throws Exception { + + CPInstance cpInstance = CPTestUtil.addCPInstanceWithRandomSku( + _commerceCatalog.getGroupId()); + + CPOption cpOption = _cpOptionLocalService.addCPOption( + null, _user.getUserId(), + RandomTestUtil.randomLocaleStringMap(), + RandomTestUtil.randomLocaleStringMap(), + CPConstants.PRODUCT_OPTION_SELECT_KEY, + RandomTestUtil.randomBoolean(), required, + skuContributor, RandomTestUtil.randomString(), _serviceContext); + + CPTestUtil.addCPDefinitionOptionRel( + _commerceCatalog.getGroupId(), cpInstance.getCPDefinitionId(), + cpOption.getCPOptionId()); + + CPTestUtil.addCPDefinitionOptionValueRel( + cpInstance.getCPDefinitionId(), cpOption.getCPOptionId(), + RandomTestUtil.randomString(), RandomTestUtil.randomString(), + CPConstants.PRODUCT_OPTION_PRICE_TYPE_STATIC, required, + skuContributor, _serviceContext); + + return new OrderItem() { + { + bookedQuantityId = RandomTestUtil.randomLong(); + deliveryGroup = StringUtil.toLowerCase( + RandomTestUtil.randomString()); + discountManuallyAdjusted = RandomTestUtil.randomBoolean(); + externalReferenceCode = RandomTestUtil.randomString(); + id = RandomTestUtil.randomLong(); + options = ""; + orderExternalReferenceCode = + _commerceOrder.getExternalReferenceCode(); + orderId = _commerceOrder.getCommerceOrderId(); + priceManuallyAdjusted = RandomTestUtil.randomBoolean(); + printedNote = StringUtil.toLowerCase( + RandomTestUtil.randomString()); + quantity = BigDecimal.valueOf(RandomTestUtil.randomInt(1, 100)); + replacedSkuExternalReferenceCode = + RandomTestUtil.randomString(); + requestedDeliveryDate = RandomTestUtil.nextDate(); + shippable = RandomTestUtil.randomBoolean(); + shippedQuantity = BigDecimal.valueOf( + RandomTestUtil.randomInt()); + shippingAddressExternalReferenceCode = + RandomTestUtil.randomString(); + shippingAddressId = RandomTestUtil.randomLong(); + sku = cpInstance.getSku(); + skuExternalReferenceCode = + cpInstance.getExternalReferenceCode(); + skuId = cpInstance.getCPInstanceId(); + subscription = RandomTestUtil.randomBoolean(); + unitOfMeasure = StringUtil.toLowerCase( + RandomTestUtil.randomString()); + } + }; + } + private OrderItem _getOrderItem(long fileEntryId, String url) throws Exception { @@ -448,6 +522,27 @@ private OrderItem _getOrderItem(long fileEntryId, String url) }; } + private void _testPostOrderItemWithMissingRequiredOption() + throws Exception { + + OrderItem orderItem = _addProductWithMissingOptionJSON( + true, false); + + assertHttpResponseStatusCode( + 400, + orderItemResource.postOrderIdOrderItemHttpResponse( + _commerceOrder.getCommerceOrderId(), orderItem)); + } + + private void _testPostOrderItemWithSkuContributorOption() throws Exception { + OrderItem orderItem = _addProductWithMissingOptionJSON(false, true); + + assertHttpResponseStatusCode( + 200, + orderItemResource.postOrderIdOrderItemHttpResponse( + _commerceOrder.getCommerceOrderId(), orderItem)); + } + private AccountEntry _accountEntry; @Inject @@ -464,6 +559,9 @@ private OrderItem _getOrderItem(long fileEntryId, String url) @Inject private CommerceCurrencyLocalService _commerceCurrencyLocalService; + @Inject + private CPOptionLocalService _cpOptionLocalService; + private CommerceOrder _commerceOrder; @Inject @@ -483,6 +581,9 @@ private OrderItem _getOrderItem(long fileEntryId, String url) @Inject private DLAppLocalService _dlAppLocalService; + @Inject + private JSONFactory _jsonFactory; + @Inject private Portal _portal; diff --git a/modules/test/playwright/tests/commerce/commerce-product-content-web/productCard.spec.ts b/modules/test/playwright/tests/commerce/commerce-product-content-web/productCard.spec.ts index 38d9ec45dca52f..ec2eb0863b8408 100644 --- a/modules/test/playwright/tests/commerce/commerce-product-content-web/productCard.spec.ts +++ b/modules/test/playwright/tests/commerce/commerce-product-content-web/productCard.spec.ts @@ -256,3 +256,102 @@ test('COMMERCE-6193. As a buyer, I want the first selectable quantity of a produ ); } }); + +test('LPD-25497 Users should not be able to instantly add to cart for product card if options exist', async ({ + apiHelpers, + commerceThemeMiniumCatalogPage, + page, +}) => { + const {site} = await miniumSetUp(apiHelpers); + + const account = await apiHelpers.headlessAdminUser.postAccount({ + name: getRandomString(), + type: 'business', + }); + apiHelpers.data.push({id: account.id, type: 'account'}); + + const user = + await apiHelpers.headlessAdminUser.getUserAccountByEmailAddress( + 'demo.unprivileged@liferay.com' + ); + const rolesResponse = await apiHelpers.headlessAdminUser.getAccountRoles( + account.id + ); + + const accountRoleBuyer = rolesResponse?.items?.filter((role) => { + return role.name === 'Buyer'; + }); + + await apiHelpers.headlessAdminUser.assignAccountRoles( + account.externalReferenceCode, + accountRoleBuyer[0].id, + user.emailAddress + ); + + const siteRole = + await apiHelpers.headlessAdminUser.getRoleByName('Site Member'); + + await apiHelpers.headlessAdminUser.assignUserToSite( + siteRole.id, + site.id, + user.id + ); + + await apiHelpers.headlessAdminUser.assignUserToAccountByEmailAddress( + account.id, + [user.emailAddress] + ); + + const option = await apiHelpers.headlessCommerceAdminCatalog.postOption( + 'select', + getRandomString(), + 'Color', + 1 + ); + + const catalog = await apiHelpers.headlessCommerceAdminCatalog.postCatalog(); + + const product = await apiHelpers.headlessCommerceAdminCatalog.postProduct({ + catalogId: catalog.id, + name: {en_US: getRandomString()}, + productOptions: [ + { + fieldType: 'select', + key: option.key, + name: option.name, + optionId: option.id, + priceType: 'dynamic', + priority: 1, + productOptionValues: [ + { + key: 'black', + name: { + en_US: 'Black', + }, + priority: 1, + quantity: 1, + }, + { + key: 'white', + name: { + en_US: 'White', + }, + priority: 2, + quantity: 1, + }, + ], + }, + ], + }); + + await performLogout(page); + await performLogin(page, 'demo.unprivileged'); + + await page.goto(`/web/${site.name}`); + + const productName = product.name['en_US']; + + await expect( + commerceThemeMiniumCatalogPage.productCard(productName) + ).toHaveText('View all variants'); +});