4
\$\begingroup\$

Although this question has been asked a lot of times, I was hoping for a feedback on my approach.

Tax Calculator Basic sales tax is applicable at a rate of 10% on all goods, except books, food, and medical products that are exempt. Import duty is an additional sales tax applicable on all imported goods at a rate of 5%, with no exemptions.

When I purchase items I receive a receipt which lists the name of all the items and their price (including tax), finishing with the total cost of the items, and the total amounts of sales taxes paid. The rounding rules for sales tax are that for a tax rate of n%, a shelf price of p contains (np/100 rounded up to the nearest 0.05) amount of sales tax.

Write an application that prints out the receipt details ...

Input 1:

1 book at 12.49  
1 music CD at 14.99   
1 chocolate bar at 0.85 

Output 1:

1 book: 12.49  
1 music CD: 16.49  
1 chocolate bar: 0.85
Sales Taxes: 1.50   
Total: 29.83  

Input 2:

1 imported box of chocolates at 10.00  
1 imported bottle of perfume at 47.50  

Output 2:

1 imported box of chocolates: 10.50  
1 imported bottle of perfume: 54.65  
Sales Taxes: 7.65  
Total: 65.15  

Input 3:

1 imported bottle of perfume at 27.99  
1 bottle of perfume at 18.99  
1 packet of headache pills at 9.75  
1 box of imported chocolates at 11.25  

Output 3:

1 imported bottle of perfume: 32.19  
1 bottle of perfume: 20.89  
1 packet of headache pills: 9.75  
1 imported box of chocolates: 11.85  
Sales Taxes: 6.70  
Total: 74.68  

TaxCalculatorApplication

class TaxCalculatorApplication {

    Receipt generateReceipt(String[] inputs) {
        List<Item> items = ItemFactory.from(inputs);
        return new Receipt(items);
    }
}

ItemFactory

public class ItemFactory {

    public static List<Item> from(String[] inputs) {
        return Arrays.stream(inputs)
                .map(ItemFactory::from)
                .collect(Collectors.toList());
    }

    private static Item from(String input) {
        Item item = ItemAdapter.createItemFromString(input);
        ItemTaxCalculator.applyTaxes(item);
        return item;
    }

}

ItemAdapter

public class ItemAdapter {
    private static final String ITEM_ENTRY_REGEX = "(\\d+) ([\\w\\s]* )at (\\d+.\\d{2})";

    public static Item createItemFromString(String input) {
        Pattern pattern = Pattern.compile(ITEM_ENTRY_REGEX);
        Matcher matcher = pattern.matcher(input);
        matcher.find();

        return new Item(matcher.group(1), matcher.group(2), matcher.group(3));
    }
}

ItemTaxCalculator

public class ItemTaxCalculator {
    private static final double SALES_TAX_PERCENT = 0.1;
    private static final double ADDITIONAL_SALES_TAX_PERCENT = 0.05;

    public static void applyTaxes(Item item) {
        if (!item.isExempted()) {
            item.setBasicSalesTaxAmount(SALES_TAX_PERCENT);
        }

        if (item.isImported()) {
            item.setAdditionalSalesTax(ADDITIONAL_SALES_TAX_PERCENT);
        }
    }
}

Receipt

public class Receipt {
    private double totalSalesTax = 0.0;
    private double totalAmount = 0.0;
    private String itemDetails;

    public Receipt(List<Item> items) {

        StringBuilder itemDetails = new StringBuilder();

        for (Item item : items) {
            itemDetails.append(item.toString()).append("\n");
            totalSalesTax += item.getTaxAmount();
            totalAmount += item.getFinalPrice();
        }
        totalAmount = MathUtils.roundOffAmount(totalAmount);
        totalSalesTax = MathUtils.roundOffAmount(totalSalesTax);

        this.itemDetails = itemDetails.toString();
    }

    public double getTotalAmount() {
        return totalAmount;
    }

    public double getTotalSalesTax() {
        return totalSalesTax;
    }

    @Override
    public String toString() {
        return "Receipt" + "\n"
                + itemDetails
                + "Sales Taxes: " + totalSalesTax + "\n"
                + "Total: " + totalAmount
                +"\n*******************************\n";
    }
}

Item

public class Item {

    private String name;
    private int quantity;
    private double basePrice;
    private double basicSalesTaxAmount;
    private double additionalSalesTaxAmount;

    public Item(String quantity, String name, String basePrice) {
        this.name = name;
        this.quantity = Integer.valueOf(quantity);
        this.basePrice = Double.valueOf(basePrice);
    }

    public double getFinalPrice() {
        return MathUtils.roundOffAmount(quantity * basePrice + getTaxAmount());
    }

    public double getTaxAmount() {
        return quantity * (basicSalesTaxAmount + additionalSalesTaxAmount);
    }

    public boolean isImported() {
        return name.contains("imported");
    }

