I am writing a simple application that prints out the details of a receipt containing items purchased by a customer. The receipt should list the number of items, the name, the final price (including taxes) of each of the item purchased, plus two extra lines showing the total amount of the receipt and the total amount of taxes.
Taxes are calculated with a rate of 17.5%, rounding the result to the upper 0.05. Please note that medical products are exempt and an additional 1.25 fixed amount is added as an extra tax on CDs.
Examples:
Input:
- One book at 29.49
- One music CD at 15.99
- List one chocolate snack at 0.75
Output:
- 1 book: 34.69
- 1 music CD: 20.04
- 1 chocolate snack: 0.90
- Sales Taxes: 9.40
- Total: 55.63
Input:
- One bottle of wine at 20.99
- One box of tooth ache pills at 4.15
- One box of pins at 11.25
- One music CD at 14.99
Output:
- 1 bottle of wine: 24.69
- 1 box of headache pills: 4.15
- 1 box of pins: 13.25
- 1 music CD: 18.89
- Sales Taxes: 9.60
- Total: 60.98
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);
}
}