diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0a322217 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target/ +bin/ + +## Intellij IDEA ## +.idea +.iml + +*.log \ No newline at end of file diff --git a/Assumptions b/Assumptions new file mode 100644 index 00000000..2ce674f5 --- /dev/null +++ b/Assumptions @@ -0,0 +1,6 @@ +Notes: +1. Each offer is applied independently, offer applied previously have no bearing on next offer. +2. May be rules be externalized, may be a rules engine like Drools. +3. Possibly could have implemented visitor pattern. +4. Could have added more options at command line i.e show available products, offers or discount applied. but kept it to minimal to calculate the final price after applying discounts. +5. \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..de41e143 --- /dev/null +++ b/lombok.config @@ -0,0 +1,6 @@ +lombok.anyConstructor.addConstructorProperties=true +lombok.log.fieldName=LOGGER +lombok.log.log4j2.flagUsage=error +lombok.log.log4j.flagUsage=error +lombok.log.javaUtilLogging.flagUsage=error +lombok.log.apacheCommons.flagUsage=error \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..df7671e4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + com.industriallogic.henrysgroceries + PriceGroceryBasket + 1.0-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.apache.commons + commons-lang3 + 3.0 + + + + org.projectlombok + lombok + provided + + + + junit + junit + 4.12 + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/industriallogic/henrysgroceries/PriceShoppingBasketApplication.java b/src/main/java/com/industriallogic/henrysgroceries/PriceShoppingBasketApplication.java new file mode 100644 index 00000000..44e8cdab --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/PriceShoppingBasketApplication.java @@ -0,0 +1,73 @@ +package com.industriallogic.henrysgroceries; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.service.ShoppingBasketServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +@SpringBootApplication +@Slf4j +public class PriceShoppingBasketApplication implements CommandLineRunner { + + @Autowired + private ShoppingBasketServiceImpl shoppingBasketService; + + public static void main(String[] args) { + SpringApplication.run(PriceShoppingBasketApplication.class, args); + } + + @Override + public void run(String... args) { + startShopping(); + } + + public void startShopping() { + try (Scanner scanner = new Scanner(System.in)) { + while (true) { + LOGGER.info("Enter 1. To add products to basket | Enter 2. To checkout | Enter X. To EXIT"); + String option = scanner.next(); + switch (option) { + case "1": + addProductsToBasket( scanner); + break; + case "2": + checkout( ); + break; + case "X": + return; + default: + LOGGER.info("Invalid entry, please re-enter '1' OR '2' OR 'X'"); + } + } + } + } + + private void addProductsToBasket( Scanner scanner) { + LOGGER.info("Enter products separated with space to add to basket: "); + scanner.nextLine(); + List productsNameList = Arrays.asList(StringUtils.split(scanner.nextLine())); + productsNameList.forEach(productName -> { + try { + BigDecimal curntTotalAmount = shoppingBasketService.addProductToBasket(productName); + LOGGER.info(String.format("Added %s to basket. Current total amount %.2f " ,productName, curntTotalAmount)); + } catch (ProductNotFoundException e) { + LOGGER.error("Product not found for .. " + productName); + } + }); + } + + private void checkout( ) { + BigDecimal toPay = shoppingBasketService.totalPriceToPay(); + LOGGER.info("Total to Pay after discount " + toPay); + } + +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/exception/ProductNotFoundException.java b/src/main/java/com/industriallogic/henrysgroceries/exception/ProductNotFoundException.java new file mode 100644 index 00000000..9f70ae42 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/exception/ProductNotFoundException.java @@ -0,0 +1,15 @@ +package com.industriallogic.henrysgroceries.exception; + +public class ProductNotFoundException extends Exception { + + private static final long serialVersionUID = 1L; + + public ProductNotFoundException(String message) { + super(message); + } + + public ProductNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/src/main/java/com/industriallogic/henrysgroceries/model/MeasurementUnit.java b/src/main/java/com/industriallogic/henrysgroceries/model/MeasurementUnit.java new file mode 100644 index 00000000..3f64725b --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/model/MeasurementUnit.java @@ -0,0 +1,5 @@ +package com.industriallogic.henrysgroceries.model; + +public enum MeasurementUnit { + TIN, LOAF, BOTTLE, SINGLE +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/model/Product.java b/src/main/java/com/industriallogic/henrysgroceries/model/Product.java new file mode 100644 index 00000000..f75178c6 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/model/Product.java @@ -0,0 +1,16 @@ +package com.industriallogic.henrysgroceries.model; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; + +@RequiredArgsConstructor +@Data +public class Product { + @NonNull private String productCode; + @NonNull private String name; + @NonNull private BigDecimal price; + @NonNull private MeasurementUnit unit; +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/model/ShoppingBasket.java b/src/main/java/com/industriallogic/henrysgroceries/model/ShoppingBasket.java new file mode 100644 index 00000000..7812ecd9 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/model/ShoppingBasket.java @@ -0,0 +1,48 @@ +package com.industriallogic.henrysgroceries.model; + + +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@NoArgsConstructor +@Component +public class ShoppingBasket { + + private final Map basket = new HashMap<>(); + private LocalDate shoppingDate = LocalDate.now(); + private BigDecimal curTotalAmount = BigDecimal.ZERO; + + public BigDecimal addProductToBasket(Product product) { + basket.compute(product, + (k, v) -> { + return v == null ? 1 : v + 1; + }); + curTotalAmount = curTotalAmount.add(product.getPrice()); + return curTotalAmount; + } + + public LocalDate getShoppingDate() { + return this.shoppingDate; + } + + public Map getProductsInBasket() { + return Collections.unmodifiableMap(basket); + } + + public BigDecimal getCurTotalAmount() { + return curTotalAmount; + } + + public void setShoppingDate(LocalDate shoppingDate) { + this.shoppingDate = shoppingDate; + } +} + + + diff --git a/src/main/java/com/industriallogic/henrysgroceries/offers/ComboDiscountOffer.java b/src/main/java/com/industriallogic/henrysgroceries/offers/ComboDiscountOffer.java new file mode 100644 index 00000000..7fbc094d --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/offers/ComboDiscountOffer.java @@ -0,0 +1,49 @@ +package com.industriallogic.henrysgroceries.offers; + +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@RequiredArgsConstructor +@Data +public class ComboDiscountOffer implements Offer { + + @NonNull + private String offerName; + @NonNull + private Product qualifyingProduct; + @NonNull + private Integer qualifyingProdMinQnty; + @NonNull + private Product offerOnProduct; + @NonNull + private BigDecimal discountFactor; + @NonNull + private LocalDate offerStartDate; + @NonNull + private LocalDate offerEndDate; + + /** + * Applies Combo discount offer if basket qualifies for the offer + * @param basket - basket with products + * @return - discount amount to be applied on the basket + */ + @Override + public BigDecimal getDiscount(ShoppingBasket basket) { + if (isOfferStillValid(basket.getShoppingDate(), offerStartDate, offerEndDate)) { + Integer qualifyingProdCountInBasket = basket.getProductsInBasket().getOrDefault(qualifyingProduct, 0); + if (qualifyingProdCountInBasket >= qualifyingProdMinQnty) { + int purchaseQnty = basket.getProductsInBasket().getOrDefault(offerOnProduct, 0); + int eligibleOfferCount = Math.min(qualifyingProdCountInBasket/qualifyingProdMinQnty, purchaseQnty); + return offerOnProduct.getPrice().multiply(discountFactor).divide(ONE_HUNDRED ).multiply( new BigDecimal(eligibleOfferCount)).setScale(2); + } + } + return BigDecimal.ZERO; + } +} + diff --git a/src/main/java/com/industriallogic/henrysgroceries/offers/Offer.java b/src/main/java/com/industriallogic/henrysgroceries/offers/Offer.java new file mode 100644 index 00000000..b128bad4 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/offers/Offer.java @@ -0,0 +1,17 @@ +package com.industriallogic.henrysgroceries.offers; + +import com.industriallogic.henrysgroceries.model.ShoppingBasket; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public interface Offer { + + BigDecimal ONE_HUNDRED = new BigDecimal(100); + + default boolean isOfferStillValid(LocalDate shoppingDate, LocalDate offerStartDate, LocalDate offerEndDate) { + return ( shoppingDate.compareTo(offerStartDate) >= 0 && shoppingDate.compareTo(offerEndDate) <= 0) ; + } + + public BigDecimal getDiscount(ShoppingBasket basket); +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/offers/PercentageDiscountOffer.java b/src/main/java/com/industriallogic/henrysgroceries/offers/PercentageDiscountOffer.java new file mode 100644 index 00000000..6245e651 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/offers/PercentageDiscountOffer.java @@ -0,0 +1,38 @@ +package com.industriallogic.henrysgroceries.offers; + +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@RequiredArgsConstructor +@Data +public class PercentageDiscountOffer implements Offer { + @NonNull + private String offerName; + @NonNull + private Product product; + @NonNull + private BigDecimal discountPercentage; + @NonNull + private LocalDate offerStartDate; + @NonNull + private LocalDate offerEndDate; + + /** + * Apples percentage discount offer if basket qualifies for the offer + * @param basket - basket with products + * @return - discount amount to be applied on the basket + */ + public BigDecimal getDiscount(ShoppingBasket basket) { + if (isOfferStillValid(basket.getShoppingDate(), offerStartDate, offerEndDate)) { + Integer productQuantity = basket.getProductsInBasket().getOrDefault(product, 0); + return product.getPrice().multiply(discountPercentage).divide(ONE_HUNDRED ).multiply(new BigDecimal(productQuantity)); + } + return BigDecimal.ZERO; + } +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/provider/OffersProvider.java b/src/main/java/com/industriallogic/henrysgroceries/provider/OffersProvider.java new file mode 100644 index 00000000..4bcefaa8 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/provider/OffersProvider.java @@ -0,0 +1,49 @@ +package com.industriallogic.henrysgroceries.provider; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.offers.ComboDiscountOffer; +import com.industriallogic.henrysgroceries.offers.Offer; +import com.industriallogic.henrysgroceries.offers.PercentageDiscountOffer; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Component +public class OffersProvider { + + private ProductProvider productProvider; + + private List offers; + + public OffersProvider(ProductProvider productProvider) { + this.productProvider = productProvider; + } + + /** + * Populate the offers available + * + */ + @PostConstruct + public void init() throws ProductNotFoundException { + Product apples = productProvider.getProduct("Apples") ; + Product soup = productProvider.getProduct("Soup") ; + Product bread = productProvider.getProduct("Bread"); + + LocalDate firstOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); + LocalDate endOfNextMonth = firstOfNextMonth.with(TemporalAdjusters.lastDayOfMonth()); + PercentageDiscountOffer applesOffer = new PercentageDiscountOffer("Apple 10% OFF", apples, BigDecimal.valueOf(10), LocalDate.now().plusDays(3), endOfNextMonth); + ComboDiscountOffer discountOffer = new ComboDiscountOffer("COMBO OFFER", soup, 2, bread, BigDecimal.valueOf(50), LocalDate.now().minusDays(1), LocalDate.now().plusDays(6)); + offers = Collections.unmodifiableList(Arrays.asList(applesOffer, discountOffer)); + } + + public List getOffers() { + return offers; + } +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/provider/ProductProvider.java b/src/main/java/com/industriallogic/henrysgroceries/provider/ProductProvider.java new file mode 100644 index 00000000..367723b6 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/provider/ProductProvider.java @@ -0,0 +1,41 @@ +package com.industriallogic.henrysgroceries.provider; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +public class ProductProvider { + + private Map productList; + + /** + * Populates with all available products + */ + @PostConstruct + public void init() { + Map stock = new HashMap<>(); + stock.put("APPLES", new Product("A123","Apple", BigDecimal.valueOf(.10) , MeasurementUnit.SINGLE)); + stock.put("BREAD", new Product("B123","Bread", BigDecimal.valueOf(.80) , MeasurementUnit.LOAF)); + stock.put("MILK", new Product("M123", "Milk", BigDecimal.valueOf(1.30) , MeasurementUnit.BOTTLE)); + stock.put("SOUP", new Product("S123","Soup", BigDecimal.valueOf(.65) , MeasurementUnit.TIN)); + productList = Collections.unmodifiableMap(stock); + } + + public Product getProduct(String productName) throws ProductNotFoundException { + return Optional.ofNullable(productList.get(productName.toUpperCase())).orElseThrow(() -> { + LOGGER.error("No Product found for", productName); + return new ProductNotFoundException("No Product found for - " + productName); + }); + } +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/service/ShoppingBasketService.java b/src/main/java/com/industriallogic/henrysgroceries/service/ShoppingBasketService.java new file mode 100644 index 00000000..aa210cb9 --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/service/ShoppingBasketService.java @@ -0,0 +1,14 @@ +package com.industriallogic.henrysgroceries.service; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; + +import java.math.BigDecimal; + +public interface ShoppingBasketService { + + BigDecimal totalPriceToPay( ) ; + + BigDecimal getTotalDiscount( ) ; + + BigDecimal addProductToBasket(String productName) throws ProductNotFoundException ; +} diff --git a/src/main/java/com/industriallogic/henrysgroceries/service/ShoppingBasketServiceImpl.java b/src/main/java/com/industriallogic/henrysgroceries/service/ShoppingBasketServiceImpl.java new file mode 100644 index 00000000..1132027e --- /dev/null +++ b/src/main/java/com/industriallogic/henrysgroceries/service/ShoppingBasketServiceImpl.java @@ -0,0 +1,58 @@ +package com.industriallogic.henrysgroceries.service; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import com.industriallogic.henrysgroceries.provider.OffersProvider; +import com.industriallogic.henrysgroceries.provider.ProductProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +@Slf4j +public class ShoppingBasketServiceImpl implements ShoppingBasketService { + + private ProductProvider productProvider; + + private OffersProvider offersProvider; + + private ShoppingBasket basket; + + public ShoppingBasketServiceImpl(ProductProvider productProvider, OffersProvider offersProvider, ShoppingBasket basket) { + this.productProvider = productProvider; + this.offersProvider = offersProvider; + this.basket = basket; + } + + /** + * Calculates the final amount to pay after applying discount if any + * @return totalAmount to pay + */ + @Override + public BigDecimal totalPriceToPay( ) { + return basket.getCurTotalAmount().subtract(getTotalDiscount()).setScale(2); + } + + /** + * Calculates the total discount amount to apply on the basket + * @return total discount + */ + @Override + public BigDecimal getTotalDiscount() { + return offersProvider.getOffers().stream() + .map(offer -> offer.getDiscount(basket)) + .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(2); + } + + /** + * Adds a product in to basket + * @param productName + * @return current total amount + * @throws ProductNotFoundException, if product is not found in product repository + */ + @Override + public BigDecimal addProductToBasket(String productName) throws ProductNotFoundException { + return basket.addProductToBasket(productProvider.getProduct(productName)).setScale(2); + } +} diff --git a/src/test/java/com/industriallogic/henrysgroceries/integration/ShoppingBasketServiceIntegrationTest.java b/src/test/java/com/industriallogic/henrysgroceries/integration/ShoppingBasketServiceIntegrationTest.java new file mode 100644 index 00000000..d59bfced --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/integration/ShoppingBasketServiceIntegrationTest.java @@ -0,0 +1,127 @@ +package com.industriallogic.henrysgroceries.integration; + + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import com.industriallogic.henrysgroceries.provider.OffersProvider; +import com.industriallogic.henrysgroceries.provider.ProductProvider; +import com.industriallogic.henrysgroceries.service.ShoppingBasketServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {ShoppingBasketServiceImpl.class, ProductProvider.class,OffersProvider.class, + ShoppingBasket.class} ) +@Slf4j +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class ShoppingBasketServiceIntegrationTest { + + @Autowired + private ShoppingBasketServiceImpl shoppingBasketService; + + @Autowired + private ProductProvider productProvider; + @Autowired + private OffersProvider offersProvider; + @Autowired + private ShoppingBasket basket; + + + @Test + public void price3SoupAnd2BreadComboOfferAppliedOn1BreadTest() throws ProductNotFoundException { + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + BigDecimal currTotal = shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalDiscount = shoppingBasketService.getTotalDiscount(); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay( ); + assertEquals(BigDecimal.valueOf(3.55), currTotal); + assertEquals(BigDecimal.valueOf(0.40).setScale(2), totalDiscount); + assertEquals(BigDecimal.valueOf(3.15), totalPriceToPay); + } + + @Test + public void price6ApplesAndMilkBoughtTodayNoOfferAppliedTest() throws ProductNotFoundException { + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + BigDecimal currTotal = shoppingBasketService.addProductToBasket("Milk"); + BigDecimal totalDiscount = shoppingBasketService.getTotalDiscount(); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay( ); + assertEquals(BigDecimal.valueOf(1.90).setScale(2), currTotal); + assertEquals(BigDecimal.valueOf(0.00).setScale(2), totalDiscount); + assertEquals(BigDecimal.valueOf(1.90).setScale(2), totalPriceToPay); + } + + @Test + public void price1SoupAnd1BreadNoOfferAppliedTest() throws ProductNotFoundException { + shoppingBasketService.addProductToBasket("Soup"); + BigDecimal currTotal = shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalDiscount = shoppingBasketService.getTotalDiscount(); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay( ); + assertEquals(BigDecimal.valueOf(1.45).setScale(2), currTotal); + assertEquals(BigDecimal.valueOf(0.00).setScale(2), totalDiscount); + assertEquals(BigDecimal.valueOf(1.45).setScale(2), totalPriceToPay); + } + + @Test + public void price2SoupAnd1BreadComboOfferAppliedTest() throws ProductNotFoundException { + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + BigDecimal currTotal = shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalDiscount = shoppingBasketService.getTotalDiscount(); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay( ); + assertEquals(BigDecimal.valueOf(2.10).setScale(2), currTotal); + assertEquals(BigDecimal.valueOf(0.40).setScale(2), totalDiscount); + assertEquals(BigDecimal.valueOf(1.70).setScale(2), totalPriceToPay); + } + + @Test + public void price6ApplesAnd1MilkBoughtIn5DaysTest() throws ProductNotFoundException { + basket.setShoppingDate(LocalDate.now().plusDays(5)); + ShoppingBasketServiceImpl shoppingService = new ShoppingBasketServiceImpl( productProvider, offersProvider, basket) ; + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + BigDecimal currTotal = shoppingService.addProductToBasket("Milk"); + BigDecimal totalDiscount = shoppingService.getTotalDiscount(); + BigDecimal totalPriceToPay = shoppingService.totalPriceToPay( ); + assertEquals(BigDecimal.valueOf(1.90).setScale(2), currTotal); + assertEquals(BigDecimal.valueOf(0.06).setScale(2), totalDiscount); + assertEquals(BigDecimal.valueOf(1.84).setScale(2), totalPriceToPay); + } + + @Test + public void price3Apples2SoupAnd1BreadBoughtIn5DaysTest() throws ProductNotFoundException { + basket.setShoppingDate(LocalDate.now().plusDays(5)); + ShoppingBasketServiceImpl shoppingService = new ShoppingBasketServiceImpl( productProvider, offersProvider, basket) ; + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Apples"); + shoppingService.addProductToBasket("Soup"); + shoppingService.addProductToBasket("Soup"); + BigDecimal currTotal = shoppingService.addProductToBasket("Bread"); + BigDecimal totalDiscount = shoppingService.getTotalDiscount(); + BigDecimal totalPriceToPay = shoppingService.totalPriceToPay( ); + assertEquals(BigDecimal.valueOf(2.40).setScale(2), currTotal); + assertEquals(BigDecimal.valueOf(0.43).setScale(2), totalDiscount); + assertEquals(BigDecimal.valueOf(1.97).setScale(2), totalPriceToPay); + } +} diff --git a/src/test/java/com/industriallogic/henrysgroceries/offers/ComboDiscountOfferTest.java b/src/test/java/com/industriallogic/henrysgroceries/offers/ComboDiscountOfferTest.java new file mode 100644 index 00000000..f1ff231b --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/offers/ComboDiscountOfferTest.java @@ -0,0 +1,63 @@ +package com.industriallogic.henrysgroceries.offers; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import com.industriallogic.henrysgroceries.provider.ProductProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class ComboDiscountOfferTest { + + private ComboDiscountOffer comboDiscountOffer; + + @Mock + private ProductProvider productProvider; + + @Mock + private static ShoppingBasket shoppingBasket; + @Before + public void setUP() throws ProductNotFoundException { + when(productProvider.getProduct("Bread")).thenReturn(new Product("B123", "Bread", BigDecimal.valueOf(.80), MeasurementUnit.LOAF)); + when(productProvider.getProduct("Soup")).thenReturn(new Product("S123", "Soup", BigDecimal.valueOf(.65), MeasurementUnit.TIN)); + + comboDiscountOffer = new ComboDiscountOffer("COMBO OFFER", productProvider.getProduct("Soup"), 2, productProvider.getProduct("Bread"), BigDecimal.valueOf(50), LocalDate.now().minusDays(1), LocalDate.now().plusDays(6)); + } + + @Test + public void getDiscountOn2SoupsAnd1Bread() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + assertEquals( BigDecimal.valueOf(.40).setScale(2), comboDiscountOffer.getDiscount(shoppingBasket)); + } + + @Test + public void getDiscountOn2SoupsAnd2Bread() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 2); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + assertEquals( BigDecimal.valueOf(.40).setScale(2), comboDiscountOffer.getDiscount(shoppingBasket)); + } +} \ No newline at end of file diff --git a/src/test/java/com/industriallogic/henrysgroceries/offers/PercentageDiscountOfferTest.java b/src/test/java/com/industriallogic/henrysgroceries/offers/PercentageDiscountOfferTest.java new file mode 100644 index 00000000..ff40b0c6 --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/offers/PercentageDiscountOfferTest.java @@ -0,0 +1,65 @@ +package com.industriallogic.henrysgroceries.offers; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import com.industriallogic.henrysgroceries.provider.ProductProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class PercentageDiscountOfferTest { + + private PercentageDiscountOffer percentDiscountOffer; + + @Mock + private ProductProvider productProvider; + + @Mock + private static ShoppingBasket shoppingBasket; + + @Before + public void setUP() throws ProductNotFoundException { + when(productProvider.getProduct("Apples")).thenReturn(new Product("A123", "Apple", BigDecimal.valueOf(.10), MeasurementUnit.SINGLE)); + + LocalDate firstOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); + LocalDate endOfNextMonth = firstOfNextMonth.with(TemporalAdjusters.lastDayOfMonth()); + percentDiscountOffer = new PercentageDiscountOffer("Apple 10% OFF", productProvider.getProduct("Apples"), BigDecimal.valueOf(10), LocalDate.now().plusDays(3), endOfNextMonth); + } + + + @Test + public void getDiscountOn3ApplesBoughtToday() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Apples"), 3); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + Assertions.assertEquals( BigDecimal.valueOf(0), percentDiscountOffer.getDiscount(shoppingBasket)); + } + + @Test + public void getDiscountOn3ApplesBoughtIn5Days() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now().plusDays(5)); + + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Apples"), 3); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + Assertions.assertEquals( BigDecimal.valueOf(.03).setScale(2), percentDiscountOffer.getDiscount(shoppingBasket)); + } +} \ No newline at end of file diff --git a/src/test/java/com/industriallogic/henrysgroceries/provider/OffersProviderTest.java b/src/test/java/com/industriallogic/henrysgroceries/provider/OffersProviderTest.java new file mode 100644 index 00000000..b6b14085 --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/provider/OffersProviderTest.java @@ -0,0 +1,41 @@ +package com.industriallogic.henrysgroceries.provider; + + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.testutil.TestMockUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class OffersProviderTest { + + @InjectMocks + private OffersProvider offersProvider; + + @Mock + private ProductProvider productProvider; + + @Before + public void prepare() throws ProductNotFoundException { + when(productProvider.getProduct("Bread")).thenReturn(new Product("B123", "Bread", BigDecimal.valueOf(.80), MeasurementUnit.LOAF)); + when(productProvider.getProduct("Soup")).thenReturn(new Product("S123", "Soup", BigDecimal.valueOf(.65), MeasurementUnit.TIN)); + when(productProvider.getProduct("Apples")).thenReturn(new Product("A123", "Apple", BigDecimal.valueOf(.10), MeasurementUnit.SINGLE)); + offersProvider.init(); + } + + @Test + public void getOffers() { + assertEquals(TestMockUtil.getOffersList(), offersProvider.getOffers()); + } +} \ No newline at end of file diff --git a/src/test/java/com/industriallogic/henrysgroceries/provider/ProductProviderTest.java b/src/test/java/com/industriallogic/henrysgroceries/provider/ProductProviderTest.java new file mode 100644 index 00000000..a7d17511 --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/provider/ProductProviderTest.java @@ -0,0 +1,42 @@ +package com.industriallogic.henrysgroceries.provider; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import java.math.BigDecimal; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = ProductProvider.class) +public class ProductProviderTest { + + @Autowired + private ProductProvider productProvider; + + @Before + public void prepare() { + productProvider.init(); + } + + @Test + public void getProduct() throws ProductNotFoundException { + Product apple = new Product("A123", "Apple", BigDecimal.valueOf(.10), MeasurementUnit.SINGLE); + assertEquals(productProvider.getProduct("Apples"), apple); + } + + @Test(expected=ProductNotFoundException.class) + public void getProductWithSpace() throws ProductNotFoundException { + productProvider.getProduct(" "); + } + + @Test(expected=ProductNotFoundException.class) + public void getInvalidProduct() throws ProductNotFoundException { + productProvider.getProduct("chocolate"); + } +} \ No newline at end of file diff --git a/src/test/java/com/industriallogic/henrysgroceries/service/ShoppingBasketServiceTest.java b/src/test/java/com/industriallogic/henrysgroceries/service/ShoppingBasketServiceTest.java new file mode 100644 index 00000000..4ac4fdac --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/service/ShoppingBasketServiceTest.java @@ -0,0 +1,251 @@ +package com.industriallogic.henrysgroceries.service; + +import com.industriallogic.henrysgroceries.exception.ProductNotFoundException; +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.model.ShoppingBasket; +import com.industriallogic.henrysgroceries.provider.OffersProvider; +import com.industriallogic.henrysgroceries.provider.ProductProvider; +import com.industriallogic.henrysgroceries.testutil.TestMockUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class ShoppingBasketServiceTest { + + @InjectMocks + private ShoppingBasketServiceImpl shoppingBasketService; + + @Mock + private OffersProvider OffersProvider; + + @Mock + private static ProductProvider productProvider; + + @Mock + private static ShoppingBasket shoppingBasket; + + @Before + public void setUP() throws ProductNotFoundException { + when(productProvider.getProduct("Apples")).thenReturn(new Product("A123", "Apple", BigDecimal.valueOf(.10), MeasurementUnit.SINGLE)); + when(productProvider.getProduct("Bread")).thenReturn(new Product("B123", "Bread", BigDecimal.valueOf(.80), MeasurementUnit.LOAF)); + when(productProvider.getProduct("Milk")).thenReturn(new Product("M123", "Milk", BigDecimal.valueOf(1.30), MeasurementUnit.BOTTLE)); + when(productProvider.getProduct("Soup")).thenReturn(new Product("S123", "Soup", BigDecimal.valueOf(.65), MeasurementUnit.TIN)); + + when(OffersProvider.getOffers()).thenReturn(TestMockUtil.getOffersList()); + + when(shoppingBasket.addProductToBasket(productProvider.getProduct("Apples"))).thenReturn(BigDecimal.valueOf(0.10)); + when(shoppingBasket.addProductToBasket(productProvider.getProduct("Soup"))).thenReturn(BigDecimal.valueOf(0.65)); + when(shoppingBasket.addProductToBasket(productProvider.getProduct("Bread"))).thenReturn(BigDecimal.valueOf(0.80)); + when(shoppingBasket.addProductToBasket(productProvider.getProduct("Milk"))).thenReturn(BigDecimal.valueOf(1.30)); + } + + @Test + public void add2ApplesToBasketTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(0.20).setScale(2)); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + assertEquals(BigDecimal.valueOf(0.20).setScale(2), shoppingBasketService.totalPriceToPay()); + } + + @Test + public void price1SoupAnd1BreadNoOfferAppliedTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(1.45)); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.45), totalPriceToPay); + } + + @Test + public void price2SoupAnd1BreadComboOfferAppliedTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(2.10)); + + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.70).setScale(2), totalPriceToPay); + } + + @Test + public void price2SoupAnd1BreadBoughtYesterdayComboOfferAppliedTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now().minusDays(1)); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(2.10)); + + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.70).setScale(2), totalPriceToPay); + } + + @Test + public void price2SoupAnd1BreadBoughtIn6DaysComboOfferAppliedTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now().plusDays(6)); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(2.10)); + + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.70).setScale(2), totalPriceToPay); + } + + @Test + public void price2SoupAnd1BreadBoughtIn7DaysNoOfferAppliedTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now().plusDays(7)); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(2.10)); + + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(2.10).setScale(2), totalPriceToPay); + } + + @Test + public void price3SoupAnd2BreadComboOfferAppliedOn1BreadTest() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Soup"), 3); + put(productProvider.getProduct("Bread"), 2); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(3.55)); + + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(3.15), totalPriceToPay); + } + + + @Test + public void price6ApplesAndMilkBoughtTodayNoOfferApplied() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now()); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Apples"), 6); + put(productProvider.getProduct("Milk"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(1.90)); + + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Milk"); + + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.90).setScale(2), totalPriceToPay); + } + + @Test + public void price6ApplesAndMilkBoughtIn5DaysPercentOfferApplied() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now().plusDays(5)); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Apples"), 6); + put(productProvider.getProduct("Milk"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(1.90)); + + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Milk"); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.84).setScale(2), totalPriceToPay); + } + + @Test + public void price6ApplesBoughtAfterEndOfFollowingMonthOfferApplied() throws ProductNotFoundException { + LocalDate firstOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); + LocalDate endOfNextMonth = firstOfNextMonth.with(TemporalAdjusters.lastDayOfMonth()); + when(shoppingBasket.getShoppingDate()).thenReturn(endOfNextMonth.plusDays(1)); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Apples"), 6); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(0.60)); + + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(.60).setScale(2), totalPriceToPay); + } + + + @Test + public void price3ApplesAnd2SoupAnd1BreadBoughtIn5DaysComboAndPercentOfferApplied() throws ProductNotFoundException { + when(shoppingBasket.getShoppingDate()).thenReturn(LocalDate.now().plusDays(5)); + Map productMap = new HashMap() {{ + put(productProvider.getProduct("Apples"), 3); + put(productProvider.getProduct("Soup"), 2); + put(productProvider.getProduct("Bread"), 1); + }}; + when(shoppingBasket.getProductsInBasket()).thenReturn(productMap); + when(shoppingBasket.getCurTotalAmount()).thenReturn(BigDecimal.valueOf(2.40)); + + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Apples"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Soup"); + shoppingBasketService.addProductToBasket("Bread"); + BigDecimal totalPriceToPay = shoppingBasketService.totalPriceToPay(); + assertEquals(BigDecimal.valueOf(1.97), totalPriceToPay); + } +} diff --git a/src/test/java/com/industriallogic/henrysgroceries/testutil/TestMockUtil.java b/src/test/java/com/industriallogic/henrysgroceries/testutil/TestMockUtil.java new file mode 100644 index 00000000..95bd9f47 --- /dev/null +++ b/src/test/java/com/industriallogic/henrysgroceries/testutil/TestMockUtil.java @@ -0,0 +1,29 @@ +package com.industriallogic.henrysgroceries.testutil; + +import com.industriallogic.henrysgroceries.model.MeasurementUnit; +import com.industriallogic.henrysgroceries.model.Product; +import com.industriallogic.henrysgroceries.offers.ComboDiscountOffer; +import com.industriallogic.henrysgroceries.offers.Offer; +import com.industriallogic.henrysgroceries.offers.PercentageDiscountOffer; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class TestMockUtil { + + public static List getOffersList() { + Product apples = new Product("A123", "Apple", BigDecimal.valueOf(.10), MeasurementUnit.SINGLE); + Product bread = new Product("B123", "Bread", BigDecimal.valueOf(.80), MeasurementUnit.LOAF); + Product milk = new Product("M123", "Milk", BigDecimal.valueOf(1.30), MeasurementUnit.BOTTLE); + Product soup = new Product("S123", "Soup", BigDecimal.valueOf(.65), MeasurementUnit.TIN); + LocalDate firstOfNextMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); + LocalDate endOfNextMonth = firstOfNextMonth.with(TemporalAdjusters.lastDayOfMonth()); + PercentageDiscountOffer applesOffer = new PercentageDiscountOffer("Apple 10% OFF", apples, BigDecimal.valueOf(10), LocalDate.now().plusDays(3), endOfNextMonth); + ComboDiscountOffer discountOffer = new ComboDiscountOffer("COMBO OFFER", soup, 2, bread, BigDecimal.valueOf(50), LocalDate.now().minusDays(1), LocalDate.now().plusDays(6)); + return Collections.unmodifiableList(Arrays.asList(applesOffer, discountOffer)); + } + +}