Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stian Rusvik #123

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
599f764
added setup code and made first test fail.
Aug 16, 2024
f3518f6
made first test pass.
Aug 16, 2024
6845820
updated test.
Aug 19, 2024
f16cae3
made remove products fail.
Aug 19, 2024
ffb6dd4
made remove products pass.
Aug 19, 2024
6c51982
updated remove product method.
Aug 19, 2024
168153a
made is full test fail.
Aug 19, 2024
db0ee7a
made is full test pass.
Aug 19, 2024
2f6db26
updated custom exception to inherit from the Exception class.
Aug 19, 2024
fd0258e
made change capacity test fail.
Aug 19, 2024
b28f272
made change capacity test pass.
Aug 19, 2024
79e2e65
made remove product when product does not exists fail.
Aug 19, 2024
ed32430
made remove product when product does not exists pass.
Aug 19, 2024
cb1e84b
.
Aug 19, 2024
567f832
wrote test for get total cost in basket.
Aug 20, 2024
f51fa72
created a product factory.
Aug 20, 2024
d641dbb
lots of refactoring to make the tests adapt to the new factory.
Aug 20, 2024
5997685
more refactoring of existing code.
Aug 20, 2024
8f1e681
added builder pattern to Bagel for fillings.
Aug 20, 2024
0ac043a
made testCreateBagelWithFillings fail.
Aug 20, 2024
32f6565
made testCreateBagelWithFillings pass.
Aug 20, 2024
c710111
made testPriceOfFilling fail.
Aug 20, 2024
5aa099d
made testPriceOfFilling pass.
Aug 20, 2024
093e33d
finished core exercises.
Aug 20, 2024
c9fceb1
lots of work
Aug 21, 2024
8a85f6a
lots of bug hunting!
Aug 22, 2024
aac70b0
wrote test on first discount - Extension 1
Aug 22, 2024
86f63df
wrote test on second discount - Extension 1
Aug 22, 2024
f2da9bc
wrote test on third discount - Extension 1
Aug 22, 2024
0ca633f
wrote some more tests for Extension 1
Aug 22, 2024
7efa7a4
trying to solve difficult bug.
Aug 22, 2024
022e75a
removed code that broke the tests.
Aug 22, 2024
e84a093
refactoring.
Aug 22, 2024
4a2fa4e
updated domain model.
Aug 22, 2024
e97f94d
Started implementing receipt for extension 2
Aug 22, 2024
76583cd
.
Aug 23, 2024
7e886ac
Finished extension 2 - Bobs bagel
Aug 23, 2024
93d24a0
Updated domain model and added class diagram.
Aug 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added classdiagram.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
168 changes: 168 additions & 0 deletions domain-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Domain Model - Bob's Bagels - OOP

---------------------------------------------------------------------------------

## Overview of the classes

| Class | Members | Types |
|-----------|---------------|----------------------|
| `Order` | `basket` | `Basket` |
| | `total` | `Integer` |
| `Basket` | `bagels` | `ArrayList<Product>` |
| | `capacity` | `Integer` |
| `Product` | `sku` | `SKU` |
| | `name` | `String` |
| | `price` | `Double` |
| | `variant` | `MenuCategory` |
| | `hasDiscount` | `Boolean` |

These are the classes that will inherit from the **Product** class.

| Extends Product |
|-----------------|
| `Bagel` |
| `Coffee` |
| `Filling` |

**Note:** 'BagelType', 'CoffeeType' and 'FillingType' is their own enum classes that
implements the 'MenuCategory' interface. See the class diagram for more details.

### Factory class
There is also a **ProductFactory** class that is responsible for instantiation of the extended products.

| Class | Method | Returns |
|------------------|------------------------------------|-----------|
| `ProductFactory` | `getProduct(MenuCategory variant)` | `Product` |


-------------------------------------------------------------------------------

## Solution to the user stories

| User Story | Class | Method | Note |
|---------------------------------------------------------------------------------------|-----------|----------------------------------------------------------------------|------------------------------------------------------|
| I'd like to add a specific<br/> type of bagel to my basket | `Basket` | `addProduct(Product product)` | |
| I'd like to remove a bagel<br/> from my basket | `Basket` | `removeProduct(BagelType variant)` | Covers user story 2 and 5. |
| I'd like to know when my<br/> basket is full | `Basket` | `isFull()` | Returns true if full, else false. |
| I'd like to change the <br/>capacity of baskets | `Basket` | `changeCapacity()` | |
| I'd like to know the total<br/> cost of items in my basket. | `Order` | `getTotalCost()` | Returns a double value. |
| I'd like to know the cost of <br/>a bagel before I add it to my basket. | `Product` | `getPrice()` | Returns a double value. |
| I'd like to be able to choose <br/>fillings for my bagel | `Bagel` | `createBagelWithFilling(BagelType variant, FillingType... fillings)` | Returns a Bagel object. |
| I'd like to know the cost of each<br/> filling before I add it to my bagel order | `Bagel` | `getFillingPrice(FillingType variant)` | Returns the price (double) for the specific variant. |

