diff --git a/java/pom.xml b/java/pom.xml index 73ba4a7..c62567d 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -45,8 +45,8 @@ maven-compiler-plugin 3.8.0 - 22 - 22 + 17 + 17 diff --git a/java/src/main/java/dojo/supermarket/HtmlReceiptFormatter.java b/java/src/main/java/dojo/supermarket/HtmlReceiptFormatter.java new file mode 100644 index 0000000..a044a59 --- /dev/null +++ b/java/src/main/java/dojo/supermarket/HtmlReceiptFormatter.java @@ -0,0 +1,82 @@ +package dojo.supermarket; + +import dojo.supermarket.model.*; + +import java.util.Locale; + +public class HtmlReceiptFormatter implements ReceiptFormatter { + + @Override + public String formatReceipt(Receipt receipt, int columns) { + StringBuilder result = new StringBuilder(); + + result.append("\n\n"); + result.append("

Receipt

\n"); + result.append("\n"); + + for (ReceiptItem item : receipt.getItems()) { + String receiptItem = presentReceiptItem(item); + result.append(receiptItem); + } + + for (Discount discount : receipt.getDiscounts()) { + String discountPresentation = presentDiscount(discount); + result.append(discountPresentation); + } + + result.append(presentTotal(receipt)); + result.append("
\n\n"); + return result.toString(); + } + + private String presentReceiptItem(ReceiptItem item) { + StringBuilder result = new StringBuilder(); + result.append(" \n"); + result.append(" ").append(escapeHtml(item.getProduct().getName())).append("\n"); + result.append(" ").append(presentPrice(item.getTotalPrice())).append("\n"); + result.append(" \n"); + + if (item.getQuantity() != 1) { + result.append(" \n"); + result.append(" ").append(presentPrice(item.getPrice())) + .append(" * ").append(presentQuantity(item)).append("\n"); + result.append(" \n"); + } + return result.toString(); + } + + private String presentDiscount(Discount discount) { + String name = discount.getDescription() + "(" + discount.getProduct().getName() + ")"; + String value = presentPrice(discount.getDiscountAmount()); + + return " \n" + + " " + escapeHtml(name) + "\n" + + " " + value + "\n" + + " \n"; + } + + private String presentTotal(Receipt receipt) { + return " \n" + + " Total:\n" + + " " + presentPrice(receipt.getTotalPrice()) + "\n" + + " \n"; + } + + private static String presentPrice(double price) { + return String.format(Locale.UK, "%.2f", price); + } + + private static String presentQuantity(ReceiptItem item) { + return ProductUnit.EACH.equals(item.getProduct().getUnit()) + ? String.format("%d", (int)item.getQuantity()) + : String.format(Locale.UK, "%.3f", item.getQuantity()); + } + + private static String escapeHtml(String text) { + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/ReceiptFormatter.java b/java/src/main/java/dojo/supermarket/ReceiptFormatter.java new file mode 100644 index 0000000..e8d131e --- /dev/null +++ b/java/src/main/java/dojo/supermarket/ReceiptFormatter.java @@ -0,0 +1,7 @@ +package dojo.supermarket; + +import dojo.supermarket.model.Receipt; + +public interface ReceiptFormatter { + String formatReceipt(Receipt receipt, int columns); +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/ReceiptPrinter.java b/java/src/main/java/dojo/supermarket/ReceiptPrinter.java new file mode 100644 index 0000000..5a802cb --- /dev/null +++ b/java/src/main/java/dojo/supermarket/ReceiptPrinter.java @@ -0,0 +1,30 @@ +package dojo.supermarket; + +import dojo.supermarket.model.*; + +public class ReceiptPrinter { + + private final int columns; + private final ReceiptFormatter formatter; + + public ReceiptPrinter() { + this(40, new TextReceiptFormatter()); + } + + public ReceiptPrinter(int columns) { + this(columns, new TextReceiptFormatter()); + } + + public ReceiptPrinter(ReceiptFormatter formatter) { + this(40, formatter); + } + + public ReceiptPrinter(int columns, ReceiptFormatter formatter) { + this.columns = columns; + this.formatter = formatter; + } + + public String printReceipt(Receipt receipt) { + return formatter.formatReceipt(receipt, columns); + } +} diff --git a/java/src/test/java/dojo/supermarket/ReceiptPrinter.java b/java/src/main/java/dojo/supermarket/TextReceiptFormatter.java similarity index 71% rename from java/src/test/java/dojo/supermarket/ReceiptPrinter.java rename to java/src/main/java/dojo/supermarket/TextReceiptFormatter.java index 070a13f..4e155ce 100644 --- a/java/src/test/java/dojo/supermarket/ReceiptPrinter.java +++ b/java/src/main/java/dojo/supermarket/TextReceiptFormatter.java @@ -4,39 +4,32 @@ import java.util.Locale; -public class ReceiptPrinter { +public class TextReceiptFormatter implements ReceiptFormatter { - private final int columns; - - public ReceiptPrinter() { - this(40); - } - - public ReceiptPrinter(int columns) { - this.columns = columns; - } - - public String printReceipt(Receipt receipt) { + @Override + public String formatReceipt(Receipt receipt, int columns) { StringBuilder result = new StringBuilder(); + for (ReceiptItem item : receipt.getItems()) { - String receiptItem = presentReceiptItem(item); + String receiptItem = presentReceiptItem(item, columns); result.append(receiptItem); } + for (Discount discount : receipt.getDiscounts()) { - String discountPresentation = presentDiscount(discount); + String discountPresentation = presentDiscount(discount, columns); result.append(discountPresentation); } result.append("\n"); - result.append(presentTotal(receipt)); + result.append(presentTotal(receipt, columns)); return result.toString(); } - private String presentReceiptItem(ReceiptItem item) { + private String presentReceiptItem(ReceiptItem item, int columns) { String totalPricePresentation = presentPrice(item.getTotalPrice()); String name = item.getProduct().getName(); - String line = formatLineWithWhitespace(name, totalPricePresentation); + String line = formatLineWithWhitespace(name, totalPricePresentation, columns); if (item.getQuantity() != 1) { line += " " + presentPrice(item.getPrice()) + " * " + presentQuantity(item) + "\n"; @@ -44,23 +37,23 @@ private String presentReceiptItem(ReceiptItem item) { return line; } - private String presentDiscount(Discount discount) { + private String presentDiscount(Discount discount, int columns) { String name = discount.getDescription() + "(" + discount.getProduct().getName() + ")"; String value = presentPrice(discount.getDiscountAmount()); - return formatLineWithWhitespace(name, value); + return formatLineWithWhitespace(name, value, columns); } - private String presentTotal(Receipt receipt) { + private String presentTotal(Receipt receipt, int columns) { String name = "Total: "; String value = presentPrice(receipt.getTotalPrice()); - return formatLineWithWhitespace(name, value); + return formatLineWithWhitespace(name, value, columns); } - private String formatLineWithWhitespace(String name, String value) { + private String formatLineWithWhitespace(String name, String value, int columns) { StringBuilder line = new StringBuilder(); line.append(name); - int whitespaceSize = this.columns - name.length() - value.length(); + int whitespaceSize = columns - name.length() - value.length(); for (int i = 0; i < whitespaceSize; i++) { line.append(" "); } @@ -78,4 +71,4 @@ private static String presentQuantity(ReceiptItem item) { ? String.format("%d", (int)item.getQuantity()) : String.format(Locale.UK, "%.3f", item.getQuantity()); } -} +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/model/DiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/DiscountStrategy.java new file mode 100644 index 0000000..414b016 --- /dev/null +++ b/java/src/main/java/dojo/supermarket/model/DiscountStrategy.java @@ -0,0 +1,5 @@ +package dojo.supermarket.model; + +public interface DiscountStrategy { + Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument); +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/model/DiscountStrategyFactory.java b/java/src/main/java/dojo/supermarket/model/DiscountStrategyFactory.java new file mode 100644 index 0000000..c6eb91a --- /dev/null +++ b/java/src/main/java/dojo/supermarket/model/DiscountStrategyFactory.java @@ -0,0 +1,18 @@ +package dojo.supermarket.model; + +public class DiscountStrategyFactory { + public static DiscountStrategy createStrategy(SpecialOfferType offerType) { + switch (offerType) { + case THREE_FOR_TWO: + return new ThreeForTwoDiscountStrategy(); + case TEN_PERCENT_DISCOUNT: + return new TenPercentDiscountStrategy(); + case TWO_FOR_AMOUNT: + return new TwoForAmountDiscountStrategy(); + case FIVE_FOR_AMOUNT: + return new FiveForAmountDiscountStrategy(); + default: + throw new IllegalArgumentException("Unknown offer type: " + offerType); + } + } +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/model/FiveForAmountDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/FiveForAmountDiscountStrategy.java new file mode 100644 index 0000000..cdc7363 --- /dev/null +++ b/java/src/main/java/dojo/supermarket/model/FiveForAmountDiscountStrategy.java @@ -0,0 +1,15 @@ +package dojo.supermarket.model; + +public class FiveForAmountDiscountStrategy implements DiscountStrategy { + @Override + public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) { + int quantityAsInt = (int) quantity; + if (quantityAsInt >= 5) { + int x = 5; + int numberOfXs = quantityAsInt / x; + double discountTotal = unitPrice * quantity - (argument * numberOfXs + quantityAsInt % 5 * unitPrice); + return new Discount(product, x + " for " + argument, -discountTotal); + } + return null; + } +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/model/ShoppingCart.java b/java/src/main/java/dojo/supermarket/model/ShoppingCart.java index 9293086..bfe5a78 100644 --- a/java/src/main/java/dojo/supermarket/model/ShoppingCart.java +++ b/java/src/main/java/dojo/supermarket/model/ShoppingCart.java @@ -38,37 +38,13 @@ void handleOffers(Receipt receipt, Map offers, SupermarketCatalo if (offers.containsKey(p)) { Offer offer = offers.get(p); double unitPrice = catalog.getUnitPrice(p); - int quantityAsInt = (int) quantity; - Discount discount = null; - int x = 1; - if (offer.offerType == SpecialOfferType.THREE_FOR_TWO) { - x = 3; - - } else if (offer.offerType == SpecialOfferType.TWO_FOR_AMOUNT) { - x = 2; - if (quantityAsInt >= 2) { - double total = offer.argument * (quantityAsInt / x) + quantityAsInt % 2 * unitPrice; - double discountN = unitPrice * quantity - total; - discount = new Discount(p, "2 for " + offer.argument, -discountN); - } - - } if (offer.offerType == SpecialOfferType.FIVE_FOR_AMOUNT) { - x = 5; - } - int numberOfXs = quantityAsInt / x; - if (offer.offerType == SpecialOfferType.THREE_FOR_TWO && quantityAsInt > 2) { - double discountAmount = quantity * unitPrice - ((numberOfXs * 2 * unitPrice) + quantityAsInt % 3 * unitPrice); - discount = new Discount(p, "3 for 2", -discountAmount); - } - if (offer.offerType == SpecialOfferType.TEN_PERCENT_DISCOUNT) { - discount = new Discount(p, offer.argument + "% off", -quantity * unitPrice * offer.argument / 100.0); - } - if (offer.offerType == SpecialOfferType.FIVE_FOR_AMOUNT && quantityAsInt >= 5) { - double discountTotal = unitPrice * quantity - (offer.argument * numberOfXs + quantityAsInt % 5 * unitPrice); - discount = new Discount(p, x + " for " + offer.argument, -discountTotal); - } - if (discount != null) + + DiscountStrategy strategy = DiscountStrategyFactory.createStrategy(offer.offerType); + Discount discount = strategy.calculateDiscount(p, quantity, unitPrice, offer.argument); + + if (discount != null) { receipt.addDiscount(discount); + } } } } diff --git a/java/src/main/java/dojo/supermarket/model/TenPercentDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/TenPercentDiscountStrategy.java new file mode 100644 index 0000000..837d5a9 --- /dev/null +++ b/java/src/main/java/dojo/supermarket/model/TenPercentDiscountStrategy.java @@ -0,0 +1,8 @@ +package dojo.supermarket.model; + +public class TenPercentDiscountStrategy implements DiscountStrategy { + @Override + public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) { + return new Discount(product, argument + "% off", -quantity * unitPrice * argument / 100.0); + } +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/model/ThreeForTwoDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/ThreeForTwoDiscountStrategy.java new file mode 100644 index 0000000..3d856fb --- /dev/null +++ b/java/src/main/java/dojo/supermarket/model/ThreeForTwoDiscountStrategy.java @@ -0,0 +1,15 @@ +package dojo.supermarket.model; + +public class ThreeForTwoDiscountStrategy implements DiscountStrategy { + @Override + public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) { + int quantityAsInt = (int) quantity; + if (quantityAsInt > 2) { + int x = 3; + int numberOfXs = quantityAsInt / x; + double discountAmount = quantity * unitPrice - ((numberOfXs * 2 * unitPrice) + quantityAsInt % 3 * unitPrice); + return new Discount(product, "3 for 2", -discountAmount); + } + return null; + } +} \ No newline at end of file diff --git a/java/src/main/java/dojo/supermarket/model/TwoForAmountDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/TwoForAmountDiscountStrategy.java new file mode 100644 index 0000000..472ca15 --- /dev/null +++ b/java/src/main/java/dojo/supermarket/model/TwoForAmountDiscountStrategy.java @@ -0,0 +1,15 @@ +package dojo.supermarket.model; + +public class TwoForAmountDiscountStrategy implements DiscountStrategy { + @Override + public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) { + int quantityAsInt = (int) quantity; + if (quantityAsInt >= 2) { + int x = 2; + double total = argument * (quantityAsInt / x) + quantityAsInt % 2 * unitPrice; + double discountN = unitPrice * quantity - total; + return new Discount(product, "2 for " + argument, -discountN); + } + return null; + } +} \ No newline at end of file diff --git a/java/src/test/java/dojo/supermarket/ReceiptFormatterTest.java b/java/src/test/java/dojo/supermarket/ReceiptFormatterTest.java new file mode 100644 index 0000000..1d5266e --- /dev/null +++ b/java/src/test/java/dojo/supermarket/ReceiptFormatterTest.java @@ -0,0 +1,115 @@ +package dojo.supermarket; + +import dojo.supermarket.model.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class ReceiptFormatterTest { + + private Receipt createTestReceipt() { + SupermarketCatalog catalog = new FakeCatalog(); + Product apples = new Product("apples", ProductUnit.KILO); + catalog.addProduct(apples, 1.99); + Product toothbrush = new Product("toothbrush", ProductUnit.EACH); + catalog.addProduct(toothbrush, 0.99); + + Teller teller = new Teller(catalog); + teller.addSpecialOffer(SpecialOfferType.TEN_PERCENT_DISCOUNT, toothbrush, 10.0); + + ShoppingCart cart = new ShoppingCart(); + cart.addItemQuantity(apples, 2.5); + cart.addItemQuantity(toothbrush, 1.0); + + return teller.checksOutArticlesFrom(cart); + } + + @Test + void textFormatter_shouldFormatReceiptCorrectly() { + Receipt receipt = createTestReceipt(); + TextReceiptFormatter formatter = new TextReceiptFormatter(); + + String result = formatter.formatReceipt(receipt, 40); + + assertTrue(result.contains("apples")); + assertTrue(result.contains("toothbrush")); + assertTrue(result.contains("10.0% off")); + assertTrue(result.contains("Total:")); + // Should be plain text format + assertFalse(result.contains("")); + assertFalse(result.contains("")); + } + + @Test + void htmlFormatter_shouldFormatReceiptCorrectly() { + Receipt receipt = createTestReceipt(); + HtmlReceiptFormatter formatter = new HtmlReceiptFormatter(); + + String result = formatter.formatReceipt(receipt, 40); + + // Should contain HTML structure + assertTrue(result.contains("")); + assertTrue(result.contains("")); + assertTrue(result.contains("
")); + assertTrue(result.contains("")); + + // Should contain receipt data + assertTrue(result.contains("apples")); + assertTrue(result.contains("toothbrush")); + assertTrue(result.contains("10.0% off")); + assertTrue(result.contains("Total:")); + + // Should escape HTML characters + assertTrue(result.contains("<") || !result.contains("