Basket class:
public class Basket {
private final ProductService productService;
private final TaxService taxService;
private final Map<String, LineItem> items;
public Basket(ProductService productService, TaxService taxService) {
this.items = new HashMap<>();
this.productService = productService;
this.taxService = taxService;
}
public void addItems(String... names) throws ProductNotFoundException {
for(String name : names) {
if (items.containsKey(name.toLowerCase())) {
updateExistingLineItem(name);
} else {
addNewLineItem(name);
}
}
}
private void updateExistingLineItem(String name) throws ProductNotFoundException {
Cost cost = calculateCost(name);
items.get(name.toLowerCase()).update(cost);
}
private void addNewLineItem(String name) throws ProductNotFoundException {
Cost cost = calculateCost(name);
LineItem lineItem = new LineItem(name, cost);
items.put(name.toLowerCase(), lineItem);
}
private Cost calculateCost(String name) throws ProductNotFoundException {
BigDecimal gross = productService.price(name);
BigDecimal tax = taxService.calculateTax(name);
return new Cost(gross, tax);
}
public BigDecimal getTotalPrice() {
return items.values()
.stream().map(LineItem::getNetPrice)
.reduce(ZERO, BigDecimal::add);
}
public BigDecimal getSalesTax() {
return items.values()
.stream().map(LineItem::getTax)
.reduce(ZERO, BigDecimal::add);
}
@Override
public String toString() {
String formattedItems = items.values().toString()
.replace(",", "")
.replace("[", "")
.replace("]", "")
.trim();
return formattedItems + "\nSales Taxes: " + getSalesTax() + "\nTotal: " + getTotalPrice() + "\n";
}
}
Cost class:
public class Cost {
private final BigDecimal gross;
private final BigDecimal tax;
public Cost(BigDecimal gross, BigDecimal tax) {
this.gross = gross;
this.tax = tax;
}
public Cost add(Cost cost) {
return new Cost(this.gross.add(cost.gross), this.tax.add(cost.tax));
}
public BigDecimal getGross() {
return gross;
}
public BigDecimal getTax() {
return tax;
}
}
LineItem class:
public class LineItem {
private final String name;
private Integer quantity = 0;
private Cost cost;
public LineItem(String name, Integer quantity, Cost cost) {
this.name = name;
this.quantity = quantity;
this.cost = cost;
}
public LineItem(String name, Cost cost) {
this(name, 1, cost);
}
public void update(Cost cost) {
this.cost = this.cost.add(cost);
this.quantity++;
}
public BigDecimal getTax() {
return this.cost.getTax();
}
public BigDecimal getNetPrice() {
return cost.getGross().add(getTax());
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(" ");
joiner.add(quantity.toString());
joiner.add(name);
joiner.add(":");
joiner.add(getNetPrice().toString());
return "\n" + joiner.toString();
}
}
Product class:
public class Product {
private String name;
private BigDecimal price;
private List<TaxBand> applicableTaxes;
public Product(String name, String price, TaxBand... applicableTaxes) {
this.name = name;
this.price = new BigDecimal(price);
this.applicableTaxes = asList(applicableTaxes);
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
public List<TaxBand> getApplicableTaxes() {
return applicableTaxes;
}
}
ProductService class:
public class ProductService {
private ProductRepository productRepository;
public ProductService(ProductRepository productRepository, Product... items) {
this.productRepository = productRepository;
Arrays.stream(items).forEach(item -> this.productRepository.add(item));
}
public Product getProductByName(String name) throws ProductNotFoundException {
return productRepository.findByProductName(name);
}
public BigDecimal price(String name) throws ProductNotFoundException {
return productRepository.price(name);
}
}
MapProductRepository class:
public class MapProductRepository implements ProductRepository {
private final Map<String, Product> products = new HashMap<>();
@Override
public Product findByProductName(String name) throws ProductNotFoundException {
Product product = products.get(name.toLowerCase());
if(product == null) {
throw new ProductNotFoundException(name + " not supported");
}
return product;
}
@Override
public BigDecimal price(String name) throws ProductNotFoundException{
return findByProductName(name.toLowerCase()).getPrice();
}
@Override
public void add(Product product) {
products.put(product.getName().toLowerCase(), product);
}
}
ProductRepository interface:
public interface ProductRepository {
Product findByProductName(String name) throws ProductNotFoundException;
BigDecimal price(String name) throws ProductNotFoundException;
void add(Product product);
}
public class CompactDiskTax implements Tax {
public static final BigDecimal TAX = new BigDecimal("1.25");
@Override
public BigDecimal calculate(Product product) {
if(!product.getApplicableTaxes().contains(CD)){
return ZERO;
}
return TAX;
}
}
MiscTax class:
public class MiscTax implements Tax {
public static final BigDecimal ROUND_TO = new BigDecimal("0.05");
public static final BigDecimal TAX_RATE = new BigDecimal("17.5");
public BigDecimal calculate(Product product) {
if(!product.getApplicableTaxes().contains(TaxBand.MISC)){
return ZERO;
}
return percentage(product.getPrice(), TAX_RATE);
}
private BigDecimal percentage(BigDecimal base, BigDecimal percentage){
return round(base.multiply(percentage).divide(new BigDecimal(100)));
}
private BigDecimal round(BigDecimal value) {
BigDecimal divided = value.divide(ROUND_TO, 0, UP);
return divided.multiply(ROUND_TO);
}
}
Tax interface:
public interface Tax {
BigDecimal calculate(Product product);
}
public enum TaxBand {
MEDICAL, CD, MISC
}
public class TaxService {
private ProductService productService;
private List<Tax> taxes;
public TaxService(ProductService productService, Tax... taxes) {
this.productService = productService;
this.taxes = Arrays.asList(taxes);
}
public BigDecimal calculateTax(String productName) throws ProductNotFoundException {
Product product = productService.getProductByName(productName);
return taxes.stream()
.map(tax -> tax.calculate(product))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}