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;
billedShopppingItems.Add(new BilledShopppingItem(shoppingItem, 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}");
Console.WriteLine($"Total {TotalBillAmount}");
}
public List<BilledShopppingItem> ProcessedShoppingCart { get; }
public decimal TotalBillAmount { get; }
public decimal TotalSalesTax { get; }
}
Types of products
[Flags]
public enum ItemTypes
{
None = 0,
Book = 1 << 0,
Food = 1 << 1,
Medical = 1 << 2,
Others = 1 << 3,
SalesTaxEmptedItemTypes = Book | Food | Medical
}
I know there access modifiers are pretty liberal and naming conventions isn't the best.
However, I'm looking if this type of solution is extendable especially when we have more products and more and more tax rules.
I'm not very keen on putting isImport
inside of Product
and that depends on the geography.
The code is also on github if thats easier to read though. https://github.com/benneyman/SalesTax
IsApplicableTo
makes sense only internally so it might be a better idea to make it aprotected abstract
method of anabstract class ProductTax
. I don't think anyone else would need it but the derived types. \$\endgroup\$ImportDuty
is aSalesTax
- and I'm not sure if in futureImportDuty
class could have more functionality thanSalesTax
class. So we can makeSalesTax
methods virtual while implementing the interfaceIProductTax
and inheritImportDuty
fromSalesTax
by overriding parent class methods. Also we can make virtual properties for % values like0.1m
and Tax classes can override them (this will be more OO way). This will also make calculation inCompute
method more intuitive. \$\endgroup\$