    public boolean isExempted() {
        return Stream.of("book", "chocolate", "pill")
                .anyMatch(exemptedItem -> name.contains(exemptedItem));
    }

    public void setBasicSalesTaxAmount(double factor) {
        basicSalesTaxAmount = basePrice * factor;
    }

    public void setAdditionalSalesTax(double additionalSalesTaxPercent) {
        additionalSalesTaxAmount = MathUtils.roundOffTax(basePrice * additionalSalesTaxPercent);
    }

    public String toString() {
        return String.valueOf(quantity) +
                " " +
                name +
                " : " +
                getFinalPrice();
    }
}

MathUtils

public class MathUtils {

    public static double roundOffTax(double number) {
        return Math.ceil(number * 20) / 20;
    }

    public static double roundOffAmount(double number) {
        return Math.round(number * 100.0) / 100.0;
    }
}

The whole project is available at this link.

\$\endgroup\$
  • 1
    \$\begingroup\$ Obvious hole is the categorization of items. Items which erroneously get tax exempt status include “bookmarks” and “pillows”, and “chocolate flavoured lipstick”. \$\endgroup\$ – AJNeufeld Aug 28 '18 at 20:43
1
\$\begingroup\$

First don't mix Item class with Recipe(ShoppingCart) class. the quantity should be part of RecipeItem(ShoppingCartItem), as well as Tax&Cost. The TotalTax&TotalCost should be part of ShoppingCart.

My Item class, has only Name&Price & some readonly properties(getters) like IsImported (in c#):

class Product
{
    static readonly IDictionary<ProductType, string[]> productType_Identifiers = 
        new Dictionary<ProductType, string[]>
        {
            {ProductType.Food, new[]{ "chocolate", "chocolates" }},
            {ProductType.Medical, new[]{ "pills" }},
            {ProductType.Book, new[]{ "book" }}
        };

    public decimal ShelfPrice { get; set; }

    public string Name { get; set; }

    public bool IsImported { get { return Name.Contains("imported "); } }

    public bool IsOf(ProductType productType)
    {
        return productType_Identifiers.ContainsKey(productType) &&
            productType_Identifiers[productType].Any(x => Name.Contains(x));
    }
}

class ShoppringCart
{
    public IList<ShoppringCartItem> CartItems { get; set; }

    public decimal TotalTax { get { return CartItems.Sum(x => x.Tax); } }

    public decimal TotalCost { get { return CartItems.Sum(x => x.Cost); } }
}

class ShoppringCartItem
{
    public Product Product { get; set; }

    public int Quantity { get; set; }

    public decimal Tax { get; set; }

    public decimal Cost { get { return Quantity * (Tax + Product.ShelfPrice); } }
}

Your tax calculation part is coupled with Item. An item doesn't define tax policies it's Government's job. Based on the problem's description, there are two kind of Sales Taxes: Basic and Duty taxes. You can use Template Method Design Pattern to implement it:

abstract class SalesTax
{
    abstract public bool IsApplicable(Product item);
    abstract public decimal Rate { get; }

    public decimal Calculate(Product item)
    {
        if (IsApplicable(item))
        {
            //sales tax are that for a tax rate of n%, a shelf price of p contains (np/100)
            var tax = (item.ShelfPrice * Rate) / 100;

            //The rounding rules: rounded up to the nearest 0.05
            tax = Math.Ceiling(tax / 0.05m) * 0.05m;

            return tax;
        }

        return 0;
    }
}

class BasicSalesTax : SalesTax
{
    private ProductType[] _taxExcemptions = new[] 
    { 
        ProductType.Food, ProductType.Medical, ProductType.Book 
    };

    public override bool IsApplicable(Product item)
    {
        return !(_taxExcemptions.Any(x => item.IsOf(x)));
    }

    public override decimal Rate { get { return 10.00M; } }
}

class ImportedDutySalesTax : SalesTax
{
    public override bool IsApplicable(Product item)
    {
        return item.IsImported;
    }

    public override decimal Rate { get { return 5.00M; } }
}

And finally a class to apply taxes:

class TaxCalculator
{
    private SalesTax[] _Taxes = new SalesTax[] { new BasicSalesTax(), new ImportedDutySalesTax() };

    public void Calculate(ShoppringCart shoppringCart)
    {
        foreach (var cartItem in shoppringCart.CartItems)
        {
            cartItem.Tax = _Taxes.Sum(x => x.Calculate(cartItem.Product));
        }

    }
}

You can try them out at MyFiddle.

\$\endgroup\$
  • \$\begingroup\$ This is great. Thank you for pointing out the obvious points that I completely missed. :) \$\endgroup\$ – Ankit Soni Nov 3 '18 at 1:48
  • \$\begingroup\$ it's my pleasure :) \$\endgroup\$ – Daniel B Nov 3 '18 at 22:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.