**Note** The last user story '*I want customers to only be able to order things that we stock in our inventory*' is already covered<br/>
since the use of enums and 'ProductFactory' makes sure it is not possible to add items that does not exist in
the inventory.

### Additional information
The 'Bagel' class has an inner 'BagelBuilder' class that it uses to create
specific bagels with fillings in the `createBagelWithFilling` method.

--------------------------------------------------------------------------------------

# Extension 1: Discounts

## User Stories

```
1. As a member of the public,
So I can recieve the special offer,
I like to order 6 bagels for 2.49.
```

```
2. As a member of the public,
So I can recieve the special offer,
I like to order 12 bagels for 3.99.
```

```
3. As a member of the public,
So I can recieve the special coffee and bagel offer,
I like to order a coffee and a bagel for 1.25.
```

### Domain Model - Extension 1

All the 3 user stories will be implemented in the following class:

| Class |
|---------|
| `Order` |

And here are the methods that will be used:

| Method | Return | Note |
|----------------------------------------------------------|-----------|---------------------------------------------------------------------------------------|
| `getNumberOfItems(Basket basket)` | `Integer` | Returns the number of bagels and sets the 'hasDiscount' state on the correct items. |
| `getTotalCost()` | `Double` | Done lots of refactoring in this method. |
| `addFillingPriceToTotalCost(Bagel bagel)` | `void` | Iterate over a bagels fillings list and adds it to the total cost. |
| `addDiscount(int numBagels, int limit, double discount)` | `Integer` | Adds discount and decrement number of bagels counter. |
| `checkCoffeeAndBagelPair(Product product, Product next)` | `void` | Checks prev and next product and adds a discount if there is a coffee and bagel pair. |
| `markAsDiscounted(Basket basket, int bagelsToMark)` | `void` | Changes the 'hasDiscount' state on bagels that will have a discount. |
| `isBagel(Product product)` | `Boolean` | Checks if a product is an instance of Bagel. |
| `deliverAndResetTotalCost(Double total)` | `Double` | Deliver and resets the total cost value. |
| `applyDiscount(Basket basket, Integer bagelCounter)` | `void` | Find the number of the different discount sets and marks the bagels as discounted. |

------------------------------------------------------------------------------------------------------

# Extension 2: Receipts

## User Story

```
As a member of the public,
so I can get an overview of my order,
Id like to recieve an receipt for my order.
```

### Domain Model - Extension 2

The following classes will be used for the user story:

| Classes | Members | Type |
|---------------|----------------|--------------------------|
| `Receipt` | `order` | `Order` |
| | `receiptItems` | `ArrayList<ReceiptItem>` |
| `ReceiptItem` | `variant` | `MenuCategory` |
| | `name` | `String` |
| | `quantity` | `Integer` |
| | `priceSum` | `Double` |

These are the methods in the Receipt class:

| Method | Returns | Note |
|------------------------------------|----------------------------------------|-------------------------------------------------------------------------------------------|
| `printReceipt` | `void` | Prints out the receipt in the same structure as the assignment description. |
| `fillReceiptItems` | `void` | Fills the receiptItems list with items from the basket. |
| `groupByProductAndCount` | `Map<MenuCategory, Map<String, Long>>` | Count and group variants together. |
| `groupByProductAndPrice` | `Map<MenuCategory, Double>` | Sums the price for the different product groups. |
| `iterateMapsAndCreateReceiptItems` | `void` | Iterate over the maps and create ReceiptItems. |
| `createReceiptItemAndAddToList` | `void` | Create the items and add them to the receiptItems arraylist. |
| `getFormattedDate` | `String` | Helper method used to get the current date in a nice format (should be in a utils class). |


The 'ReceiptItem' class includes getters for all the members.

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------





















87 changes: 87 additions & 0 deletions src/main/java/com/booleanuk/core/Basket.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.booleanuk.core;
import com.booleanuk.core.exceptions.FullBasketException;
import com.booleanuk.core.exceptions.NonExistingProductException;
import com.booleanuk.core.factory.ProductFactory;
import com.booleanuk.core.inherited.Bagel;
import com.booleanuk.core.inherited.Coffee;
import com.booleanuk.core.inherited.Filling;
import com.booleanuk.core.interfaces.MenuCategory;

import java.util.ArrayList;

