diff --git a/assets/images/class_diagram_basket.png b/assets/images/class_diagram_basket.png new file mode 100644 index 000000000..b966d68cd Binary files /dev/null and b/assets/images/class_diagram_basket.png differ diff --git a/assets/images/class_diagram_inventory.png b/assets/images/class_diagram_inventory.png new file mode 100644 index 000000000..3ad443eec Binary files /dev/null and b/assets/images/class_diagram_inventory.png differ diff --git a/assets/images/class_diagram_printGenerator.png b/assets/images/class_diagram_printGenerator.png new file mode 100644 index 000000000..9c1aa9699 Binary files /dev/null and b/assets/images/class_diagram_printGenerator.png differ diff --git a/assets/images/gleek_class-diagram_old.png b/assets/images/gleek_class-diagram_old.png new file mode 100644 index 000000000..5e7dae467 Binary files /dev/null and b/assets/images/gleek_class-diagram_old.png differ diff --git a/src/main/java/com/booleanuk/core/README.md b/src/main/java/com/booleanuk/core/README.md new file mode 100644 index 000000000..97c039226 --- /dev/null +++ b/src/main/java/com/booleanuk/core/README.md @@ -0,0 +1,100 @@ +# Bob's Bagels OOP + +NOTES! +- I have done Core and Extension 1-3 +- The domain model and class diagram are in [domain-model.md](domain-model.md) +- The classes printInventoryMenu and printInventoryBasket were created during the core part and are not a part of the requirements +- I did overthink a lot in this exercise, I'm not happy with all parts at the end, I think it could be much more simplified. +- There are some code duplications and I have commented "TODO:" on things I'm unsure about or things I think could be designed better. + +## Core requirements +- [x] #1 I'd like to add a specific type of bagel to my basket. +- [x] #2 I'd like to remove a bagel from my basket. +- [x] #3 I'd like to know when my basket is full when I try adding an item beyond my basket capacity. +- [x] #4 I’d like to change the capacity of baskets. +- [x] #5 I'd like to know if I try to remove an item that doesn't exist in my basket. +- [x] #6 I'd like to know the total cost of items in my basket. +- [x] #7 I'd like to know the cost of a bagel before I add it to my basket.* +- [x] #8 I'd like to be able to choose fillings for my bagel. +- [x] #9 I'd like to know the cost of each filling before I add it to my bagel order.* +- [x] #10 I want customers to only be able to order things that we stock in our inventory.** + +\* The inventory have functionality to print out the menu, but there are no functions that follows abstraction like +'getAllBagels()' etc. There is only functionality to get the inventory Map/List that contains objects with getPrice() functions. + +** The inventory doesn't count how many of each item in stock, but customers can only choose things that are in the inventory. + +## Extension 1 + +### User Stories + +``` +1. +As a manager, +So the customers can benefit from our discounts, +I want the discounts for all items in basket to be calculated autoamtically. +``` + +### Comments +- Added the classes SpecailOffer, SpecialOfferCombination, SpecialOfferMultiPrice to 'package: inventory' +- Added the classes DiscountObjectCombination and DiscountObjectMultiPrice to 'package: calculators'. Because the discount is calculated in the PriceCalculator class. + +### Method/Calculation notes +``` +# Discounts + +if discount 6 for 2.55 + get normalPrice + normalPrice - discountPrice = discount + + get all items with SKU X + 6 // numOfItems = number of discounts + number of discounts * discount. + + result for this SKU = totalCost - totalDiscount + +if discount is Coffe + Bagel = 1.33 + get normalPrice + normalPrice - discountPrice = discount + + get all occurencies of product A + get all occurencies of product B + + if one or both is 0 -> no discount + if A = B -> discounts on all pairs + if A < B -> discount * the same number as A + and vice versa +``` + +## Extension 2 + +### User Stories + +``` +1. +As a customer, +So I can see that the order is correct, +I want to have a receipts what shows each item, the price, and how many of each item I ordered. +``` + +``` +2. +As a customer, +So I can see an overview of my order, +I want to see the total cost of my order the receipt. +``` + +``` +3. +As a customer, +So I can know when I placed my order, +I want to see the date and time for the order on the receipt. +``` + +### Comments +- Added printReceipt() in Basket +- Added PrintReceipt which extends PrintGenerator + +## Extension 3 +- Added printDiscountReceipt() in Basket +- Added PrintDiscountReceipt which extends PrintGenerator \ No newline at end of file diff --git a/src/main/java/com/booleanuk/core/basket/Bagel.java b/src/main/java/com/booleanuk/core/basket/Bagel.java new file mode 100644 index 000000000..739a73a73 --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/Bagel.java @@ -0,0 +1,30 @@ +package com.booleanuk.core.basket; + +import java.util.ArrayList; +import java.util.List; + +public class Bagel extends BasketItem { + + private List linkedFillingSKUs; + private List linkedFillingIds; // Id's for the fillings to this Bagel + + public Bagel(String SKU) { + super(SKU); + linkedFillingSKUs = new ArrayList<>(); + linkedFillingIds = new ArrayList<>(); + } + + public Bagel(String SKU, List linkedFillingsSKUs) { + super(SKU); + this.linkedFillingSKUs = linkedFillingsSKUs; + this.linkedFillingIds = new ArrayList<>(); + } + + public List getLinkedFillingSKUs() { + return linkedFillingSKUs; + } + + public List getLinkedFillingIds() { + return linkedFillingIds; + } +} diff --git a/src/main/java/com/booleanuk/core/basket/Basket.java b/src/main/java/com/booleanuk/core/basket/Basket.java new file mode 100644 index 000000000..65f7d6c66 --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/Basket.java @@ -0,0 +1,437 @@ +package com.booleanuk.core.basket; + +import com.booleanuk.core.calculators.DiscountObjectCombination; +import com.booleanuk.core.calculators.DiscountObjectMultiPrice; +import com.booleanuk.core.calculators.PriceCalculator; +import com.booleanuk.core.enums.ProductName; +import com.booleanuk.core.inventory.Inventory; +import com.booleanuk.core.inventory.InventoryItem; +import com.booleanuk.core.printgenerator.PrintBasketItems; +import com.booleanuk.core.printgenerator.PrintDiscountReceipt; +import com.booleanuk.core.printgenerator.PrintGenerator; +import com.booleanuk.core.printgenerator.PrintReceipt; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Basket { + + private Inventory inventory; + private Map basketItems; + private int idCount; + private int size; + private int maxCapacity; + + private PriceCalculator priceCalculator; + private PrintGenerator printGenerator; + + public Basket(Inventory inventory) { + this.inventory = inventory; + this.basketItems = new LinkedHashMap <>(); // LinkedHashMap because I want the items in the order they were added to the basket + this.idCount = 1; + this.size = 0; + this.maxCapacity = 20; + this.priceCalculator = new PriceCalculator(); // Should maybe have dependency injection instead. + } + + // Auto create ID + private int createId() { + + int itemId = this.idCount; + this.idCount += 1; + return itemId; + } + + // Auto create ID for fillings + private int createFillingId(String idExtension) { + + // TODO: How to make default variable like in python + + // TODO: Should check if idExtension is valid + + // TODO: Could use int input instead of String + + // Store variable id for previous bagel + int itemId = this.idCount - 1; + + // Add id extension for filling + String tmp = itemId + "0" + idExtension; + return Integer.parseInt(tmp); + } + + // Get Basket size + public int getSize() { + return size; + } + + // Get max capacity, max amount of products allowed in basket. + // Fillings doesn't count as an item, and fillings can only be added together with a Bagel. + public int getMaxCapacity() { + return this.maxCapacity; + } + + // Change max capacity + // TODO: Should make sure that newMaxCapacity is not a negative value + public void changeMaxCapacity(int newMaxCapacity) { + this.maxCapacity = newMaxCapacity; + } + + // Get all basket items + public Map getAll() { + return this.basketItems; + } + + // Get basket item based on id. + protected BasketItem getBasketItem(int itemId) { + BasketItem item = this.basketItems.get(itemId); + if (item == null) { + throw new InvalidBasketItemException("Basket item with ID #" + itemId + ", doesn't exist. Can't remove from basket."); + } + return item; + } + + // Inner function for add() + // Validates input + protected void addToBasket(int itemId, BasketItem item) { + // Validate input + if (this.getSize() == maxCapacity) { + throw new MaxCapacityException("Basket is full, can't add more items."); + } + + // TODO: Add exception for when id already exist + // TODO: Can use 'instanceof' instead of checking class name? + + // Fillings can not be added as an item itself + // Fillings that belongs to a bagel have id's over 100 + if (item.getClass().getName().equals(Filling.class.getName()) && item.getId() < 100) { + throw new InvalidBasketItemException("Fillings can't be added alone. Must belong to a bagel."); + } + + // Set id if no exception has been thrown + item.setId(itemId); + + this.basketItems.put(item.getId(), item); + + // Update size of basket + this.size++; + } + + // Add BasketItem (Coffee Bagel or Filling) to basket + public void add(BasketItem item) { + + try { + + // TODO: Duplication of id with different variables? Simplify? + // Could I use some inheritance/polymorphism? + + // Class names + String thisItemClass = item.getClass().getName(); + String BagelClass = Bagel.class.getName(); + + // Add fillings if it is a Bagel + if (thisItemClass.equals(BagelClass)) { + Bagel bagel = (Bagel) item; + List fillingSKUs = bagel.getLinkedFillingSKUs(); + + int bagelId = createId(); + this.addToBasket(bagelId, item); + item.setId(bagelId); + + // Add fillings and save the filling ids' to the bagel + if (!fillingSKUs.isEmpty()) { + List fillingIds = bagel.getLinkedFillingIds(); + + int count = 1; + for (String f_SKU : fillingSKUs) { + int fillingId = createFillingId(String.valueOf(count)); + + BasketItem filling = new Filling(f_SKU); + filling.setId(fillingId); + + this.addToBasket(fillingId, filling); + fillingIds.add(fillingId); + + count++; + } + } + } else { + int generalId = createId(); + this.addToBasket(generalId, item); + } + + } catch (Exception e) { + System.out.println(e.getClass().getName() + ": " + e.getMessage()); + } + } + + // Inner function for remove() + // Validates input + protected void removeFromBasket(int itemId) { + if (basketItems.get(itemId) == null) { + throw new InvalidBasketItemException("Basket item with ID #" + itemId + ", doesn't exist. Can't remove from basket."); + } + this.basketItems.remove(itemId); + + // Update size of basket + this.size--; + } + + // Remove item from basket based on id. + public void remove(int itemId) { + + try { + BasketItem item = this.getBasketItem(itemId); + + // Class names + String thisItemClass = item.getClass().getName(); + String BagelClass = Bagel.class.getName(); + + // Remove fillings if it is a Bagel + if (thisItemClass.equals(BagelClass)) { + + Bagel bagel = (Bagel) item; + List fillingIds = bagel.getLinkedFillingIds(); + + // Remove all fillings + for (int f_id : fillingIds) { + this.removeFromBasket(f_id); + } + } + this.removeFromBasket(itemId); + + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + // Get total cost of all items in basket + public double getTotalCost() { + // TODO Changed to double, this may be unnecessary now + + // TODO: Is it bad performance to loop like this? + // Should I instead keep track on the price everytime an item is added or removed? + + float total = 0; + for (BasketItem item : this.basketItems.values()) { + InventoryItem inventoryItem = inventory.getItem(item.getSKU()); + total += inventoryItem.getPrice(); + } + return priceCalculator.round(total, 2); + } + + // Print basket with items and total cost. + public void printBasket() { + + // TODO: Should I refactor? Feels like it's a poor solution regarding dependencies. + // Check all PrintGenerator cases. + + printGenerator = new PrintBasketItems(this.inventory, this.basketItems, this.getTotalCost()); + printGenerator.print(); + } + + + // TODO: Duplication of code, poor solution, should redo + public void printReceipt() { + + ArrayList itemsAndDiscounts = priceCalculator.calculateSpecialOfferMultiPrice( + this.inventory, + this.basketItems, + this.inventory.getSpecialOffersMultiPrice() + ); + ArrayList additionalDiscounts = priceCalculator.calculateSpecialOfferCombination( + this.inventory, + this.basketItems, + this.inventory.getSpecialOffersCombination() + ); + + + // Create a list of printable objects + // Calculates MultiPrice discounts + double totalCost = 0; + ArrayList printableListItems = new ArrayList<>(); + for (DiscountObjectMultiPrice item : itemsAndDiscounts) { + + // TODO: Refactor, unnecessary calculation + boolean hasDiscountItems = item.getNumOfDiscountItems() != 0; + boolean hasOrdinaryItems = item.getNumOfOrdinaryItems() != 0; + + InventoryItem inventoryItem = this.inventory.getItem(item.getSKU()); + String variant = inventoryItem.getVariant().toString(); + + String name = inventoryItem.getName().getString(); + String combinedName = name + " ("+variant.toLowerCase()+")"; + + int amount; + double price; + double discount = item.getDiscount(); + + if (hasDiscountItems) { + amount = item.getNumOfDiscountItems(); + price = item.getPriceForDiscountItems(); + + BasketItemFormatted formattedItem = new BasketItemFormatted( + combinedName, + amount, + price, + discount + ); + printableListItems.add(formattedItem); + totalCost += price; + } + + if (hasOrdinaryItems) { + amount = item.getNumOfOrdinaryItems(); + price = item.getPriceForOrdinaryItems(); + + BasketItemFormatted formattedItem = new BasketItemFormatted( + combinedName, + amount, + price, + discount + ); + printableListItems.add(formattedItem); + totalCost += price; + } + } + + // Add fillings as separate list, half hardcoded + ArrayList fillingsList = new ArrayList<>(); + for (BasketItem item : this.basketItems.values()) { + + InventoryItem inventoryItem = this.inventory.getItem(item.getSKU()); + String variant = inventoryItem.getVariant().toString(); + String name = inventoryItem.getName().getString(); + double price = inventoryItem.getPrice(); + + if (inventoryItem.getName() == ProductName.FILLING) { + + String combinedName = name + " ("+variant.toLowerCase()+")"; + + BasketItemFormatted formattedItem = new BasketItemFormatted( + combinedName, + 1, + price, + 0 + ); + } + totalCost += price; + } + + totalCost = priceCalculator.round((float) totalCost, 2); + + printGenerator = new PrintReceipt( + printableListItems, + totalCost + ); + + printGenerator.print(); + } + + public void printDiscountReceipt() { + + ArrayList itemsAndDiscounts = priceCalculator.calculateSpecialOfferMultiPrice( + this.inventory, + this.basketItems, + this.inventory.getSpecialOffersMultiPrice() + ); + ArrayList additionalDiscounts = priceCalculator.calculateSpecialOfferCombination( + this.inventory, + this.basketItems, + this.inventory.getSpecialOffersCombination() + ); + + + // Create a list of printable objects + // Calculates MultiPrice discounts + double totalCost = 0; + ArrayList printableListItems = new ArrayList<>(); + for (DiscountObjectMultiPrice item : itemsAndDiscounts) { + + // TODO: Refactor, unnecessary calculation + boolean hasDiscountItems = item.getNumOfDiscountItems() != 0; + boolean hasOrdinaryItems = item.getNumOfOrdinaryItems() != 0; + + InventoryItem inventoryItem = this.inventory.getItem(item.getSKU()); + String variant = inventoryItem.getVariant().toString(); + + String name = inventoryItem.getName().getString(); + String combinedName = name + " ("+variant.toLowerCase()+")"; + + int amount; + double price; + double discount; + + if (hasDiscountItems) { + amount = item.getNumOfDiscountItems(); + price = item.getPriceForDiscountItems(); + discount = item.getDiscount(); + + BasketItemFormatted formattedItem = new BasketItemFormatted( + combinedName, + amount, + price, + discount + ); + printableListItems.add(formattedItem); + totalCost += price; + } + + if (hasOrdinaryItems) { + amount = item.getNumOfOrdinaryItems(); + price = item.getPriceForOrdinaryItems(); + discount = 0.0; + + BasketItemFormatted formattedItem = new BasketItemFormatted( + combinedName, + amount, + price, + discount + ); + printableListItems.add(formattedItem); + totalCost += price; + } + } + + // Add fillings as separate list, half hardcoded + ArrayList fillingsList = new ArrayList<>(); + for (BasketItem item : this.basketItems.values()) { + + InventoryItem inventoryItem = this.inventory.getItem(item.getSKU()); + String variant = inventoryItem.getVariant().toString(); + String name = inventoryItem.getName().getString(); + double price = inventoryItem.getPrice(); + + if (inventoryItem.getName() == ProductName.FILLING) { + + String combinedName = name + " ("+variant.toLowerCase()+")"; + + BasketItemFormatted formattedItem = new BasketItemFormatted( + combinedName, + 1, + price, + 0 + ); + } + totalCost += price; + } + + totalCost = priceCalculator.round((float) totalCost, 2); + + // TODO: Add combination offer + // Calculate combination discounts +// int additionalDiscount = 0; +// for (DiscountObjectCombination item : additionalDiscounts) { +// additionalDiscount += item.getDiscountSum(); +// } +// totalCost = totalCost - additionalDiscount; + + printGenerator = new PrintDiscountReceipt( + printableListItems, + totalCost + ); + + printGenerator.print(); + } +} diff --git a/src/main/java/com/booleanuk/core/basket/BasketItem.java b/src/main/java/com/booleanuk/core/basket/BasketItem.java new file mode 100644 index 000000000..51bbdf50b --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/BasketItem.java @@ -0,0 +1,24 @@ +package com.booleanuk.core.basket; + +public class BasketItem { + + private int Id; + private String SKU; + + public BasketItem(String SKU) { + this.Id = -1; + this.SKU = SKU; + } + + protected void setId(int itemId){ + this.Id = itemId; + } + + public int getId() { + return Id; + } + + public String getSKU() { + return SKU; + } +} diff --git a/src/main/java/com/booleanuk/core/basket/BasketItemFormatted.java b/src/main/java/com/booleanuk/core/basket/BasketItemFormatted.java new file mode 100644 index 000000000..1b258ffdb --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/BasketItemFormatted.java @@ -0,0 +1,48 @@ +package com.booleanuk.core.basket; + +public class BasketItemFormatted { + + private String name; + private int amount; + private double price; + private double discount; + + public BasketItemFormatted(String name, int amount, double price, double discount) { + this.name = name; + this.amount = amount; + this.price = price; + this.discount = discount; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public double getDiscount() { + return discount; + } + + public void setDiscount(double discount) { + this.discount = discount; + } +} diff --git a/src/main/java/com/booleanuk/core/basket/Coffee.java b/src/main/java/com/booleanuk/core/basket/Coffee.java new file mode 100644 index 000000000..67117f8d6 --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/Coffee.java @@ -0,0 +1,8 @@ +package com.booleanuk.core.basket; + +public class Coffee extends BasketItem { + + public Coffee(String SKU) { + super(SKU); + } +} diff --git a/src/main/java/com/booleanuk/core/basket/Filling.java b/src/main/java/com/booleanuk/core/basket/Filling.java new file mode 100644 index 000000000..737ff6fe1 --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/Filling.java @@ -0,0 +1,8 @@ +package com.booleanuk.core.basket; + +public class Filling extends BasketItem { + + public Filling(String SKU) { + super(SKU); + } +} diff --git a/src/main/java/com/booleanuk/core/basket/InvalidBasketItemException.java b/src/main/java/com/booleanuk/core/basket/InvalidBasketItemException.java new file mode 100644 index 000000000..4544d196b --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/InvalidBasketItemException.java @@ -0,0 +1,7 @@ +package com.booleanuk.core.basket; + +public class InvalidBasketItemException extends RuntimeException { + public InvalidBasketItemException(String message) { + super(message); + } +} diff --git a/src/main/java/com/booleanuk/core/basket/MaxCapacityException.java b/src/main/java/com/booleanuk/core/basket/MaxCapacityException.java new file mode 100644 index 000000000..4bfe8810d --- /dev/null +++ b/src/main/java/com/booleanuk/core/basket/MaxCapacityException.java @@ -0,0 +1,7 @@ +package com.booleanuk.core.basket; + +public class MaxCapacityException extends RuntimeException { + public MaxCapacityException(String message) { + super(message); + } +} diff --git a/src/main/java/com/booleanuk/core/calculators/DiscountObjectCombination.java b/src/main/java/com/booleanuk/core/calculators/DiscountObjectCombination.java new file mode 100644 index 000000000..ef234c45a --- /dev/null +++ b/src/main/java/com/booleanuk/core/calculators/DiscountObjectCombination.java @@ -0,0 +1,42 @@ +package com.booleanuk.core.calculators; + +import com.booleanuk.core.enums.ProductName; + +import java.util.ArrayList; + +public class DiscountObjectCombination { + + private ArrayList offerItems; + private int numOfDiscounts; + private double discountSum; + + public DiscountObjectCombination(ArrayList offerItems, int numOfDiscounts, double discountSum) { + this.offerItems = offerItems; + this.numOfDiscounts = numOfDiscounts; + this.discountSum = discountSum; + } + + public ArrayList getOfferItems() { + return offerItems; + } + + public void setOfferItems(ArrayList offerItems) { + this.offerItems = offerItems; + } + + public int getNumOfDiscounts() { + return numOfDiscounts; + } + + public void setNumOfDiscounts(int numOfDiscounts) { + this.numOfDiscounts = numOfDiscounts; + } + + public double getDiscountSum() { + return discountSum; + } + + public void setDiscountSum(double discountSum) { + this.discountSum = discountSum; + } +} diff --git a/src/main/java/com/booleanuk/core/calculators/DiscountObjectMultiPrice.java b/src/main/java/com/booleanuk/core/calculators/DiscountObjectMultiPrice.java new file mode 100644 index 000000000..369367c4d --- /dev/null +++ b/src/main/java/com/booleanuk/core/calculators/DiscountObjectMultiPrice.java @@ -0,0 +1,89 @@ +package com.booleanuk.core.calculators; + +public class DiscountObjectMultiPrice { + + // TODO: Confusing to use this object for both items with discounts and items without discounts for a specific SKU + + private String SKU; + + private int numberOfDiscounts; + private int numOfDiscountItems; // Number of items that is counted into the discount. E.g. 6 for 2.49 + private double priceForDiscountItems; + private double discount; + + private int numOfOrdinaryItems; // Items without discountSum + private double priceForOrdinaryItems; + + public DiscountObjectMultiPrice( + String SKU, int numberOfDiscounts, + int numOfDiscountItems, + double priceForDiscountItems, + double discount, + int numOfOrdinaryItems, + double priceForOrdinaryItems) + { + this.SKU = SKU; + this.numberOfDiscounts = numberOfDiscounts; + this.numOfDiscountItems = numOfDiscountItems; + this.priceForDiscountItems = priceForDiscountItems; + this.discount = discount; + this.numOfOrdinaryItems = numOfOrdinaryItems; + this.priceForOrdinaryItems = priceForOrdinaryItems; + } + + public String getSKU() { + return SKU; + } + + public void setSKU(String SKU) { + this.SKU = SKU; + } + + public int getNumberOfDiscounts() { + return numberOfDiscounts; + } + + public void setNumberOfDiscounts(int numberOfDiscounts) { + this.numberOfDiscounts = numberOfDiscounts; + } + + public int getNumOfDiscountItems() { + return numOfDiscountItems; + } + + public void setNumOfDiscountItems(int numOfDiscountItems) { + this.numOfDiscountItems = numOfDiscountItems; + } + + public double getPriceForDiscountItems() { + return priceForDiscountItems; + } + + public void setPriceForDiscountItems(double priceForDiscountItems) { + this.priceForDiscountItems = priceForDiscountItems; + } + + public double getDiscount() { + return discount; + } + + public void setDiscount(double discount) { + this.discount = discount; + } + + public int getNumOfOrdinaryItems() { + return numOfOrdinaryItems; + } + + public void setNumOfOrdinaryItems(int numOfOrdinaryItems) { + this.numOfOrdinaryItems = numOfOrdinaryItems; + } + + public double getPriceForOrdinaryItems() { + return priceForOrdinaryItems; + } + + public void setPriceForOrdinaryItems(double priceForOrdinaryItems) { + this.priceForOrdinaryItems = priceForOrdinaryItems; + } +} diff --git a/src/main/java/com/booleanuk/core/calculators/PriceCalculator.java b/src/main/java/com/booleanuk/core/calculators/PriceCalculator.java new file mode 100644 index 000000000..19b816749 --- /dev/null +++ b/src/main/java/com/booleanuk/core/calculators/PriceCalculator.java @@ -0,0 +1,258 @@ +package com.booleanuk.core.calculators; + +import com.booleanuk.core.basket.BasketItem; +import com.booleanuk.core.enums.ProductName; +import com.booleanuk.core.inventory.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class PriceCalculator { + + // TODO: How to write a class comment? + // This class should calculate price. + // The aim is to fix so InventoryItem and basket have a common approach on rounding numbers + + // Use double or float, float saves memory, double is easier to work with + + private DiscountObjectMultiPrice discountObjectMultiPrice; + + public double round(float total, int numOfDecimals) { + + // TODO: Should I use float or double? + // change here or change on objects + // Now the object has float on price, and totalCost has double + + // Resource: https://www.baeldung.com/java-round-decimal-number + + double scale = Math.pow(10, numOfDecimals); + double rounded = Math.round(total * scale) / scale; + + return rounded; + } + + /** + * Calculate Multi-Price discounts and returns an ArrayList with + * all the items from the basket together with eventual discounts. + * @param inventory + * @param basketItems + * @param specialOffers + * @return + */ + public ArrayList calculateSpecialOfferMultiPrice( + Inventory inventory, Map basketItems, + ArrayList specialOffers) + { + ArrayList discountList = new ArrayList<>(); + + // Store list of SKU with number of occurrences (basket items). + HashMap skuOccurrences = new HashMap<>(); + for (BasketItem item : basketItems.values()) { + + String sku = item.getSKU(); + if (skuOccurrences.get(sku) == null) { + skuOccurrences.put(sku, 1); + } else { + int numOfItems = skuOccurrences.get(sku); + skuOccurrences.put(sku, numOfItems + 1); + } + } + + // Store Special Offers with the corresponding SKU as key in HashMap + HashMap skuSpecialOfferPairs = new HashMap<>(); + for (SpecialOfferMultiPrice offer : specialOffers) { + skuSpecialOfferPairs.put(offer.getSKU(), offer); + } + + // Calculate discounts + int numOfDiscounts; + int numOfDiscountItems; + int numOfOrdinaryItems; + double priceForDiscountItems; + double priceForOrdinaryItems; + double discountSum; + for (Map.Entry skuEntry : skuOccurrences.entrySet()) { + + String sku = skuEntry.getKey(); + + int numOfBasketItems = skuEntry.getValue(); + numOfOrdinaryItems = numOfBasketItems; // If there is no discount items, all basket items with this SKU are ordinary items + + // Get standard price for item with this SKU + double itemPrice = inventory.getItem(sku).getPrice(); + priceForOrdinaryItems = numOfOrdinaryItems * itemPrice; // If there is no discounts, this is the price for all items. + priceForOrdinaryItems = this.round((float) priceForOrdinaryItems, 2); + + if (skuSpecialOfferPairs.get(sku) != null) { + + int minimumNumOfItems = skuSpecialOfferPairs.get(sku).getNumOfItems(); // Minimun number of items required to get an offer + + // Count how many discounts + numOfDiscounts = Math.floorDiv(numOfBasketItems, minimumNumOfItems); + numOfDiscountItems = (numOfDiscounts * minimumNumOfItems); + numOfOrdinaryItems = numOfBasketItems - numOfDiscountItems; + + // Calculate the discount price by subtraction ordinary price by special offer price + double ordinaryPrice = minimumNumOfItems * itemPrice; + double specialOfferPrice = skuSpecialOfferPairs.get(sku).getOfferPrice(); + double diffPrice = ordinaryPrice - specialOfferPrice; + + // Calculate total discount for this offer + // add to discount + discountSum = numOfDiscounts * diffPrice; + discountSum = this.round((float) discountSum, 2); // TODO: Change fromm float to double + + priceForDiscountItems = numOfDiscounts * specialOfferPrice; + priceForOrdinaryItems = numOfOrdinaryItems * itemPrice; + + priceForDiscountItems = this.round((float) priceForDiscountItems, 2); + priceForOrdinaryItems = this.round((float) priceForOrdinaryItems, 2); + + discountList.add( + new DiscountObjectMultiPrice( + sku, + numOfDiscounts, + numOfDiscountItems, + priceForDiscountItems, + discountSum, + numOfOrdinaryItems, + priceForOrdinaryItems + )); + + } else { + discountList.add( + new DiscountObjectMultiPrice( + sku, + 0, + 0, + 0.0, + 0.0, + numOfOrdinaryItems, + priceForOrdinaryItems + )); + } + + + } + + return discountList; + } + + /** + * Calculates Combination discounts, and returns an ArrayList of discount objects with: + * A list of included product names of the items in the combination offer. E.g. 'COFFEE, BAGEL' + * the total discount price + * the number of discounts + * @param inventory + * @param basketItems + * @param specialOffers + * @return + */ + public ArrayList calculateSpecialOfferCombination( + Inventory inventory, Map basketItems, + ArrayList specialOffers) + { + ArrayList discountList = new ArrayList<>(); + + // TODO: Could this be simplified? + + // Calculate special offers for each offer. + // E.g. if there exist more than one offer like 'Coffee + Bagel for 1.25', 'Juice + cookie for 0.5' + for (SpecialOfferCombination offer : specialOffers) { + + ArrayList offerItems = offer.getOfferItems(); + + // Store an copy of an InventoryITem for each basket item, sorted into ProductNames (e.g. COFFEE, BAGEL), + // The ProductNames represents the combination of items that offer includes, e.g. 'Coffee + Bagel' offer. + HashMap> itemsSortedByProductName = new HashMap<>(); + for (ProductName productName : offerItems) { + for (BasketItem b : basketItems.values()) { + + InventoryItem inventoryItem = inventory.getItem(b.getSKU()); + + if (productName == inventoryItem.getName()){ + + if (itemsSortedByProductName.get(productName) == null) { + ArrayList list = new ArrayList<>(); + list.add(inventoryItem); + + itemsSortedByProductName.put(productName, list); + } else { + ArrayList list = itemsSortedByProductName.get(productName); + list.add(inventoryItem); + + itemsSortedByProductName.put(productName, list); + } + } + } + } + + // Find the list of least items, and save the size + // This is to find how many offers the user will get based on the amount of + // combinations that exists in the basket + int minItemSize = -1; + boolean isValidSpecialOffer = true; + for (ArrayList itemList : itemsSortedByProductName.values()) { + + // If one or more list of basket items (inventory objects) doesn't contain anything, + // no special offers available + if (itemList.isEmpty()) { + isValidSpecialOffer = false; + break; + + } else if (minItemSize == -1) { + minItemSize = itemList.size(); + } else if (itemList.size() < minItemSize) { + minItemSize = itemList.size(); + } + } + + // Sort lists by price + // I assume that the offer will be valid on the cheapest products first + // Therefore I sort the lists by price + // Resource: https://www.geeksforgeeks.org/how-to-sort-an-arraylist-of-objects-by-property-in-java/ + for (ArrayList itemList : itemsSortedByProductName.values()) { + itemList.sort((a, b) -> Double.compare(a.getPrice(), b.getPrice())); + } + + // Caclucate discount + if (isValidSpecialOffer) { + + int numOfDiscounts = 0; + double discount = 0.0; + double discountRounded = 0; + // Calculate number of discounts, and amount of discount + for (int i = 0; i < minItemSize; i++) { + + // Store the combination of items included in the offer + // Starting with the cheapest ones. + ArrayList combinationItems = new ArrayList<>(); + for (ArrayList itemList : itemsSortedByProductName.values()) { + + combinationItems.add(itemList.get(i)); + } + + double ordinaryPrice = 0; + for (InventoryItem item : combinationItems) { + ordinaryPrice += item.getPrice(); + } + double specialPrice = offer.getOfferPrice(); + double diffPrice = ordinaryPrice - specialPrice; + + // Update + numOfDiscounts++; + discount += diffPrice; + discountRounded = this.round((float) discount, 2); + } + + discountList.add(new DiscountObjectCombination(offerItems, numOfDiscounts, discountRounded)); + } + + } + + return discountList; + } +} diff --git a/src/main/java/com/booleanuk/core/calculators/SKUCalculator.java b/src/main/java/com/booleanuk/core/calculators/SKUCalculator.java new file mode 100644 index 000000000..a1136a7ab --- /dev/null +++ b/src/main/java/com/booleanuk/core/calculators/SKUCalculator.java @@ -0,0 +1,17 @@ +package com.booleanuk.core.calculators; + +public class SKUCalculator { + + /** + * Get SKU value based on name and variant of an item. + * @param name - E.g. ProductName.COFFEE + * @param variant - E.g. BagelVariant.PLAIN + * @return . String with SKU value. + */ + public String getSKU(Enum name, Enum variant) { + String productCode = name.toString().substring(0,3); + String variantCode = variant.toString().substring(0,1); + String skuCode = productCode + variantCode; + return skuCode.toUpperCase(); + } +} diff --git a/src/main/java/com/booleanuk/core/domain-model.md b/src/main/java/com/booleanuk/core/domain-model.md new file mode 100644 index 000000000..9e8b0a458 --- /dev/null +++ b/src/main/java/com/booleanuk/core/domain-model.md @@ -0,0 +1,130 @@ +# Domain Model + +## Package: enums +| Classes | Variables | Methods | Scenario | Output | +|------------------|-----------|---------|----------|--------| +| `BagelVariant` | | | | | +| `CoffeeVariant` | | | | | +| `FillingVariant` | | | | | +| `ProductName` | | | | | + +## Package: calculators +| Classes | Variables | Methods | Scenario | Output | +|-----------------------------|-------------------------------------|--------------------------------------|-------------------------------------------------------------------------------------------------------------------|--------| +| `DiscountObjectCombination` | `ArrayList offerItems` | `get/setOfferItems()` | (Extension 1) | | +| | `int numOfDiscounts` | `get/setNumOfDiscounts()` | (Extension 1) | | +| | `double discountSum` | `getDiscountSum()` | (Extension 1) | | +| | | | | | +| `DiscountObjectMultiPrice` | `String SKU` | `getSKU()` | (Extension 1) | | +| | `int numberOfDiscounts` | `getNumberOfDiscounts()` | (Extension 1) | | +| | `int numOfDiscountItems` | `getNumOfDiscountItems()` | (Extension 1)
Number of items for this SKU that is counted into the discount.
E.g. 6 items for '6 for 2.49' | | +| | `double discountSum` | `getDiscount()` | (Extension 1) | | +| | `int numOfOrdinaryItems` | `getNumOfOrdinaryItems` | (Extension 1)
Number of items for this SKU that doesn't have any discounts. | | +| | | | | | +| `PriceCalculator` | | `calculateSpecialOfferMultiPrice()` | (Extension 1) | | +| | | `calculateSpecialOfferCombination()` | (Extension 1) | | +| | | | | | +| `SKUCalculator` | | | | | + + +## Package: inventory +| Classes | Variables | Methods | Scenario | Output | +|-----------------------------------------------------|-----------------------------------------------|-----------------------------------------|-------------------------------------------------------|------------------------------| +| `Inventory` | `-Map inventoryItems ` | | | | +| | `-PrintGenerator menu` | | | | +| | | `-fillInventory()` | Initialize inventory with specified items. | | +| | | `+getAllItems()` | Get all inventory items. | Map | +| | | `+getItem(String SKU)` | If item is in inventory (valid SKU). | InventoryItem | +| | | | If item does not exist. | throw InventoryItemException | +| | | `+printMenu()` | Print menu with all items from the inventory. | Print to console/terminal | +| | | | | | +| `InventoryItem` | `-String SKU` | `+getSKU()` | | String | +| | | `#setSKU()` | Set SKU based on name and variant. | String | +| | `-float price` | `+getPrice()` | Get price calculated with PriceCalculator.round(). | double | +| | | `#setPrice(float price)` | Set price. | - | +| | `-ProductName name` | `+getName()` | Get product name. | ProductName | +| | | `#setName()` | Set product name. | ProductName | +| | `-Enum variant` | `+getVariant(Enum variant)` | Get product variant (DEFAULT, COFFE, BAGEL, FILLING). | Enum | +| | | `#setVariant()` | Set product variant. | - | +| | `-SKUCalculator skuCalculator` | | | | +| | `-PriceCalculator priceCalculator` | | | | +| | | | | | +| `CoffeeItem` extends `InventoryItem` | @Ovverride `+setName()` | | | ProductName | +| `BagelItem` extends `InventoryItem` | @Ovverride `+setName()` | | | ProductName | +| `FillingItem` extends `InventoryItem` | @Ovverride `+setName()` | | | ProductName | +| | | | | | +| `InventoryItemException` extends `RuntimeException` | | | | | +| | | | | | +| `SpecialOffer` | `double offerPrice` | | (Extension 1) | | +| `SpecialOfferCombination` extends `SpecialOffer` | `ArrayList offerItems` | | (Extension 1) | | +| `SpecialOfferMultiPrice` extends `SpecialOffer` | `String SKU` | | (Extension 1) | | +| | `int numOfItems` | | (Extension 1) | | + +## Package: basket +| Classes | Variables | Methods | Scenario | Output | +|--------------------------------|-----------------------------------------|---------------------------------------------|--------------------------------------------------------------------------------------------------------------------|--------------------------------------------------| +| `Basket(Inventory inventory)` | `-Inventory inventory` | | | | +| | `-Map basketItems` | `+getAll()` | Get all basket items. | Map | +| | | `#getBasketItem(int itemId)` | If item exist. | BasketItem | +| | | | If items doesn't exist. | throw InvalidBasketItemException | +| | | `#addToBasket(int itemId, BasketItem item)` | Inner function for add(), validates input. Add item to basket if possible. | | +| | | | If item can't be added, notify with error. | MaxCapacityException, InvalidBasketItemException | +| | | `+add(BasketItem item)` | Make a request to `#addToBasket(int itemId, BasketItem item)`. | - | +| | | | If exception is thrown, handle exception. | Print exception to console/terminal | +| | | `#removeFromBasket(int itemId)` | Inner function for remove(). If valid item id, remove item from basket. | | +| | | | if not valid item id, notify with error. | InvalidBasketItemException | +| | | `+remove(int itemId)` | Make a request to `#removeFromBasket(int itemId)`. Remove item if possible. | | +| | | | If exception is thrown, handle exception. | Print exception to console/terminal | +| | | `+getTotalCost()` | Get total cost of all items in basket. | double | +| | | `+printBasket()` | Print content of basket and total price. | Print to console/terminal | +| | `-int idCount` | `-createId()` | "Auto" creates Id for basket items except filling Ids. E.g. returns id 1. | int | +| | | `-createFillingId(String idExtension)` | "Auto" creates id for filling based on the bagels id. E.g. Bagel id: 1, filling id: 101. | int | +| | `-int size` | `+getSize()` | Get the counted size of basket
(fillings doesn't count as an item and can only be added together with a Bagel). | int | +| | `-int maxCapacity` | `+getMaxCapacity()` | Get max capacity of basket. | int | +| | | `+changeMaxCapacity(int newMaxCapacity)` | Change max capacity. | - | +| | `-PriceCalculator priceCalculator` | | | | +| | `-PrintGenerator basket` | | | | +| | | | | | +| `BasketItem(String SKU)` | `-int Id` | `+setId(int itemId)` | | | +| | | `+getId(int itemId)` | | int | +| | `-String SKU` | `+getSKU()` | | String | +| | | | | | +| `Coffee` extends `BasketItem` | | | | | +| `Bagel` extends `BasketItem` | `-List linkedFillingSKUs` | `+getLinkedFillingSKUs` | Get a list of inventory item SKU's that was added together with this bagel. | List | +| | `-List linkedFillingIds` | `getLinkedFillingIds` | Get a list of basket item ids' (the ids' of the fillings that belongs to this bagel). | List | +| `Filling` extends `BasketItem` | | | | | +| | | | | | +| `BasketItemFormatted` | | | (Extension 2 and 3) | | +| | | | | | +| `InvalidBasketItemException` | | | | | +| `MaxCapacityException` | | | | | + +## Package: printgenerator +| Classes | Variables | Methods | Scenario | Output | +|-------------------------------------------------|---------------------------------------------|-------------------------------------------------------------------------|-----------------------------------------|--------| +| `PrintGenerator` | | `printCenterTitle(String text, int totalWidth)` | Center test in console/terminal output. | | +| | ` | `print()` | | | +| | | | | | +| `PrintInventoryMenu` extends `PrintGenerator` | `Map inventoryItems` | `printMenuPart(ProductName productName, String title, int outputWidth)` | | | +| | | @Override `print()` | | | +| | | | | | +| `PrintBasketItems` extends `PrintGenerator` | `Inventory inventory` | | | | +| | `Map basketItems` | | | | +| | `double basketTotalCost` | | | | +| | | @Override `print()` | | | +| | | | | | +| `PrintReceipt` extends `PrintGenerator` | | | (Extension 2) | | +| `PrintDiscountReceipt` extends `PrintGenerator` | | | (Extension 3) | | + +# Class Diagram + + + +NOTE! Some classes are excluded from the class diagram, and just included in the domain model. + + ![Class Diagram](/assets/images/class_diagram_inventory.png) + ![Class Diagram](/assets/images/class_diagram_basket.png) + ![Class Diagram](/assets/images/class_diagram_printGenerator.png) + diff --git a/src/main/java/com/booleanuk/core/enums/BagelVariant.java b/src/main/java/com/booleanuk/core/enums/BagelVariant.java new file mode 100644 index 000000000..3db8a8357 --- /dev/null +++ b/src/main/java/com/booleanuk/core/enums/BagelVariant.java @@ -0,0 +1,8 @@ +package com.booleanuk.core.enums; + +public enum BagelVariant { + ONION, + PLAIN, + EVERYTHING, + SESAME, +} diff --git a/src/main/java/com/booleanuk/core/enums/CoffeeVariant.java b/src/main/java/com/booleanuk/core/enums/CoffeeVariant.java new file mode 100644 index 000000000..8cdb816a6 --- /dev/null +++ b/src/main/java/com/booleanuk/core/enums/CoffeeVariant.java @@ -0,0 +1,8 @@ +package com.booleanuk.core.enums; + +public enum CoffeeVariant { + BLACK, + WHITE, + CAPUCCINO, + LATTE, +} diff --git a/src/main/java/com/booleanuk/core/enums/FillingVariant.java b/src/main/java/com/booleanuk/core/enums/FillingVariant.java new file mode 100644 index 000000000..0770e32d6 --- /dev/null +++ b/src/main/java/com/booleanuk/core/enums/FillingVariant.java @@ -0,0 +1,10 @@ +package com.booleanuk.core.enums; + +public enum FillingVariant { + BACON, + EGG, + CHEESE, + CREAM_CHEESE, + SMOKED_SALMON, + HAM, +} diff --git a/src/main/java/com/booleanuk/core/enums/ProductName.java b/src/main/java/com/booleanuk/core/enums/ProductName.java new file mode 100644 index 000000000..fcba49d31 --- /dev/null +++ b/src/main/java/com/booleanuk/core/enums/ProductName.java @@ -0,0 +1,22 @@ +package com.booleanuk.core.enums; + +public enum ProductName { + + // Resource: https://www.youtube.com/watch?v=wq9SJb8VeyM + + DEFAULT("DEFAULT"), + COFFEE("Coffee"), + BAGEL("Bagel"), + FILLING("Filling"); + + private final String string; + + ProductName(String string) { + this.string = string; + } + + public String getString() { + return string; + } + +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/core/inventory/BagelItem.java b/src/main/java/com/booleanuk/core/inventory/BagelItem.java new file mode 100644 index 000000000..2ec89706a --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/BagelItem.java @@ -0,0 +1,15 @@ +package com.booleanuk.core.inventory; + +import com.booleanuk.core.enums.ProductName; + +public class BagelItem extends InventoryItem { + + public BagelItem(float price, Enum variant) { + super(price, variant); + } + + @Override + public ProductName setName() { + return ProductName.BAGEL; + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/CoffeeItem.java b/src/main/java/com/booleanuk/core/inventory/CoffeeItem.java new file mode 100644 index 000000000..52c641507 --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/CoffeeItem.java @@ -0,0 +1,15 @@ +package com.booleanuk.core.inventory; + +import com.booleanuk.core.enums.ProductName; + +public class CoffeeItem extends InventoryItem { + + public CoffeeItem(float price, Enum variant) { + super(price, variant); + } + + @Override + public ProductName setName() { + return ProductName.COFFEE; + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/FillingItem.java b/src/main/java/com/booleanuk/core/inventory/FillingItem.java new file mode 100644 index 000000000..6174016d4 --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/FillingItem.java @@ -0,0 +1,15 @@ +package com.booleanuk.core.inventory; + +import com.booleanuk.core.enums.ProductName; + +public class FillingItem extends InventoryItem{ + + public FillingItem(float price, Enum variant) { + super(price, variant); + } + + @Override + public ProductName setName() { + return ProductName.FILLING; + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/Inventory.java b/src/main/java/com/booleanuk/core/inventory/Inventory.java new file mode 100644 index 000000000..17b8b55fc --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/Inventory.java @@ -0,0 +1,134 @@ +package com.booleanuk.core.inventory; + +import com.booleanuk.core.enums.BagelVariant; +import com.booleanuk.core.enums.CoffeeVariant; +import com.booleanuk.core.enums.FillingVariant; +import com.booleanuk.core.enums.ProductName; +import com.booleanuk.core.printgenerator.PrintGenerator; +import com.booleanuk.core.printgenerator.PrintInventoryMenu; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class Inventory { + + private Map inventoryItems; + private ArrayList specialOffersMultiPrice; + private ArrayList specialOffersCombination; + // Initialized locally + private PrintGenerator menu; + + public Inventory() { + this.inventoryItems = new HashMap<>(); + fillInventory(); + createSpecialOffers(); + } + + /** + * Fill inventory with pre-prepared data. + */ + private void fillInventory() { + + // TODO: Is it okay to initialize the data here? + // For a more general approach -> Use Dependency Injection: + // make product list as input instead of creating it here. + + // TODO: A bit weird approach to put it in a list first and then loop. Is it a better way? + // I do this because I auto-generate the SKU value in the superclass 'InventoryItem'. + // So I create all objects (BagelItem, CoffeItem, FillingItem) first, and store in a temporary list + // Then I loop through all items to get the SKU value for each + // I put the SKU value as the key in the HashMap 'inventoryItems'. + // (The SKU value is then both in the objects and as a key) + + + // Create list with inventory items + ArrayList items = new ArrayList<>(); + + items.add(new BagelItem(0.49f, BagelVariant.ONION)); + items.add(new BagelItem(0.39f, BagelVariant.PLAIN)); + items.add(new BagelItem(0.49f, BagelVariant.EVERYTHING)); + items.add(new BagelItem(0.49f, BagelVariant.SESAME)); + + items.add(new CoffeeItem(0.99f, CoffeeVariant.BLACK)); + items.add(new CoffeeItem(1.19f, CoffeeVariant.WHITE)); + items.add(new CoffeeItem(1.29f, CoffeeVariant.CAPUCCINO)); + items.add(new CoffeeItem(1.29f, CoffeeVariant.LATTE)); + + items.add(new FillingItem(0.12f, FillingVariant.BACON)); + items.add(new FillingItem(0.12f, FillingVariant.EGG)); + items.add(new FillingItem(0.12f, FillingVariant.CHEESE)); + items.add(new FillingItem(0.12f, FillingVariant.CREAM_CHEESE)); + items.add(new FillingItem(0.12f, FillingVariant.SMOKED_SALMON)); + items.add(new FillingItem(0.12f, FillingVariant.HAM)); + + // Add items to inventoryItems and put the SKU value as the key + for (InventoryItem item : items) { + inventoryItems.put(item.getSKU(), item); + } + } + + // Create special offers + private void createSpecialOffers() { + + specialOffersMultiPrice = new ArrayList<>(); + specialOffersCombination = new ArrayList<>(); + + // Special offers Multi-Price + SpecialOfferMultiPrice BAGOsixMultiPriceOffer = new SpecialOfferMultiPrice("BAGO", 6,2.49); + SpecialOfferMultiPrice BAGEsixMultiPriceOffer = new SpecialOfferMultiPrice("BAGE",6,2.49); + SpecialOfferMultiPrice BAGPtwelveMultiPriceOffer = new SpecialOfferMultiPrice("BAGP", 12,3.99); + this.specialOffersMultiPrice.add(BAGOsixMultiPriceOffer); + this.specialOffersMultiPrice.add(BAGEsixMultiPriceOffer); + this.specialOffersMultiPrice.add(BAGPtwelveMultiPriceOffer); + + // Special offers Combination + ArrayList offerItems = new ArrayList<>() {{ + add(ProductName.COFFEE); + add(ProductName.BAGEL); + }}; + SpecialOfferCombination coffeAndBagelOffer = new SpecialOfferCombination(offerItems, 1.25); + this.specialOffersCombination.add(coffeAndBagelOffer); + } + + public Map getAllItems() { + return inventoryItems; + } + + // Get all special offers + // TODO: Check this structure, dublication code +// public ArrayList getSpecialOffers(SpecialOffer type) { +// if (type instanceof SpecialOfferMultiPrice) { +// return specialOffersMultiPrice; +// } +// return specialOffersCombination; +// } + + public ArrayList getSpecialOffersMultiPrice() { + return specialOffersMultiPrice; + } + + public ArrayList getSpecialOffersCombination() { + return specialOffersCombination; + } + + public InventoryItem getItem(String SKU) { + + // TODO: Should SKU be converted to uppercase here? + + InventoryItem item = inventoryItems.get(SKU); + if (item == null) { + throw new InventoryItemException("SKU '" + SKU + "' does not exist."); + } + return item; + } + + public void printMenu() { + + // TODO: Should this be refactored, not initialize it here + // Problem if so is that I need to pass in the inventoryItems + + menu = new PrintInventoryMenu(this.inventoryItems); + menu.print(); + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/InventoryItem.java b/src/main/java/com/booleanuk/core/inventory/InventoryItem.java new file mode 100644 index 000000000..ebb48364d --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/InventoryItem.java @@ -0,0 +1,65 @@ +package com.booleanuk.core.inventory; + +import com.booleanuk.core.calculators.PriceCalculator; +import com.booleanuk.core.enums.ProductName; +import com.booleanuk.core.calculators.SKUCalculator; + +public class InventoryItem { + + // TODO: should I change this to protected? + // TODO should I change this to abstract? Check if possible. + // TODO: Change 'float price' to double? Or keep float as it takes up less memory? + + private final String SKU; + private float price; + private final ProductName name; + private Enum variant; + + private final SKUCalculator skuCalculator; + private final PriceCalculator priceCalculator; + + public InventoryItem(float price, Enum variant) { + + this.price = price; + this.name = setName(); + this.variant = variant; + + this.skuCalculator = new SKUCalculator(); + this.SKU = setSKU(); + + this.priceCalculator = new PriceCalculator(); + + } + + public String getSKU() { + return SKU; + } + + protected String setSKU() { + return skuCalculator.getSKU(name, variant); + } + + public double getPrice() { + return priceCalculator.round(price, 2); + } + + protected void setPrice(float price) { + this.price = price; + } + + public ProductName getName() { + return name; + } + + protected ProductName setName() { + return ProductName.DEFAULT; + } + + public Enum getVariant() { + return variant; + } + + protected void setVariant(Enum variant) { + this.variant = variant; + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/InventoryItemException.java b/src/main/java/com/booleanuk/core/inventory/InventoryItemException.java new file mode 100644 index 000000000..42a2b44d5 --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/InventoryItemException.java @@ -0,0 +1,7 @@ +package com.booleanuk.core.inventory; + +public class InventoryItemException extends RuntimeException { + public InventoryItemException(String message) { + super(message); + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/SpecialOffer.java b/src/main/java/com/booleanuk/core/inventory/SpecialOffer.java new file mode 100644 index 000000000..cad04c249 --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/SpecialOffer.java @@ -0,0 +1,18 @@ +package com.booleanuk.core.inventory; + +public abstract class SpecialOffer { + + private double offerPrice; + + public SpecialOffer(double offerPrice) { + this.offerPrice = offerPrice; + } + + public double getOfferPrice() { + return offerPrice; + } + + public void setOfferPrice(double offerPrice) { + this.offerPrice = offerPrice; + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/SpecialOfferCombination.java b/src/main/java/com/booleanuk/core/inventory/SpecialOfferCombination.java new file mode 100644 index 000000000..3917b457e --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/SpecialOfferCombination.java @@ -0,0 +1,27 @@ +package com.booleanuk.core.inventory; + +import com.booleanuk.core.enums.ProductName; + +import java.util.ArrayList; + +public class SpecialOfferCombination extends SpecialOffer{ + + private ArrayList offerItems; // List of items that in combination generates an offer + + public SpecialOfferCombination(double offerPrice) { + super(offerPrice); + } + + public SpecialOfferCombination(ArrayList offerItems, double offerPrice) { + super(offerPrice); + this.offerItems = offerItems; + } + + public ArrayList getOfferItems() { + return offerItems; + } + + public void setOfferItems(ArrayList offerItems) { + this.offerItems = offerItems; + } +} diff --git a/src/main/java/com/booleanuk/core/inventory/SpecialOfferMultiPrice.java b/src/main/java/com/booleanuk/core/inventory/SpecialOfferMultiPrice.java new file mode 100644 index 000000000..2ddbacf87 --- /dev/null +++ b/src/main/java/com/booleanuk/core/inventory/SpecialOfferMultiPrice.java @@ -0,0 +1,34 @@ +package com.booleanuk.core.inventory; + +public class SpecialOfferMultiPrice extends SpecialOffer{ + + private String SKU; + private int numOfItems; + + public SpecialOfferMultiPrice(String SKU, double offerPrice) { + super(offerPrice); + this.SKU = SKU; + } + + public SpecialOfferMultiPrice(String SKU, int numOfItems, double offerPrice) { + super(offerPrice); + this.SKU = SKU; + this.numOfItems = numOfItems; + } + + public String getSKU() { + return SKU; + } + + public void setSKU(String SKU) { + this.SKU = SKU; + } + + public int getNumOfItems() { + return numOfItems; + } + + public void setNumOfItems(int numOfItems) { + this.numOfItems = numOfItems; + } +} diff --git a/src/main/java/com/booleanuk/core/printgenerator/PrintBasketItems.java b/src/main/java/com/booleanuk/core/printgenerator/PrintBasketItems.java new file mode 100644 index 000000000..c82a5eb05 --- /dev/null +++ b/src/main/java/com/booleanuk/core/printgenerator/PrintBasketItems.java @@ -0,0 +1,66 @@ +package com.booleanuk.core.printgenerator; + +import com.booleanuk.core.basket.BasketItem; +import com.booleanuk.core.inventory.Inventory; +import com.booleanuk.core.inventory.InventoryItem; + +import java.util.Map; + +public class PrintBasketItems extends PrintGenerator { + + private Inventory inventory; + private Map basketItems; + private double basketTotalCost; + + public PrintBasketItems(Inventory inventory, Map basketItems, double basketTotalCost) { + this.inventory = inventory; + this.basketItems = basketItems; + this.basketTotalCost = basketTotalCost; + } + + @Override + public void print() { + + int outputWidth = 47; + + // Variables for e.g. "%-15s %-15s %n", keep blank space + String leftAlignSmall = "%-7s "; + String leftAlign = "%-15s "; + String newLine = "%n"; + String divider = "-----------------------------------------------"; + + System.out.println(); + printCenterTitle("=== Bob's Bagels Menu ===", outputWidth); + printCenterTitle("~ Basket ~", outputWidth); + System.out.println(); + + // Print items in basket + if (basketItems.isEmpty()) { + System.out.println("\tBasket is empty."); + } else { + System.out.printf( + leftAlignSmall + leftAlignSmall + leftAlignSmall + leftAlign + leftAlign + newLine, + "SKU | ", "ID", "Product", "Variant", "Price" + ); + System.out.println(divider); + for (Map.Entry item : basketItems.entrySet()) { + + int key = item.getKey(); + BasketItem basketItem = item.getValue(); + + InventoryItem product = this.inventory.getItem(basketItem.getSKU()); + System.out.printf( + leftAlignSmall + leftAlignSmall + leftAlignSmall + leftAlign + leftAlign + newLine, + product.getSKU()+" | ", key, product.getName(), product.getVariant().toString(), "$" + product.getPrice() + ); + } + System.out.println(divider); + System.out.printf( + "%s %33s" + newLine, + "Total cost: ", "$"+basketTotalCost + ); + } + System.out.println(divider); + System.out.println(); + } +} diff --git a/src/main/java/com/booleanuk/core/printgenerator/PrintDiscountReceipt.java b/src/main/java/com/booleanuk/core/printgenerator/PrintDiscountReceipt.java new file mode 100644 index 000000000..314b29daf --- /dev/null +++ b/src/main/java/com/booleanuk/core/printgenerator/PrintDiscountReceipt.java @@ -0,0 +1,76 @@ +package com.booleanuk.core.printgenerator; + +import com.booleanuk.core.basket.BasketItemFormatted; +import com.booleanuk.core.calculators.DiscountObjectMultiPrice; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +public class PrintDiscountReceipt extends PrintGenerator{ + + private String dateCreated; + private ArrayList pritableItemsList; + private double totalCost; + + public PrintDiscountReceipt(ArrayList pritableItemsList, double totalCost) { + this.dateCreated = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + this.pritableItemsList = pritableItemsList; + this.totalCost = totalCost; + } + + @Override + public void print() { + + // TODO: not good to let a print function calculate, should be refactored + double totalDiscount = 0; + + // Variables for e.g. "%-15s %-15s %n", keep blank space + String leftAlignSmall = "%7s "; + String leftAlign = "%-23s "; + String newLine = "%n"; + String divider = "----------------------------------------"; + int totalwidth = 40; + + System.out.println(); + printCenterTitle("~~~Bob's Bagels ~~~", totalwidth); + System.out.println(); + printCenterTitle(dateCreated.toString(), totalwidth); + System.out.println(); + System.out.println(divider); + System.out.println(); + + double total = 0.0; + for (BasketItemFormatted item : pritableItemsList) { + total += item.getDiscount(); // TODO: Should not calculate here, should refactor + + System.out.printf( + leftAlign + leftAlignSmall + leftAlignSmall + newLine, + item.getName(), item.getAmount(), "£"+item.getPrice() + ); + + if (item.getDiscount() != 0.0) { + System.out.printf( + leftAlign + leftAlignSmall + leftAlignSmall + newLine, + "", "", "(-£"+item.getDiscount()+")" + ); + } + } + + System.out.println("\n"+divider); + System.out.printf( + "%s %26s" + newLine, + "Total cost: ", "£"+totalCost + ); + + System.out.println(); + printCenterTitle("You saved a total of £"+total, totalwidth); + printCenterTitle("on this shop!", totalwidth); + System.out.println(); + + System.out.println(); + printCenterTitle("Thank you", totalwidth); + printCenterTitle("for your order!", totalwidth); + System.out.println(); + } +} diff --git a/src/main/java/com/booleanuk/core/printgenerator/PrintGenerator.java b/src/main/java/com/booleanuk/core/printgenerator/PrintGenerator.java new file mode 100644 index 000000000..456049a51 --- /dev/null +++ b/src/main/java/com/booleanuk/core/printgenerator/PrintGenerator.java @@ -0,0 +1,22 @@ +package com.booleanuk.core.printgenerator; + +public class PrintGenerator { + + /** + * Prints centered text + * @param text - Text + * @param totalWidth - Total width of the output / layout + */ + protected void printCenterTitle(String text, int totalWidth) { + + // Resource: https://www.baeldung.com/java-center-text-output + String padding = "%" + ((totalWidth - text.length()) / 2) + "s"; + String centeredText = String.format(padding + "%s" + padding, "", text, ""); + System.out.println(centeredText); + } + + public void print() { + System.out.println("Hello World!"); + } + +} diff --git a/src/main/java/com/booleanuk/core/printgenerator/PrintInventoryMenu.java b/src/main/java/com/booleanuk/core/printgenerator/PrintInventoryMenu.java new file mode 100644 index 000000000..59ab73272 --- /dev/null +++ b/src/main/java/com/booleanuk/core/printgenerator/PrintInventoryMenu.java @@ -0,0 +1,52 @@ +package com.booleanuk.core.printgenerator; + +import com.booleanuk.core.enums.ProductName; +import com.booleanuk.core.inventory.InventoryItem; + +import java.util.Map; + +public class PrintInventoryMenu extends PrintGenerator{ + + private Map inventoryItems; + + public PrintInventoryMenu(Map inventoryItems) { + this.inventoryItems = inventoryItems; + } + + private void printMenuPart(ProductName productName, String title, int outputWidth) { + + // Variables for e.g. "%-15s %-15s %n", keep blank space + String center = "%16s "; + String skuAlign = "%-8s "; + String leftAlign = "%-16s "; + String newLine = "%n"; + String divider = "-------------------------------"; + + printCenterTitle(title, outputWidth); + + for (InventoryItem item : inventoryItems.values()) { + if (item.getName() == productName) { + System.out.printf( + skuAlign + leftAlign + leftAlign + newLine, + item.getSKU()+" | ", item.getVariant().toString(), "$" + item.getPrice() + ); + } + } + System.out.println(); + } + + @Override + public void print() { + + int outputWidth = 32; + + System.out.println(); + printCenterTitle("=== Bob's Bagels Menu ===", outputWidth); + System.out.println(); + + printMenuPart(ProductName.COFFEE, "~ Coffee ~", outputWidth); + printMenuPart(ProductName.BAGEL, "~ Bagels ~", outputWidth); + printMenuPart(ProductName.FILLING, "~ Bagel Fillings ~", outputWidth); + } + +} diff --git a/src/main/java/com/booleanuk/core/printgenerator/PrintReceipt.java b/src/main/java/com/booleanuk/core/printgenerator/PrintReceipt.java new file mode 100644 index 000000000..1fd8ca73e --- /dev/null +++ b/src/main/java/com/booleanuk/core/printgenerator/PrintReceipt.java @@ -0,0 +1,65 @@ +package com.booleanuk.core.printgenerator; + +import com.booleanuk.core.basket.BasketItemFormatted; +import com.booleanuk.core.calculators.DiscountObjectCombination; +import com.booleanuk.core.calculators.DiscountObjectMultiPrice; +import com.booleanuk.core.inventory.Inventory; +import com.booleanuk.core.inventory.InventoryItem; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +public class PrintReceipt extends PrintGenerator{ + + private String dateCreated; + private ArrayList pritableItemsList; + private double totalCost; + + public PrintReceipt(ArrayList pritableItemsList, double totalCost) { + this.dateCreated = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + this.pritableItemsList = pritableItemsList; + this.totalCost = totalCost; + } + + @Override + public void print() { + + // TODO: not good to let a print function calculate, should be refactored + double totalDiscount = 0; + + // Variables for e.g. "%-15s %-15s %n", keep blank space + String leftAlignSmall = "%7s "; + String leftAlign = "%-23s "; + String newLine = "%n"; + String divider = "----------------------------------------"; + int totalwidth = 40; + + System.out.println(); + printCenterTitle("~~~Bob's Bagels ~~~", totalwidth); + System.out.println(); + printCenterTitle(dateCreated.toString(), totalwidth); + System.out.println(); + System.out.println(divider); + System.out.println(); + + for (BasketItemFormatted item : pritableItemsList) { + + System.out.printf( + leftAlign + leftAlignSmall + leftAlignSmall + newLine, + item.getName(), item.getAmount(), "£"+item.getPrice() + ); + } + + System.out.println("\n"+divider); + System.out.printf( + "%s %20s" + newLine, + "Total cost: ", "£"+totalCost + ); + System.out.println(); + printCenterTitle("Thank you", totalwidth); + printCenterTitle("for your order!", totalwidth); + System.out.println(); + + } +} diff --git a/src/test/java/com/booleanuk/core/basket/BasketTest.java b/src/test/java/com/booleanuk/core/basket/BasketTest.java new file mode 100644 index 000000000..5b3b12b4a --- /dev/null +++ b/src/test/java/com/booleanuk/core/basket/BasketTest.java @@ -0,0 +1,235 @@ +package com.booleanuk.core.basket; + + +import com.booleanuk.core.inventory.Inventory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class BasketTest { + + Inventory inventory; + Basket basket; + + // Print exception method function + // TODO: Duplication from InventoryTest + // TODO: Make this code to own class and use for output in exceptions, maybe color it red, or maybe make an interface to the Exception class + public void printExceptionMessageToConsole(Exception e) { + System.out.println("\nException message:"); + System.out.println("\t" + e.getMessage() + "\n"); + } + + @Test + public void printBasket() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + basket.printBasket(); + } + + // User story #1: Add specific bagel + // Specific type is here to select SKU for specific bagel variant + @Test + public void addItemaToBasket() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + basket.add(new Coffee("COFC")); + basket.add(new Bagel("BAGE")); + basket.add(new Filling("FILB")); + + // Fillings should not be able to be added without a bagel + Assertions.assertEquals(2, basket.getAll().size()); + Assertions.assertEquals(2, basket.getSize()); + + basket.printBasket(); + } + + + // TODO: Add test for adding something that do not exist + // This is an inventory error, maybe add the test there. + + + // User story #3: Throw exception when trying to add items and maxCapacity of basket is reached + @Test + public void exceedMaxCapacityShouldThrowException() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + basket.changeMaxCapacity(2); + basket.add(new Coffee("COFC")); + basket.add(new Bagel("BAGE")); + + MaxCapacityException e = Assertions.assertThrows( + MaxCapacityException.class, + () -> { basket.addToBasket(11, new Bagel("BAGE")); } + ); + Assertions.assertEquals("Basket is full, can't add more items.", e.getMessage()); + printExceptionMessageToConsole(e); + } + + @Test + public void exceedMaxCapacityShouldHandleException() { + + // TODO: Check format on this exception + + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + basket.changeMaxCapacity(2); + basket.add(new Coffee("COFC")); + basket.add(new Bagel("BAGE")); + + // Check if basket.add() handles error correct (no exception) + Assertions.assertDoesNotThrow(() -> basket.add(new Bagel("BAGE"))); + } + + // User story #4: Change max capacity for basket + @Test + public void changeMaxCapacity() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + basket.changeMaxCapacity(3); + Assertions.assertEquals(3, basket.getMaxCapacity()); + + basket.changeMaxCapacity(12); + Assertions.assertEquals(12, basket.getMaxCapacity()); + } + + // User story #8: Add bagel with filling + @Test + public void addBagelAndFilling() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + basket.printBasket(); + + Assertions.assertEquals("BAGE", basket.getAll().get(1).getSKU()); + Assertions.assertEquals("FILS", basket.getAll().get(101).getSKU()); + } + + // User story #2: Remove a bagel + @Test + public void removeBagelShouldAlsoRemoveFillings() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + basket.printBasket(); + + // TODO: Add test + basket.remove(1); + Assertions.assertNull(basket.getAll().get(1)); + Assertions.assertNull(basket.getAll().get(101)); + basket.printBasket(); + } + + // User story #5: Throw exception when trying to remove item that doesn't exist. + @Test + public void removeItemThatDoesNotExistShouldThrowException() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + // TODO: I need to set removeFromBasket() to 'protected' instead of 'private', is it ok? + // It's because I can't test private methods. + // And I want to test this exception + // If I just test the "parent method" remove() it will not throw an exception + // because I handle the exception with try/catch in remove() + // remove() then calls removeFromBasket() which is the function that can throw exceptions + InvalidBasketItemException e = Assertions.assertThrows( + InvalidBasketItemException.class, + () -> { basket.removeFromBasket(11); } + ); + Assertions.assertEquals("Basket item with ID #11, doesn't exist. Can't remove from basket.", e.getMessage()); + printExceptionMessageToConsole(e); + } + + @Test + public void removeItemThatDoesNotExistShouldHandleException() { + + // TODO: Check format on this exception + + inventory = new Inventory(); + basket = new Basket(new Inventory()); + + // Check if basket.add() handles error correct (no exception) + Assertions.assertDoesNotThrow(() -> basket.remove(12)); + } + + // User story #6: Get total cost of basket + @Test + public void getTotalCostOfBasket() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + Assertions.assertEquals(0, basket.getTotalCost()); + + basket.add(new Coffee("COFW")); + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + + // COFW, 1.19 + // BAGE, 0.49 + // FILS, 0.12 + // FILB, 0.12 + // = 1.92 + Assertions.assertEquals(1.92, basket.getTotalCost()); + } + + // Extension 2 + @Test + public void printReceipt() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + basket.changeMaxCapacity(40); + + for (int i = 0; i < 2; i++) { + basket.add(new Bagel("BAGO")); + } + + for (int i = 0; i < 12; i++) { + basket.add(new Bagel("BAGP")); + } + + for (int i = 0; i < 6; i++) { + basket.add(new Bagel("BAGE")); + } + + for (int i = 0; i < 3; i++) { + basket.add(new Bagel("COFB")); + } + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + + basket.printReceipt(); + + // NOTE Tests are in priceCalculator + } + + // Extension 3 + @Test + public void printDiscountReceipt() { + inventory = new Inventory(); + basket = new Basket(new Inventory()); + basket.changeMaxCapacity(40); + + for (int i = 0; i < 2; i++) { + basket.add(new Bagel("BAGO")); + } + + for (int i = 0; i < 12; i++) { + basket.add(new Bagel("BAGP")); + } + + for (int i = 0; i < 6; i++) { + basket.add(new Bagel("BAGE")); + } + + for (int i = 0; i < 3; i++) { + basket.add(new Bagel("COFB")); + } + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + basket.printDiscountReceipt(); + + // NOTE Tests are in priceCalculator + } +} diff --git a/src/test/java/com/booleanuk/core/calculators/PriceCalculatorTest.java b/src/test/java/com/booleanuk/core/calculators/PriceCalculatorTest.java new file mode 100644 index 000000000..0d7251ba3 --- /dev/null +++ b/src/test/java/com/booleanuk/core/calculators/PriceCalculatorTest.java @@ -0,0 +1,213 @@ +package com.booleanuk.core.calculators; + +import com.booleanuk.core.basket.Bagel; +import com.booleanuk.core.basket.Basket; +import com.booleanuk.core.basket.BasketItem; +import com.booleanuk.core.basket.Coffee; +import com.booleanuk.core.inventory.Inventory; +import com.booleanuk.core.inventory.SpecialOffer; +import com.booleanuk.core.inventory.SpecialOfferCombination; +import com.booleanuk.core.inventory.SpecialOfferMultiPrice; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + +public class PriceCalculatorTest { + PriceCalculator priceCalculator; + Inventory inventory; + Basket basket; + + /** + * EXTENSION 1 + */ + @Test + public void calculateBagelOnionDicountShouldGetDiscount() { + inventory = new Inventory(); + ArrayList specialOffers = inventory.getSpecialOffersMultiPrice(); + + basket = new Basket(new Inventory()); + for (int i = 0; i < 6; i++) { + basket.add(new Bagel("BAGO")); + } + for (int i = 0; i < 12; i++) { + basket.add(new Bagel("BAGP")); + } + Map basketItems = basket.getAll(); + + // TODO : Looks confusing with dependecies and names + priceCalculator = new PriceCalculator(); + ArrayList discountList = priceCalculator.calculateSpecialOfferMultiPrice( + inventory, basketItems, specialOffers); + + // Print values + for (DiscountObjectMultiPrice d : discountList) { + System.out.println("SKU: "+d.getSKU() + + ", x"+d.getNumOfDiscountItems()+" items gives" + + " x"+d.getNumberOfDiscounts()+" discounts" + + ", ordinary items x"+d.getNumOfOrdinaryItems() + + ", discount: "+d.getDiscount()); + } + + // Standard price 6 * 0.49 = 2.94 + // Discount 2.94 - 2.49 = 0.45 + Assertions.assertEquals(1, discountList.get(1).getNumberOfDiscounts()); + Assertions.assertEquals(6, discountList.get(1).getNumOfDiscountItems()); + Assertions.assertEquals(0, discountList.get(1).getNumOfOrdinaryItems()); + Assertions.assertEquals(0.45, discountList.get(1).getDiscount()); + + // Standard price 12 * 0.39 = 4.68 + // Discount 4.68 - 3.99 = 0.69 + Assertions.assertEquals(1, discountList.get(0).getNumberOfDiscounts()); + Assertions.assertEquals(12, discountList.get(0).getNumOfDiscountItems()); + Assertions.assertEquals(0, discountList.get(0).getNumOfOrdinaryItems()); + Assertions.assertEquals(0.69, discountList.get(0).getDiscount()); + } + + @Test + public void calculateBagelOnionDicountShouldNotGetDiscountForFiveBagels() { + inventory = new Inventory(); + ArrayList specialOffers = inventory.getSpecialOffersMultiPrice(); + + basket = new Basket(new Inventory()); + for (int i = 0; i < 5; i++) { + basket.add(new Bagel("BAGO")); + } + Map basketItems = basket.getAll(); + + // Setup calculation and print result + priceCalculator = new PriceCalculator(); + ArrayList discountList = priceCalculator.calculateSpecialOfferMultiPrice( + inventory, basketItems, specialOffers); + + for (DiscountObjectMultiPrice d : discountList) { + System.out.println("SKU: "+d.getSKU() + + ", x"+d.getNumOfDiscountItems()+" items gives" + + " x"+d.getNumberOfDiscounts()+" discounts" + + ", ordinary items x"+d.getNumOfOrdinaryItems() + + ", discount: "+d.getDiscount()); + } + + Assertions.assertEquals(0, discountList.get(0).getNumberOfDiscounts()); + Assertions.assertEquals(0, discountList.get(0).getNumOfDiscountItems()); + Assertions.assertEquals(5, discountList.get(0).getNumOfOrdinaryItems()); + Assertions.assertEquals(0.0, discountList.get(0).getDiscount()); + } + + @Test + public void calculateBagelOnionDicountShouldGetDiscountOnlyForSixBagels() { + inventory = new Inventory(); + ArrayList specialOffers = inventory.getSpecialOffersMultiPrice(); + + basket = new Basket(new Inventory()); + for (int i = 0; i < 7; i++) { + basket.add(new Bagel("BAGO")); + } + Map basketItems = basket.getAll(); + + // Setup calculation and print result + priceCalculator = new PriceCalculator(); + ArrayList discountList = priceCalculator.calculateSpecialOfferMultiPrice( + inventory, basketItems, specialOffers); + + for (DiscountObjectMultiPrice d : discountList) { + System.out.println("SKU: "+d.getSKU() + + ", x"+d.getNumOfDiscountItems()+" items gives" + + " x"+d.getNumberOfDiscounts()+" discounts" + + ", ordinary items x"+d.getNumOfOrdinaryItems() + + ", discount: "+d.getDiscount()); + } + + // Standard price 6 * 0.49 = 2.94 + // Discount 2.94 - 2.49 = 0.45 + Assertions.assertEquals(1, discountList.get(0).getNumberOfDiscounts()); + Assertions.assertEquals(6, discountList.get(0).getNumOfDiscountItems()); + Assertions.assertEquals(1, discountList.get(0).getNumOfOrdinaryItems()); + Assertions.assertEquals(0.45, discountList.get(0).getDiscount()); + } + + + // TODO: Test fillings + + @Test + public void calculateCombinationDiscountShouldReturnDiscount() { + inventory = new Inventory(); + ArrayList specialOffers = inventory.getSpecialOffersCombination(); + + basket = new Basket(new Inventory()); + basket.add(new Coffee("COFB")); + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + basket.add(new Bagel("BAGP")); + basket.add(new Bagel("BAGE")); + basket.add(new Bagel("BAGO")); + Map basketItems = basket.getAll(); + + // Setup calculation and print result + priceCalculator = new PriceCalculator(); + ArrayList discountList = priceCalculator.calculateSpecialOfferCombination( + inventory, basketItems, specialOffers); + + for (DiscountObjectCombination d : discountList) { + System.out.println("Offer items: "+d.getOfferItems() + + " x"+d.getNumOfDiscounts()+" discounts" + + ", discount: "+d.getDiscountSum()); + } + + // Special offer for COFB: Coffee + Bagel for 1.25 + // BAGE 0.49 + // BAGP 0.39 + // COFB 0.99 + // (cheapest combo) Ordinary price 0.99 + 0.39 = 1.38 + // Ordinary - specialPrice 1.38 - 1.25 = 0.13 + Assertions.assertEquals(1, discountList.get(0).getNumOfDiscounts()); + Assertions.assertEquals(0.13, discountList.get(0).getDiscountSum()); + } + + @Test + public void calculateCombinationDiscountShouldReturnDiscounts() { + inventory = new Inventory(); + ArrayList specialOffers = inventory.getSpecialOffersCombination(); + + basket = new Basket(new Inventory()); + basket.add(new Coffee("COFB")); + basket.add(new Bagel("BAGE", Arrays.asList("FILS","FILB"))); + basket.add(new Bagel("BAGP")); + basket.add(new Bagel("BAGE")); + basket.add(new Bagel("BAGO")); + basket.add(new Coffee("COFW")); + Map basketItems = basket.getAll(); + + // Setup calculation and print result + priceCalculator = new PriceCalculator(); + ArrayList discountList = priceCalculator.calculateSpecialOfferCombination( + inventory, basketItems, specialOffers); + + for (DiscountObjectCombination d : discountList) { + System.out.println("Offer items: "+d.getOfferItems() + + " x"+d.getNumOfDiscounts()+" discounts" + + ", discount: "+d.getDiscountSum()); + } + + // Special offer for COFB: Coffee + Bagel for 1.25 + // BAGE 0.49 + // BAGP 0.39 + // COFB 0.99 + // COFW 1.19 + + // (combo 1) Ordinary price 0.99 + 0.39 = 1.38 + // Ordinary - specialPrice 1.38 - 1.25 = 0.13 + + // (combo 2) Ordinary price 1.19 + 0.49 = 1.68 + // Ordinary - specialPrice 1.68 - 1.25 = 0.43 + + // 0.13 + 0.43 = 0.56 + + Assertions.assertEquals(2, discountList.get(0).getNumOfDiscounts()); + Assertions.assertEquals(0.56, discountList.get(0).getDiscountSum()); + } + + // TODO: + // NOTE: I have different SKU's than in the examples because I create SKU differently +} diff --git a/src/test/java/com/booleanuk/core/inventory/InventoryTest.java b/src/test/java/com/booleanuk/core/inventory/InventoryTest.java new file mode 100644 index 000000000..f1a9ff88f --- /dev/null +++ b/src/test/java/com/booleanuk/core/inventory/InventoryTest.java @@ -0,0 +1,74 @@ +package com.booleanuk.core.inventory; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +public class InventoryTest { + Inventory inventory; + + // Print exception method function + public void printExceptionMessageToConsole(Exception e) { + System.out.println("\nException message:"); + System.out.println("\t" + e.getMessage() + "\n"); + } + + @Test + public void checkInventoryInitialization() { + inventory = new Inventory(); + Map items = inventory.getAllItems(); + + for (InventoryItem i : items.values()) { + System.out.println( + i.getSKU()+", " + + i.getPrice()+", " + + i.getName()+", " + + i.getVariant()+", " + ); + } + } + + @Test + public void getItemBasedOnSKU() { + inventory = new Inventory(); + Assertions.assertEquals("BAGE", inventory.getItem("BAGE").getSKU()); + } + + // User story #10: Can't add something that are not in the inventory / in stock + // TODO: This inventory don't keep track on number of items in stock, so it is infinite + @Test + public void throwExceptionWhenItemIsNotInInventory() { + inventory = new Inventory(); + + // Try to invoke exception + InventoryItemException e = Assertions.assertThrows( + InventoryItemException.class, + () -> { inventory.getItem("HELLO"); } + ); + Assertions.assertEquals("SKU 'HELLO' does not exist.", e.getMessage()); + printExceptionMessageToConsole(e); + } + + // User story #7: Get price of bagel + @Test + public void getPriceOfBagel() { + inventory = new Inventory(); + BagelItem bagel = (BagelItem) inventory.getItem("BAGE"); + Assertions.assertEquals(0.49, bagel.getPrice()); + } + + // User story #9: Get price of filling + @Test + public void getPriceOfFilling() { + inventory = new Inventory(); + FillingItem filling = (FillingItem) inventory.getItem("FILB"); + Assertions.assertEquals(0.12, filling.getPrice()); + } + + @Test + public void printInventoryMenu() { + inventory = new Inventory(); + inventory.printMenu(); + } +}