Note that there are some explanatory texts on larger screens.

plurals
  1. PODiscount strategy in shopping cart and orders
    text
    copied!<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>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload