title: Discount strategy in shopping cart and orders
body: <p>I am trying to implement a system that can handle multiple discounts applied to my cart/completed orders. I have applied a strategy type pattern to encapsulate the processing of the discounts within the discounts. </p>
  
  <p>I have come up with the following: an abstract discount base class with subclasses making up the concrete discounts. These are then applied to either an order/cart object and will process the contents of the order/cart when added to the cart/order. </p>
  
  <p>Would love some comments on the code attached. Various protected constructors and members marked "virtual" needed for nhibernate.</p>
  
  <p>Chev</p>
  
  <pre><code>using System;
  using System.Collections.Generic;
  using System.Linq;
  using NUnit.Framework;
  
  namespace CodeCollective.RaceFace.DiscountEngine
  {
  [TestFixture]
  public class TestAll
  {
      #region Tests
  
      [Test]
      public void Can_Add_Items_To_Cart()
      {
          Cart cart = LoadCart();
  
          // display the cart contents
          foreach (LineItem lineItem in cart.LineItems)
          {
              Console.WriteLine("Product: {0}\t Price: {1:c}\t Quantity: {2} \t Subtotal: {4:c} \t Discount: {3:c} \t| Discounts Applied: {5}", lineItem.Product.Name, lineItem.Product.Price, lineItem.Quantity, lineItem.DiscountAmount, lineItem.Subtotal, lineItem.Discounts.Count);
          }
      }
  
      [Test]
      public void Can_Add_Items_To_An_Order()
      {
          // create the cart
          Order order = new Order(new Member("Chev"));
  
          // add items to the cart
          GenericProduct hat = new GenericProduct("Cap", 110m);
          order.AddLineItem(hat, 5);
  
          EventItem race = new EventItem("Ticket", 90m);
          order.AddLineItem(race, 1);
  
          // add discounts 
          Discount percentageOff = new PercentageOffDiscount("10% off all items", 0.10m);
          percentageOff.CanBeUsedInJuntionWithOtherDiscounts = false;
          order.AddDiscount(percentageOff);
  
          Discount spendXgetY = new SpendMoreThanXGetYDiscount("Spend more than R100 get 10% off", 100m, 0.1m);
          spendXgetY.SupercedesOtherDiscounts = true;
          order.AddDiscount(spendXgetY);
  
          Discount buyXGetY = new BuyXGetYFree("Buy 4 hats get 2 hat free", new List&lt;Product&gt; { hat }, 4, 2);
          buyXGetY.CanBeUsedInJuntionWithOtherDiscounts = false;
          buyXGetY.SupercedesOtherDiscounts = true;
          order.AddDiscount(buyXGetY);
  
          // display the cart contents
          foreach (LineItem lineItem in order.LineItems)
          {
              Console.WriteLine("Product: {0}\t Price: {1:c}\t Quantity: {2} \t Subtotal: {4:c} \t Discount: {3:c} \t| Discounts Applied: {5}", lineItem.Product.Name, lineItem.Product.Price, lineItem.Quantity, lineItem.DiscountAmount, lineItem.Subtotal, lineItem.Discounts.Count);
          }
      }
  
      [Test]
      public void Can_Process_A_Cart_Into_An_Order()
      {
          Cart cart = LoadCart();
  
          Order order = ProcessCartToOrder(cart);
  
          // display the cart contents
          foreach (LineItem lineItem in order.LineItems)
          {
              Console.WriteLine("Product: {0}\t Price: {1:c}\t Quantity: {2} \t Subtotal: {4:c} \t Discount: {3:c} \t| Discounts Applied: {5}", lineItem.Product.Name, lineItem.Product.Price, lineItem.Quantity, lineItem.DiscountAmount, lineItem.Subtotal, lineItem.Discounts.Count);
          }
      }
  
      private static Cart LoadCart()
      {
          // create the cart
          Cart cart = new Cart(new Member("Chev"));
  
          // add items to the cart
          GenericProduct hat = new GenericProduct("Cap", 110m);
          cart.AddLineItem(hat, 5);
  
          EventItem race = new EventItem("Ticket", 90m);
          cart.AddLineItem(race, 1);
  
          // add discounts 
          Discount percentageOff = new PercentageOffDiscount("10% off all items", 0.10m);
          percentageOff.CanBeUsedInJuntionWithOtherDiscounts = false;
          cart.AddDiscount(percentageOff);
  
          Discount spendXgetY = new SpendMoreThanXGetYDiscount("Spend more than R100 get 10% off", 100m, 0.1m);
          spendXgetY.SupercedesOtherDiscounts = true;
          cart.AddDiscount(spendXgetY);
  
          Discount buyXGetY = new BuyXGetYFree("Buy 4 hats get 2 hat free", new List&lt;Product&gt; { hat }, 4, 2);
          buyXGetY.CanBeUsedInJuntionWithOtherDiscounts = false;
          buyXGetY.SupercedesOtherDiscounts = true;
          cart.AddDiscount(buyXGetY);
  
          return cart;
      }
  
      private static Order ProcessCartToOrder(Cart cart)
      {
          Order order = new Order(cart.Member);
          foreach(LineItem lineItem in cart.LineItems)
          {
              order.AddLineItem(lineItem.Product, lineItem.Quantity);
              foreach(Discount discount in lineItem.Discounts)
              {
                  order.AddDiscount(discount);    
              }
          }
          return order;
      }
  
      #endregion
  }
  
  #region Discounts
  
  [Serializable]
  public abstract class Discount : EntityBase
  {
      protected internal Discount()
      {
      }
  
      public Discount(string name)
      {
          Name = name;
      }
  
      public virtual bool CanBeUsedInJuntionWithOtherDiscounts { get; set; }
      public virtual bool SupercedesOtherDiscounts { get; set; }
      public abstract OrderBase ApplyDiscount();
      public virtual OrderBase OrderBase { get; set; }
      public virtual string Name { get; private set; }
  }
  
  [Serializable]
  public class PercentageOffDiscount : Discount
  {
      protected internal PercentageOffDiscount()
      {
      }
  
      public PercentageOffDiscount(string name, decimal discountPercentage)
          : base(name)
      {
          DiscountPercentage = discountPercentage;
      }
  
      public override OrderBase ApplyDiscount()
      {
          // custom processing
          foreach (LineItem lineItem in OrderBase.LineItems)
          {
              lineItem.DiscountAmount = lineItem.Product.Price * DiscountPercentage;
              lineItem.AddDiscount(this);
          }
          return OrderBase;
      }
  
      public virtual decimal DiscountPercentage { get; set; }
  }
  
  [Serializable]
  public class BuyXGetYFree : Discount
  {
      protected internal BuyXGetYFree()
      {
      }
  
      public BuyXGetYFree(string name, IList&lt;Product&gt; applicableProducts, int x, int y)
          : base(name)
      {
          ApplicableProducts = applicableProducts;
          X = x;
          Y = y;
      }
  
      public override OrderBase ApplyDiscount()
      {
          // custom processing
          foreach (LineItem lineItem in OrderBase.LineItems)
          {
              if(ApplicableProducts.Contains(lineItem.Product) &amp;&amp; lineItem.Quantity &gt; X)
              {
                  lineItem.DiscountAmount += ((lineItem.Quantity / X) * Y) * lineItem.Product.Price;
                  lineItem.AddDiscount(this);    
              }
          }
          return OrderBase;
      }
  
      public virtual IList&lt;Product&gt; ApplicableProducts { get; set; }
      public virtual int X { get; set; }
      public virtual int Y { get; set; }
  }
  
  [Serializable]
  public class SpendMoreThanXGetYDiscount : Discount
  {
      protected internal SpendMoreThanXGetYDiscount()
      {
      }
  
      public SpendMoreThanXGetYDiscount(string name, decimal threshold, decimal discountPercentage)
          : base(name)
      {
          Threshold = threshold;
          DiscountPercentage = discountPercentage;
      }
  
      public override OrderBase ApplyDiscount()
      {
          // if the total for the cart/order is more than x apply discount
          if(OrderBase.GrossTotal &gt; Threshold)
          {
              // custom processing
              foreach (LineItem lineItem in OrderBase.LineItems)
              {
                  lineItem.DiscountAmount += lineItem.Product.Price * DiscountPercentage;
                  lineItem.AddDiscount(this);
              }
          }
          return OrderBase;
      }
  
      public virtual decimal Threshold { get; set; }
      public virtual decimal DiscountPercentage { get; set; }
  }
  
  #endregion
  
  #region Order
  
  [Serializable]
  public abstract class OrderBase : EntityBase
  {
      private IList&lt;LineItem&gt; _LineItems = new List&lt;LineItem&gt;();
      private IList&lt;Discount&gt; _Discounts = new List&lt;Discount&gt;();
  
      protected internal OrderBase() { }
  
      protected OrderBase(Member member)
      {
          Member = member;
          DateCreated = DateTime.Now;
      }
  
      public virtual Member Member { get; set; }
  
      public LineItem AddLineItem(Product product, int quantity)
      {
          LineItem lineItem = new LineItem(this, product, quantity);
          _LineItems.Add(lineItem);
          return lineItem;
      }
  
      public void AddDiscount(Discount discount)
      {
          discount.OrderBase = this;
          discount.ApplyDiscount();
          _Discounts.Add(discount);
      }
  
      public virtual decimal GrossTotal
      {
          get
          {
              return LineItems
                  .Sum(x =&gt; x.Product.Price * x.Quantity);
          }
      }
      public virtual DateTime DateCreated { get; private set; }
      public IList&lt;LineItem&gt; LineItems
      {
          get
          {
              return _LineItems;
          }
      }
  }
  
  [Serializable]
  public class Order : OrderBase
  {
      protected internal Order() { }
  
      public Order(Member member)
          : base(member)
      {
      }
  }
  
  #endregion
  
  #region LineItems
  
  [Serializable]
  public class LineItem : EntityBase
  {
      private IList&lt;Discount&gt; _Discounts = new List&lt;Discount&gt;();
  
      protected internal LineItem() { }
  
      public LineItem(OrderBase order, Product product, int quantity)
      {
          Order = order;
          Product = product;
          Quantity = quantity;
      }
  
      public virtual void AddDiscount(Discount discount)
      {
          _Discounts.Add(discount);
      }
  
      public virtual OrderBase Order { get; private set; }
      public virtual Product Product { get; private set; }
      public virtual int Quantity { get; private set; }
      public virtual decimal DiscountAmount { get; set; }
      public virtual decimal Subtotal
      {
          get { return (Product.Price*Quantity) - DiscountAmount; }
      }
      public virtual IList&lt;Discount&gt; Discounts
      {
          get { return _Discounts.ToList().AsReadOnly(); }
      }
  }
  #endregion
  
  #region Member
  
  [Serializable]
  public class Member : EntityBase
  {
      protected internal Member() { }
  
      public Member(string name)
      {
          Name = name;
      }
  
      public virtual string Name { get; set; }
  }
  
  #endregion
  
  #region Cart
  
  [Serializable]
  public class Cart : OrderBase
  {
      protected internal Cart()
      {
      }
  
      public Cart(Member member)
          : base(member)
      {
      }
  }
  
  #endregion
  
  #region Products
  
  [Serializable]
  public abstract class Product : EntityBase
  {
      protected internal Product()
      {
      }
  
      public Product(string name, decimal price)
      {
          Name = name;
          Price = price;
      }
  
      public virtual string Name { get; set; }
      public virtual decimal Price { get; set; }
  }
  
  // generic product used in most situations for simple products 
  [Serializable]
  public class GenericProduct : Product
  {
      protected internal GenericProduct()
      {
      }
  
      public GenericProduct(String name, Decimal price) : base(name, price)
      {
      }
  }
  
  // custom product with additional properties and methods
  [Serializable]
  public class EventItem : Product
  {
      protected internal EventItem()
      {
      }
  
      public EventItem(string name, decimal price) : base(name, price)
      {
      }
  }
  
  #endregion
  
  #region EntityBase
  
  [Serializable]
  public abstract class EntityBase
  {
      private readonly Guid _id;
  
      protected EntityBase() : this(GenerateGuidComb())
      {
      }
  
      protected EntityBase(Guid id)
      {
          _id = id;
      }
  
      public virtual Guid Id
      {
          get { return _id; }
      }
  
      private static Guid GenerateGuidComb()
      {
          var destinationArray = Guid.NewGuid().ToByteArray();
          var time = new DateTime(0x76c, 1, 1);
          var now = DateTime.Now;
          var span = new TimeSpan(now.Ticks - time.Ticks);
          var timeOfDay = now.TimeOfDay;
          var bytes = BitConverter.GetBytes(span.Days);
          var array = BitConverter.GetBytes((long)(timeOfDay.TotalMilliseconds / 3.333333));
          Array.Reverse(bytes);
          Array.Reverse(array);
          Array.Copy(bytes, bytes.Length - 2, destinationArray, destinationArray.Length - 6, 2);
          Array.Copy(array, array.Length - 4, destinationArray, destinationArray.Length - 4, 4);
          return new Guid(destinationArray);
      }
  
      public virtual int Version { get; protected set; }
  
      #region Equality Tests
  
      public override bool Equals(object entity)
      {
          return entity != null
              &amp;&amp; entity is EntityBase
              &amp;&amp; this == (EntityBase)entity;
      }
  
      public static bool operator ==(EntityBase base1,
          EntityBase base2)
      {
          // check for both null (cast to object or recursive loop)
          if ((object)base1 == null &amp;&amp; (object)base2 == null)
          {
              return true;
          }
  
          // check for either of them == to null
          if ((object)base1 == null || (object)base2 == null)
          {
              return false;
          }
  
          if (base1.Id != base2.Id)
          {
              return false;
          }
  
          return true;
      }
  
      public static bool operator !=(EntityBase base1, EntityBase base2)
      {
          return (!(base1 == base2));
      }
  
      public override int GetHashCode()
      {
          {
              return Id.GetHashCode();
          }
      }
  
      #endregion
  
  #endregion
  }
  </code></pre>
  
  <p>}</p>