Copied to clipboard

Flag this post as spam?

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


  • Claudiu Bria 32 posts 143 karma points
    May 15, 2017 @ 17:26
    Claudiu Bria
    0

    Product - how to change variant in the basket

    Hi Rusty,

    I'm trying for some hours now to make available on the /basket/ page a feature for the end-user to be able to change an option of the product in the basket, therefore - if i understand correctly - to change the sku of the current variant into the sku of the other variant that contains the new value of the option.

    Could you please point me into some directions here ? Do I need to change the TBasketModel in an overwritten BasketControllerBase{T} ? Can I do that without changing the ILineItem from the BasketModelFactory ?

    Any code example would be much appreciated. Thank you.

  • Rusty Swayne 1655 posts 4970 karma points c-trib
    May 17, 2017 @ 02:35
    Rusty Swayne
    0

    Hey Claudiu

    This is a new one but doable. As I've never done it before, I don't have any example code but you can part together stuff that is there...

    The item in your cart will have a ProductKey in the ExtendedDataCollection which can be retrieved via the "GetProductKey()" ExtendedDataCollection extension.

     var productKey = basketItem.ExtendedData.GetProductKey();
    

    From there you can get the base product (not the variant) and then create/tweak the AddItemModel used in FastTrack and Merchello.Store ... so if the user changes the variant, you would actually be removing the existing item (e.g. deleting it from the basket) and then adding the new newly selected variant with current quantity selected in it's place.

  • Claudiu Bria 32 posts 143 karma points
    May 19, 2017 @ 19:54
    Claudiu Bria
    0

    Thank you Rusty,

    Our options for a product are:

    • Establishing - with - without

    • Users

      • 1
      • 2
      • ...
      • 10

    At runtime the Establishing option is fixed on one choice only (on with or without), so the only available variants are on one Establishing choice only, so in the end the list of available variants will be given by the list of available Users choices for the product.

    With this in mind, I started by separating the basket surface controller end point in my own

    /js/ajour.merchello.ui.settings.js:

    if (MUI !== undefined) {
    ...
        MUI.Settings.Endpoints.basketSurface = '/umbraco/Merchello/AjourStoreBasket/';
    }
    

    The ajour.merchello.ui.settings.js was then added in /views/FastTrack.cshtml:

    <!-- new Merchello UI scripts -->
    <script src="~/App_Plugins/Merchello/client/js/merchello.ui.min.js"></script>
    <script src="~/App_Plugins/Merchello/client/js/merchello.ui.settings.js"></script>
    <script src="~/js/ajour.merchello.ui.settings.js"></script>
    <script src="~/App_Plugins/Merchello/client/js/fasttrack.js"></script>
    

    This meant the need of a new controller, AjourStoreBasketController:

    using AjourCms.Factories;
    using AjourCms.Models;
    using AjourCms.Models.Async;
    
    namespace AjourCms.Controllers
    {
        using System;
        using System.Web.Mvc;
    
        using Merchello.Core;
        using Merchello.Web.Controllers;
        using Merchello.Web.Factories;
        using Merchello.Web.Store.Factories;
        using Merchello.Web.Store.Models;
        using Merchello.Web.Store.Models.Async;
    
        using Umbraco.Web.Mvc;
    
        /// <summary>
        /// The default (generic) basket controller.
        /// </summary>
        [PluginController("Merchello")]
        public class AjourStoreBasketController : AjourBasketControllerBase<AjourStoreBasketModel, StoreLineItemModel, StoreAddItemModel>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="AjourStoreBasketController"/> class.
            /// </summary>
            /// <remarks>
            /// This constructor allows you to inject your custom model factory overrides so that you can
            /// extended the various model interfaces with site specific models.  In this case, we have overridden 
            /// the BasketModelFactory and the AddItemModelFactory.  The BasketItemExtendedDataFactory has not been overridden.
            /// 
            /// Views rendered by this controller are placed in "/Views/QuickMartBasket/" and correspond to the method name.  
            /// 
            /// e.g.  the "AddToBasketForm" corresponds the the AddToBasketForm method in BasketControllerBase. 
            /// 
            /// This is just an generic MVC pattern and nothing to do with Umbraco
            /// </remarks>
            public AjourStoreBasketController()
                : this(new BasketItemExtendedDataFactory<StoreAddItemModel>())
            {
            }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="AjourStoreBasketController"/> class.
            /// </summary>
            /// <param name="addItemExtendedDataFactory">
            /// The <see cref="BasketItemExtendedDataFactory{StoreAddItemModel}"/>.
            /// </param>
            public AjourStoreBasketController(BasketItemExtendedDataFactory<StoreAddItemModel> addItemExtendedDataFactory)
                : base(addItemExtendedDataFactory, new AddItemModelFactory(), new AjourStoreBasketModelFactory())
            {
            }
    
            /// <summary>
            /// Handles the successful basket update.
            /// </summary>
            /// <param name="model">
            /// The <see cref="StoreBasketModel"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            /// <remarks>
            /// Customization of the handling of an add item success
            /// </remarks>
            protected override ActionResult HandleAddItemSuccess(StoreAddItemModel model)
            {
                if (Request.IsAjaxRequest())
                {
                    // Construct the response object to return
                    var resp = new AddItemAsyncResponse
                        {
                            Success = true,
                            ItemCount = this.GetBasketItemCountForDisplay()
                        };
    
                    return this.Json(resp);
                }
    
                return base.HandleAddItemSuccess(model);
            }
    
            /// <summary>
            /// Handles an add item operation exception.
            /// </summary>
            /// <param name="model">
            /// The <see cref="StoreAddItemModel"/>.
            /// </param>
            /// <param name="ex">
            /// The <see cref="Exception"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            protected override ActionResult HandleAddItemException(StoreAddItemModel model, Exception ex)
            {
                if (Request.IsAjaxRequest())
                {
                    // in case of Async call we need to construct the response
                    var resp = new AddItemAsyncResponse { Success = false, Messages = { ex.Message } };
                    return this.Json(resp);
                }
    
                return base.HandleAddItemException(model, ex);
            }
    
            /// <summary>
            /// Handles the successful basket update.
            /// </summary>
            /// <param name="model">
            /// The <see cref="StoreBasketModel"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            /// <remarks>
            /// Handles the customization of the redirection after a custom update basket operation
            /// </remarks>
            protected override ActionResult HandleUpdateBasketSuccess(AjourStoreBasketModel model)
            {
                if (Request.IsAjaxRequest())
                {
                    // in case of Async call we need to construct the response
                    var resp = new AjourUpdateQuantityAsyncResponse { Success = true };
                    try
                    {
                        resp.AddUpdatedItems(this.Basket.Items);
                        resp.FormattedTotal = this.Basket.TotalBasketPrice.AsFormattedCurrency();
                        resp.ItemCount = this.GetBasketItemCountForDisplay();
                        return this.Json(resp);
                    }
                    catch (Exception ex)
                    {
                        resp.Success = false;
                        resp.Messages.Add(ex.Message);
                        return this.Json(resp);
                    }
                }
    
                return base.HandleUpdateBasketSuccess(model);
            }
    
        }
    }
    

    with its base class, AjourBasketControllerBase{T}.cs:

    using AjourCms.Factories;
    using AjourCms.Models.Ui;
    using Merchello.Examine.DataServices;
    using Merchello.Web.Controllers;
    
    namespace AjourCms.Controllers
    {
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Web.Mvc;
    
        using Merchello.Core;
        using Merchello.Core.Logging;
        using Merchello.Core.Models;
        using Merchello.Web;
        using Merchello.Web.Factories;
        using Merchello.Web.Models.ContentEditing;
        using Merchello.Web.Models.Ui;
        using Merchello.Web.Models.Ui.Async;
        using Merchello.Web.Models.VirtualContent;
        using Merchello.Web.Mvc;
        using Merchello.Web.Workflow;
    
        using Newtonsoft.Json;
        using Umbraco.Core;
    
    
        /// <summary>
        /// A base controller used for Basket implementations.
        /// </summary>
        /// <typeparam name="TAjourBasketModel">
        /// The type of <see cref="IBasketModel{TBasketItemModel}"/>
        /// </typeparam>
        /// <typeparam name="TAjourBasketItemModel">
        /// The type of the basket item model
        /// </typeparam>
        /// <typeparam name="TAddItem">
        /// The type of <see cref="IAddItemModel"/>
        /// </typeparam>
        public abstract class AjourBasketControllerBase<TAjourBasketModel, TAjourBasketItemModel, TAddItem> : MerchelloUIControllerBase
            where TAjourBasketItemModel : class, ILineItemModel, new()
            where TAjourBasketModel : class, IAjourBasketModel<TAjourBasketItemModel>, new()
            where TAddItem : class, IAddItemModel, new()
        {
            /// <summary>
            /// The factory responsible for building the <see cref="IBasketModel{TBasketItemModel}"/>.
            /// </summary>
            private readonly AjourBasketModelFactory<TAjourBasketModel, TAjourBasketItemModel> _basketModelFactory;
    
            /// <summary>
            /// The factory responsible for building the <see cref="IAddItemModel"/>s.
            /// </summary>
            private readonly AddItemModelFactory<TAddItem> _addItemFactory;
    
            /// <summary>
            /// The factory responsible for building <see cref="ExtendedDataCollection"/>s when adding items to the basket.
            /// </summary>
            private readonly BasketItemExtendedDataFactory<TAddItem> _addItemExtendedDataFactory;
    
            #region Constructors
    
            /// <summary>
            /// Initializes a new instance of the <see cref="BasketControllerBase{TBasketModel,TBasketItemModel,TAddItem}"/> class. 
            /// </summary>
            protected AjourBasketControllerBase()
                : this(
                      new BasketItemExtendedDataFactory<TAddItem>(),
                      new AddItemModelFactory<TAddItem>(),
                      new AjourBasketModelFactory<TAjourBasketModel, TAjourBasketItemModel>())
            {
            }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="BasketControllerBase{TBasketModel,TBasketItemModel,TAddItem}"/> class.
            /// </summary>
            /// <param name="addItemExtendedDataFactory">
            /// The <see cref="BasketItemExtendedDataFactory{TAddItemModel}"/>.
            /// </param>
            protected AjourBasketControllerBase(BasketItemExtendedDataFactory<TAddItem> addItemExtendedDataFactory)
                : this(
                        addItemExtendedDataFactory,
                        new AddItemModelFactory<TAddItem>(),
                        new AjourBasketModelFactory<TAjourBasketModel, TAjourBasketItemModel>())
            {
            }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="AjourBasketControllerBase{TAjourBasketModel,TAjourBasketItemModel,TAddItem}"/> class. 
            /// </summary>
            /// <param name="addItemExtendedDataFactory">
            /// The <see cref="BasketItemExtendedDataFactory{TAddItemModel}"/>.
            /// </param>
            /// <param name="addItemFactory">
            /// The <see cref="AddItemModelFactory{TAddItemModel}"/>
            /// </param>
            /// <param name="basketModelFactory">
            /// The <see cref="BasketModelFactory{TBasketModel, TBasketItemModel}"/>.
            /// </param>
            protected AjourBasketControllerBase(
                BasketItemExtendedDataFactory<TAddItem> addItemExtendedDataFactory,
                AddItemModelFactory<TAddItem> addItemFactory,
                AjourBasketModelFactory<TAjourBasketModel, TAjourBasketItemModel> basketModelFactory)
            {
                Ensure.ParameterNotNull(basketModelFactory, "basketModelFactory");
                Ensure.ParameterNotNull(addItemFactory, "addItemFactory");
                Ensure.ParameterNotNull(addItemExtendedDataFactory, "addItemExtendedDataFactory");
    
                this._basketModelFactory = basketModelFactory;
                this._addItemFactory = addItemFactory;
                this._addItemExtendedDataFactory = addItemExtendedDataFactory;
            }
    
            #endregion
    
            /// <summary>
            /// Responsible for adding a product to the basket.
            /// </summary>
            /// <param name="model">
            /// The model.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            [HttpPost]
            [ValidateAntiForgeryToken]
            public virtual ActionResult AddBasketItem(TAddItem model)
            {
                // Instantiating the ExtendedDataCollection in this manner allows for additional values 
                // to be added in the factory OnCreate override.
                // e.g. if you need to store custom extended data values, create your own factory
                // inheriting from BasketItemExtendedDataFactory and override the "OnCreate" method to store
                // any addition values you have added to the model
                var extendedData = this._addItemExtendedDataFactory.Create(model);
    
                // We've added some data modifiers that can handle such things as including taxes in product
                // pricing.  The data modifiers can either get executed when the item is added to the basket or
                // as a result from a MerchelloHelper query - you just don't want them to execute twice.
    
                // In this case we want to get the product without any data modification
                var merchello = new MerchelloHelper(false);
    
                try
                {
                    var product = merchello.Query.Product.GetByKey(model.ProductKey);
    
                    // ensure the quantity on the model
                    var quantity = model.Quantity <= 0 ? 1 : model.Quantity;
    
                    // In the event the product has options we want to add the "variant" to the basket.
                    // -- If a product that has variants is defined, the FIRST variant will be added to the cart. 
                    // -- This was done so that we did not have to throw an error since the Master variant is no
                    // -- longer valid for sale.
                    if (model.OptionChoices != null && model.OptionChoices.Any())
                    {
                        var variant = product.GetProductVariantDisplayWithAttributes(model.OptionChoices);
    
                        // Log the option choice for this variant in the extend data collection
                        var choiceExplainations = new Dictionary<string, string>();
                        foreach (var choice in variant.Attributes)
                        {
                            var option = product.ProductOptions.FirstOrDefault(x => x.Key == choice.OptionKey);
                            if (option != null)
                            {
                                choiceExplainations.Add(option.Name, choice.Name);
                            }
                        }
    
                        // store the choice explainations in the extended data collection
                        extendedData.SetValue(Merchello.Core.Constants.ExtendedDataKeys.BasketItemCustomerChoice, JsonConvert.SerializeObject(choiceExplainations));
                        this.Basket.AddItem(variant, variant.Name, quantity, extendedData);
                    }
                    else
                    {
                        this.Basket.AddItem(product, product.Name, quantity, extendedData);
                    }
    
                    this.Basket.Save();
    
                    // If this request is not an AJAX request return the redirect
                    return this.HandleAddItemSuccess(model);
                }
                catch (Exception ex)
                {
                    var logData = new ExtendedLoggerData();
                    logData.AddCategory("Merchello");
                    logData.AddCategory("Controllers");
                    MultiLogHelper.Error<AjourBasketControllerBase<TAjourBasketModel, TAjourBasketItemModel, TAddItem>>("Failed to add item to basket", ex, logData);
                    return this.HandleAddItemException(model, ex);
                }
            }
    
    
            /// <summary>
            /// Responsible for updating the quantities of items in the basket
            /// </summary>
            /// <param name="model">The <see cref="IBasketModel{TBasketItemModel}"/></param>
            /// <returns>Redirects to the current Umbraco page (generally the basket page)</returns>
            [HttpPost]
            [ValidateAntiForgeryToken]
            public virtual ActionResult UpdateBasket(TAjourBasketModel model)
            {
                if (!this.ModelState.IsValid) return this.CurrentUmbracoPage();
    
                var merchello = new MerchelloHelper(false);
    
                var modelSkuHasChanged = false;
                foreach (var item in model.Items)
                {
                    if (this.Basket.Items.All(x => x.Key != item.Key)) continue;
                    var basketItem = this.Basket.Items.First(x => x.Key == item.Key);
                    if (basketItem != null && basketItem.Sku != item.Sku)
                    {
                        modelSkuHasChanged = true;
                        break;
                    }
                }
    
                if (modelSkuHasChanged)
                {
                    foreach (var item in model.Items.Where(item => this.Basket.Items.FirstOrDefault(x => x.Key == item.Key)?.Sku != item.Sku))
                    {
                        var currentBasketItem = this.Basket.Items.First(x => x.Key == item.Key);
    
    //                    this.Basket.EnableDataModifiers = true;
                        this.Basket.RemoveItem(currentBasketItem.Key);
    
                        var productKey = currentBasketItem.ExtendedData.GetProductKey();
                        var productDisplay = merchello.Query.Product.GetByKey(productKey);
                        var productVariantDisplay = merchello.Query.Product.GetProductVariantBySku(item.Sku);
                        var addItemModel = new TAddItem()
                        {
                            ProductKey = productKey,
                            Quantity = item.Quantity,
                            ProductOptions = productDisplay.ProductOptions,
                            OptionChoices = productVariantDisplay.Attributes.Select(x => x.Key).ToArray()
                        };
                        var extendedData = this._addItemExtendedDataFactory.Create(addItemModel);
                        try
                        {
                            var product = merchello.Query.Product.GetByKey(addItemModel.ProductKey);
                            var quantity = addItemModel.Quantity <= 0 ? 1 : addItemModel.Quantity;
                            if (addItemModel.OptionChoices != null && addItemModel.OptionChoices.Any())
                            {
                                var variant = product.GetProductVariantDisplayWithAttributes(addItemModel.OptionChoices);
                                var choiceExplainations = new Dictionary<string, string>();
                                foreach (var choice in variant.Attributes)
                                {
                                    var option = product.ProductOptions.FirstOrDefault(x => x.Key == choice.OptionKey);
                                    if (option != null)
                                    {
                                        choiceExplainations.Add(option.Name, choice.Name);
                                    }
                                }
                                extendedData.SetValue(Merchello.Core.Constants.ExtendedDataKeys.BasketItemCustomerChoice, JsonConvert.SerializeObject(choiceExplainations));
                                this.Basket.AddItem(variant, variant.Name, quantity, extendedData);
    
                            }
                            else
                            {
                                this.Basket.AddItem(product, product.Name, quantity, extendedData);
                            }
    
                        }
                        catch (Exception ex)
                        {
                            var logData = new ExtendedLoggerData();
                            logData.AddCategory("Merchello");
                            logData.AddCategory("Controllers");
                            MultiLogHelper.Error<AjourBasketControllerBase<TAjourBasketModel, TAjourBasketItemModel, TAddItem>>("Failed to add item to basket", ex, logData);
                            throw;
                        }
    
                    }
                }
                else
                {
                    var modelQuantityHasChanged = false;
                    foreach (var item in model.Items)
                    {
                        if (this.Basket.Items.All(x => x.Key != item.Key)) continue;
                        var basketItem = this.Basket.Items.First(x => x.Key == item.Key);
                        if (basketItem != null && basketItem.Quantity != item.Quantity)
                        {
                            modelQuantityHasChanged = true;
                            break;
                        }
                    }
                    if (modelQuantityHasChanged)
                    {
                        foreach (var item in model.Items.Where(item => this.Basket.Items.FirstOrDefault(x => x.Key == item.Key)?.Quantity != item.Quantity))
                        {
                            this.Basket.UpdateQuantity(item.Key, item.Quantity);
                        }
                    }
                }
    
                this.Basket.Save();
    
                this.Basket.Refresh();
    
               // return this.RedirectToCurrentUmbracoPage();
    
               // var url = Request.Url;
               // var baseUrl = string.Format("{0}://{1}{2}", url.Scheme, url.Host, url.IsDefaultPort ? "" : ":" + url.Port);
               // var refreshUrl = baseUrl + "/basket";
               // return Redirect(refreshUrl);
    
               // model = this._basketModelFactory.Create(this.Basket);
               //// return this.PartialView("BasketForm", model);
               // return RedirectToAction("BasketForm","AjourStoreBasket");
    
               // if (Request.UrlReferrer != null) return Redirect(Request.UrlReferrer.ToString());
               // return this.RedirectToUmbracoPage(1098);
    
    
                return this.HandleUpdateBasketSuccess(model);
            }
    
            /// <summary>
            /// Removes an item from the basket.
            /// </summary>
            /// <param name="lineItemKey">
            /// The unique key (GUID) of the line item to be removed from the basket.
            /// </param>
            /// <param name="redirectId">
            /// The Umbraco content Id of the page to redirect to after removing the basket item.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            [HttpGet]
            public virtual ActionResult RemoveBasketItem(Guid lineItemKey, int redirectId)
            {
                this.EnsureOwner(this.Basket.Items, lineItemKey);
    
                // remove the item by it's pk.  
                this.Basket.RemoveItem(lineItemKey);
    
                this.Basket.Save();
    
                return this.RedirectToUmbracoPage(redirectId);
            }
    
            /// <summary>
            /// Moves an item from the Basket to the WishList.
            /// </summary>
            /// <param name="lineItemKey">
            /// The unique key (GUID) of the line item to be moved from the basket to the wish list.
            /// </param>
            /// <param name="successRedirectId">
            /// The Umbraco content id of the page to redirect to after the basket item has been moved.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            /// <remarks>
            /// Anonymous customers do not have wish lists, so this method will redirect to the current page if the
            /// customer has not authenticated.
            /// </remarks>
            [HttpGet]
            public virtual ActionResult MoveItemToWishList(Guid lineItemKey, int successRedirectId)
            {
                // Assert the customer is not anonymous
                if (this.CurrentCustomer.IsAnonymous) return this.RedirectToCurrentUmbracoPage();
    
                // Ensure the basket item reference is in the current customer's basket
                // e.g. it is not a reference to some other customer's basket
                this.EnsureOwner(this.Basket.Items, lineItemKey);
    
                // Move the item to the wish list collection
                this.Basket.MoveItemToWishList(lineItemKey);
    
                return this.RedirectToUmbracoPage(successRedirectId);
            }
    
            #region ChildActions
    
            /// <summary>
            /// Renders the basket partial view.
            /// </summary>
            /// <param name="view">
            /// The optional view.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            [ChildActionOnly]
            public virtual ActionResult BasketForm(string view = "")
            {
                var model = this._basketModelFactory.Create(this.Basket);
                return view.IsNullOrWhiteSpace() ? this.PartialView(model) : this.PartialView(view, model);
            }
    
    
            /// <summary>
            /// Responsible for rendering the Add Item Form.
            /// </summary>
            /// <param name="model">
            /// The <see cref="IProductContent"/>.
            /// </param>
            /// <param name="quantity">
            /// The quantity to be added
            /// </param>
            /// <param name="view">
            /// The name of the view to render.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            [ChildActionOnly]
            public virtual ActionResult AddProductToBasketForm(IProductContent model, int quantity = 1, string view = "AddToBasketForm")
            {
                var addItem = this._addItemFactory.Create(model, quantity);
                return this.AddToBasketForm(addItem, view);
            }
    
            /// <summary>
            /// Responsible for rendering the Add Item Form.
            /// </summary>
            /// <param name="model">
            /// The <see cref="ProductDisplay"/>.
            /// </param>
            /// <param name="quantity">
            /// The quantity to be added
            /// </param>
            /// <param name="view">
            /// The name of the view to render.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            [ChildActionOnly]
            public virtual ActionResult AddProductDisplayToBasketForm(ProductDisplay model, int quantity = 1, string view = "AddToBasketForm")
            {
                var addItem = this._addItemFactory.Create(model, quantity);
                return this.AddToBasketForm(addItem, view);
            }
    
            /// <summary>
            /// Renders the add to basket form.
            /// </summary>
            /// <param name="model">
            /// The model.
            /// </param>
            /// <param name="view">
            /// The name of the view to render.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            [ChildActionOnly]
            public virtual ActionResult AddToBasketForm(TAddItem model, string view = "")
            {
                return view.IsNullOrWhiteSpace() ? this.PartialView(model) : this.PartialView(view, model);
            }
    
            #endregion
    
            #region Operation Handlers
    
            /// <summary>
            /// Handles the successful basket update.
            /// </summary>
            /// <param name="model">
            /// The <see cref="IBasketModel{TBasketItemModel}"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            /// <remarks>
            /// Allows for customization of the redirection after a custom update basket operation
            /// </remarks>
            protected virtual ActionResult HandleUpdateBasketSuccess(TAjourBasketModel model)
            {
                return this.RedirectToCurrentUmbracoPage();
            }
    
            /// <summary>
            /// Handles a basket update exception
            /// </summary>
            /// <param name="model">
            /// The model.
            /// </param>
            /// <param name="ex">
            /// The ex.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            /// <remarks>
            /// Allows for customization of the redirection after a custom update basket operation
            /// </remarks>
            protected virtual ActionResult HandleUpdateBasketException(TAjourBasketModel model, Exception ex)
            {
                throw ex;
            }
    
            /// <summary>
            /// Handles a successful add item operation.
            /// </summary>
            /// <param name="model">
            /// The <see cref="IAddItemModel"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            protected virtual ActionResult HandleAddItemSuccess(TAddItem model)
            {
                return this.CurrentUmbracoPage();
            }
    
            /// <summary>
            /// Handles an add item operation exception.
            /// </summary>
            /// <param name="model">
            /// The <see cref="IAddItemModel"/>.
            /// </param>
            /// <param name="ex">
            /// The <see cref="Exception"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ActionResult"/>.
            /// </returns>
            protected virtual ActionResult HandleAddItemException(TAddItem model, Exception ex)
            {
                var logData = new ExtendedLoggerData();
                logData.AddCategory("Merchello");
                MultiLogHelper.Error<AjourBasketControllerBase<TAjourBasketModel, TAjourBasketItemModel, TAddItem>>("Failed to add item to the basket", ex, logData);
                throw ex;
            }
    
            #endregion
    
            /// <summary>
            /// Gets the total basket count.
            /// </summary>
            /// <returns>
            /// The <see cref="int"/>.
            /// </returns>
            /// <remarks>
            /// This is generally used in navigations and labels.  Some implementations show the total number of line items while
            /// others show the total number of items (total sum of product quantities - default).
            /// 
            /// Method is used in Async responses to allow for easier HTML label updates 
            /// </remarks>
            protected virtual int GetBasketItemCountForDisplay()
            {
                return this.Basket.TotalQuantityCount;
            }
        }
    }
    

    The magic mainly happens in the UpdateBasket, where now the two scenarios for changing an item given by the two flags modelSkuHasChanged and modelQuantityHasChanged are exclusive towards one another. This is because the change of a variant is detected in the same way as the change of the quantity, using the same muiaction binding in the client scripting. I created a separated razor for the Basket Form, the /App_Plugins/Merchello/views/AjourStoreBasket/BasketForm.cshtml:

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<AjourCms.Models.AjourStoreBasketModel>
    @using System.Web.Mvc.Html
    @using AjourCms.Controllers
    @using Merchello.Core
    @using Merchello.Web
    @using Merchello.Web.Models.Ui
    @using Merchello.Web.Models.VirtualContent
    @using Merchello.Web.Store.Controllers
    @using Umbraco.Core.Models
    @using Umbraco.Web
    @using AjourHelpers = AjourCms.Ajour.Helpers;
    @{
        var merchelloStoreCulture = AjourHelpers.MerchelloStoreCulture;
        System.Threading.Thread.CurrentThread.CurrentCulture = merchelloStoreCulture;
        System.Threading.Thread.CurrentThread.CurrentUICulture = merchelloStoreCulture;
        var localizedTextService = ApplicationContext.Services.TextService;
        var localizedProduct = localizedTextService.Localize("ajourShopBasketForm/Product", merchelloStoreCulture);
        var localizedStartup = localizedTextService.Localize("ajourShopBasketForm/Startup", merchelloStoreCulture);
        var localizedSubscription = localizedTextService.Localize("ajourShopBasketForm/Subscription", merchelloStoreCulture);
        var localizedEstablishingFee = localizedTextService.Localize("ajourShopBasketForm/establishingFee", merchelloStoreCulture);
        var localizedNoEstablishingFee = localizedTextService.Localize("ajourShopBasketForm/noEstablishingFee", merchelloStoreCulture);
        var localizedUsers = localizedTextService.Localize("ajourShopBasketForm/users", merchelloStoreCulture);
        var localizedPrice = localizedTextService.Localize("ajourShopBasketForm/Price", merchelloStoreCulture);
        var localizedQuantity = localizedTextService.Localize("ajourShopBasketForm/Quantity", merchelloStoreCulture);
        var localizedTotal = localizedTextService.Localize("ajourShopBasketForm/Total", merchelloStoreCulture);
        var localizedExcludingVat = localizedTextService.Localize("ajourShopBasketForm/excludingVat", merchelloStoreCulture);
        var localizedRemove = localizedTextService.Localize("ajourShopBasketForm/Remove", merchelloStoreCulture);
        var localizedSubTotal = localizedTextService.Localize("ajourShopBasketForm/SubTotal", merchelloStoreCulture);
    
        var currentPage = Umbraco.TypedContent(UmbracoContext.Current.PageId);
    
        var foundAtLeastOneProductHavingOptionEstablishing = false;
        var foundAtLeastOneProductHavingOptionUsers = false;
        if (Model.Items != null && Model.Items.Any())
        {
            for (var i = 0; i < Model.Items.Count(); i++)
            {
                var product = Model.Items[i].Product;
                var isProduct = product != null;
                if (isProduct)
                {
                    var productHasOptionEstablishing = product.Options.Any(x => x.Name == "Establishing");
                    if (productHasOptionEstablishing)
                    {
                        foundAtLeastOneProductHavingOptionEstablishing = true;
                    }
                    var productHasOptionUsers = product.Options.Any(x => x.Name == "Users");
                    if (productHasOptionUsers)
                    {
                        foundAtLeastOneProductHavingOptionUsers = true;
                    }
                }
            }
        }
    
        using (Html.BeginUmbracoForm<AjourStoreBasketController>("UpdateBasket", new {area = "FastTrack"}, new {data_muifrm = "basket"}))
        {
    
            @Html.AntiForgeryToken()
    
            <table class="table mui-basket mui-default">
                <thead>
                <tr>
                    <th>@localizedProduct</th>
                    @if (foundAtLeastOneProductHavingOptionEstablishing)
                    {
                        <th>@localizedStartup</th>
                    }
                    @if (foundAtLeastOneProductHavingOptionUsers)
                    {
                        <th>@localizedSubscription</th>
                    }
                    <th>@localizedPrice</th>
                    <th>@localizedQuantity</th>
                    <th>@localizedTotal <span style="font-weight: normal">@localizedExcludingVat</span></th>
                    <th></th>
                </tr>
                </thead>
                <tbody>
    
                @for (var i = 0; i < Model.Items.Count(); i++)
                {
                    var productContent = Model.Items[i].Product;
                    var isProduct = productContent != null;
    
                    var productHasOptionEstablishing = false;
                    var productHasOptionUsers = false;
                    if (isProduct)
                    {
                        productHasOptionEstablishing = productContent.Options.Any(x => x.Name == "Establishing");
                        productHasOptionUsers = productContent.Options.Any(x => x.Name == "Users");
                            @*for(int j = 0; j < Model.Items[i].ExtendedData.ToList().Count; j++)
                                {
                                    @Html.Hidden("Items[" + i + "].ExtendedData["+ j + "]." + Model.Items[i].ExtendedData.ToList()[j].Key, Model.Items[i].ExtendedData.ToList()[j].Value, htmlAttributes: new {id="Items_"+i+"__ExtendedData_" + Model.Items[i].ExtendedData.ToList()[j].Key })
                                }*@
                    }
    
    
                    <tr>
                        <td class="image">
                            @Html.HiddenFor(model => Model.Items[i].Key)
                            @if (isProduct)
                            {
                                // This is a property reference to a content type on the default starter kit
                                // and may need to change for site specific implementations.  Example!
                                if (productContent.HasValue("image"))
                                {
                                    var mediaId = productContent.GetPropertyValue<string>("image");
                                    var image = Umbraco.TypedMedia(mediaId);
    
                                    <a href="@productContent.Url"><img src="@image.GetCropUrl(width: 20)" alt="@productContent.Name" width="20"/></a>
                                }
                                <a href="@productContent.Url">
                                    <span>@productContent.Name</span>
                                    @{
                                        if (!productHasOptionEstablishing && !productHasOptionUsers)
                                        {
                                            foreach (var choice in Model.Items[i].CustomerOptionChoices)
                                            {
                                                <span class="option-choice">@choice.Key - @choice.Value</span>
                                            }
                                        }
                                    }
                                </a>
                            }
                            else
                            {
                                // not a product (custom implementation) so just put the name here
                                <span>@Model.Items[i].Name</span>
                            }
                        </td>
                        @if (foundAtLeastOneProductHavingOptionEstablishing)
                        {
                            if (productHasOptionEstablishing)
                            {
                                <td>
                                    @{
                                        var establishingCustomerOptionChoice = Model.Items[i].CustomerOptionChoices.FirstOrDefault(x => x.Key == "Establishing");
                                        if (!establishingCustomerOptionChoice.Equals(default(KeyValuePair<string, string>)) && !string.IsNullOrEmpty(establishingCustomerOptionChoice.Key))
                                        {
                                            var optionChoice = establishingCustomerOptionChoice.Value;
                                            var localizedOptionChoice = AjourHelpers.LocalizeProductEstablishingOptionChoice(optionChoice, localizedEstablishingFee, localizedNoEstablishingFee);
                                            <span class="option-choice">@localizedOptionChoice</span>
                                        }
                                    }
                                </td>
                            }
                            if (productHasOptionUsers)
                            {
                                <td nowrap="nowrap">
                                    <span style="white-space: nowrap">
                                        @localizedUsers
                                        @{
                                            var establishingModelProductOption = Model.Items[i].Product.Options.First(x => x.Name == "Establishing");
                                            var usersModelProductOption = Model.Items[i].Product.Options.First(x => x.Name == "Users");
                                            var productContentProductVariants = productContent.ProductVariants;
                                            var productVariantKey = Guid.Parse(Model.Items[i].ExtendedData.First(y => y.Key == Merchello.Core.Constants.ExtendedDataKeys.ProductVariantKey).Value);
                                            var productVariantContent = productContentProductVariants.First(x => x.Key == productVariantKey);
                                            var establishingProductVariantContentOptionChoice = productVariantContent.Attributes.First(x => x.OptionKey == establishingModelProductOption.Key);
                                            var establishingModelProductOptionChoice = establishingModelProductOption.Choices.First(x => x.Key == establishingProductVariantContentOptionChoice.Key);
                                            var usersCustomerOptionChoice = Model.Items[i].CustomerOptionChoices.FirstOrDefault(x => x.Key == "Users");
                                            var usersModelProductOptionChoices = usersModelProductOption.Choices.OrderBy(x => x.SortOrder).Select(choice => new System.Web.Mvc.SelectListItem {Value = productContent.GetProductVariantDisplayWithAttributes(new Guid[] {establishingModelProductOptionChoice.Key, choice.Key}).Sku, Text = choice.Name, Selected = choice.Name == usersCustomerOptionChoice.Value}).ToList();
                                                            @*@Html.DropDownList("Items["+i+"].Sku",usersModelProductOptionChoices, htmlAttributes: new {@type="string", data_muiaction = "updatequantity"})*@
                                            @Html.DropDownListFor(model => Model.Items[i].Sku, usersModelProductOptionChoices, htmlAttributes: new {@type = "string", data_muiaction = "updatequantity"})
                                        }
                                    </span>
                                </td>
                            }
                        }
                        <td data-muivalue="lineamount">
                            @Model.Items[i].Amount.AsFormattedCurrency()
                        </td>
                        <td>
                            @if (isProduct)
                            {
                                @Html.TextBoxFor(model => Model.Items[i].Quantity, new {@type = "number", min = "1", data_muiaction = "updatequantity"})
                            }
                            else
                            {
                                // assume this can't be changed if it is not a product
                                @Html.HiddenFor(model => Model.Items[i].Quantity)
                            }
                        </td>
                        <td data-muivalue="linetotal">@Model.Items[i].Total().AsFormattedCurrency()</td>
                        <td class="text-right">
                            @*@if (Model.WishListEnabled && isProduct) // only products can be added to the wishlist
                                    {
                                        // the 'area' parameter in the route values should match the PluginController attribute alias
                                        @Html.ActionLink("Wish List +", "MoveItemToWishList", "AjourStoreBasket", new { area = "Merchello", lineItemKey = Model.Items[i].Key, successRedirectId = currentPage.Id }, new { @class = "btn btn-info" })
                                    }*@
    
                            @Html.ActionLink(localizedRemove, "RemoveBasketItem", "AjourStoreBasket", new {area = "Merchello", lineItemKey = "dummyValue", redirectId = currentPage.Id}, new {@class = "btn btn-danger", @id = "Items_" + i + "__Remove"})
                        </td>
                    </tr>
    
    
                }
                <tr>
                    <td colspan="3" class="text-right"><strong>@localizedSubTotal</strong></td>
                    <td colspan="2" data-muivalue="total"><strong>@Model.Total().AsFormattedCurrency()</strong></td>
                </tr>
                <tr class="hide">
                    <td colspan="3">&nbsp;</td>
                    <td colspan="2"><input type="submit" id="update-cart" class="btn btn-default" name="update" value="Update" data-muibtn="update"/></td>
                </tr>
                </tbody>
            </table>
    
        }
    }
    

    It is in this Basket Form that I implemented our initial rule on getting the available variants:

        <td nowrap="nowrap">
            <span style="white-space: nowrap">
                @localizedUsers
                @{
                    var establishingModelProductOption = Model.Items[i].Product.Options.First(x => x.Name == "Establishing");
                    var usersModelProductOption = Model.Items[i].Product.Options.First(x => x.Name == "Users");
                    var productContentProductVariants = productContent.ProductVariants;
                    var productVariantKey = Guid.Parse(Model.Items[i].ExtendedData.First(y => y.Key == Merchello.Core.Constants.ExtendedDataKeys.ProductVariantKey).Value);
                    var productVariantContent = productContentProductVariants.First(x => x.Key == productVariantKey);
                    var establishingProductVariantContentOptionChoice = productVariantContent.Attributes.First(x => x.OptionKey == establishingModelProductOption.Key);
                    var establishingModelProductOptionChoice = establishingModelProductOption.Choices.First(x => x.Key == establishingProductVariantContentOptionChoice.Key);
                    var usersCustomerOptionChoice = Model.Items[i].CustomerOptionChoices.FirstOrDefault(x => x.Key == "Users");
                    var usersModelProductOptionChoices = usersModelProductOption.Choices.OrderBy(x => x.SortOrder).Select(choice => new System.Web.Mvc.SelectListItem {Value = productContent.GetProductVariantDisplayWithAttributes(new Guid[] {establishingModelProductOptionChoice.Key, choice.Key}).Sku, Text = choice.Name, Selected = choice.Name == usersCustomerOptionChoice.Value}).ToList();
                                    @*@Html.DropDownList("Items["+i+"].Sku",usersModelProductOptionChoices, htmlAttributes: new {@type="string", data_muiaction = "updatequantity"})*@
                    @Html.DropDownListFor(model => Model.Items[i].Sku, usersModelProductOptionChoices, htmlAttributes: new {@type = "string", data_muiaction = "updatequantity"})
                }
            </span>
        </td>
    

    As seen in here, the Model.Items[i].Sku is needed, so the basket model is re-created separatedly in the following supporting classes:

    AjourStoreBasketModel.cs:

    using AjourCms.Models.Ui;
    using Merchello.Web.Store.Models;
    
    namespace AjourCms.Models
    {
        using Merchello.Web.Models.Ui;
    
        /// <summary>
        /// A model to represent a basket in the UI.
        /// </summary>
        public class AjourStoreBasketModel : IAjourBasketModel<StoreLineItemModel>
        {
            /// <summary>
            /// Gets or sets the basket items.
            /// </summary>
            public StoreLineItemModel[] Items { get; set; }
    
            /// <summary>
            /// Gets or sets a value indicating whether the wish list is enabled.
            /// </summary>
            public bool WishListEnabled { get; set; }
        }
    }
    

    IAjourBasketModel.cs:

    using Merchello.Web.Models.Ui;
    
    namespace AjourCms.Models.Ui
    {
        /// <summary>
        /// Defines a basket UI component.
        /// </summary>
        /// <typeparam name="TAjourBasketItemModel">
        /// The type of the basket item.
        /// </typeparam>
        public interface IAjourBasketModel<TAjourBasketItemModel> : IItemCacheModel<TAjourBasketItemModel>
            where TAjourBasketItemModel : class, ILineItemModel, new()
        {
            /// <summary>
            /// Gets or sets a value indicating whether the wish list is enabled.
            /// </summary>
            bool WishListEnabled { get; set; }
        }
    }
    

    AjourStoreBasketModelFactory.cs:

    using AjourCms.Models;
    
    namespace AjourCms.Factories
    {
        using System;
        using System.Linq;
    
        using Merchello.Core;
        using Merchello.Core.Models;
        using Merchello.Web;
        using Merchello.Web.Factories;
        using Merchello.Web.Models.Ui;
        using Merchello.Web.Models.VirtualContent;
        using Merchello.Web.Store.Models;
        using Merchello.Web.Workflow;
    
        using Umbraco.Core;
    
        /// <summary>
        /// The basket model factory for the default implementation.
        /// </summary>
        public class AjourStoreBasketModelFactory : AjourBasketModelFactory<AjourStoreBasketModel, StoreLineItemModel>
        {
            /// <summary>
            /// The <see cref="MerchelloHelper"/>.
            /// </summary>
            private readonly MerchelloHelper _merchello;
    
            /// <summary>
            /// Initializes a new instance of the <see cref="AjourStoreBasketModelFactory"/> class.
            /// </summary>
            public AjourStoreBasketModelFactory()
                : this(new MerchelloHelper())
            {
            }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="AjourStoreBasketModelFactory"/> class.
            /// </summary>
            /// <param name="merchello">
            /// The <see cref="MerchelloHelper"/>.
            /// </param>
            public AjourStoreBasketModelFactory(MerchelloHelper merchello)
            {
                Mandate.ParameterNotNull(merchello, "merchello");
                this._merchello = merchello;
            }
    
            /// <summary>
            /// Overrides the base basket model creation.
            /// </summary>
            /// <param name="basketModel">
            /// The <see cref="StoreBasketModel"/>.
            /// </param>
            /// <param name="basket">
            /// The <see cref="IBasket"/>.
            /// </param>
            /// <returns>
            /// The modified <see cref="StoreBasketModel"/>.
            /// </returns>
            protected override AjourStoreBasketModel OnCreate(AjourStoreBasketModel basketModel, IBasket basket)
            {
                // Ensure to order of the basket items is in alphabetical order.
                basketModel.Items = basketModel.Items.OrderBy(x => x.Name).ToArray();
    
                return base.OnCreate(basketModel, basket);
            }
    
            /// <summary>
            /// Overrides the base basket item model creation.
            /// </summary>
            /// <param name="storeLineItem">
            /// The <see cref="StoreLineItemModel"/>.
            /// </param>
            /// <param name="lineItem">
            /// The <see cref="ILineItem"/>.
            /// </param>
            /// <returns>
            /// The modified <see cref="StoreLineItemModel"/>.
            /// </returns>
            protected override StoreLineItemModel OnCreate(StoreLineItemModel storeLineItem, ILineItem lineItem)
            {
                // Get the product key from the extended data collection
                // This is added internally when the product was added to the basket
                var productKey = lineItem.ExtendedData.GetProductKey();
    
                // Get an instantiated IProductContent for use in the basket table design
                var product = lineItem.LineItemType == LineItemType.Product ?
                                this.GetProductContent(productKey) :
                                null;
    
                // Get a list of choices the customer made.  This can also be done by looking at the variant (Attributes)
                // but this is a bit quicker and is something commonly done.
                var customerChoices = lineItem.GetProductOptionChoicePairs();
    
                // Modifiy the BasketItemModel generated in the base factory
                storeLineItem.Product = product;
                storeLineItem.ProductKey = productKey;
                storeLineItem.CustomerOptionChoices = customerChoices;
    
                return base.OnCreate(storeLineItem, lineItem);
            }
    
            /// <summary>
            /// Gets the <see cref="IProductContent"/>.
            /// </summary>
            /// <param name="productKey">
            /// The product key.
            /// </param>
            /// <returns>
            /// The <see cref="IProductContent"/>.
            /// </returns>
            private IProductContent GetProductContent(Guid productKey)
            {
                if (productKey.Equals(Guid.Empty)) return null;
                return this._merchello.TypedProductContent(productKey);
            }
        }
    }
    

    AjourBasketModelFactory.cs:

    using AjourCms.Models.Ui;
    using Merchello.Web.Models.Ui;
    
    namespace AjourCms.Factories
    {
        using System.Linq;
    
        using Merchello.Core.Models;
        using Merchello.Web.Workflow;
    
        /// <summary>
        /// A factory responsible for building <see cref="IAjourBasketModel{TAjourBasketItemModel}"/> and <see cref="ILineItemModel"/>.
        /// </summary>
        /// <typeparam name="TAjourBasketModel">
        /// The type of <see cref="IAjourBasketModel{TAjourBasketItemModel}"/>
        /// </typeparam>
        /// <typeparam name="TAjourBasketItemModel">
        /// The type of <see cref="ILineItemModel"/>
        /// </typeparam>
        public class AjourBasketModelFactory<TAjourBasketModel, TAjourBasketItemModel>
            where TAjourBasketModel : class, IAjourBasketModel<TAjourBasketItemModel>, new()
            where TAjourBasketItemModel : class, ILineItemModel, new()
        {
            /// <summary>
            /// Creates <see cref="IAjourBasketModel{TAjourBasketItemModel}"/> from <see cref="IBasket"/>.
            /// </summary>
            /// <param name="basket">
            /// The <see cref="IBasket"/>.
            /// </param>
            /// <returns>
            /// The <see cref="IBasketModel{TBasketItemModel}"/>.
            /// </returns>
            public TAjourBasketModel Create(IBasket basket)
            {
                var basketItems = basket.Items.Select(this.Create).ToArray();
    
                var basketModel = new TAjourBasketModel
                {
                    WishListEnabled = !basket.Customer.IsAnonymous,
                    Items = basketItems
                };
    
                return this.OnCreate(basketModel, basket);
            }
    
            /// <summary>
            /// Creates <see cref="ILineItemModel"/> from <see cref="ILineItem"/>.
            /// </summary>
            /// <param name="lineItem">
            /// The <see cref="ILineItem"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ILineItemModel"/>.
            /// </returns>
            public TAjourBasketItemModel Create(ILineItem lineItem)
            {
                var basketItem = new TAjourBasketItemModel
                    {
                        Key = lineItem.Key,
                        Name = lineItem.Name,
                        Amount = lineItem.Price,
                        Quantity = lineItem.Quantity,
                        LineItemType = lineItem.LineItemType, // M-1105 - can't assume this is a product
                        ExtendedData = lineItem.ExtendedData.AsEnumerable(),
                        Sku = lineItem.Sku
                    };
    
                return this.OnCreate(basketItem, lineItem);
            }
    
            /// <summary>
            /// Allows for overriding the creation of <see cref="IAjourBasketModel{TAjourBasketItemModel}"/> from <see cref="IBasket"/>.
            /// </summary>
            /// <param name="basketModel">
            /// The <see cref="IAjourBasketModel{TAjourBasketItemModel}"/>.
            /// </param>
            /// <param name="basket">
            /// The <see cref="IBasket"/>.
            /// </param>
            /// <returns>
            /// The <see cref="ILineItemModel"/>.
            /// </returns>
            protected virtual TAjourBasketModel OnCreate(TAjourBasketModel basketModel, IBasket basket)
            {
                return basketModel;
            }
    
            /// <summary>
            /// Allows for overriding the creation of <see cref="ILineItemModel"/> from <see cref="ILineItem"/>.
            /// </summary>
            /// <param name="basketItem">
            /// The basket item.
            /// </param>
            /// <param name="lineItem">
            /// The line item.
            /// </param>
            /// <returns>
            /// The <see cref="ILineItemModel"/>.
            /// </returns>
            protected virtual TAjourBasketItemModel OnCreate(TAjourBasketItemModel basketItem, ILineItem lineItem)
            {
                return basketItem;
            } 
        }
    }
    

    For the async responses to the client js, the AjourUpdateQuantityAsyncResponse (which is a straight copy of the original Merchello version):

    namespace AjourCms.Models.Async
    {
        using System.Collections.Generic;
        using System.Diagnostics.CodeAnalysis;
    
        using Merchello.Core;
        using Merchello.Core.Models;
        using Merchello.Web.Models.Ui.Async;
    
        /// <summary>
        /// A response object to for an AJAX UpdateQuantity operation.
        /// </summary>
        internal class AjourUpdateQuantityAsyncResponse : AsyncResponse, IEmitsBasketItemCount
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="AjourUpdateQuantityAsyncResponse"/> class.
            /// </summary>
            public AjourUpdateQuantityAsyncResponse()
            {
                this.UpdatedItems = new List<AjourUpdateQuantityResponseItem>();
            }
    
            /// <summary>
            /// Gets or sets the formatted total.
            /// </summary>
            public string FormattedTotal { get; set; }
    
            /// <summary>
            /// Gets or sets the updated items.
            /// </summary>
            public List<AjourUpdateQuantityResponseItem> UpdatedItems { get; set; }
    
            /// <summary>
            /// Gets or sets the item count.
            /// </summary>
            public int ItemCount { get; set; }
        }
    
        /// <summary>
        /// Extension methods for <see cref="UpdateQuantityAsyncResponse"/>.
        /// </summary>
        [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:StaticElementsMustAppearBeforeInstanceElements", Justification = "Reviewed. Suppression is OK here.")]
        internal static class UpdateQuantityAsyncResponseExtensions
        {
            /// <summary>
            /// Maps a collection of <see cref="ILineItem"/> to a collection of <see cref="UpdateQuantityResponseItem"/>.
            /// </summary>
            /// <param name="resp">
            /// The resp.
            /// </param>
            /// <param name="items">
            /// The items.
            /// </param>
            public static void AddUpdatedItems(this AjourUpdateQuantityAsyncResponse resp, IEnumerable<ILineItem> items)
            {
                foreach (var item in items)
                {
                    resp.UpdatedItems.Add(
                        new AjourUpdateQuantityResponseItem
                            {
                                Key = item.Key,
                                FormattedAmount = item.Price.AsFormattedCurrency(),
                                Quantity = item.Quantity,
                                FormattedTotal = item.TotalPrice.AsFormattedCurrency()
                            });
                }
            }
        }
    }
    

    but needed to enclose the modified version of the AjourUpdateQuantityResponseItem.cs, which holds the new FormattedAmount:

    namespace AjourCms.Models.Async
    {
        using System;
    
        /// <summary>
        /// Represents an updated line item.
        /// </summary>
        internal class AjourUpdateQuantityResponseItem
        {
            /// <summary>
            /// Gets or sets the key.
            /// </summary>
            public Guid Key { get; set; }
    
            /// <summary>
            /// Gets or sets the quantity.
            /// </summary>
            public int Quantity { get; set; }
    
            /// <summary>
            /// Gets or sets the formatted amount.
            /// </summary>
            public string FormattedAmount { get; set; }
    
            /// <summary>
            /// Gets or sets the formatted total.
            /// </summary>
            public string FormattedTotal { get; set; }
        }
    }
    

    Finally, I had to "branch out" the MUI.Basket section from the merchello.ui.js, into our ajour.merchello.ui.js:

    MUI.Basket = {
    
        // initialize the basket
        init: function () {
            if (MUI.Settings.Endpoints.basketSurface === undefined || MUI.Settings.Endpoints.basketSurface === '') return;
    
            var frm = $('[data-muifrm="basket"]');
            if (frm.length > 0) {
                MUI.Basket.bind.form(frm[0]);
            }
        },
    
        // binds the form
        bind: {
            form: function (frm) {
                // Watch for changes in the input fields
                $(frm).find(':input[data-muiaction="updatequantity"]').change(function () {
                    var frmRef = $(this).closest('form');
    
                    // post the form to update the basket quantities
                    var url = MUI.Settings.Endpoints.basketSurface + 'UpdateBasket';
                    $.ajax({
                        type: 'POST',
                        url: url,
                        data: $(frmRef).serialize()
                    }).then(function (results) {
    
                        // update the line items sub totals
                        $.each(results.UpdatedItems, function (idx, item) {
                            var hid = $('input:hidden[name="Items[' + idx + '].Key"]');
                            if (hid.length > 0) {
                                $(hid).val(item.Key);
                                var amount = $(hid).closest('tr').find('[data-muivalue="lineamount"]');
                                if (amount.length > 0) {
                                    $(amount).html(item.FormattedAmount);
                                }
                                var subtotal = $(hid).closest('tr').find('[data-muivalue="linetotal"]');
                                if (subtotal.length > 0) {
                                    $(subtotal).html(item.FormattedTotal);
                                }
                            }
                        });
    
                        // set the new basket total
                        var total = $(frmRef).find('[data-muivalue="total"]');
                        if (total.length > 0) {
                            $(total).html(results.FormattedTotal);
                        }
    
                        // Emit the event so that labels can update if handled
                        MUI.emit('AddItem.added', results);
    
                    }, function (err) {
                        MUI.Logger.captureError(err);
                    });
                });
    
                $('[id ^=Items][id $=Remove]').click(function () {
                    var hiddenKeyId = this.id.replace("Remove", "Key");
                    var hiddenKey = $("#" + hiddenKeyId);
                    if (hiddenKey.length > 0) {
                        var key = hiddenKey.val();
                        this.href = this.href.replace("dummyValue", key);
                    }
                });
    
                var btn = $(frm).find('[data-muibtn="update"]');
                if (btn.length > 0) {
                    $(btn).hide();
                }
    
            }
        }
    };
    

    So finally the FastTrack.cshtml has the following Merchello UI script lines:

    <!-- new Merchello UI scripts -->
    <script src="~/App_Plugins/Merchello/client/js/merchello.ui.min.js"></script>
    <script src="~/App_Plugins/Merchello/client/js/merchello.ui.settings.js"></script>
    <script src="~/js/ajour.merchello.ui.js"></script>
    <script src="~/js/ajour.merchello.ui.settings.js"></script>
    <script src="~/App_Plugins/Merchello/client/js/fasttrack.js"></script>
    

    Aalso, the ftBasket.cshtml needs to call the new AjourStoreBasket controller:

    @if (CurrentCustomer.Basket().Items.Any())
    {
        @Html.Action("BasketForm", "AjourStoreBasket", new { area = "Merchello" })
    

    And that's the way the product variant gets replaced in the basket using the same procedure as changing the quantity.

    Of course, this is not an optimized code, not even a good architectural solution (the new controller should have dedicated method for such feature), it's more like a quick workaround based on the existing code but separated from Merchello base-code. Please note the Remove ActionLink "hack" using the htmlAttribute id and the "dummyValue" sent to the js listener :

        $('[id ^=Items][id $=Remove]').click(function () {
            var hiddenKeyId = this.id.replace("Remove", "Key");
            var hiddenKey = $("#" + hiddenKeyId);
            if (hiddenKey.length > 0) {
                var key = hiddenKey.val();
                this.href = this.href.replace("dummyValue", key);
            }
        });
    

    This is because initially Merchello's way to grab the itemLine key is from the server-sided model, but the UpdateBasket response changes the item key value from the hidden input, so then the Remove ActionLink must send the latest value of the hidden input to the server to match the EnsureOwner check further in the api.

    I hope I posted everything here, Rusty (thanks again for your guidance), whenever you have some time please take a look and let me know if you spot critical flaws in this approach, I'll look forward to see this feature nicely implemented in Merchello in future versions. Our need here was not the most general one, so for sure a better generalized way to switch among variants should come in the future.

    My next task is to change the way we compute the line totals. We have the Establishing price, we have the variant price and the total price for the item line should be the sum of the two. The Establishing price will be time dependent, something like 5 days from downloading the digital product it will have a value, then a different value. The variant price is fixed, but different on the variant of course. Any pointers to that, Rusty ?

  • Claudiu Bria 32 posts 143 karma points
    May 21, 2017 @ 15:32
    Claudiu Bria
    0

    Updates to the solution above:

    In practice, our solution does not need more than one item in the basket, and that item has both options (Establishing and Users) only. Therefore the solution presented above will work in those conditions only.

    To have it work with multiple items in the basket i needed to remove the name sorting in the AjourStoreBasketModelFactory.cs:

    /// <summary>
    /// Overrides the base basket model creation.
    /// </summary>
    /// <param name="basketModel">
    /// The <see cref="StoreBasketModel"/>.
    /// </param>
    /// <param name="basket">
    /// The <see cref="IBasket"/>.
    /// </param>
    /// <returns>
    /// The modified <see cref="StoreBasketModel"/>.
    /// </returns>
    protected override AjourStoreBasketModel OnCreate(AjourStoreBasketModel basketModel, IBasket basket)
    {
        // Ensure to order of the basket items is in alphabetical order.
        //basketModel.Items = basketModel.Items.OrderBy(x => x.Name).ToArray();
        // no re-ordering, because of the possible key changing
        basketModel.Items = basketModel.Items.ToArray();
    
        return base.OnCreate(basketModel, basket);
    }
    

    This means the basket will always show the order of items as they were added. This now has implications on how the update of the sku is performed internally on the serrver, keeping the basket order the same while an item - that could be followed by other items - is already removed and added back to the basket. The basket does not allow insertion of an item, so my approach then is a straightforward serial remove and re-add of all the following items after the update. Then the UpdateBasket(TAjourBasketModel model) api method in AjourBasketControllerBase{T}.cs becomes (with refactoring):

    /// <summary>
    /// Responsible for updating the quantities of items in the basket
    /// </summary>
    /// <param name="model">The <see cref="IBasketModel{TBasketItemModel}"/></param>
    /// <returns>Redirects to the current Umbraco page (generally the basket page)</returns>
    [HttpPost]
    [ValidateAntiForgeryToken]
    public virtual ActionResult UpdateBasket(TAjourBasketModel model)
    {
        if (!this.ModelState.IsValid) return this.CurrentUmbracoPage();
    
        var merchello = new MerchelloHelper(false);
    
        var modelSkuHasChanged = false;
        foreach (var item in model.Items)
        {
            if (this.Basket.Items.All(x => x.Key != item.Key)) continue;
            var basketItem = this.Basket.Items.First(x => x.Key == item.Key);
            if (basketItem != null && basketItem.Sku != item.Sku)
            {
                modelSkuHasChanged = true;
                break;
            }
        }
    
        if (modelSkuHasChanged)
        {
            var postUpdateItems = new List<ILineItem>();
            foreach (var item in model.Items.Where(item => this.Basket.Items.FirstOrDefault(x => x.Key == item.Key)?.Sku != item.Sku))
            {
                var currentBasketItem = this.Basket.Items.First(x => x.Key == item.Key);
                var itemIndex = this.Basket.Items.FindIndex(x => x.Key == currentBasketItem.Key);
                if (this.Basket.Items.Count > itemIndex + 1)
                {
                    for (int i = itemIndex + 1; i < this.Basket.Items.Count; i++)
                    {
                        postUpdateItems.Add(this.Basket.Items.ElementAt(i));
                    }
                }
                //                    this.Basket.EnableDataModifiers = true;
                this.Basket.RemoveItem(currentBasketItem.Key);
                ReAddBasketItem(merchello, currentBasketItem, item);
    
            }
            if (postUpdateItems.Count > 0)
            {
                foreach (var postUpdateItem in postUpdateItems)
                {
                    this.Basket.RemoveItem(postUpdateItem.Key);
                    ReAddBasketItem(merchello, postUpdateItem);
                }
            }
        }
        else
        {
            var modelQuantityHasChanged = false;
            foreach (var item in model.Items)
            {
                if (this.Basket.Items.All(x => x.Key != item.Key)) continue;
                var basketItem = this.Basket.Items.First(x => x.Key == item.Key);
                if (basketItem != null && basketItem.Quantity != item.Quantity)
                {
                    modelQuantityHasChanged = true;
                    break;
                }
            }
            if (modelQuantityHasChanged)
            {
                foreach (var item in model.Items.Where(item => this.Basket.Items.FirstOrDefault(x => x.Key == item.Key)?.Quantity != item.Quantity))
                {
                    this.Basket.UpdateQuantity(item.Key, item.Quantity);
                }
            }
        }
    
        this.Basket.Save();
        this.Basket.Refresh();
    
        return this.HandleUpdateBasketSuccess(model);
    }
    
    private void ReAddBasketItem(MerchelloHelper merchello, ILineItem basketItem, TAjourBasketItemModel updateModel = null)
    {
        var productKey = basketItem.ExtendedData.GetProductKey();
        var productDisplay = merchello.Query.Product.GetByKey(productKey);
        var productVariantDisplay = merchello.Query.Product.GetProductVariantBySku(updateModel != null ? updateModel.Sku : basketItem.Sku);
        var addItemModel = new TAddItem()
        {
            ProductKey = productKey,
            Quantity = updateModel != null ? updateModel.Quantity : basketItem.Quantity,
            ProductOptions = productDisplay?.ProductOptions,
            OptionChoices = productVariantDisplay?.Attributes.Select(x => x.Key).ToArray()
        };
        var extendedData = this._addItemExtendedDataFactory.Create(addItemModel);
        try
        {
            var product = merchello.Query.Product.GetByKey(addItemModel.ProductKey);
            var quantity = addItemModel.Quantity <= 0 ? 1 : addItemModel.Quantity;
            if (addItemModel.OptionChoices != null && addItemModel.OptionChoices.Any())
            {
                var variant = product.GetProductVariantDisplayWithAttributes(addItemModel.OptionChoices);
                var choiceExplainations = new Dictionary<string, string>();
                foreach (var choice in variant.Attributes)
                {
                    var option = product.ProductOptions.FirstOrDefault(x => x.Key == choice.OptionKey);
                    if (option != null)
                    {
                        choiceExplainations.Add(option.Name, choice.Name);
                    }
                }
                extendedData.SetValue(Merchello.Core.Constants.ExtendedDataKeys.BasketItemCustomerChoice, JsonConvert.SerializeObject(choiceExplainations));
                this.Basket.AddItem(variant, variant.Name, quantity, extendedData);
            }
            else
            {
                this.Basket.AddItem(product, product.Name, quantity, extendedData);
            }
        }
        catch (Exception ex)
        {
            var logData = new ExtendedLoggerData();
            logData.AddCategory("Merchello");
            logData.AddCategory("Controllers");
            MultiLogHelper.Error<AjourBasketControllerBase<TAjourBasketModel, TAjourBasketItemModel, TAddItem>>("Failed to add item to basket", ex, logData);
            throw;
        }
    }
    

    Finally, the views/AjourStoreBasket/BasketForm.cs needs to accommodate the mixed items display:

    @inherits Umbraco.Web.Mvc.UmbracoViewPage<AjourCms.Models.AjourStoreBasketModel>
    @using System.Web.Mvc.Html
    @using AjourCms.Controllers
    @using Merchello.Core
    @using Merchello.Web
    @using Merchello.Web.Models.Ui
    @using Merchello.Web.Models.VirtualContent
    @using Merchello.Web.Store.Controllers
    @using Umbraco.Core.Models
    @using Umbraco.Web
    @using AjourHelpers = AjourCms.Ajour.Helpers;
    @{
        var merchelloStoreCulture = AjourHelpers.MerchelloStoreCulture;
        System.Threading.Thread.CurrentThread.CurrentCulture = merchelloStoreCulture;
        System.Threading.Thread.CurrentThread.CurrentUICulture = merchelloStoreCulture;
        var localizedTextService = ApplicationContext.Services.TextService;
        var localizedProduct = localizedTextService.Localize("ajourShopBasketForm/Product", merchelloStoreCulture);
        var localizedStartup = localizedTextService.Localize("ajourShopBasketForm/Startup", merchelloStoreCulture);
        var localizedSubscription = localizedTextService.Localize("ajourShopBasketForm/Subscription", merchelloStoreCulture);
        var localizedEstablishingFee = localizedTextService.Localize("ajourShopBasketForm/establishingFee", merchelloStoreCulture);
        var localizedNoEstablishingFee = localizedTextService.Localize("ajourShopBasketForm/noEstablishingFee", merchelloStoreCulture);
        var localizedUser = localizedTextService.Localize("ajourShopBasketForm/user", merchelloStoreCulture);
        var localizedYear = localizedTextService.Localize("ajourShopBasketForm/year", merchelloStoreCulture);
        var localizedUsers = localizedTextService.Localize("ajourShopBasketForm/users", merchelloStoreCulture);
        var localizedPrice = localizedTextService.Localize("ajourShopBasketForm/Price", merchelloStoreCulture);
        var localizedQuantity = localizedTextService.Localize("ajourShopBasketForm/Quantity", merchelloStoreCulture);
        var localizedTotal = localizedTextService.Localize("ajourShopBasketForm/Total", merchelloStoreCulture);
        var localizedExcludingVat = localizedTextService.Localize("ajourShopBasketForm/excludingVat", merchelloStoreCulture);
        var localizedRemove = localizedTextService.Localize("ajourShopBasketForm/Remove", merchelloStoreCulture);
        var localizedSubTotal = localizedTextService.Localize("ajourShopBasketForm/SubTotal", merchelloStoreCulture);
    
        var currentPage = Umbraco.TypedContent(UmbracoContext.Current.PageId);
    
        var foundAtLeastOneProductHavingOptionEstablishing = false;
        var foundAtLeastOneProductHavingOptionUsers = false;
        var foundAtleastOneProductWithSubscription = false;
        var foundAtLeastOneProductWithoutSubscription = false;
        if (Model.Items != null && Model.Items.Any())
        {
            for (var i = 0; i < Model.Items.Count(); i++)
            {
                var foundSubscription = false;
                var product = Model.Items[i].Product;
                var isProduct = product != null;
                if (isProduct)
                {
                    var productHasOptionEstablishing = product.Options.Any(x => x.Name == "Establishing");
                    if (productHasOptionEstablishing)
                    {
                        foundAtLeastOneProductHavingOptionEstablishing = true;
                    }
                    var productHasOptionUsers = product.Options.Any(x => x.Name == "Users");
                    if (productHasOptionUsers)
                    {
                        foundAtLeastOneProductHavingOptionUsers = true;
                        foundSubscription = true;
                        foundAtleastOneProductWithSubscription = true;
                    }
                }
                if (!foundSubscription)
                {
                    foundAtLeastOneProductWithoutSubscription = true;
                }
            }
        }
    
        using (Html.BeginUmbracoForm<AjourStoreBasketController>("UpdateBasket", new { area = "FastTrack" }, new { data_muifrm = "basket" }))
        {
    
            @Html.AntiForgeryToken()
    
            <table class="table mui-basket mui-default">
                <thead>
                    <tr>
                        <th>@localizedProduct</th>
                        @if (foundAtLeastOneProductHavingOptionEstablishing)
                    {
                            <th>@localizedStartup</th>
                        }
                        @if (foundAtLeastOneProductHavingOptionUsers)
                    {
                            <th>@localizedSubscription</th>
                        }
                        @if (foundAtLeastOneProductWithoutSubscription)
                        {
                            <th>@localizedPrice <span style="font-weight: normal">@localizedExcludingVat</span></th>
                            <th>@localizedQuantity</th>
                        }
    
                        <th>@localizedTotal <span style="font-weight: normal">@localizedExcludingVat</span></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
    
                    @for (var i = 0; i < Model.Items.Count(); i++)
                    {
                        var productContent = Model.Items[i].Product;
                        var isProduct = productContent != null;
    
                        var productHasOptionEstablishing = false;
                        var productHasOptionUsers = false;
                        if (isProduct)
                        {
                            productHasOptionEstablishing = productContent.Options.Any(x => x.Name == "Establishing");
                            productHasOptionUsers = productContent.Options.Any(x => x.Name == "Users");
                            @*for(int j = 0; j < Model.Items[i].ExtendedData.ToList().Count; j++)
                                {
                                    @Html.Hidden("Items[" + i + "].ExtendedData["+ j + "]." + Model.Items[i].ExtendedData.ToList()[j].Key, Model.Items[i].ExtendedData.ToList()[j].Value, htmlAttributes: new {id="Items_"+i+"__ExtendedData_" + Model.Items[i].ExtendedData.ToList()[j].Key })
                                }*@
                        }
    
    
                        <tr>
                            <td class="image">
                                @Html.HiddenFor(model => Model.Items[i].Key)
                                @if (isProduct)
                                {
                                    // This is a property reference to a content type on the default starter kit
                                    // and may need to change for site specific implementations.  Example!
                                    if (productContent.HasValue("image"))
                                    {
                                        var mediaId = productContent.GetPropertyValue<string>("image");
                                        var image = Umbraco.TypedMedia(mediaId);
    
                                        <a href="@productContent.Url"><img src="@image.GetCropUrl(width: 20)" alt="@productContent.Name" width="20" /></a>
                                    }
                                    <a href="@productContent.Url">
                                        <span>@productContent.Name</span>
                                        @{
                                            if (!productHasOptionEstablishing && !productHasOptionUsers)
                                            {
                                                foreach (var choice in Model.Items[i].CustomerOptionChoices)
                                                {
                                                    <span class="option-choice">@choice.Key - @choice.Value</span>
                                                }
                                            }
                                        }
                                    </a>
                                            }
                                            else
                                            {
                                                // not a product (custom implementation) so just put the name here
                                                <span>@Model.Items[i].Name</span>
                                            }
                            </td>
                            @if (foundAtLeastOneProductHavingOptionEstablishing)
                            {
                                if (productHasOptionEstablishing)
                                {
                                    <td>
                                        @{
                                            var establishingCustomerOptionChoice = Model.Items[i].CustomerOptionChoices.FirstOrDefault(x => x.Key == "Establishing");
                                            if (!establishingCustomerOptionChoice.Equals(default(KeyValuePair<string, string>)) && !string.IsNullOrEmpty(establishingCustomerOptionChoice.Key))
                                            {
                                                var optionChoice = establishingCustomerOptionChoice.Value;
                                                var localizedOptionChoice = AjourHelpers.LocalizeProductEstablishingOptionChoice(optionChoice, localizedEstablishingFee, localizedNoEstablishingFee);
                                                <span class="option-choice">@localizedOptionChoice</span>
                                            }
                                        }
                                    </td>
                                }
                                else
                                {
                                    <td></td>
                                }
                                if (productHasOptionUsers)
                                {
                                    <td nowrap="nowrap">
                                        <span style="white-space: nowrap">
                                            @localizedUsers
                                            @{
                                                var establishingModelProductOption = Model.Items[i].Product.Options.First(x => x.Name == "Establishing");
                                                var usersModelProductOption = Model.Items[i].Product.Options.First(x => x.Name == "Users");
                                                var productContentProductVariants = productContent.ProductVariants;
                                                var productVariantKey = Guid.Parse(Model.Items[i].ExtendedData.First(y => y.Key == Merchello.Core.Constants.ExtendedDataKeys.ProductVariantKey).Value);
                                                var productVariantContent = productContentProductVariants.First(x => x.Key == productVariantKey);
                                                var establishingProductVariantContentOptionChoice = productVariantContent.Attributes.First(x => x.OptionKey == establishingModelProductOption.Key);
                                                var establishingModelProductOptionChoice = establishingModelProductOption.Choices.First(x => x.Key == establishingProductVariantContentOptionChoice.Key);
                                                var usersCustomerOptionChoice = Model.Items[i].CustomerOptionChoices.FirstOrDefault(x => x.Key == "Users");
                                                var usersModelProductOptionChoices = usersModelProductOption.Choices.OrderBy(x => x.SortOrder).Select(choice => new System.Web.Mvc.SelectListItem {Value = productContent.GetProductVariantDisplayWithAttributes(new Guid[] {establishingModelProductOptionChoice.Key, choice.Key}).Sku, Text = choice.Name, Selected = choice.Name == usersCustomerOptionChoice.Value}).ToList();
                                                            @*@Html.DropDownList("Items["+i+"].Sku",usersModelProductOptionChoices, htmlAttributes: new {@type="string", data_muiaction = "updatequantity"})*@
                                                @Html.DropDownListFor(model => Model.Items[i].Sku, usersModelProductOptionChoices, htmlAttributes: new {@type = "string", data_muiaction = "updatequantity"})
                                            }
                                            @{
                                                var userSubscriptionText = "" + ((decimal) productContent.GetProperty("subscriptionPrice").Value).AsFormattedCurrency() + "/" + localizedUser + "/" + localizedYear;
                                                @Html.Label("Items[" + i + "].UserSubscription", userSubscriptionText, new {@class = "control-label", @style = "font-weight:normal"})
                                            }
                                        </span>
                                    </td>
                                }
                                else
                                {
                                    <td>
                                        @Html.HiddenFor(model => Model.Items[i].Sku)
                                    </td>
                                }
                            }
                            else
                            {
                                @Html.HiddenFor(model => Model.Items[i].Sku)
                            }
    
                            @if (foundAtLeastOneProductWithoutSubscription)
                            {
                                if (productHasOptionEstablishing || productHasOptionUsers)
                                {
                                    <td></td>
                                    <td>
                                        @Html.HiddenFor(model => Model.Items[i].Quantity)
                                    </td>
                                }
                                else
                                {
    
                                    <td data-muivalue="lineamount">
                                        @Model.Items[i].Amount.AsFormattedCurrency()
                                    </td>
                                    <td>
                                        @if (isProduct)
                                        {
                                            @Html.TextBoxFor(model => Model.Items[i].Quantity, new {@type = "number", min = "1", data_muiaction = "updatequantity"})
                                        }
                                        else
                                        {
                                            // assume this can't be changed if it is not a product
                                            @Html.HiddenFor(model => Model.Items[i].Quantity)
                                        }
                                    </td>
                                }
                            }
                            else
                            {
                                @Html.HiddenFor(model => Model.Items[i].Quantity)
                            }
                            <td data-muivalue="linetotal">@Model.Items[i].Total().AsFormattedCurrency()</td>
                            <td class="text-right">
                                @*@if (Model.WishListEnabled && isProduct) // only products can be added to the wishlist
                                    {
                                        // the 'area' parameter in the route values should match the PluginController attribute alias
                                        @Html.ActionLink("Wish List +", "MoveItemToWishList", "AjourStoreBasket", new { area = "Merchello", lineItemKey = Model.Items[i].Key, successRedirectId = currentPage.Id }, new { @class = "btn btn-info" })
                                    }*@
    
                                @Html.ActionLink(localizedRemove, "RemoveBasketItem", "AjourStoreBasket", new { area = "Merchello", lineItemKey = "dummyValue", redirectId = currentPage.Id }, new { @class = "btn btn-danger", @id = "Items_" + i + "__Remove" })
                            </td>
                        </tr>
                                                            }
                    <tr>
                        <td colspan="3" class="text-right"><strong>@localizedSubTotal</strong></td>
                        <td colspan="2" data-muivalue="total"><strong>@Model.Total().AsFormattedCurrency()</strong></td>
                    </tr>
                    <tr class="hide">
                        <td colspan="3">&nbsp;</td>
                        <td colspan="2"><input type="submit" id="update-cart" class="btn btn-default" name="update" value="Update" data-muibtn="update" /></td>
                    </tr>
                </tbody>
            </table>
    
                                                            }
    }
    

    With this updates on my solution I can now use our basket items for which i can change the variant in the basket, but also i can have the other items for which no special treatment has been applied.

    For clarity, here are some screenshots on my project running locally. First, the type of products, the Ajour Office is our customized product with the 2 options (Establishing and Users), the other two shirts are the origibnal Merchello products, also the whole page is translated in Norwegian:enter image description here

    The next screen is about the basket containing the usual items only: enter image description here

    The next screen is about the basket containing our special item only:enter image description here

    Finally, the basket can now contain a mix of these items: enter image description here

    In the middle, changing the "Brukere" value of the dropdown will actually change the variant in the basket.

Please Sign in or register to post replies

Write your reply to:

Draft