patterncsharpMinor
Case Study - Basic sales tax
Viewed 0 times
casestudytaxsalesbasic
Problem
The solution was to be assessed, not just on completing the task but also on code quality and design, presence of testing, robustness, maintainability etc.
Problem Statement
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.
Samples:
Here's what I submitted:
```
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
Problem Statement
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.
Samples:
- Case 1:
- Input:
- 1 book at 12.49
- 1 music CD at 14.99
- 1 chocolate bar at 0.85
- Output:
- 1 book: $12.49
- 1 music CD: $16.49
- 1 chocolate bar: $0.85
- Sales Taxes: $1.50
- Total: $29.83
- Case 2:
- Input:
- 1 imported box of chocolates at 10.00
- 1 imported bottle of perfume at 47.50
- Output:
- 1 imported box of chocolates: 10.50
- 1 imported bottle of perfume: 54.65
- Sales Taxes: 7.65
- Total: 65.15
- Case 3:
- Input:
- 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:
- 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
Here's what I submitted:
```
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
Solution
I think part of the problem was that your object model didn't implement the proper Separation of Concerns. Each product object was responsible for knowing whether it was imported, and if it was imported what the tax rate was. Plus it knew whether or not it was tax exempt. What if medical supplies become taxable in the future? You would need to change every line of code that instantiates a medical supply. Yikes.
This also introduces the need for multiple
I think a more appropriate object model would be:
First, let's explore the
Product class
It needs a name, a price, where it was manufactured (its origin) and its type. Furthermore, you need to calculate the tax on the product, which could be a combination of a sales tax and import duty, but not necessarily both. It could also be one or the other.
The
Now, an imported book:
The tax on each item will be different:
We saw in the
ProductType class
The
The available types are:
We know that books, food and medical are tax exempt, so we pass the
The
ProductOrigin class
W
This also introduces the need for multiple
ifs and switches in your code to determine if something is imported or taxable. By utilizing Domain Driven Design you can largely eliminate both problems, though you introduce more layers to your object model (related: Crafting Wicked Domain Models).I think a more appropriate object model would be:
- Product: Basic information about a product, including:
- Name
- Price
- Product type
- Product origin (local or imported)
- ProductType
- Name
- Tax rate
- ProductOrigin
- Name
- Tax rate
- TaxRate
- Name
- Rate
- InvoiceItem
- Product
- Quantity
- TotalPrice
- TotalTax
- TotalAmount
First, let's explore the
Product class.Product class
It needs a name, a price, where it was manufactured (its origin) and its type. Furthermore, you need to calculate the tax on the product, which could be a combination of a sales tax and import duty, but not necessarily both. It could also be one or the other.
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
public ProductOrigin Origin { get; private set; }
public ProductType Type { get; private set; }
public Product(ProductType type, ProductOrigin origin)
{
Type = type;
Origin = origin;
}
public Product(ProductType type, ProductOrigin origin, string name, double price)
: this(type)
{
Name = name;
Price = price;
}
public double CalculateTax()
{
return Type.CalculateTax(Price) + Origin.CalculateTax(Price);
}
}The
CalculateTax method would delegate to the ProductType and ProductOrigin classes to determine the real tax. Books are tax exempt, but if they are imported books, they still have an import duty. To create a Book Product:var localBook = new Product(ProductType.Book,
ProductOrigin.Local,
"How to do something",
20.99);Now, an imported book:
var importedBook = new Product(ProductType.Book,
ProductOrigin.Imported,
"Something Else",
20.99);The tax on each item will be different:
localBook.CalculateTax(); -> 20.99 * 0.00 = 0
importedBook.CalculateTax(); -> 20.99 * 0.05 = 1.0495We saw in the
Product.CalculateTax() method that we delegate this to the ProductType and ProductOrigin classes, so let's dive a level deeper and explore that.ProductType class
The
ProductType class is responsible for knowing what its tax rate is. Plus, we don't want people to create arbitrary product types. We can limit this by creating a class with a private constructor, and providing public static readonly properties for the available product types:public class ProductType
{
public static readonly ProductType Beauty = new ProductType("Beauty", TaxRate.SalesTax);
public static readonly ProductType Book = new ProductType("Book", TaxRate.Exempt);
public static readonly ProductType Entertainment = new ProductType("Entertainment", TaxRate.SalesTax);
public static readonly ProductType Food = new ProductType("Food", TaxRate.Exempt);
public static readonly ProductType Medical = new ProductType("Medical", TaxRate.Exempt);
public string Name { get; private set; }
public TaxRate TaxRate { get; private set; }
private ProductType(string name, TaxRate taxRate)
{
Name = name;
TaxRate = taxRate;
}
public double CalculateTax(double price)
{
return TaxRate.CalculateTax(price);
}
}The available types are:
- Beauty
- Book
- Food
- Medical
We know that books, food and medical are tax exempt, so we pass the
TaxRate.Exempt object in the constructor. Also notice that the ProductType class doesn't even calculate the tax. It delegates this to the TaxRate class.The
ProductOrigin class looks very similar.ProductOrigin class
public class ProductOrigin
{
public static readonly ProductOrigin Local = new ProductOrigin("Local", TaxRate.Exempt);
public static readonly ProductOrigin Imported = new ProductOrigin("Imported", TaxRate.ImportDuty);
public string Name { get; private set; }
public TaxRate TaxRate { get; private set; }
private ProductOrigin(string name, TaxRate, taxRate)
{
Name = name;
TaxRate = taxRate;
}
public double CalculateTax(double price)
{
return TaxRate.CalculateTax(price);
}
}W
Code Snippets
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
public ProductOrigin Origin { get; private set; }
public ProductType Type { get; private set; }
public Product(ProductType type, ProductOrigin origin)
{
Type = type;
Origin = origin;
}
public Product(ProductType type, ProductOrigin origin, string name, double price)
: this(type)
{
Name = name;
Price = price;
}
public double CalculateTax()
{
return Type.CalculateTax(Price) + Origin.CalculateTax(Price);
}
}var localBook = new Product(ProductType.Book,
ProductOrigin.Local,
"How to do something",
20.99);var importedBook = new Product(ProductType.Book,
ProductOrigin.Imported,
"Something Else",
20.99);localBook.CalculateTax(); -> 20.99 * 0.00 = 0
importedBook.CalculateTax(); -> 20.99 * 0.05 = 1.0495public class ProductType
{
public static readonly ProductType Beauty = new ProductType("Beauty", TaxRate.SalesTax);
public static readonly ProductType Book = new ProductType("Book", TaxRate.Exempt);
public static readonly ProductType Entertainment = new ProductType("Entertainment", TaxRate.SalesTax);
public static readonly ProductType Food = new ProductType("Food", TaxRate.Exempt);
public static readonly ProductType Medical = new ProductType("Medical", TaxRate.Exempt);
public string Name { get; private set; }
public TaxRate TaxRate { get; private set; }
private ProductType(string name, TaxRate taxRate)
{
Name = name;
TaxRate = taxRate;
}
public double CalculateTax(double price)
{
return TaxRate.CalculateTax(price);
}
}Context
StackExchange Code Review Q#94778, answer score: 8
Revisions (0)
No revisions yet.