Campaign prices stop working then adding custom IIndexingProviderPreProcessor

I’m trying to implement an IIndexingProviderPreProcessor that should (amongst other things) load and index campaign prices for variants. But as soon as I inject an ICampaignPriceCalculator in the constructor (or resolve it through IoC.Resolve), the campaign price calculation seems to stop working and no campaign prices are shown on the site anymore. As soon as I remove the ICampaignPriceCalculator from my class, the campaign prices start working again.

I’m able to recreate the behavior in Accelerator v6.2.1.

Is this a bug or am I doing it the wrong way? Should I be using something else instead of ICampaignPriceCalculator to get campaign prices?

Litium version: 6.2.1, 6.2.2

If you remove the blackhold-filter for the logging <logger name="Litium.Foundation.Search.Providers.IndexingProvider+IndexPreProcessor" minlevel="Trace" writeTo="BlackHole" final="true" /> will you get any more information out that can help understand why the IIndexingProviderPreProcessor not is working?

No info in the log. My IIndexingProviderPreProcessor is working like a charm in itself, it’s just that as soon as I inject an ICampaignPriceCalculator into it, the method ICampaignPriceCalculator.GetCampaignPrices starts returning null for all products. Regardless of where the method is called from.

How do you build the CampaignPriceCalculatorArgs in your IIndexingProviderPreProcessor?

As for now I’m not even calling the ICampaignPriceCalculator from my IIndexingProviderPreProcessor, I’m just injecting a dependency to it. But when the calculator is called from other parts of the site (like product details page) the args are built like this:

public static decimal GetCampaignPrice(Guid variantId, bool withVat = false)
    {
        var currency = CurrentState.Current.WebSite.Currency;
        var language = CurrentState.Current.WebSite.Language;
        var websiteId = CurrentState.Current.WebSite.ID;
        var token = CurrentState.Current.Token;

        if (currency == null || language == null || websiteId == Guid.Empty)
        {
            return 0M;
        }

        var priceCalculatorArgs = new CampaignPriceCalculatorArgs
        {
            CurrencySystemId = currency.ID,
            DateTimeUtc = DateTime.Now,
            VariantSystemId = variantId,
            Quantity = 1,
            WebSiteID = websiteId,
            UserID = token.UserID
        };

        _campaignPriceCalculator.GetCampaignPrices(priceCalculatorArgs).TryGetValue(priceCalculatorArgs.VariantSystemId, out var listPrice);
        return (listPrice == null) ? 0M : withVat ? listPrice.CampaignPriceWithVAT : listPrice.CampaignPrice;
    }

The indexing is executed on background threads and have no access to CurrentState.Current or HttpContext.Current. Also the CampaignPriceCalculatorArgs has an argument for OrderCarrier that need to be set if you not execute the ICampaignPriceCalculator in a context that have access to HttpContext.Current.

If you debug your code, where will it throw exception or is it just return 0M ?

Good to know!

It returns null. No Exception throwing.

I installed a fresh Accelerator (6.2.2, B2C version) and was able to reproduce the problem by:

  1. creating an assortment wide “Reduce product price by percentage” campaign, activating the campaign and verifying that the campaign prices were shown on the site

  2. modifying the CampaignIndexingProviderPreProcessor ctor like this:

    public CampaignIndexingProviderPreProcessor(ICampaignDataHolder campaignDataHolder)
    {
    _campaignDataHolder = campaignDataHolder;
    var test = IoC.Resolve<ICampaignPriceCalculator>();
    }

And no exception during startup of the website that says that the CampaignIndexingProviderPreProcessor not could be created?

No exception. Added a breakpoint to the CampaignIndexingProviderPreProcessor to verify that it’s running, and this breakpoint was hit when I updated a product through Backoffice.

If I move the row var test = IoC.Resolve<ICampaignPriceCalculator>(); from the constructor to the PreProcessDocument method the problem goes away.

The problem you are getting is that IIndexingProviderPreProcessor is initiated before the module ECommerce have been initiated and the implementation of ICampaignPriceCalculator is checking if the ECommerce have been initialized when it is created and in this case it says that the ECommerce does not exists and skipping all usage of the campaign engine, both for indexing and to requests to the public website.

If you build an preprocessor like the following we are initializing the ICampaignPriceCalculator at a later stage.

using System;
using System.Linq;
using Litium.Foundation.Modules.CMS;
using Litium.Foundation.Modules.ECommerce;
using Litium.Foundation.Modules.ECommerce.Carriers;
using Litium.Foundation.Modules.ECommerce.Plugins.PriceCalculator;
using Litium.Foundation.Modules.ProductCatalog.Search;
using Litium.Foundation.Search.Providers;
using Litium.Framework.Search.Indexing;

namespace Litium.Accelerator.Search.Filtering
{
    internal class SetCampaignPriceFromCampaignPriceCalculator : IIndexingProviderPreProcessor
    {
        private ICampaignPriceCalculator _campaignPriceCalculator;

        public IndexDocument PreProcessDocument(IndexDocument document, string indexName)
        {
            if (ProductCatalogSearchDomains.Products.Equals(indexName, StringComparison.OrdinalIgnoreCase))
            {
                var articleIds = document.Tags.Where(x => x.Name.Equals(TagNames.VariantSystemId, StringComparison.OrdinalIgnoreCase)).Select(x => (Guid)x.OriginalValue).ToList();
                foreach (var item in articleIds
                    .Select(x => GetCampaignPrice(x, true)))
                {
                    document.Tags.Add(new ReadableDocumentTag("campaignprice-with-vat-from-campaign-engin", item));
                }
            }
            return document;
        }

        public decimal GetCampaignPrice(Guid variantId, bool withVat = false)
        {
            var campaignPriceCalculator = _campaignPriceCalculator ?? (ModuleECommerce.ExistsInstance ? (_campaignPriceCalculator = IoC.Resolve<ICampaignPriceCalculator>()) : null);
            if (campaignPriceCalculator == null)
            {
                return 0M;
            }

            var currency = CurrentState.Current.WebSite.Currency;
            var language = CurrentState.Current.WebSite.Language;
            var websiteId = CurrentState.Current.WebSite.ID;
            var token = CurrentState.Current.Token;

            if (currency == null || language == null || websiteId == Guid.Empty)
            {
                return 0M;
            }

            var priceCalculatorArgs = new CampaignPriceCalculatorArgs
            {
                CurrencySystemId = currency.ID,
                DateTimeUtc = DateTime.Now,
                VariantSystemId = variantId,
                Quantity = 1,
                WebSiteID = websiteId,
                UserID = token.UserID,
                OrderCarrier = new OrderCarrier()
            };

            return campaignPriceCalculator.GetCampaignPrices(priceCalculatorArgs)
                .TryGetValue(priceCalculatorArgs.VariantSystemId, out var listPrice)
                ? (withVat ? listPrice.CampaignPriceWithVAT : listPrice.CampaignPrice)
                : 0M;
        }
    }
}

So you can basically break the campaign engine if you inject the ICampaignPriceCalculator in the wrong place? That sounds dangerous, it should be documented in some way. Maybe some kind of flow chart/dependency diagram of Litium’s initialization process?

Your suggested solution works fine. Thanks!