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));
+ }
+
+}