From 5dac09b07ecddb5250bb390bf8046a7cb8e36ea3 Mon Sep 17 00:00:00 2001 From: Slava Rudkovskiy Date: Mon, 11 May 2026 14:05:03 +0300 Subject: [PATCH] add DiscountService --- .../labs64/netlicensing/domain/Constants.java | 3 + .../netlicensing/service/DiscountService.java | 99 ++++++++++ .../service/DiscountServiceTest.java | 180 ++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 NetLicensingClient/src/main/java/com/labs64/netlicensing/service/DiscountService.java create mode 100644 NetLicensingClient/src/test/java/com/labs64/netlicensing/service/DiscountServiceTest.java diff --git a/NetLicensingClient/src/main/java/com/labs64/netlicensing/domain/Constants.java b/NetLicensingClient/src/main/java/com/labs64/netlicensing/domain/Constants.java index c3be1798..ee036ff8 100644 --- a/NetLicensingClient/src/main/java/com/labs64/netlicensing/domain/Constants.java +++ b/NetLicensingClient/src/main/java/com/labs64/netlicensing/domain/Constants.java @@ -82,6 +82,9 @@ public static final class Product { public static final String PROP_VAT_MODE = "vatMode"; public static final class Discount { + public static final String ENDPOINT_PATH = "discount"; + public static final String ENDPOINT_RESOLVE_PATH = "resolve"; + public static final String CART_TOTAL = "cartTotal"; public static final String TOTAL_PRICE = "totalPrice"; public static final String AMOUNT_FIX = "amountFix"; public static final String AMOUNT_PERCENT = "amountPercent"; diff --git a/NetLicensingClient/src/main/java/com/labs64/netlicensing/service/DiscountService.java b/NetLicensingClient/src/main/java/com/labs64/netlicensing/service/DiscountService.java new file mode 100644 index 00000000..de085fdf --- /dev/null +++ b/NetLicensingClient/src/main/java/com/labs64/netlicensing/service/DiscountService.java @@ -0,0 +1,99 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.labs64.netlicensing.service; + +import java.math.BigDecimal; + +import org.apache.commons.lang3.StringUtils; + +import com.labs64.netlicensing.domain.Constants; +import com.labs64.netlicensing.domain.entity.LicenseTemplate; +import com.labs64.netlicensing.domain.vo.Context; +import com.labs64.netlicensing.exception.NetLicensingException; +import com.labs64.netlicensing.exception.ServiceException; +import com.labs64.netlicensing.provider.Form; +import com.labs64.netlicensing.util.CheckUtils; + +/** + * Provides discount handling routines. + */ +public class DiscountService { + + private static final int HTTP_STATUS_NOT_FOUND = 404; + private static final String NOT_FOUND_EXCEPTION_PREFIX = "NotFoundException:"; + + /** + * Resolves a discount license template by product number and promo code. + * + * @param context + * determines the vendor on whose behalf the call is performed + * @param productNumber + * product number that defines promo code uniqueness scope + * @param promoCode + * promo code to resolve + * @return resolved discount license template or null if discount was not found + * @throws NetLicensingException + * any non-not-found service error + */ + public static LicenseTemplate resolve(final Context context, final String productNumber, final String promoCode) + throws NetLicensingException { + return resolve(context, productNumber, promoCode, null); + } + + /** + * Resolves a discount license template by product number and promo code. + * + * @param context + * determines the vendor on whose behalf the call is performed + * @param productNumber + * product number that defines promo code uniqueness scope + * @param promoCode + * promo code to resolve + * @param cartTotal + * optional cart total used by the API to validate minimum cart total + * @return resolved discount license template or null if discount was not found + * @throws NetLicensingException + * any non-not-found service error + */ + public static LicenseTemplate resolve(final Context context, final String productNumber, final String promoCode, + final BigDecimal cartTotal) throws NetLicensingException { + CheckUtils.paramNotEmpty(productNumber, "productNumber"); + CheckUtils.paramNotEmpty(promoCode, "promoCode"); + + final Form form = new Form(); + if (cartTotal != null) { + form.param(Constants.Product.Discount.CART_TOTAL, cartTotal.toString()); + } + + try { + return NetLicensingService.getInstance().post(context, resolveEndpoint(productNumber, promoCode), form, + LicenseTemplate.class); + } catch (final ServiceException e) { + if (isNotFound(e)) { + return null; + } + throw e; + } + } + + private static String resolveEndpoint(final String productNumber, final String promoCode) { + return Constants.Product.ENDPOINT_PATH + "/" + productNumber + "/" + + Constants.Product.Discount.ENDPOINT_PATH + "/" + promoCode + "/" + + Constants.Product.Discount.ENDPOINT_RESOLVE_PATH; + } + + private static boolean isNotFound(final ServiceException e) { + return (e.getStatusCode() == HTTP_STATUS_NOT_FOUND) + || StringUtils.startsWith(e.getMessage(), NOT_FOUND_EXCEPTION_PREFIX); + } +} diff --git a/NetLicensingClient/src/test/java/com/labs64/netlicensing/service/DiscountServiceTest.java b/NetLicensingClient/src/test/java/com/labs64/netlicensing/service/DiscountServiceTest.java new file mode 100644 index 00000000..e5fd1fdd --- /dev/null +++ b/NetLicensingClient/src/test/java/com/labs64/netlicensing/service/DiscountServiceTest.java @@ -0,0 +1,180 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.labs64.netlicensing.service; + +import java.math.BigDecimal; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.labs64.netlicensing.domain.Constants; +import com.labs64.netlicensing.domain.entity.LicenseTemplate; +import com.labs64.netlicensing.domain.vo.Context; +import com.labs64.netlicensing.domain.vo.LicenseType; +import com.labs64.netlicensing.exception.ServiceException; +import com.labs64.netlicensing.schema.SchemaFunction; +import com.labs64.netlicensing.schema.context.InfoEnum; +import com.labs64.netlicensing.schema.context.Item; +import com.labs64.netlicensing.schema.context.Netlicensing; +import com.labs64.netlicensing.schema.context.ObjectFactory; +import com.labs64.netlicensing.schema.context.Property; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Integration tests for {@link DiscountService}. + */ +public class DiscountServiceTest extends BaseServiceTest { + + private static final String PRODUCT_NUMBER = "P001-TEST"; + private static final String PROMO_CODE = "PROMO-001"; + private static final String LICENSE_TEMPLATE_NUMBER = "LT-DISCOUNT-001"; + private static final String MIN_CART_TOTAL = "minCartTotal"; + private static final String PROMO_CODE_PROPERTY = "promoCode"; + + private static Context context; + + @BeforeAll + public static void setup() { + context = createContext(); + } + + @BeforeEach + public void resetResourceState() { + DiscountServiceResource.lastProductNumber = null; + DiscountServiceResource.lastPromoCode = null; + DiscountServiceResource.lastCartTotal = null; + } + + @Test + public void testResolve() throws Exception { + final LicenseTemplate discount = DiscountService.resolve(context, PRODUCT_NUMBER, PROMO_CODE); + + assertNotNull(discount); + assertEquals(LICENSE_TEMPLATE_NUMBER, discount.getNumber()); + assertEquals("Promo discount", discount.getName()); + assertEquals(LicenseType.FEATURE, discount.getLicenseType()); + assertEquals("25", discount.getProperties().get("discountAmount")); + assertEquals(PROMO_CODE, discount.getProperties().get(PROMO_CODE_PROPERTY)); + assertEquals(PRODUCT_NUMBER, DiscountServiceResource.lastProductNumber); + assertEquals(PROMO_CODE, DiscountServiceResource.lastPromoCode); + assertNull(DiscountServiceResource.lastCartTotal); + } + + @Test + public void testResolveWithCartTotal() throws Exception { + final LicenseTemplate discount = DiscountService.resolve(context, PRODUCT_NUMBER, PROMO_CODE, + new BigDecimal("100.50")); + + assertNotNull(discount); + assertEquals(LICENSE_TEMPLATE_NUMBER, discount.getNumber()); + assertEquals("100.50", DiscountServiceResource.lastCartTotal); + } + + @Test + public void testResolveReturnsNullWhenDiscountDoesNotExist() throws Exception { + final LicenseTemplate discount = DiscountService.resolve(context, PRODUCT_NUMBER, "UNKNOWN"); + + assertNull(discount); + } + + @Test + public void testResolveRethrowsNonNotFoundServiceException() { + final ServiceException ex = assertThrows(ServiceException.class, + () -> DiscountService.resolve(context, PRODUCT_NUMBER, "BROKEN")); + + assertEquals("IllegalOperationException: Discount resolve failed", ex.getMessage()); + } + + @Override + protected Class getResourceClass() { + return DiscountServiceResource.class; + } + + @Path(REST_API_PATH + "/product/{productNumber}/discount/{promoCode}/resolve") + public static class DiscountServiceResource { + + private static String lastProductNumber; + private static String lastPromoCode; + private static String lastCartTotal; + + private final ObjectFactory objectFactory = new ObjectFactory(); + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response resolve(@PathParam("productNumber") final String productNumber, + @PathParam("promoCode") final String promoCode, final MultivaluedMap formParams) { + lastProductNumber = productNumber; + lastPromoCode = promoCode; + lastCartTotal = formParams.getFirst(Constants.Product.Discount.CART_TOTAL); + + if ("UNKNOWN".equals(promoCode)) { + return notFoundResponse(); + } + if ("BROKEN".equals(promoCode)) { + return errorResponse("IllegalOperationException", "Discount resolve failed"); + } + + final Netlicensing netlicensing = objectFactory.createNetlicensing(); + netlicensing.setItems(objectFactory.createNetlicensingItems()); + + final Item item = objectFactory.createItem(); + item.setType("LicenseTemplate"); + netlicensing.getItems().getItem().add(item); + + addProperty(item, Constants.NUMBER, LICENSE_TEMPLATE_NUMBER); + addProperty(item, Constants.NAME, "Promo discount"); + addProperty(item, Constants.ACTIVE, Boolean.TRUE.toString()); + addProperty(item, Constants.LicenseTemplate.LICENSE_TYPE, LicenseType.FEATURE.value()); + addProperty(item, Constants.LicenseTemplate.AUTOMATIC, Boolean.FALSE.toString()); + addProperty(item, Constants.LicenseTemplate.HIDDEN, Boolean.TRUE.toString()); + addProperty(item, Constants.LicenseTemplate.HIDE_LICENSES, Boolean.TRUE.toString()); + addProperty(item, Constants.ProductModule.PRODUCT_MODULE_NUMBER, "PM-DISCOUNT-001"); + addProperty(item, PROMO_CODE_PROPERTY, promoCode); + addProperty(item, "discountAmount", "25"); + addProperty(item, MIN_CART_TOTAL, "100"); + + return Response.ok(netlicensing).build(); + } + + private Response notFoundResponse() { + final Netlicensing netlicensing = objectFactory.createNetlicensing(); + SchemaFunction.addInfo(netlicensing, "NotFoundException", InfoEnum.ERROR, + "Requested discount does not exist."); + return Response.status(Response.Status.NOT_FOUND).entity(netlicensing).build(); + } + + private Response errorResponse(final String exceptionId, final String errorMessage) { + final Netlicensing netlicensing = objectFactory.createNetlicensing(); + SchemaFunction.addInfo(netlicensing, exceptionId, InfoEnum.ERROR, errorMessage); + return Response.status(Response.Status.BAD_REQUEST).entity(netlicensing).build(); + } + + private void addProperty(final Item item, final String name, final String value) { + final Property property = new Property(value, name); + item.getProperty().add(property); + } + } +}