public class Basket {
private final ArrayList<Product> products;
private Integer capacity;
private static final Integer DEFAULT_CAPACITY = 5;
private final ProductFactory factory;

public Basket() {
this.products = new ArrayList<>();
this.capacity = DEFAULT_CAPACITY;
this.factory = new ProductFactory();
}

public void addProduct(MenuCategory variant) throws FullBasketException {
if (!isFull()) {
this.getProducts().add(factory.getProduct(variant));
} else {
throw new FullBasketException("Your basket is full, cannot add product!");
}
}
public void addProduct(Product product) throws FullBasketException {
if (!isFull()) {
this.getProducts().add(product);
} else {
throw new FullBasketException("Your basket is full, cannot add product!");
}
}

public void removeProduct(MenuCategory variant) throws NonExistingProductException {
for (Product product : this.getProducts()) {
Product specificProduct = checkAndGetProduct(product);

if (!specificProduct.getVariant().equals(variant)) {
String message = "Product does not exist in the basket, cannot remove.";
throw new NonExistingProductException(message);
}

if (specificProduct.getVariant() == variant) {
this.getProducts().remove(product);
break;
}
}
}

public void changeCapacity(int newCapacity) {
setCapacity(newCapacity);
}

private Boolean isFull() {
return this.getProducts().size() == capacity;
}

private Product checkAndGetProduct(Product product) {
if (product instanceof Bagel bagel) {
return bagel;
}
if (product instanceof Coffee coffee) {
return coffee;
}
if (product instanceof Filling filling) {
return filling;
}
return null;
}

public ArrayList<Product> getProducts() {
return products;
}

private void setCapacity(Integer capacity) {
this.capacity = capacity;
}

public Integer getCapacity() {
return capacity;
}
}
120 changes: 120 additions & 0 deletions src/main/java/com/booleanuk/core/Order.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.booleanuk.core;
import com.booleanuk.core.inherited.Bagel;
import com.booleanuk.core.inherited.Coffee;

public class Order {
private final Basket basket;
private Double totalCost;

public Order() {
this.basket = new Basket();
this.totalCost = 0.0;
}

public Basket getBasket() {
return basket;
}

private int getNumberOfItems(Basket basket) {
int bagelCounter = 0;

for (Product product : basket.getProducts()) {
if (isBagel(product)) bagelCounter++;
}
applyDiscount(basket, bagelCounter);

return bagelCounter;
}

public double getTotalCost() {
int numBagels = getNumberOfItems(this.getBasket());

// Add discount for 12 and 6 bagel sets.
numBagels = addDiscount(numBagels, 12, 3.99);
addDiscount(numBagels, 6, 2.49);

for (int i = 0; i < this.basket.getProducts().size(); i++) {
Product product = this.basket.getProducts().get(i);

// Add filling price to the total.
if (isBagel(product)) {
addFillingPriceToTotalCost((Bagel) product);
}

// Check for coffee and bagel pairs and give discount.
int j = i+1;
while (j < this.basket.getProducts().size()) {
Product nextProduct = this.basket.getProducts().get(j);
checkCoffeeAndBagelPair(product, nextProduct);
checkCoffeeAndBagelPair(nextProduct, product);
j++;
}

// Give normal price if product does not have a discount.
if (!product.getHasDiscount()) {
this.totalCost += product.getPrice();
}
}

return deliverAndResetTotalCost(this.totalCost);
}

private int addDiscount(int numBagels, int limit, double discount) {
while (numBagels >= limit) {
this.totalCost += discount;
numBagels -= limit;
}
return numBagels;
}

// Used to find numbers of sets for 12 and 6 bagels.
private void applyDiscount(Basket basket, Integer bagelCounter) {
int twelveSet = bagelCounter / 12;
int remainingAfterTwelve = bagelCounter % 12;
int sixSet = remainingAfterTwelve / 6;

markAsDiscounted(basket, (twelveSet * 12));
markAsDiscounted(basket, (sixSet * 6));
}

// Internal helper method.
private void addFillingPriceToTotalCost(Bagel bagel) {
if (!bagel.getFillings().isEmpty()) {
bagel.getFillings().forEach(filling -> this.totalCost += filling.getPrice());
}
}

// Internal helper method.
private void checkCoffeeAndBagelPair(Product product, Product next) {
if (next instanceof Bagel && product instanceof Coffee) {
if (!next.getHasDiscount() && !product.getHasDiscount()) {
this.totalCost += 1.25;
next.setHasDiscount(true);
product.setHasDiscount(true);
}
}
}

// Internal helper method.
private void markAsDiscounted(Basket basket, int bagelsToMark) {
for (Product product : basket.getProducts()) {
if (isBagel(product) && bagelsToMark > 0) {
if (product.getHasDiscount()) continue;
product.setHasDiscount(true);
bagelsToMark--;
}
}
}

// Internal helper method.
private boolean isBagel(Product product) {
return product instanceof Bagel;
}

// Internal helper method.
private Double deliverAndResetTotalCost(Double total) {
double cost = total;
this.totalCost = 0.0;
return cost;
}
}
Loading