diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..54cf682f --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Compiled class file +*.class + +target + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.idea +*.iml + +/.gradle/ +*/.gradle +/.gradle/ +!gradle-wrapper.jar +*/out +*/build +*/target/** +*build +**/resources/pacts + +*.DS_Store + +#ignore terraform plugins and statefiles + +terraform/.terraform +terraform/.terraform/* +classes + +.terraform diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..a43c72bb --- /dev/null +++ b/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + com.test.supermarket + henrys_groceries + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + junit + junit + 4.12 + test + + + log4j + log4j + 1.2.17 + + + diff --git a/src/main/java/com/test/harrys/InventoryClient.java b/src/main/java/com/test/harrys/InventoryClient.java new file mode 100644 index 00000000..42decb07 --- /dev/null +++ b/src/main/java/com/test/harrys/InventoryClient.java @@ -0,0 +1,126 @@ +package com.test.harrys; + +import com.test.harrys.basket.ShoppingBasket; +import com.test.harrys.model.ShoppingListItem; + +import java.io.PrintWriter; +import java.math.BigDecimal; +import java.util.Scanner; +import java.util.logging.Logger; + +import static com.test.harrys.ShoppingTill.calculateBill; +import static com.test.harrys.ShoppingTill.getProductPrice; +import static com.test.harrys.control.InventoryControl.initialiseInventory; + +public class InventoryClient extends Thread { + private static final String ADD = "add"; + private static final String BILL = "bill"; + private static final String RESET ="reset"; + private static final Logger log = Logger.getLogger(InventoryClient.class.getName()); + private static final String COMMAND_DELIMITER = " "; + public static final String START = "start"; + public static final String END = "end"; + + ShoppingBasket basket = new ShoppingBasket(); + + PrintWriter out = new PrintWriter(System.out, true); + + public InventoryClient() { + initialiseInventory(); + } + + /** + * starts a thread and sends the + * client a set of instructions to use the service + */ + public void run() { + Scanner inputS = new Scanner(System.in); + userPrompt(); + while (true) { + String input = inputS.nextLine(); + if (input == null || input.equals("exit")) { + break; + } + processInput(input); + } + } + + private void displayInventory() { + out.println("The inventory consists of :"); + ShoppingTill.getProductOffering().stream().forEach(item -> out.println(item.getName())); + } + + private void userPrompt() { + out.println("Enter the word 'exit' to quit"); + out.println("Enter 'start' to start shopping"); + out.println("Enter 'add' to add a product to the basket eg 'add soup' "); + out.println("Enter 'bill' to display bill to customer"); + out.println("Enter 'end' to settle bill, ends session"); + out.println("Enter 'reset' to reset all system data to defaults"); + displayInventory(); + } + + /** + * processes the input by way of interpreting the command sent across + * the command string is delimiterred with a space and parsed to determine the command + * and the relative parameters, the 1st string in the list is the actual command + */ + private void processInput(String input) { + String[] command = input.split(COMMAND_DELIMITER); + switch (command[0]) { + case START: + startShopping(); + break; + case END: + endShopping(); + break; + case BILL: + displayBill(); + break; + case ADD: + addItemToBasket(command[1]); + break; + case RESET: + out.println("Service reset to default state"); + basket = new ShoppingBasket(); + break; + default: + userPrompt(); + break; + } + } + + private void endShopping() { + displayBill(); + basket = new ShoppingBasket(); + } + + private void startShopping() { + + } + + + /** + * adds item to basket + * @param productCode + */ + private void addItemToBasket(String productCode) { + try{ + basket.addItem(new ShoppingListItem(productCode)); + out.println(String.format("added %s to basket, unit price : %s",productCode, getProductPrice(productCode))); + }catch (IllegalArgumentException iae){ + log.warning(iae.getMessage()); + displayInventory(); + } + } + + private void displayBill(){ + BigDecimal bill = calculateBill(basket); + out.println(String.format("your bill is : %s", bill)); + } + + public static void main(String[] args) { + log.info("The client has been started "); + new InventoryClient().start(); + } +} diff --git a/src/main/java/com/test/harrys/ShoppingTill.java b/src/main/java/com/test/harrys/ShoppingTill.java new file mode 100644 index 00000000..a5583a26 --- /dev/null +++ b/src/main/java/com/test/harrys/ShoppingTill.java @@ -0,0 +1,91 @@ +package com.test.harrys; + +import com.test.harrys.basket.ShoppingBasket; +import com.test.harrys.model.Product; +import com.test.harrys.model.ShoppingDiscount; +import com.test.harrys.model.ShoppingListItem; +import org.apache.log4j.Logger; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.*; + + +public class ShoppingTill { + final static Logger LOGGER = Logger.getLogger(ShoppingTill.class); + + private static final Set discounts = new HashSet<>(); + + private static final Set PRODUCT_SET = new HashSet<>(); + + private static final int myNumDecimals = 2; + + /** + * get product instance using the code + * @param productCode + * @return gets product instance + */ + public static Product getProductByCode(String productCode) { + return PRODUCT_SET.stream() + .filter(product -> product.getProductCode().equals(productCode)) + .findFirst().orElseThrow(() -> new IllegalArgumentException( + String.format("Cannot find product with product code : [%s] in product offerings", + productCode))); + } + + /** + * get price of product + * @param productCode + * @return price of related product + */ + public static BigDecimal getProductPrice(String productCode){ + return getProductByCode(productCode).getPrice(); + } + + /** + * adds any discounts entries to the invoice if any apply to the items purchased + * @param listItem item purchased + * @return total discount amount for item purchased + */ + public static BigDecimal calculateDiscountTotal(ShoppingBasket basket, ShoppingListItem listItem){ + BigDecimal discountAmount = BigDecimal.ZERO; + Optional shoppingDiscount = discounts.stream() + .filter(discount -> discount.getProductCode().equals(listItem.getProductCode())) + .findFirst(); + if(shoppingDiscount.isPresent()){ + discountAmount = shoppingDiscount.get().calculateDiscountAmount(basket); + } + return discountAmount; + } + + static BigDecimal calculateBill(ShoppingBasket basket) { + double subTotal = basket.getShoppingListItems().stream().mapToDouble(basketItem -> + getProductPrice(basketItem.getProductCode()).doubleValue() * basketItem.getQuantity()).sum(); + double discountTotal = basket.getShoppingListItems().stream().mapToDouble(items -> + calculateDiscountTotal(basket, items).doubleValue()).sum(); + return BigDecimal.valueOf(subTotal - discountTotal).setScale( myNumDecimals, RoundingMode.HALF_UP); + } + + static BigDecimal calculateBill(String[] shoppingList) { + ShoppingBasket basket = new ShoppingBasket(); + Arrays.stream(shoppingList).forEach(p -> basket.addItem(new ShoppingListItem(p))); + return calculateBill(basket); + } + + public static void setProductOffering(Set catalogue) { + ShoppingTill.PRODUCT_SET.addAll(catalogue); + } + + public static void setDiscounts(Set discounts) { + ShoppingTill.discounts.clear(); + ShoppingTill.discounts.addAll(discounts); + } + + + public static Set getProductOffering() { + return Collections.unmodifiableSet(PRODUCT_SET); + } +} + diff --git a/src/main/java/com/test/harrys/basket/ShoppingBasket.java b/src/main/java/com/test/harrys/basket/ShoppingBasket.java new file mode 100644 index 00000000..26c159e6 --- /dev/null +++ b/src/main/java/com/test/harrys/basket/ShoppingBasket.java @@ -0,0 +1,49 @@ +package com.test.harrys.basket; + +import com.test.harrys.model.ShoppingListItem; +import org.apache.log4j.Logger; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + + +/** + * @author kay + *encapsulates a shopping basket + */ +public class ShoppingBasket { + private LocalDate shoppingDate; + + final static Logger LOGGER = Logger.getLogger(ShoppingBasket.class); + + private final Set listItem = new HashSet<>(); + + public ShoppingBasket() { + shoppingDate = LocalDate.now(); + } + + /** + * adds one or more items of a specific product to a shopping basket + * @param item shopping list item + */ + public void addItem(ShoppingListItem item) { + if(!listItem.add(item)){ + listItem.stream().filter(lItem -> lItem.getProductCode().equals(item.getProductCode())). + findAny().ifPresent(lItem -> lItem.increaseQuantity(item.getQuantity())); + } + } + + public Collection getShoppingListItems() { + return this.listItem; + } + + public LocalDate getShoppingDate() { + return shoppingDate; + } + + public void setShoppingDate(LocalDate shoppingDate) { + this.shoppingDate = shoppingDate; + } +} diff --git a/src/main/java/com/test/harrys/control/InventoryControl.java b/src/main/java/com/test/harrys/control/InventoryControl.java new file mode 100644 index 00000000..001943ca --- /dev/null +++ b/src/main/java/com/test/harrys/control/InventoryControl.java @@ -0,0 +1,107 @@ +package com.test.harrys.control; + +import com.test.harrys.basket.ShoppingBasket; +import com.test.harrys.model.Product; +import com.test.harrys.model.ShoppingDiscount; +import com.test.harrys.model.ShoppingListItem; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static com.test.harrys.ShoppingTill.*; + +public class InventoryControl { + + public static void initialiseInventory(){ + initInventory(); + setDiscounts(Set.of(createApplesDiscount(),createSoupDiscount())); + } + + public static ShoppingDiscount createApplesDiscount(){ + ShoppingDiscount discount = new ShoppingDiscount() { + @Override + public BigDecimal calculateDiscountAmount(ShoppingBasket basket) { + BigDecimal discount = BigDecimal.ZERO; + + if(isActive(basket.getShoppingDate())){ + double discountAmount = basket.getShoppingListItems().stream() + .filter(item -> item.getProductCode().equals(getProductCode())) + .mapToDouble(item -> getDiscountAmount() * item.getQuantity() * + getProductPrice(getProductCode()).doubleValue()) + .findFirst().orElse(0); + discount = new BigDecimal( discountAmount).setScale( 2, RoundingMode.HALF_UP); + } + return discount; + } + }; + LocalDate today = LocalDate.now(); + discount.setStartDate(today.plus(3, ChronoUnit.DAYS)); + discount.setEndDate(today.plus(2, ChronoUnit.MONTHS)); + discount.setDiscountAmount(0.10); + discount.setProductCode("apple"); + discount.setTriggerQuantity(1); + discount.setDiscountDescription("Apples have a 10% discount"); + return discount; + } + + public static ShoppingDiscount createSoupDiscount(){ + ShoppingDiscount discount = new ShoppingDiscount() { + @Override + public BigDecimal calculateDiscountAmount(ShoppingBasket basket) { + BigDecimal discount = BigDecimal.ZERO; + Collection items = basket.getShoppingListItems(); + if(items.contains(new ShoppingListItem("bread")) && isActive(basket.getShoppingDate())){ + Optional listItem = items.stream().filter(item -> + item.getProductCode().equals(getProductCode()) && + item.getQuantity() >= getTriggerQuantity()).findAny(); + if(listItem.isPresent()){ + discount = BigDecimal.valueOf(getProductPrice("bread").doubleValue() / 2); + } + } + return discount; + } + }; + LocalDate today = LocalDate.now(); + discount.setStartDate(today.minus(1, ChronoUnit.DAYS)); + discount.setEndDate(today.plus(6, ChronoUnit.DAYS)); + discount.setDiscountAmount(0.5); + discount.setProductCode("soup"); + discount.setTriggerQuantity(2.0); + discount.setDiscountDescription("Buy 2 tins of soup and get a loaf of bread half price"); + return discount; + } + + public static void initInventory(){ + Set products = new HashSet(); + Product product = new Product(); + product.setName("apple"); + product.setPrice(new BigDecimal("0.10")); + product.setProductCode("apple"); + products.add(product); + + product = new Product(); + product.setName("soup"); + product.setPrice(new BigDecimal("0.65")); + product.setProductCode("soup"); + products.add(product); + + product = new Product(); + product.setName("bread"); + product.setPrice(new BigDecimal("0.80")); + product.setProductCode("bread"); + products.add(product); + + product = new Product(); + product.setName("milk"); + product.setPrice(new BigDecimal("1.30")); + product.setProductCode("milk"); + products.add(product); + setProductOffering(products); + } +} diff --git a/src/main/java/com/test/harrys/model/Product.java b/src/main/java/com/test/harrys/model/Product.java new file mode 100644 index 00000000..1dd7f71c --- /dev/null +++ b/src/main/java/com/test/harrys/model/Product.java @@ -0,0 +1,52 @@ +package com.test.harrys.model; + +import java.math.BigDecimal; +import java.util.Objects; + +public class Product { + + private String name; + private String productCode; + private BigDecimal pricePerUnit; + + public Product(){ + + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public BigDecimal getPrice() { + return pricePerUnit; + } + + public void setPrice(BigDecimal price) { + this.pricePerUnit = price; + } + + public String getProductCode() { + return productCode; + } + + public void setProductCode(String productCode) { + this.productCode = productCode; + } + + @Override + public boolean equals(Object o) { + Product toBeCompared = (Product)o; + return productCode.equals(toBeCompared.getProductCode()); + } + + @Override + public int hashCode() { + return Objects.hash(productCode); + } + +} + diff --git a/src/main/java/com/test/harrys/model/ShoppingDiscount.java b/src/main/java/com/test/harrys/model/ShoppingDiscount.java new file mode 100644 index 00000000..f0132aae --- /dev/null +++ b/src/main/java/com/test/harrys/model/ShoppingDiscount.java @@ -0,0 +1,100 @@ +package com.test.harrys.model; + +import com.test.harrys.basket.ShoppingBasket; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Objects; + +import static java.util.Objects.isNull; + +/** + * @author kay + *encapsulates discount that may be applied to a product based on number items purchased + */ +public abstract class ShoppingDiscount { + + private double discountAmount; + private String productCode; + private double triggerQuantity; + private String discountDescription; + private LocalDate startDate; + private LocalDate endDate; + + + + /** + * calculates discount that may apply to specific items purchased + * @param item + * @return amount of discount + */ + public abstract BigDecimal calculateDiscountAmount(ShoppingBasket basket); + + @Override + public boolean equals(Object o) { + ShoppingDiscount toBeCompared = (ShoppingDiscount)o; + return productCode.equals(toBeCompared.getProductCode()); + } + + @Override + public int hashCode() { + return Objects.hash(productCode); + } + + public boolean isActive(LocalDate shoppingDate){ + boolean isActive = !(isNull(startDate) || isNull(endDate)); + if(isActive){ + isActive = startDate.isBefore(shoppingDate) && endDate.isAfter(shoppingDate); + } + return isActive; + } + + public double getDiscountAmount() { + return discountAmount; + } + + public void setDiscountAmount(double discountAmount) { + this.discountAmount = discountAmount; + } + + public String getProductCode() { + return productCode; + } + + public void setProductCode(String productCode) { + this.productCode = productCode; + } + + public String getDiscountDescription() { + return discountDescription; + } + + public void setDiscountDescription(String discountDescription) { + this.discountDescription = discountDescription; + } + + public double getTriggerQuantity() { + return triggerQuantity; + } + + public void setTriggerQuantity(double triggerQuantity) { + this.triggerQuantity = triggerQuantity; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } +} + diff --git a/src/main/java/com/test/harrys/model/ShoppingListItem.java b/src/main/java/com/test/harrys/model/ShoppingListItem.java new file mode 100644 index 00000000..b4b6e539 --- /dev/null +++ b/src/main/java/com/test/harrys/model/ShoppingListItem.java @@ -0,0 +1,62 @@ +package com.test.harrys.model; + +import com.test.harrys.ShoppingTill; + +import java.util.Objects; + +import static com.test.harrys.ShoppingTill.getProductByCode; + +/** + * @author kay + *Class encapsulates total number of a specific item purchased + *consider this a line on a shopping list + *i.e 6 bananas. A Collection of this instance will represent the + *contents of a shopping basket. + */ +public class ShoppingListItem { + private String productCode; + private int quantity; + + public ShoppingListItem(String productCode){ + getProductByCode(productCode); + this.productCode = productCode; + this.quantity = 1; + } + + public String getProductCode() { + return productCode; + } + + public void setProductCode(String productCode) { + this.productCode = productCode; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public void increaseQuantity(int quantity) { + this.quantity += quantity; + } + + public void decreaseQuantity(int quantity) { + this.quantity -= quantity; + } + + @Override + public boolean equals(Object o) { + ShoppingListItem toBeCompared = (ShoppingListItem)o; + return productCode.equals(toBeCompared.getProductCode()); + } + + @Override + public int hashCode() { + return Objects.hash(productCode); + } + +} + diff --git a/src/test/java/com/test/harrys/SuperMarketTest.java b/src/test/java/com/test/harrys/SuperMarketTest.java new file mode 100644 index 00000000..48e772bb --- /dev/null +++ b/src/test/java/com/test/harrys/SuperMarketTest.java @@ -0,0 +1,140 @@ +package com.test.harrys; + +import com.test.harrys.basket.ShoppingBasket; +import com.test.harrys.model.ShoppingDiscount; +import com.test.harrys.model.ShoppingListItem; +import org.junit.*; +import org.junit.rules.ExpectedException; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.*; + +import static com.test.harrys.ShoppingTill.*; +import static com.test.harrys.control.InventoryControl.*; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; + +public class SuperMarketTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @BeforeClass + public static void init(){ + initialiseInventory(); + } + + @Test + public void whenItemsAreAddedToBaskeItemShouldTally(){ + ShoppingBasket basket = new ShoppingBasket( ); + ShoppingListItem listItem = new ShoppingListItem("apple"); + basket.addItem(listItem); + basket.addItem(listItem); + Collection items = basket.getShoppingListItems(); + assertTrue(items.contains(listItem)); + Optional optionalItem = + items.stream().filter(lineItem -> lineItem.getProductCode().equals("apple")).findFirst(); + assertEquals(2,optionalItem.get().getQuantity(),0.00001); + } + + @Test + public void whenShoppingItemIsAddedToBasketItShouldBeAvaialble(){ + ShoppingBasket basket = new ShoppingBasket( ); + ShoppingListItem listItem = new ShoppingListItem("apple"); + basket.addItem(listItem); + Collection items = basket.getShoppingListItems(); + assertTrue(items.contains(listItem)); + } + + + @Test() + public void attemptToAddInvalidItemToBasketShouldThrowException(){ + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Cannot find product with product code : "); + ShoppingBasket basket = new ShoppingBasket( ); + ShoppingListItem listItem = new ShoppingListItem("bananas"); + basket.addItem(listItem); + } + + @Test + public void calculateTotalTest(){ + double expectedTotal = + 0.10 + 0.10 + 0.65 + 0.80 + 0.80 + 1.30; + BigDecimal total = new BigDecimal(expectedTotal). + setScale( 2, RoundingMode.HALF_UP);; + String[] shoppingList = + {"apple", "apple", "soup","bread","bread","milk"}; + BigDecimal billTotal = calculateBill(shoppingList); + assertEquals(total, billTotal); + } + + @Test + public void determineIfDiscountIsActive(){ + ShoppingDiscount discount = createSoupDiscount(); + assertTrue(discount.isActive(LocalDate.now())); + assertFalse(discount.isActive(LocalDate.now().plus(8,ChronoUnit.DAYS))); + } + + @Test + public void sixApplesAndABottleOfMilkBoughtToday(){ + String[] shoppingList = + {"apple", "apple", "apple","apple","apple","apple","milk"}; + double expectedTotal = 1.90; + BigDecimal total = new BigDecimal(expectedTotal). + setScale( 2, RoundingMode.HALF_UP); + BigDecimal billTotal = calculateBill(shoppingList); + assertEquals(total, billTotal); + } + + @Test + public void sixApplesAndABottleOfMilkBoughtInFiveDays(){ + String[] shoppingList = + {"apple","apple", "apple","apple","apple","apple","milk"}; + double expectedTotal = 1.84; + BigDecimal total = new BigDecimal(expectedTotal). + setScale( 2, RoundingMode.HALF_UP); + + ShoppingBasket basket = new ShoppingBasket(); + Arrays.stream(shoppingList).forEach(p -> basket.addItem(new ShoppingListItem(p))); + basket.setShoppingDate(LocalDate.now().plus(5,ChronoUnit.DAYS)); + + BigDecimal billTotal = calculateBill(basket); + assertEquals(total, billTotal); + } + + @Test + public void threeSoupTinsTwoLoafBoughtToday(){ + String[] shoppingList = + {"soup","soup","soup","bread","bread"}; + double expectedTotal = 3.15; + BigDecimal total = new BigDecimal(expectedTotal). + setScale( 2, RoundingMode.HALF_UP); + + ShoppingBasket basket = new ShoppingBasket(); + Arrays.stream(shoppingList).forEach(p -> basket.addItem(new ShoppingListItem(p))); + basket.setShoppingDate(LocalDate.now()); + + BigDecimal billTotal = calculateBill(basket); + assertEquals(total, billTotal); + } + + @Test + public void threeApplesTwoSoupTinsOneLoafBoughtInFiveDays(){ + String[] shoppingList = + {"apple","apple", "apple","soup","soup","bread"}; + double expectedTotal = 1.97; + BigDecimal total = new BigDecimal(expectedTotal). + setScale( 2, RoundingMode.HALF_UP); + + ShoppingBasket basket = new ShoppingBasket(); + Arrays.stream(shoppingList).forEach(p -> basket.addItem(new ShoppingListItem(p))); + basket.setShoppingDate(LocalDate.now().plus(5,ChronoUnit.DAYS)); + + BigDecimal billTotal = calculateBill(basket); + assertEquals(total, billTotal); + } +}