Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Marc Ferrold 24 posts 105 karma points
    Jul 30, 2015 @ 13:20
    Marc Ferrold
    2

    Tiered Pricing

    Hey, hi, hello

    So, I've been looking into tiered pricing, and found a solution I thought I'd share. I believe it's being built into Merchello so it'll be around soon(tm), but 'til then, I thought I'd share my implementation.

    I've based it upon dfberry's BasketController, and that example in general, for various methods. It also uses Archetype to store the tiers.

    There may be better ways to implement it, and do the calculations, but I hope this will get people started.

    Now, with this method, you'll want to create Merchello products as you always would - no change is being made to the products themselves or really anything Merchello related. However, the content nodes you add them to, you'll also want an Archetype on, which includes the quantity (or whatever) you need to buy, and the price it'll cost once that point is reached. For example, my Archetype looks like this:

    Pricing Tiers Archetype Note: Do not look directly at the Danish text. It may just burn your eyes out.

    In this case, each tier is based on the amount a customer wishes to buy of a given item. So let's say a customer wants 10 t-shirts which normally cost $20 each, but because he buys 10 at a time, maybe he gets them for $18 each.

    Easy so far, if you're familiar with Archetype (you should be, all the cool kids like it).

    But here comes the slightly more tricky part, which Rusty was kind enough to help me with: Namely, adding stuff to the basket. This part is largely based on dfberry's BasketController.

    Some of this might be messy, but I'll just throw my code at you:

    [HttpPost]
        public ActionResult AddToBasket(AddItemModel model)
        {
            // Add to Logical Basket 
    
            // add Umbraco content id to Merchello Product extended properties
            var extendedData = new ExtendedDataCollection();
            extendedData.SetValue("umbracoContentId", model.ContentId.ToString(CultureInfo.InvariantCulture));
    
            // get Merchello product 
            var product = Services.ProductService.GetByKey(model.ProductKey);
    
            // get Merchello product variant
            var variant = model.OptionChoices != null ? Services.ProductVariantService.GetProductVariantWithAttributes(product, model.OptionChoices) : null;          
    
            // attach appropriate extended data
            if(variant != null)
            {
                extendedData.AddProductVariantValues(variant);
                List<string> attributes = variant.Attributes.Select(x => x.Name).ToList();
                extendedData.SetValue("productAttributes", JsonConvert.SerializeObject(attributes));
            }
            else
            {
                var masterVariant = product.GetProductVariantForPurchase();
                extendedData.AddProductVariantValues(masterVariant);
            }
    
            // get Quantity being added to basket
            var Quantity = model.Quantity;
    
            // get pricing tiers for the product
            Dictionary<int, string> pricingTiers = JsonConvert.DeserializeObject<Dictionary<int, string>>(model.PricingTiers);
    
            //Update the price accordingly
            decimal updatedPrice = product.Price;
            if(pricingTiers.Any())
            {
                updatedPrice = UpdatePrice(Quantity, pricingTiers, product.Price);
            }
    
            // add a single item of the Product to the logical Basket
            if(variant == null)
                Basket.AddItem(product.Name, product.Sku, Quantity, updatedPrice, extendedData);
            else
                Basket.AddItem(variant.Name, variant.Sku, Quantity, updatedPrice, extendedData);
    
    
            // Save to Database tables: merchItemCache, merchItemCacheItem
            Basket.Save();
    
            return RedirectToUmbracoPage(BasketContentId);
        }
    

    This is the basic logic of adding an item to the basket. Couple of things to note:

    • I get my pricing tiers from a string - this was the easiest way for me to get it both in to and out of the model (which I will get to).
    • I have defined an UpdatePrice, which is used if any pricing tiers are found for a given product.
    • I go through some hoops to get the full extended data bits. This is what gave me trouble and Rusty so graciously helped me with - the extended data is important to the basket and thusly the checkout flow.
    • I add the product to the basket with an overloaded method, in stead of adding an "IProduct"

    Now, my model... Messy? Maybe, but functional!.. So far.

    namespace Models
    {
        public class OrderModel
        {
            //Order details
            public Guid CustomerKey { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string FirstName { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string LastName { get; set; }
            [Required(ErrorMessage = "Skal udfyldes"), EmailAddress(ErrorMessage = "Skal være en gyldig email")]
            public string Email { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string Address1 { get; set; }
            public string Address2 { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string Locality { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string PostalCode { get; set; }
            public string CountryCode { get; set; }
            public string Region { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string Organization { get; set; }
            public string Department { get; set; }
            public bool IsCommercial { get; set; }
            [Required(ErrorMessage = "Skal udfyldes"), Phone(ErrorMessage = "Skal være et telefonnummer")]
            public string Phone { get; set; }
            [Required(ErrorMessage = "Skal udfyldes")]
            public string CVR { get; set; }
            public string Comment { get; set; }
            [FileSize(1000000, 10000000)]
            [FileTypes("png,gif,bnp,tiff,pdf,eps,ai")]
            public HttpPostedFileBase UploadImage { get; set; }
    
            //Tech
            public int BasketPageId { get; set; }
            public int PaymentPageId { get; set; }
            public int ReceiptPageId { get; set; }
    
            //Mail
            public string FromMail { get; set; }
    
        public string ToMail { get; set; }
        public string Subject { get; set; }
    }
    
    public static class OrderModelExtensions
    {
        /// <summary>
        /// Gets an IAddress based on the OrderModel
        /// </summary>
        /// <param name="address"></param>
        /// <returns>An IAddress based on the OrderModel</returns>
        public static IAddress ToAddress(this OrderModel order)
        {
            string name = "";
            if(!string.IsNullOrWhiteSpace(order.Department))
            {
                name = order.Organization + ", Afd: " + order.Department + " - " + order.FirstName + " " + order.LastName;
            }
            else
            {
                name = order.Organization + " - " + order.FirstName + " " + order.LastName;
            }
            return new Address()
            {
                Address1 = order.Address1,
                Address2 = order.Address2,
                CountryCode = order.CountryCode,
                Email = order.Email,
                IsCommercial = order.IsCommercial,
                Locality = order.Locality,
                Name = name,
                Organization = order.Organization,
                Phone = order.Phone,
                PostalCode = order.PostalCode,
                Region = order.Region
            };
        }
      }
    }
    

    I store a lot of stuff here. You probably wont need all of this, and maybe you know a better way of passing stuff along the chain of command, but I don't, so... This is what I did: Adding an item to the basket in my case, was done with an UmbracoForm. In this form, I have a hidden input field, which is used to pass along my dictionary of pricing tiers:

    //Get Pricing Tiers and store them in a Dictionary<int, decimal>
    Dictionary<string, string> pricingTiers = new Dictionary<string, string>();
    foreach (var fieldset in currentPage.GetPropertyValue<ArchetypeModel>("pricingTiers"))
    {
        pricingTiers.Add(fieldset.GetValue("qty"), fieldset.GetValue("price"));
    } 
    string pricingTierString = JsonConvert.SerializeObject(pricingTiers);
    

    This was an easy way (for me) to pass it along as a simple string value. So we have the tiers, now we need to update the pricing. This is handled in the BasketController with this method:

        /// <summary>
        /// Gets a price appropriate for the quantity added
        /// </summary>
        /// <param name="qty">Quantity being added (int)</param>
        /// <param name="tiers">The pricing tiers (Dictionary<string, string>)</param>
        /// <param name="originalPrice">The original price of the product (decimal)</param>
        /// <returns>A price appropriate for the quantity added (decimal)</returns>
        private decimal UpdatePrice(int qty, Dictionary<int, string> tiers, decimal originalPrice)
        {
            decimal price = originalPrice;
            List<int> quantities = tiers.Keys.ToList();
            quantities.Sort();
            for (int i = 0; i < quantities.Count; i++)
            {
                if (qty < quantities.First())
                {
                    price = originalPrice;
                    return price;
                }
                else if (qty >= quantities.Last())
                {
                    price = Convert.ToDecimal(tiers.FirstOrDefault(x => (Convert.ToInt32(x.Key)) == quantities.Last()).Value, new CultureInfo("da-DK"));
                    return price;
                }
                else if (quantities[i] <= qty && qty < quantities[i + 1])
                {
                    price = Convert.ToDecimal(tiers.FirstOrDefault(x => (Convert.ToInt32(x.Key)) == quantities[i]).Value, new CultureInfo("da-DK"));
                    return price;
                }
            }
            return price;
        }
    

    I'm almost certain the first if-statement is redundant, but I honestly just needed to be certain for this particular case. Unfortunately, it still falls short a bit, because of the CultureInfo. It means that with the punctuation, I can't actually have decimal prices in my backend, like 3,50 (or 3.50, depending on culture). It doesn't matter in my case, but if you're wondering why an order's total always comes to 0,00, the problem may be in there somewhere.

    If you already know how to do a checkout, that should be it - any problems I had with it seemed to arise as a result of products not being "complete" when added to the basket, but that should be alright now.

    Again, this is not meant to be perfect, but I hope it helps people get started, or maybe this just happens to work for someone else, so I wanted to share.

  • Tom Steer 161 posts 596 karma points
    Jul 30, 2015 @ 14:34
    Tom Steer
    0

    Thanks for sharing Marc :) it's great to see how other people are handling these kind of scenarios.

    I really like the way teacommerce lets you handle this kind of custom price scenarios. They have the concept of an OrderlineCalculator which you can override, the great thing about this is it gets called anytime a change to an orderline is made which keeps all your logic in one place. It would be great to see something similar come to Merchello as currently I guess you would have have this check in multiple places e.g add to basket, update basket etc also how would it work if the order line qty was modified in Merchello back office for example. Again cheers for sharing your implementation for this I'm sure it will help a lot of people :)

    Tom

  • Biagio Paruolo 1593 posts 1824 karma points c-trib
    Aug 12, 2015 @ 05:53
    Biagio Paruolo
    0

    Interesting

  • Tito 314 posts 623 karma points
    Dec 21, 2015 @ 13:51
    Tito
    0

    Thanks Marc! The method "GetProductVariantForPurchase" does not exist anymore, what can i use?

Please Sign in or register to post replies

Write your reply to:

Draft