# Straightforward tax calculator OO design

Similar questions have been asked here before and my solution is pretty much in line with some of the answers. I wanted to work on this a bit.

Scenario

Basic sales tax is applicable at a rate of 10% on all goods – except books, food and medical products, which are exempt. Import duty is an additional sales tax applicable on all imported goods at a rate of 5%, with no exemptions. The tax rates or item categories may change in future.

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 (n*p/100 rounded up to the nearest 0.05) amount of sales tax.

I defined a interface IProductTax

public interface IProductTax
{
bool IsApplicableTo(IProduct product);
decimal Compute(IProduct product);
}


which each tax class, Sales, Import must implement. IsApplicableTo checks if that tax is valid for the input Product and Compute computes the tax on this product.

The TaxProcessor class takes in a list of taxes during construction and for each product thereby passed it runs the entire tax rules and check if it is valid for the product and then applies it.

public class TaxProcessor : ITaxProcessor
{
List<IProductTax> SalesTaxes;
public TaxProcessor()
{
SalesTaxes = new List<IProductTax>()
{
new SalesTax(),
new ImportDuty()
};
}
public decimal ComputeTotalTax(IProduct product)
{
decimal computedSalesTax = 0;
foreach (var tax in SalesTaxes)
{
computedSalesTax += tax.Compute(product);
}

return computedSalesTax;
}
}


One drawback of this approach is that the single product will pretty much run through the entire set of possible taxes to compute the taxes. I'm not sure how to overcome this in a OO way.

public class SalesTax : IProductTax
{
public decimal Compute(IProduct product)
{
if (IsApplicableTo(product))
return product.Price * 0.1m;
return 0m;
}

public bool IsApplicableTo(IProduct product)
{
return !product.ItemType.HasFlag(ItemTypes.SalesTaxEmptedItemTypes);
}
}

public class ImportDuty : IProductTax
{
public decimal Compute(IProduct product)
{
if (IsApplicableTo(product))
return product.Price * 0.05m;
return 0m;
}

public bool IsApplicableTo(IProduct product)
{
return product.IsImport;
}
}


The BillProcessor class return a Receipt DTO after processing the shopping cart

public class BillProcessor
{
public BillProcessor(ITaxProcessor salesTaxProcess)
{
SalesTaxProcessor = salesTaxProcess;
}

public Reciept ProcessCart(List<ShoppingItem> shoppingCart)
{
var billedShopppingItems = new List<BilledShopppingItem>();
decimal totalTaxForCart = 0;
decimal totalBilledAmount = 0;
foreach (var shoppingItem in shoppingCart)
{
decimal individualTax = SalesTaxProcessor.ComputeTotalTax(shoppingItem.Product);
decimal taxForAllProducts = individualTax * shoppingItem.Quantity;

decimal totalPrice = shoppingItem.Product.Price * shoppingItem.Quantity;
decimal totalPriceAfterTax = totalPrice + taxForAllProducts;

totalPriceAfterTax));

totalTaxForCart += taxForAllProducts;
totalBilledAmount += totalPriceAfterTax;
}
return new Reciept(billedShopppingItems, totalBilledAmount, totalTaxForCart);
}

public ITaxProcessor SalesTaxProcessor { get; }
}


And a few DTOs and enum

Shopping Item

public class ShoppingItem
{
public ShoppingItem(IProduct product, int quantity)
{
Product = product;
Quantity = quantity;
}

public IProduct Product { get; }
public int Quantity { get; }
}


Product

public class Product : IProduct
{
public Product(string name, decimal price, bool isImport, ItemTypes itemType)
{
Name = name;
Price = price;
IsImport = isImport;
ItemType = itemType;
}

public string Name { get; }
public decimal Price { get; }
public bool IsImport { get; }
public ItemTypes ItemType { get; }
}


BilledShoppingItem

public class BilledShopppingItem
{
public BilledShopppingItem(ShoppingItem shoppingItem, decimal tax, decimal totalPrice)
{
ShoppingItem = shoppingItem;
Tax = tax;
TotalPrice = totalPrice;
}

public ShoppingItem ShoppingItem { get; }
public decimal Tax { get; }
public decimal TotalPrice { get; }
}


Reciept

public class Reciept
{
public Reciept(List<BilledShopppingItem> processedShoppingCart, decimal totalBillAmount, decimal totalSalesTax)
{
ProcessedShoppingCart = processedShoppingCart;
TotalBillAmount = totalBillAmount;
TotalSalesTax = totalSalesTax;
}

public void PrintBill()
{
foreach (var processedItem in ProcessedShoppingCart)
{
Console.WriteLine($"{processedItem.ShoppingItem.Product.Name} { processedItem.TotalPrice }"); } Console.WriteLine($"Sales Taxes {TotalSalesTax}");