diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ee14693d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +target/ +*.iml +*.class diff --git a/README.md b/README.md index e75f4549..c254212f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Java Exercise -This is a simple exercise to allow you to demostrate your software engineering skillset. It's completly up to you how long you give yourself, stop when you're happy with the quality of your work, but we don't expect it to take too long. +This is a simple exercise to allow you to demonstrate your software engineering skillset. It's completely up to you how long you give yourself, stop when you're happy with the quality of your work, but we don't expect it to take too long. + +## To Run + 1. First build with `mvn clean install`. + 2. Run with `java -jar /insert/path/to/your/maven/repository/henrys-groceries-1.0-SNAPSHOT-jar-with-dependencies.jar`. + 3. Alternatively, just run `BasketRunner` class in your IDE. ## Instructions 1. Please fork this repository and work on your fork. @@ -18,7 +23,7 @@ This is a simple exercise to allow you to demostrate your software engineering s A local shop, Henry’s Grocery, has asked you to author an IT solution for them to price up a basket of shopping for their customers. -Henry’s Grocery, currently only stocks four items and has two promotions. These are as follows: +Henry’s Grocery currently only stocks four items and has two promotions. These are as follows: ### Stock Items diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..776291f0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + com.ford.henrysgroceries + henrys-groceries + 1.0-SNAPSHOT + + 1.8 + 1.8 + + 4.11 + + + + + junit + junit + ${junit.version} + test + + + org.hamcrest + hamcrest-core + + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + org.mockito + mockito-core + 2.20.1 + test + + + + + + + maven-assembly-plugin + + + package + + single + + + + + + + true + com.ford.henrysgroceries.BasketRunner + + + + jar-with-dependencies + + + + + + diff --git a/src/main/java/com/ford/henrysgroceries/Basket.java b/src/main/java/com/ford/henrysgroceries/Basket.java new file mode 100644 index 00000000..7ba5a386 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/Basket.java @@ -0,0 +1,72 @@ +package com.ford.henrysgroceries; + +import com.ford.henrysgroceries.offers.Offer; +import com.ford.henrysgroceries.products.Product; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.time.Clock; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Basket { + + private List products; + private List offers; + private Clock clock; + + public Basket() { + products = new ArrayList<>(); + offers = new ArrayList<>(); + } + + public Basket(List offers) { + products = new ArrayList<>(); + this.offers = offers; + } + + public Basket(List offers, Clock fixedClock, Product... products) { + this.products = Arrays.asList(products); + this.offers = offers; + clock = fixedClock; + } + + public BigDecimal calculateTotal() { + offers.forEach(offer -> offer.apply(this, getDate())); + + return products.stream() + .map(product -> product.hasDiscount() ? product.getDiscountPrice() : product.getPrice()) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private LocalDate getDate() { + return clock == null ? LocalDate.now() : LocalDate.now(clock); + } + + public List getProducts() { + return products; + } + + public void addProduct(Product product) { + this.products.add(product); + calculateTotal(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Basket:\n"); + products.forEach(product -> display(sb, product)); + sb.append("Total: ").append(format(calculateTotal())).append("\n"); + return sb.toString(); + } + + private StringBuilder display(StringBuilder sb, Product product) { + return sb.append(product.getName()).append(" ").append(format(product.getDisplayPrice())).append("\n"); + } + + private String format(BigDecimal price) { + return NumberFormat.getCurrencyInstance().format(price); + } +} diff --git a/src/main/java/com/ford/henrysgroceries/BasketRunner.java b/src/main/java/com/ford/henrysgroceries/BasketRunner.java new file mode 100644 index 00000000..ed7dbf89 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/BasketRunner.java @@ -0,0 +1,78 @@ +package com.ford.henrysgroceries; + +import com.ford.henrysgroceries.offers.BuyTwoSoupsGetBreadHalfPriceOffer; +import com.ford.henrysgroceries.offers.Offer; +import com.ford.henrysgroceries.offers.TenPercentOffApplesOffer; + +import java.io.PrintStream; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +import static com.ford.henrysgroceries.products.ProductHelper.*; + +public class BasketRunner { + private final Basket basket; + + private final Scanner scanner; + + private final PrintStream printStream; + + public BasketRunner(Basket basket, Scanner scanner, PrintStream printStream) { + this.basket = basket; + this.scanner = scanner; + this.printStream = printStream; + } + + public static void main(String[] args) { + LocalDate today = LocalDate.now(); + Offer applesOffer = new TenPercentOffApplesOffer(today); + Offer breadOffer = new BuyTwoSoupsGetBreadHalfPriceOffer(today); + List offers = Arrays.asList(applesOffer, breadOffer); + BasketRunner basketRunner = new BasketRunner(new Basket(offers), new Scanner(System.in), System.out); + basketRunner.run(); + } + + void run() { + boolean addMoreToBasket = true; + + do { + printStream.print("Please enter the first letter of the product you wish to add to your basket: [S]oup, [B]read, [M]ilk, [A]pples or [Q]uit: "); + String input = scanner.nextLine().toUpperCase(); + + switch (input) { + case "S": + printStream.print("You added: Soup\n"); + basket.addProduct(soup()); + break; + + case "B": + printStream.print("You added: Bread\n"); + basket.addProduct(bread()); + break; + + case "M": + printStream.print("You added: Milk\n"); + basket.addProduct(milk()); + break; + + case "A": + printStream.print("You added: Apples\n"); + basket.addProduct(apples()); + break; + + case "Q": + printStream.print("\n"); + addMoreToBasket = false; + break; + + default: + printStream.print("Product not recognised\n"); + } + + if (addMoreToBasket) + printStream.println(basket); + } while (addMoreToBasket); + } +} diff --git a/src/main/java/com/ford/henrysgroceries/offers/AbstractOffer.java b/src/main/java/com/ford/henrysgroceries/offers/AbstractOffer.java new file mode 100644 index 00000000..398a9555 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/offers/AbstractOffer.java @@ -0,0 +1,12 @@ +package com.ford.henrysgroceries.offers; + +import java.time.LocalDate; + +public abstract class AbstractOffer implements Offer { + LocalDate start; + LocalDate end; + + boolean notApplicable(LocalDate date) { + return date.isBefore(start) || date.isAfter(end); + } +} diff --git a/src/main/java/com/ford/henrysgroceries/offers/BuyTwoSoupsGetBreadHalfPriceOffer.java b/src/main/java/com/ford/henrysgroceries/offers/BuyTwoSoupsGetBreadHalfPriceOffer.java new file mode 100644 index 00000000..d30c484d --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/offers/BuyTwoSoupsGetBreadHalfPriceOffer.java @@ -0,0 +1,43 @@ +package com.ford.henrysgroceries.offers; + +import com.ford.henrysgroceries.Basket; +import com.ford.henrysgroceries.products.Product; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static com.ford.henrysgroceries.products.ProductHelper.bread; +import static com.ford.henrysgroceries.products.ProductHelper.soup; + +public class BuyTwoSoupsGetBreadHalfPriceOffer extends AbstractOffer { + public BuyTwoSoupsGetBreadHalfPriceOffer(LocalDate today) { + start = today.minusDays(1); + end = start.plusDays(7); + } + + @Override + public Basket apply(Basket basket, LocalDate date) { + if (notApplicable(date)) + return basket; + + List products = basket.getProducts(); + long numberOfSoup = products.stream() + .filter(product -> product.getName().equals(soup().getName())) + .count(); + + if (numberOfSoup < 2) + return basket; + + long numberOfDiscountsToApply = numberOfSoup / 2; + + for (Product product : products) { + if (product.getName().equals(bread().getName()) && numberOfDiscountsToApply > 0) { + product.setDiscountPrice(product.getPrice().divide(new BigDecimal(2))); + numberOfDiscountsToApply--; + } + } + + return basket; + } +} diff --git a/src/main/java/com/ford/henrysgroceries/offers/Offer.java b/src/main/java/com/ford/henrysgroceries/offers/Offer.java new file mode 100644 index 00000000..feabb858 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/offers/Offer.java @@ -0,0 +1,10 @@ +package com.ford.henrysgroceries.offers; + +import com.ford.henrysgroceries.Basket; + +import java.time.LocalDate; + +public interface Offer { + + Basket apply(Basket basket, LocalDate date); +} diff --git a/src/main/java/com/ford/henrysgroceries/offers/TenPercentOffApplesOffer.java b/src/main/java/com/ford/henrysgroceries/offers/TenPercentOffApplesOffer.java new file mode 100644 index 00000000..2b7604d9 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/offers/TenPercentOffApplesOffer.java @@ -0,0 +1,36 @@ +package com.ford.henrysgroceries.offers; + +import com.ford.henrysgroceries.Basket; +import com.ford.henrysgroceries.products.Product; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import static com.ford.henrysgroceries.products.ProductHelper.apples; +import static java.time.temporal.TemporalAdjusters.lastDayOfMonth; + +public class TenPercentOffApplesOffer extends AbstractOffer { + + public TenPercentOffApplesOffer(LocalDate today) { + start = today.plusDays(3); + end = today.plusMonths(1).with(lastDayOfMonth()); + } + + @Override + public Basket apply(Basket basket, LocalDate date) { + if (notApplicable(date)) + return basket; + + basket.getProducts().stream() + .filter(product -> product.getName().equals(apples().getName())) + .forEach(product -> product.setDiscountPrice(setDiscountPrice(product))); + return basket; + } + + private BigDecimal setDiscountPrice(Product product) { + BigDecimal price = product.getPrice(); + BigDecimal percentage = new BigDecimal(10); + BigDecimal discount = price.divide(new BigDecimal("100.00")).multiply(percentage); + return price.subtract(discount); + } +} diff --git a/src/main/java/com/ford/henrysgroceries/products/Product.java b/src/main/java/com/ford/henrysgroceries/products/Product.java new file mode 100644 index 00000000..caee2701 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/products/Product.java @@ -0,0 +1,49 @@ +package com.ford.henrysgroceries.products; + +import java.math.BigDecimal; + +public class Product { + + private final String name; + + private final String unit; + + private final BigDecimal price; + + private BigDecimal discountPrice; + + public Product(String name, String unit, BigDecimal price) { + this.name = name; + this.unit = unit; + this.price = price; + discountPrice = BigDecimal.ZERO; + } + + public String getName() { + return name; + } + + public String getUnit() { + return unit; + } + + public BigDecimal getPrice() { + return price; + } + + public BigDecimal getDiscountPrice() { + return discountPrice; + } + + public BigDecimal getDisplayPrice() { + return hasDiscount() ? discountPrice : price; + } + + public void setDiscountPrice(BigDecimal discountPrice) { + this.discountPrice = discountPrice; + } + + public boolean hasDiscount() { + return discountPrice.compareTo(BigDecimal.ZERO) != 0; + } +} diff --git a/src/main/java/com/ford/henrysgroceries/products/ProductHelper.java b/src/main/java/com/ford/henrysgroceries/products/ProductHelper.java new file mode 100644 index 00000000..40d1abc7 --- /dev/null +++ b/src/main/java/com/ford/henrysgroceries/products/ProductHelper.java @@ -0,0 +1,22 @@ +package com.ford.henrysgroceries.products; + +import java.math.BigDecimal; + +public class ProductHelper { + + public static Product soup() { + return new Product("Soup", "tin", new BigDecimal("0.65")); + } + + public static Product bread() { + return new Product("Bread", "loaf", new BigDecimal("0.80")); + } + + public static Product milk() { + return new Product("Milk", "bottle", new BigDecimal("1.30")); + } + + public static Product apples() { + return new Product("Apples", "single", new BigDecimal("0.10")); + } +} diff --git a/src/test/java/com/ford/henrysgroceries/BasketRunnerTest.java b/src/test/java/com/ford/henrysgroceries/BasketRunnerTest.java new file mode 100644 index 00000000..4ac714dc --- /dev/null +++ b/src/test/java/com/ford/henrysgroceries/BasketRunnerTest.java @@ -0,0 +1,58 @@ +package com.ford.henrysgroceries; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.math.BigDecimal; +import java.util.Scanner; + +import static com.ford.henrysgroceries.products.ProductHelper.milk; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class BasketRunnerTest { + private static final String INSTRUCTIONS = "Please enter the first letter of the product you wish to add to your basket: [S]oup, [B]read, [M]ilk, [A]pples or [Q]uit: "; + + @Mock + private Basket basket; + + private ByteArrayOutputStream output; + + @Before + public void setUp() { + output = new ByteArrayOutputStream(); + } + + @Test + public void addNothingToBasketThenQuit() { + BasketRunner basketRunner = new BasketRunner(basket, new Scanner("Q\n"), new PrintStream(output)); + + basketRunner.run(); + + assertThat(output.toString(), is(INSTRUCTIONS + "\n")); + } + + @Test + public void addMilkToBasketThenQuit() { + BigDecimal price = milk().getPrice(); + when(basket.toString()).thenReturn("Basket:\nMilk £" + price + "\nTotal: £" + price + "\n"); + BasketRunner basketRunner = new BasketRunner(basket, new Scanner("M\nQ\n"), new PrintStream(output)); + + basketRunner.run(); + + assertThat(output.toString(), is( + INSTRUCTIONS + "You added: Milk\n" + + "Basket:\n" + + "Milk £1.30\n" + + "Total: £1.30\n\r" + + "\n" + + INSTRUCTIONS + "\n")); + } +} diff --git a/src/test/java/com/ford/henrysgroceries/BasketTest.java b/src/test/java/com/ford/henrysgroceries/BasketTest.java new file mode 100644 index 00000000..717d043e --- /dev/null +++ b/src/test/java/com/ford/henrysgroceries/BasketTest.java @@ -0,0 +1,142 @@ +package com.ford.henrysgroceries; + +import com.ford.henrysgroceries.offers.BuyTwoSoupsGetBreadHalfPriceOffer; +import com.ford.henrysgroceries.offers.Offer; +import com.ford.henrysgroceries.offers.TenPercentOffApplesOffer; +import com.ford.henrysgroceries.products.Product; +import org.junit.Test; + +import java.math.BigDecimal; +import java.time.*; +import java.util.Arrays; +import java.util.List; + +import static com.ford.henrysgroceries.products.ProductHelper.*; +import static com.ford.henrysgroceries.utils.EqualsBigDecimalMatcher.is; +import static org.junit.Assert.assertThat; + +public class BasketTest { + private Clock fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + + private Basket basket; + private Offer applesOffer = new TenPercentOffApplesOffer(LocalDate.now()); + private Offer breadOffer = new BuyTwoSoupsGetBreadHalfPriceOffer(LocalDate.now()); + private List offers = Arrays.asList(applesOffer, breadOffer); + + @Test + public void emptyBasketWithNoOffersTotalsToZero() { + basket = new Basket(); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(BigDecimal.ZERO)); + } + + @Test + public void emptyBasketWithOffersTotalsToZero() { + basket = new Basket(offers); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(BigDecimal.ZERO)); + } + + @Test + public void givesCorrectTotalForSingleProducts() { + for (Product product : Arrays.asList(soup(), bread(), milk(), apples())) { + givenBasketHasProduct(product); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(product.getPrice())); + } + } + + @Test + public void givesCorrectTotalForMilkAndBread() { + givenBasketHasProducts(milk(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("2.10"))); + } + + @Test + public void givesCorrectTotalForMoreThanOneProductOfSameType() { + givenBasketHasProducts( + apples(), apples(), + soup(), soup() + ); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.50"))); + } + + @Test + public void threeTinsOfSoupAndTwoLoavesOfBreadBoughtTodayCosts3Pounds15Pence() { + givenBasketHasProducts( + soup(), soup(), soup(), + bread(), bread() + ); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("3.15"))); + } + + @Test + public void sixApplesAndOneBottleOfMilkBoughtTodayCosts1Pound90Pence() { + givenBasketHasProducts( + apples(), apples(), apples(), + apples(), apples(), apples(), + milk() + ); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.90"))); + } + + @Test + public void sixApplesAndOneBottleOfMilkBoughtIn5DaysTimeCosts1Pound84Pence() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(5)); + givenBasketHasProducts( + clock, + apples(), apples(), apples(), + apples(), apples(), apples(), + milk() + ); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.84"))); + } + + @Test + public void threeApplesAndTwoTinsOfSoupAndAndOneLoafOfBreadBoughtIn5DaysTimeCosts1Pound97Pence() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(5)); + givenBasketHasProducts( + clock, + apples(), apples(), apples(), + soup(), soup(), + bread() + ); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.97"))); + } + + private void givenBasketHasProduct(Product product) { + givenBasketHasProducts(product); + } + + private void givenBasketHasProducts(Product... products) { + givenBasketHasProducts(fixedClock, products); + } + + private void givenBasketHasProducts(Clock clock, Product... products) { + basket = new Basket(offers, clock, products); + } +} diff --git a/src/test/java/com/ford/henrysgroceries/offers/BuyTwoSoupsGetBreadHalfPriceOfferTest.java b/src/test/java/com/ford/henrysgroceries/offers/BuyTwoSoupsGetBreadHalfPriceOfferTest.java new file mode 100644 index 00000000..f0de9bf6 --- /dev/null +++ b/src/test/java/com/ford/henrysgroceries/offers/BuyTwoSoupsGetBreadHalfPriceOfferTest.java @@ -0,0 +1,135 @@ +package com.ford.henrysgroceries.offers; + +import com.ford.henrysgroceries.Basket; +import com.ford.henrysgroceries.products.Product; +import org.junit.Test; + +import java.math.BigDecimal; +import java.time.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.ford.henrysgroceries.products.ProductHelper.*; +import static com.ford.henrysgroceries.utils.EqualsBigDecimalMatcher.is; +import static org.junit.Assert.assertThat; + +public class BuyTwoSoupsGetBreadHalfPriceOfferTest { + private Clock fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + private LocalDate today = LocalDate.now(fixedClock); + private List offers = Collections.singletonList(new BuyTwoSoupsGetBreadHalfPriceOffer(today)); + + @Test + public void emptyBasketTotalsToZero() { + Basket basket = new Basket(offers, fixedClock); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(BigDecimal.ZERO)); + } + + @Test + public void offerDoesNotApplyTwoDaysAgo() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(-2)); + Basket basket = new Basket(offers, clock, soup(), soup(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("2.10"))); + } + + @Test + public void offerStartsFromYesterday() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(-1)); + Basket basket = new Basket(offers, clock, soup(), soup(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.70"))); + } + + @Test + public void offerAppliesForSevenDays() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(6)); + Basket basket = new Basket(offers, clock, soup(), soup(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.70"))); + } + + @Test + public void offerEndsAfterSevenDays() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(7)); + Basket basket = new Basket(offers, clock, soup(), soup(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("2.10"))); + } + + @Test + public void offerAppliesToday() { + Basket basket = new Basket(offers, fixedClock, soup(), soup(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.70"))); + } + + @Test + public void noDiscountWhenNoSoupBought() { + Basket basket = new Basket(offers, fixedClock, bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("0.80"))); + } + + @Test + public void noDiscountWhenOnlyOneSoupBought() { + Basket basket = new Basket(offers, fixedClock, soup(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("1.45"))); + } + + @Test + public void onlyDiscountedAppliedWhenTwoSoupsBought() { + Basket basket = new Basket(offers, fixedClock, soup(), soup(), bread(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("2.50"))); + } + + @Test + public void onlyOneDiscountAppliedWhenThreeSoupsBought() { + Basket basket = new Basket(offers, fixedClock, soup(), soup(), soup(), bread(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("3.15"))); + } + + @Test + public void twoDiscountsAppliedWhenFourSoupsBought() { + Basket basket = new Basket(offers, fixedClock, soup(), soup(), soup(), soup(), bread(), bread()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("3.40"))); + } + + @Test + public void offerNotAppliedToOtherProducts() { + for (Product product : Arrays.asList(milk(), apples())) { + Basket basket = new Basket(offers, fixedClock, product); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(product.getPrice())); + } + } +} diff --git a/src/test/java/com/ford/henrysgroceries/offers/TenPercentOffApplesOfferTest.java b/src/test/java/com/ford/henrysgroceries/offers/TenPercentOffApplesOfferTest.java new file mode 100644 index 00000000..d3256ecd --- /dev/null +++ b/src/test/java/com/ford/henrysgroceries/offers/TenPercentOffApplesOfferTest.java @@ -0,0 +1,118 @@ +package com.ford.henrysgroceries.offers; + +import com.ford.henrysgroceries.Basket; +import com.ford.henrysgroceries.products.Product; +import org.junit.Test; + +import java.math.BigDecimal; +import java.time.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.ford.henrysgroceries.products.ProductHelper.*; +import static com.ford.henrysgroceries.utils.EqualsBigDecimalMatcher.is; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.TemporalAdjusters.lastDayOfMonth; +import static org.junit.Assert.assertThat; + +public class TenPercentOffApplesOfferTest { + private Clock fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + private LocalDate today = LocalDate.now(fixedClock); + private List offers = Collections.singletonList(new TenPercentOffApplesOffer(today)); + + @Test + public void emptyBasketNoOffersTotalIsZero() { + List offers = Collections.emptyList(); + Basket basket = new Basket(offers, fixedClock); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(BigDecimal.ZERO)); + } + + @Test + public void emptyBasketWithOffersTotalIsZero() { + Basket basket = new Basket(offers, fixedClock); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(BigDecimal.ZERO)); + } + + @Test + public void offerDoesNotApplyTwoDaysFromNow() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(2)); + Basket basket = new Basket(offers, clock, apples()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(apples().getPrice())); + } + + @Test + public void offerStartsThreeDaysFromNow() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(3)); + Basket basket = new Basket(offers, clock, apples()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("0.09"))); + } + + @Test + public void offerAppliesTillEndOfFollowingMonth() { + Clock clock = Clock.offset(fixedClock, lastDayOfferApplies()); + Basket basket = new Basket(offers, clock, apples()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("0.09"))); + } + + @Test + public void offerDoesNotApplyOneDayAfterEndOfFollowingMonth() { + Clock clock = Clock.offset(fixedClock, lastDayOfferApplies().plusDays(1)); + Basket basket = new Basket(offers, clock, apples()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(apples().getPrice())); + } + + @Test + public void oneAppleIsDiscountedBy10Percent() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(10)); + Basket basket = new Basket(offers, clock, apples()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("0.09"))); + } + + @Test + public void manyApplesAreDiscountedBy10Percent() { + Clock clock = Clock.offset(fixedClock, Duration.ofDays(10)); + Basket basket = new Basket(offers, clock, apples(), apples(), apples()); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(new BigDecimal("0.27"))); + } + + @Test + public void offerNotAppliedToOtherProducts() { + for (Product product : Arrays.asList(soup(), bread(), milk())) { + Basket basket = new Basket(offers, fixedClock, product); + + BigDecimal total = basket.calculateTotal(); + + assertThat(total, is(product.getPrice())); + } + } + + private Duration lastDayOfferApplies() { + LocalDate lastDayOfferApplies = today.plusMonths(1).with(lastDayOfMonth()); + return Duration.ofDays(DAYS.between(today, lastDayOfferApplies)); + } +} diff --git a/src/test/java/com/ford/henrysgroceries/products/ProductTest.java b/src/test/java/com/ford/henrysgroceries/products/ProductTest.java new file mode 100644 index 00000000..08660eae --- /dev/null +++ b/src/test/java/com/ford/henrysgroceries/products/ProductTest.java @@ -0,0 +1,24 @@ +package com.ford.henrysgroceries.products; + +import org.junit.Test; + +import java.math.BigDecimal; + +import static com.ford.henrysgroceries.products.ProductHelper.milk; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class ProductTest { + @Test + public void hasDiscountReturnsTrueWhenDiscountedPriceExists() { + Product milk = milk(); + milk.setDiscountPrice(new BigDecimal("1.00")); + + assertThat(milk.hasDiscount(), is(true)); + } + + @Test + public void hasDiscountReturnsFalseWhenNoDiscountedPrice() { + assertThat(milk().hasDiscount(), is(false)); + } +} diff --git a/src/test/java/com/ford/henrysgroceries/utils/EqualsBigDecimalMatcher.java b/src/test/java/com/ford/henrysgroceries/utils/EqualsBigDecimalMatcher.java new file mode 100644 index 00000000..5d1004ae --- /dev/null +++ b/src/test/java/com/ford/henrysgroceries/utils/EqualsBigDecimalMatcher.java @@ -0,0 +1,33 @@ +package com.ford.henrysgroceries.utils; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.math.BigDecimal; +import java.text.NumberFormat; + +public class EqualsBigDecimalMatcher extends TypeSafeMatcher { + + private BigDecimal expected; + + public static Matcher is(BigDecimal expected) { + return new EqualsBigDecimalMatcher(expected); + } + + private EqualsBigDecimalMatcher(BigDecimal expected) { + this.expected = expected; + } + + @Override + protected boolean matchesSafely(BigDecimal actual) { + return actual.setScale(2).compareTo(expected.setScale(2)) == 0; + } + + @Override + public void describeTo(Description description) { + description.appendText("equals " + NumberFormat.getCurrencyInstance().format(expected)); + } + + +}