Copied to clipboard

Flag this post as spam?

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


  • William Zhang 39 posts 243 karma points
    Jun 05, 2016 @ 08:06
    William Zhang
    0

    Shipping address is null when basket contains product with stale data

    Hi,

    I've encountered a strange issue during my checkout process. Whenever I have a product whose price has changed (not on sale -> on sale with a lowered price) since the cutsomer added it to the basket, the shipping address is null when trying to trying to access it through CheckoutManagerBase.Customer.GetShipToAddress().

    From what I've managed to find out so far is that it seems to be caused by the CheckoutContext not being cached properly, thus causing the extended data (where the shipping address is temporarily saved) to be reset between checkout steps.

    The caching issue in turn is caused by a new basket version being generated every time I try to retrieve the CheckoutManager with basket.GetCheckoutManager(). When I debug I can see in the call stack that this is due to the ValidateProductPriceTask invalidating the "stale" product and saving the basket.

    What's strange is that the ValidateProductPriceTask runs multiple times, and invalidating the product and saving over and over again. I would expect that it would do this the first time and validating the product instead on subsequent calls.

    For what it's worth I've also managed to reproduce the same issue in the Bazaar project - when I update the price on a product currently in the basket the shipping address is null.

  • Rusty Swayne 1655 posts 4993 karma points c-trib
    Jun 05, 2016 @ 15:17
    Rusty Swayne
    0

    Hi William,

    From your description, it sounds like the CheckoutManager is actually working perfectly.

    The CheckoutManager is only intended to be used during a checkout and it assumes that ALL basket operations have been completed. It does this by asserting that it's version is the same as the basket version. So any time an item is added, removed, or changed in the basket, the basket version (GUID) is changed.

    If the basket version changes, the checkout is considered invalidated and the CheckoutManager resets.

    The reset is required to ensure shipping and taxation are calculated correctly. Say for example the shipping was based off of total order price, and you were far enough along in the checkout process - then changed the price of the line items in the basket. Or if the quantity of an item changes changes the entire total. (This would impact total weight as well). In both cases, tax charges would be probably be different and, depending on constraints in discounts, a coupon that once was valid could become invalid.

    The Validation chain (Tasks) do kick off any time a new CustomerItemCacheBase is created and at the end of the checkout process to make a final assertion before an invoice is generated (since it will be a permanent record).

    In customer saved baskets and wishlist lists, a customer can save an item for future purchase - say a month ago. Then when they return continue with the purchase. In that span of time, an item that was on sale could have gone off sale or maybe was deleted entirely from the catalog.

    However, it is difficult to anticipate every stores' requirements in regards to validation rules, which is why these were put into a task chain which could be manipulated via configuration. If there is an assertion that does not work with your particular workflow, you can simply comment out the task OR better yet, replace the task with a custom task you write that accounts for the specific circumstances in your checkout.

    It seems like you are trying to toggle the OnSale value or change the price of an item in the basket during the checkout ... but you did not exactly say so I'm making that presumption =)

    This invalidates the CheckoutManager because the items in the CheckoutManager are actually a literal COPY of the items added to the basket so any time the basket changes, the CheckoutManager will clear itself and make a New Copy of the items and kick off the validation.

    The CheckoutManager's copy can be accessed via

      var items = CheckoutManager.Context.ItemCache.Items;
    
  • William Zhang 39 posts 243 karma points
    Jun 05, 2016 @ 16:01
    William Zhang
    0

    Hi Rusty,

    I understand the reasoning behind the validation chain, but what I don't understand how I'm supposed to proceed if the validation tasks keep resetting the CheckoutManager, since the shipping info that is set in step 1 (customer details) in my checkout flow is no longer there when I request the CheckoutManager again in step 2 (shipping options). Below is a more detailed explanation of the checkout process, which is based on Angular + WebAPI, and has the following steps:

    1) Customer details: the user enters personal details and address, and upon form submission and AJAX request is made to the WebAPI that saves this info using the methods:

    CheckoutManagerBase.Customer.SaveBillToAddress(billingInfo);
    CheckoutManagerBase.Customer.SaveShipToAddress(shippingInfo);
    

    Once this is done the WebAPI responds with the available shipping methods.

    2) Shipping options: the user selects a shipping method, and once again an AJAX request is made with the selected shipping method key. However, when I try to retrieve the shipping address with:

    var shippingAddress = CheckoutManagerBase.Customer.GetShipToAddress();
    

    the shippingAddress is null, since the CheckoutManager is reset when calling CheckoutManagerBase.Customer.

    As you can see, I'm not really doing anything out of the ordinary - the only notable thing is that one of the products in the customer's saved basket has gone on sale since the customer initially added it to the basket. This causes the price validation task to kick in and update the basket, which is correct, but it gets into this "infinite loop" where it repeats this each time the CheckoutManager is requested (through basket.GetCheckoutManager()), preventing the CheckoutManager from retaining any info between the checkout steps since a new basket version is generated after each price validation.

    It almost seems as if the price validation task doesn't "remember" that it has updated the basket previously, and does it over and over again.

  • Rusty Swayne 1655 posts 4993 karma points c-trib
    Jun 05, 2016 @ 16:24
    Rusty Swayne
    0

    You can preserve the addresses by adjusting the CheckoutContext parameters: https://merchello.readme.io/docs/checkoutcontext (the checkout context is an optional constructor parameter). In version 2.1.0 you can set the defaults in the Merchello.config file so that you don't need to pass them everytime the CheckoutManager is created.

    If you set the ResetCustomerManagerDataOnVersionChange = false the addresses will not be removed when the CheckoutManager resets.

    the only notable thing is that one of the products in the customer's saved basket has gone on sale since the customer initially added it to the basket

    This is the reason the validation is kicking and altering the basket. Does this happen every time? I don't understand why this would happen in most checkouts.

    You can remove the validation task in the Merchello.config file ...

       <taskChain alias="ItemCacheValidation">
      <!-- Added Merchello Version 1.11.0
      This chain validates basket and wish list items against values in the back office to assert that the customer has not
      added items to their basket that were subsequently changed in the back office prior to checkout.  The process is needed
      as the relation between the basket and wish list items are decoupled from the actual persisted values.
      -->
        <tasks>
            <task type="Merchello.Web.Validation.Tasks.ValidateProductsExistTask, Merchello.Web" />
            <!--
                The following task is intended to assert that pricing and/or on sale value has not changed in the back office since the
                customer has placed an item into their basket or wish list. If you have made custom pricing modifications in your
                implementation, you may either remove this task or adjust your code to add a new extended data value
                merchLineItemAllowsValidation = false
                to the line item so that it is skipped in the validation process.
            -->
            <task type="Merchello.Web.Validation.Tasks.ValidateProductPriceTask, Merchello.Web" />
            <!--
                Validates that products are still in inventory
            -->
            <task type="Merchello.Web.Validation.Tasks.ValidateProductInventoryTask, Merchello.Web" />
        </tasks>
      </taskChain>
    
  • William Zhang 39 posts 243 karma points
    Jun 05, 2016 @ 17:08
    William Zhang
    0

    Does this happen every time? I don't understand why this would happen in most checkouts.

    Not really sure what you're referring to? :)

    I'll give theResetCustomerManagerDataOnVersionChange = false a try tonight.

  • William Zhang 39 posts 243 karma points
    Jun 06, 2016 @ 18:06
    William Zhang
    100

    I ended up creating my own ValidateProductPriceTask, which does exactly the same thing as the built in version, except the commented out part:

    public override Attempt<ValidationResult<CustomerItemCacheBase>> PerformTask(ValidationResult<CustomerItemCacheBase> value)
        {
            var visitor = new ProductPricingValidationVisitor(Merchello);
    
            value.Validated.Accept(visitor);
    
            if (visitor.InvalidPrices.Any())
            {
                foreach (var result in visitor.InvalidPrices.ToArray())
                {
                    var lineItem = result.Key;
                    var quantity = lineItem.Quantity;
                    var name = lineItem.Name;
                    var removedEd = lineItem.ExtendedData.AsEnumerable();
                    value.Validated.RemoveItem(lineItem.Sku);
    
                    var extendedData = new ExtendedDataCollection();
                    ProductVariantDisplay variant;
                    if (result.Value is ProductDisplay)
                    {
                        var product = result.Value as ProductDisplay;
                        variant = product.AsMasterVariantDisplay();
                    }
                    else
                    {
                        variant = result.Value as ProductVariantDisplay;
                    }
    
                    if (variant == null)
                    {
                        var nullReference = new NullReferenceException("ProductVariantDisplay cannot be null");
                        LogHelper.Error<ValidateProductPriceTask>("Exception occurred when attempting to adjust pricing information", nullReference);
                        throw nullReference;
                    }
    
                    extendedData.AddProductVariantValues(variant);
                    extendedData.MergeDataModifierLogs(variant);
                    extendedData.MergeDataModifierLogs(variant);
    
                    // preserve any custom extended data values
                    foreach (var val in removedEd.Where(val => !extendedData.ContainsKey(val.Key)))
                    {
                        extendedData.SetValue(val.Key, val.Value);
                    }
    
                    var price = variant.OnSale ? extendedData.GetSalePriceValue() : extendedData.GetPriceValue();
    
                    // Removed this part since it overwrites the updated ExtendedData with the old values, such as the merchOnSale
                    // and merchSalePrice values. This causes the ProductPricingValidationVisitor to run over and over again,
                    // since the basket never becomes valid.
                    //var keys = lineItem.ExtendedData.Keys.Where(x => extendedData.Keys.Any(y => y != x));
                    //foreach (var k in keys)
                    //{
                    //  extendedData.SetValue(k, lineItem.ExtendedData.GetValue(k));
                    //}
    
                    value.Validated.AddItem(string.IsNullOrEmpty(name) ? variant.Name : name, variant.Sku, quantity, price, extendedData);
                    value.Messages.Add("Price updated for " + variant.Sku + " to " + price);
                }
    
                value.Validated.Save();
            }
    
            return Attempt<ValidationResult<CustomerItemCacheBase>>.Succeed(value);
        }
    
Please Sign in or register to post replies

Write your reply to:

Draft