Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

FacebookPixelService.cs

using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Orders;
using Nop.Core.Http.Extensions;
using Nop.Data;
using Nop.Plugin.Widgets.FacebookPixel.Domain;
using Nop.Services.Catalog;
using Nop.Services.Cms;
using Nop.Services.Common;
using Nop.Services.Directory;
using Nop.Services.Logging;
using Nop.Services.Orders;
using Nop.Services.Tax;
using Nop.Web.Framework.Models.Cms;
using Nop.Web.Models.Catalog;

namespace Nop.Plugin.Widgets.FacebookPixel.Services;

/// <summary>
/// Represents Facebook Pixel service
/// </summary>
public class FacebookPixelService
{
    #region Constants

    /// <summary>
    /// Get default tabs number to format scripts indentation
    /// </summary>
    protected const int TABS_NUMBER = 2;

    #endregion

    #region Fields

    protected readonly CurrencySettings _currencySettings;
    protected readonly FacebookConversionsHttpClient _facebookConversionsHttpClient;
    protected readonly ICategoryService _categoryService;
    protected readonly ICountryService _countryService;
    protected readonly ICurrencyService _currencyService;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly ILogger _logger;
    protected readonly IOrderService _orderService;
    protected readonly IOrderTotalCalculationService _orderTotalCalculationService;
    protected readonly IPriceCalculationService _priceCalculationService;
    protected readonly IProductService _productService;
    protected readonly IRepository<FacebookPixelConfiguration> _facebookPixelConfigurationRepository;
    protected readonly IShoppingCartService _shoppingCartService;
    protected readonly IStateProvinceService _stateProvinceService;
    protected readonly IStaticCacheManager _staticCacheManager;
    protected readonly IStoreContext _storeContext;
    protected readonly ITaxService _taxService;
    protected readonly IWebHelper _webHelper;
    protected readonly IWidgetPluginManager _widgetPluginManager;
    protected readonly IWorkContext _workContext;

    #endregion

    #region Ctor

    public FacebookPixelService(CurrencySettings currencySettings,
        FacebookConversionsHttpClient facebookConversionsHttpClient,
        ICategoryService categoryService,
        ICountryService countryService,
        ICurrencyService currencyService,
        IGenericAttributeService genericAttributeService,
        IHttpContextAccessor httpContextAccessor,
        ILogger logger,
        IOrderService orderService,
        IOrderTotalCalculationService orderTotalCalculationService,
        IPriceCalculationService priceCalculationService,
        IProductService productService,
        IRepository<FacebookPixelConfiguration> facebookPixelConfigurationRepository,
        IShoppingCartService shoppingCartService,
        IStateProvinceService stateProvinceService,
        IStaticCacheManager staticCacheManager,
        IStoreContext storeContext,
        ITaxService taxService,
        IWebHelper webHelper,
        IWidgetPluginManager widgetPluginManager,
        IWorkContext workContext)
    {
        _currencySettings = currencySettings;
        _facebookConversionsHttpClient = facebookConversionsHttpClient;
        _categoryService = categoryService;
        _countryService = countryService;
        _currencyService = currencyService;
        _genericAttributeService = genericAttributeService;
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
        _orderService = orderService;
        _orderTotalCalculationService = orderTotalCalculationService;
        _priceCalculationService = priceCalculationService;
        _productService = productService;
        _facebookPixelConfigurationRepository = facebookPixelConfigurationRepository;
        _shoppingCartService = shoppingCartService;
        _stateProvinceService = stateProvinceService;
        _staticCacheManager = staticCacheManager;
        _storeContext = storeContext;
        _taxService = taxService;
        _webHelper = webHelper;
        _widgetPluginManager = widgetPluginManager;
        _workContext = workContext;
    }

    #endregion

    #region Utilities

    /// <summary>
    /// Handle function and get result
    /// </summary>
    /// <typeparam name="TResult">Result type</typeparam>
    /// <param name="function">Function</param>
    /// <param name="logErrors">Whether to log errors</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the function result
    /// </returns>
    protected async Task<TResult> HandleFunctionAsync<TResult>(Func<Task<TResult>> function, bool logErrors = true)
    {
        try
        {
            //check whether the plugin is active
            if (!await PluginActiveAsync())
                return default;

            //invoke function
            return await function();
        }
        catch (Exception exception)
        {
            if (!logErrors)
                return default;

            var customer = await _workContext.GetCurrentCustomerAsync();
            if (customer.IsSearchEngineAccount() || customer.IsBackgroundTaskAccount())
                return default;

            //log errors
            var error = $"{FacebookPixelDefaults.SystemName} error: {Environment.NewLine}{exception.Message}";
            await _logger.ErrorAsync(error, exception, customer);

            return default;
        }
    }

    /// <summary>
    /// Check whether the plugin is active for the current user and the current store
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// </returns>
    protected async Task<bool> PluginActiveAsync()
    {
        var customer = await _workContext.GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();
        return await _widgetPluginManager.IsPluginActiveAsync(FacebookPixelDefaults.SystemName, customer, store.Id);
    }

    /// <summary>
    /// Prepare scripts
    /// </summary>
    /// <param name="configurations">Enabled configurations</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    protected async Task<string> PrepareScriptsAsync(IList<FacebookPixelConfiguration> configurations)
    {
        return await PrepareInitScriptAsync(configurations) +
               await PrepareUserPropertiesScriptAsync(configurations) +
               await PreparePageViewScriptAsync(configurations) +
               await PrepareTrackedEventsScriptAsync(configurations);
    }

    /// <summary>
    /// Prepare user info (used with Advanced Matching feature)
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the user info
    /// </returns>
    protected async Task<string> GetUserObjectAsync()
    {
        //prepare user object
        var customer = await _workContext.GetCurrentCustomerAsync();
        var email = customer.Email;
        var firstName = customer.FirstName;
        var lastName = customer.LastName;
        var phone = customer.Phone;
        var gender = customer.Gender;
        var birthday = customer.DateOfBirth;
        var city = customer.City;
        var countryId = customer.CountryId;
        var countryName = (await _countryService.GetCountryByIdAsync(countryId))?.TwoLetterIsoCode;
        var stateId = customer.StateProvinceId;
        var stateName = (await _stateProvinceService.GetStateProvinceByIdAsync(stateId))?.Abbreviation;
        var zipcode = customer.ZipPostalCode;

        return FormatEventObject(
        [
            ("em", JavaScriptEncoder.Default.Encode(email?.ToLowerInvariant() ?? string.Empty)),
            ("fn", JavaScriptEncoder.Default.Encode(firstName?.ToLowerInvariant() ?? string.Empty)),
            ("ln", JavaScriptEncoder.Default.Encode(lastName?.ToLowerInvariant() ?? string.Empty)),
            ("ph", new string(phone?.Where(c => char.IsDigit(c)).ToArray()) ?? string.Empty),
            ("external_id", customer.CustomerGuid.ToString().ToLowerInvariant()),
            ("ge", gender?.FirstOrDefault().ToString().ToLowerInvariant()),
            ("db", birthday?.ToString("yyyyMMdd")),
            ("ct", JavaScriptEncoder.Default.Encode(city?.ToLowerInvariant() ?? string.Empty)),
            ("st", stateName?.ToLowerInvariant()),
            ("zp", JavaScriptEncoder.Default.Encode(zipcode?.ToLowerInvariant() ?? string.Empty)),
            ("cn", countryName?.ToLowerInvariant())
        ]);
    }

    /// <summary>
    /// Prepare script to init Facebook Pixel
    /// </summary>
    /// <param name="configurations">Enabled configurations</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    protected async Task<string> PrepareInitScriptAsync(IList<FacebookPixelConfiguration> configurations)
    {
        //prepare init script
        return await FormatScriptAsync(configurations, async configuration =>
        {
            var customer = await _workContext.GetCurrentCustomerAsync();

            var additionalParameter = configuration.PassUserProperties
                ? $", {{uid: '{customer.CustomerGuid}'}}"
                : (configuration.UseAdvancedMatching
                    ? $", {await GetUserObjectAsync()}"
                    : null);
            return $"fbq('init', '{configuration.PixelId}'{additionalParameter});";
        });
    }

    /// <summary>
    /// Prepare script to pass user properties
    /// </summary>
    /// <param name="configurations">Enabled configurations</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    protected async Task<string> PrepareUserPropertiesScriptAsync(IList<FacebookPixelConfiguration> configurations)
    {
        //filter active configurations
        var activeConfigurations = configurations.Where(configuration => configuration.PassUserProperties).ToList();
        if (!activeConfigurations.Any())
            return string.Empty;

        //prepare user object
        var customer = await _workContext.GetCurrentCustomerAsync();
        var createdOn = new DateTimeOffset(customer.CreatedOnUtc).ToUnixTimeSeconds().ToString();
        var city = customer.City;
        var countryId = customer.CountryId;
        var countryName = (await _countryService.GetCountryByIdAsync(countryId))?.TwoLetterIsoCode;
        var currency = (await _workContext.GetWorkingCurrencyAsync())?.CurrencyCode;
        var gender = customer.Gender;
        var language = (await _workContext.GetWorkingLanguageAsync())?.UniqueSeoCode;
        var stateId = customer.StateProvinceId;
        var stateName = (await _stateProvinceService.GetStateProvinceByIdAsync(stateId))?.Abbreviation;
        var zipcode = customer.ZipPostalCode;

        var userObject = FormatEventObject(
        [
            ("$account_created_time", createdOn),
            ("$city", JavaScriptEncoder.Default.Encode(city ?? string.Empty)),
            ("$country", countryName),
            ("$currency", currency),
            ("$gender", gender?.FirstOrDefault().ToString()),
            ("$language", language),
            ("$state", stateName),
            ("$zipcode", JavaScriptEncoder.Default.Encode(zipcode ?? string.Empty))
        ]);

        //prepare script
        return await FormatScriptAsync(activeConfigurations, configuration =>
            Task.FromResult($"fbq('setUserProperties', '{configuration.PixelId}', {userObject});"));
    }

    /// <summary>
    /// Prepare script to track "PageView" event
    /// </summary>
    /// <param name="configurations">Enabled configurations</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    protected async Task<string> PreparePageViewScriptAsync(IList<FacebookPixelConfiguration> configurations)
    {
        //a single active configuration is enough to track PageView event
        var activeConfigurations = configurations.Where(configuration => configuration.TrackPageView).Take(1).ToList();
        return await FormatScriptAsync(activeConfigurations, configuration =>
            Task.FromResult($"fbq('track', '{FacebookPixelDefaults.PAGE_VIEW}');"));
    }

    /// <summary>
    /// Prepare scripts to track events
    /// </summary>
    /// <param name="configurations">Enabled configurations</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    protected async Task<string> PrepareTrackedEventsScriptAsync(IList<FacebookPixelConfiguration> configurations)
    {
        //get previously stored events and remove them from the session data
        var events = (await _httpContextAccessor.HttpContext.Session
                         .GetAsync<IList<TrackedEvent>>(FacebookPixelDefaults.TrackedEventsSessionValue))
                     ?? new List<TrackedEvent>();
        var store = await _storeContext.GetCurrentStoreAsync();
        var customer = await _workContext.GetCurrentCustomerAsync();
        var activeEvents = events.Where(trackedEvent =>
                trackedEvent.CustomerId == customer.Id && trackedEvent.StoreId == store.Id)
            .ToList();
        await _httpContextAccessor.HttpContext.Session.SetAsync(
            FacebookPixelDefaults.TrackedEventsSessionValue,
            events.Except(activeEvents).ToList());

        if (!activeEvents.Any())
            return string.Empty;

        return await activeEvents.AggregateAwaitAsync(string.Empty, async (preparedScripts, trackedEvent) =>
        {
            //filter active configurations
            var activeConfigurations = trackedEvent.EventName switch
            {
                FacebookPixelDefaults.ADD_TO_CART => configurations.Where(configuration => configuration.TrackAddToCart).ToList(),
                FacebookPixelDefaults.PURCHASE => configurations.Where(configuration => configuration.TrackPurchase).ToList(),
                FacebookPixelDefaults.VIEW_CONTENT => configurations.Where(configuration => configuration.TrackViewContent).ToList(),
                FacebookPixelDefaults.ADD_TO_WISHLIST => configurations.Where(configuration => configuration.TrackAddToWishlist).ToList(),
                FacebookPixelDefaults.INITIATE_CHECKOUT => configurations.Where(configuration => configuration.TrackInitiateCheckout).ToList(),
                FacebookPixelDefaults.SEARCH => configurations.Where(configuration => configuration.TrackSearch).ToList(),
                FacebookPixelDefaults.CONTACT => configurations.Where(configuration => configuration.TrackContact).ToList(),
                FacebookPixelDefaults.COMPLETE_REGISTRATION => configurations.Where(configuration => configuration.TrackCompleteRegistration).ToList(),
                _ => new List<FacebookPixelConfiguration>()
            };
            if (trackedEvent.IsCustomEvent)
            {
                activeConfigurations = await configurations.WhereAwait(async configuration =>
                    (await GetCustomEventsAsync(configuration.Id)).Any(customEvent => customEvent.EventName == trackedEvent.EventName)).ToListAsync();
            }

            //prepare event scripts
            return preparedScripts + await trackedEvent.EventObjects.AggregateAwaitAsync(string.Empty, async (preparedEventScripts, eventObject) =>
            {
                return preparedEventScripts + await FormatScriptAsync(activeConfigurations, configuration =>
                {
                    //used for accurate event tracking with multiple Facebook Pixels
                    var actionName = configurations.Count > 1
                        ? (trackedEvent.IsCustomEvent ? "trackSingleCustom" : "trackSingle")
                        : (trackedEvent.IsCustomEvent ? "trackCustom" : "track");
                    var additionalParameter = configurations.Count > 1 ? $", '{configuration.PixelId}'" : null;

                    //prepare event script
                    var eventObjectParameter = !string.IsNullOrEmpty(eventObject) ? $", {eventObject}" : null;
                    return Task.FromResult($"fbq('{actionName}'{additionalParameter}, '{trackedEvent.EventName}'{eventObjectParameter});");
                });
            });
        });
    }

    /// <summary>
    /// Prepare script to track event and store it for the further using
    /// </summary>
    /// <param name="eventName">Event name</param>
    /// <param name="eventObject">Event object</param>
    /// <param name="customerId">Customer identifier</param>
    /// <param name="storeId">Store identifier</param>
    /// <param name="isCustomEvent">Whether the event is a custom one</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected async Task PrepareTrackedEventScriptAsync(string eventName, string eventObject,
        int? customerId = null, int? storeId = null, bool isCustomEvent = false)
    {
        //prepare script and store it into the session data, we use this later
        var customer = await _workContext.GetCurrentCustomerAsync();
        customerId ??= customer.Id;
        var store = await _storeContext.GetCurrentStoreAsync();
        storeId ??= store.Id;
        var events = await _httpContextAccessor.HttpContext.Session
                         .GetAsync<IList<TrackedEvent>>(FacebookPixelDefaults.TrackedEventsSessionValue)
                     ?? new List<TrackedEvent>();
        var activeEvent = events.FirstOrDefault(trackedEvent =>
            trackedEvent.EventName == eventName && trackedEvent.CustomerId == customerId && trackedEvent.StoreId == storeId);
        if (activeEvent == null)
        {
            activeEvent = new TrackedEvent
            {
                EventName = eventName,
                CustomerId = customerId.Value,
                StoreId = storeId.Value,
                IsCustomEvent = isCustomEvent
            };
            events.Add(activeEvent);
        }
        activeEvent.EventObjects.Add(eventObject);
        await _httpContextAccessor.HttpContext.Session.SetAsync(FacebookPixelDefaults.TrackedEventsSessionValue, events);
    }

    /// <summary>
    /// Format event object to look pretty
    /// </summary>
    /// <param name="properties">Event object properties</param>
    /// <param name="tabsNumber">Tabs number for indentation script</param>
    /// <returns>Script code</returns>
    protected string FormatEventObject(List<(string Name, object Value)> properties, int? tabsNumber = null)
    {
        //local function to format list of objects
        string formatObjectList(List<List<(string Name, object Value)>> objectList)
        {
            var formattedList = objectList.Aggregate(string.Empty, (preparedObjects, propertiesList) =>
            {
                if (propertiesList != null)
                {
                    var value = FormatEventObject(propertiesList, (tabsNumber ?? TABS_NUMBER) + 1);
                    preparedObjects += $"{Environment.NewLine}{new string('\t', (tabsNumber ?? TABS_NUMBER) + 1)}{value},";
                }

                return preparedObjects;
            }).TrimEnd(',');
            return $"[{formattedList}]";
        }

        //format single object
        var formattedObject = properties.Aggregate(string.Empty, (preparedObject, property) =>
        {
            if (!string.IsNullOrEmpty(property.Value?.ToString()))
            {
                //format property value
                var value = property.Value is string valueString
                    ? $"'{valueString.Replace("'", "\\'")}'"
                    : (property.Value is List<List<(string Name, object Value)>> valueList
                        ? formatObjectList(valueList)
                        : (property.Value is decimal valueDecimal
                            ? valueDecimal.ToString("F", CultureInfo.InvariantCulture)
                            : property.Value.ToString().ToLowerInvariant()));

                //format object property
                preparedObject += $"{Environment.NewLine}{new string('\t', (tabsNumber ?? TABS_NUMBER) + 1)}{property.Name}: {value},";
            }

            return preparedObject;
        }).TrimEnd(',');

        return $"{{{formattedObject}{Environment.NewLine}{new string('\t', tabsNumber ?? TABS_NUMBER)}}}";
    }

    /// <summary>
    /// Format script to look pretty
    /// </summary>
    /// <param name="configurations">Enabled configurations</param>
    /// <param name="getScript">Function to get script for the passed configuration</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    protected async Task<string> FormatScriptAsync(IList<FacebookPixelConfiguration> configurations, Func<FacebookPixelConfiguration, Task<string>> getScript)
    {
        if (!configurations.Any())
            return string.Empty;

        //format script
        var formattedScript = await configurations.AggregateAwaitAsync(string.Empty, async (preparedScripts, configuration) =>
            preparedScripts + Environment.NewLine + new string('\t', TABS_NUMBER) + await getScript(configuration));
        formattedScript += Environment.NewLine;

        return formattedScript;
    }

    /// <summary>
    /// Format custom event data to look pretty
    /// </summary>
    /// <param name="customData">Custom data</param>
    /// <returns>Script code</returns>
    protected string FormatCustomData(ConversionsEventCustomData customData)
    {
        List<(string Name, object Value)> getProperties(JObject jObject)
        {
            var result = jObject.ToObject<Dictionary<string, object>>();
            foreach (var pair in result)
            {
                if (pair.Value is JObject nestedObject)
                    result[pair.Key] = getProperties(nestedObject);
                if (pair.Value is JArray nestedArray && nestedArray.OfType<JObject>().Any())
                    result[pair.Key] = nestedArray.OfType<JObject>().Select(obj => getProperties(obj)).ToList();
            }

            return result.Select(pair => (pair.Key, pair.Value)).ToList();
        }

        try
        {
            var customDataObject = JObject.FromObject(customData, new JsonSerializer { NullValueHandling = NullValueHandling.Ignore });

            return FormatEventObject(getProperties(customDataObject));

        }
        catch
        {
            //if something went wrong, just serialize the data without format
            return JsonConvert.SerializeObject(customData, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
        }
    }

    /// <summary>
    /// Get configurations
    /// </summary>
    /// <param name="storeId">Store identifier; pass 0 to load all records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the list of configurations
    /// </returns>
    protected async Task<IList<FacebookPixelConfiguration>> GetConfigurationsAsync(int storeId = 0)
    {
        var key = _staticCacheManager.PrepareKeyForDefaultCache(FacebookPixelDefaults.ConfigurationsCacheKey, storeId);

        var query = _facebookPixelConfigurationRepository.Table;

        //filter by the store
        if (storeId > 0)
            query = query.Where(configuration => configuration.StoreId == storeId);

        query = query.OrderBy(configuration => configuration.Id);

        return await _staticCacheManager.GetAsync(key, async () => await query.ToListAsync());
    }

    /// <summary>
    /// Prepare Pixel script and send requests to Conversions API for the passed event
    /// </summary>
    /// <param name="prepareModel">Function to prepare model</param>
    /// <param name="eventName">Event name</param>
    /// <param name="storeId">Store identifier; pass null to load records for the current store</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the value whether handling was successful
    /// </returns>
    protected async Task<bool> HandleEventAsync(Func<Task<ConversionsEvent>> prepareModel, string eventName, int? storeId = null)
    {
        storeId ??= (await _storeContext.GetCurrentStoreAsync()).Id;
        var configurations = (await GetConfigurationsAsync(storeId ?? 0)).Where(configuration => eventName switch
        {
            FacebookPixelDefaults.ADD_TO_CART => configuration.TrackAddToCart,
            FacebookPixelDefaults.ADD_TO_WISHLIST => configuration.TrackAddToWishlist,
            FacebookPixelDefaults.PURCHASE => configuration.TrackPurchase,
            FacebookPixelDefaults.VIEW_CONTENT => configuration.TrackViewContent,
            FacebookPixelDefaults.INITIATE_CHECKOUT => configuration.TrackInitiateCheckout,
            FacebookPixelDefaults.PAGE_VIEW => configuration.TrackPageView,
            FacebookPixelDefaults.SEARCH => configuration.TrackSearch,
            FacebookPixelDefaults.CONTACT => configuration.TrackContact,
            FacebookPixelDefaults.COMPLETE_REGISTRATION => configuration.TrackCompleteRegistration,
            _ => false
        }).ToList();

        var conversionsApiConfigurations = configurations.Where(configuration => configuration.ConversionsApiEnabled).ToList();
        var pixelConfigurations = configurations.Where(configuration => configuration.PixelScriptEnabled).ToList();
        if (!conversionsApiConfigurations.Any() && !pixelConfigurations.Any())
            return false;

        var model = await prepareModel();

        if (pixelConfigurations.Any())
            await PrepareEventScriptAsync(model);

        var logErrors = true; //set it to false to ignore Conversions API errors 
        foreach (var configuration in conversionsApiConfigurations)
        {
            await HandleFunctionAsync(async () =>
            {
                var response = await _facebookConversionsHttpClient.SendEventAsync(configuration, model);
                var error = JsonConvert.DeserializeAnonymousType(response, new { Error = new ApiError() })?.Error;
                if (!string.IsNullOrEmpty(error?.Message))
                    throw new NopException($"{error.Code} - {error.Message}{Environment.NewLine}Debug ID: {error.DebugId}");

                return true;
            }, logErrors);
        }

        return true;
    }

    /// <summary>
    /// Prepare user data for conversions api
    /// </summary>
    /// <returns>
    /// <param name="customer">Customer</param>
    /// A task that represents the asynchronous operation
    /// The task result contains the user data
    /// </returns>
    protected async Task<ConversionsEventUserData> PrepareUserDataAsync(Customer customer = null)
    {
        //prepare user object
        customer ??= await _workContext.GetCurrentCustomerAsync();
        var twoLetterCountryIsoCode = (await _countryService.GetCountryByIdAsync(customer.CountryId))?.TwoLetterIsoCode;
        var stateName = (await _stateProvinceService.GetStateProvinceByIdAsync(customer.StateProvinceId))?.Abbreviation;
        var ipAddress = _webHelper.GetCurrentIpAddress();
        var userAgent = _httpContextAccessor.HttpContext?.Request?.Headers[HeaderNames.UserAgent].ToString();

        return new ConversionsEventUserData
        {
            EmailAddress = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.Email?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            FirstName = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.FirstName?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            LastName = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.LastName?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            PhoneNumber = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.Phone?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            ExternalId = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer?.CustomerGuid.ToString()?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            Gender = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.Gender?.FirstOrDefault().ToString() ?? string.Empty), "SHA256")],
            DateOfBirth = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.DateOfBirth?.ToString("yyyyMMdd") ?? string.Empty), "SHA256")],
            City = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.City?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            State = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(stateName?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            Zip = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(customer.ZipPostalCode?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            Country = [HashHelper.CreateHash(Encoding.UTF8.GetBytes(twoLetterCountryIsoCode?.ToLowerInvariant() ?? string.Empty), "SHA256")],
            ClientIpAddress = ipAddress?.ToLowerInvariant(),
            ClientUserAgent = userAgent?.ToLowerInvariant(),
            Id = customer.Id
        };
    }

    /// <summary>
    /// Prepare add to cart event model
    /// </summary>
    /// <param name="item">Shopping cart item</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PrepareAddToCartEventModelAsync(ShoppingCartItem item)
    {
        ArgumentNullException.ThrowIfNull(item);

        //check whether the shopping was initiated by the customer
        var customer = await _workContext.GetCurrentCustomerAsync();

        var store = await _storeContext.GetCurrentStoreAsync();

        if (item.CustomerId != customer.Id)
            throw new NopException("Shopping was not initiated by customer");

        var eventName = item.ShoppingCartTypeId == (int)ShoppingCartType.ShoppingCart
            ? FacebookPixelDefaults.ADD_TO_CART
            : FacebookPixelDefaults.ADD_TO_WISHLIST;

        var product = await _productService.GetProductByIdAsync(item.ProductId);
        var categoryMapping = (await _categoryService.GetProductCategoriesByProductIdAsync(product?.Id ?? 0)).FirstOrDefault();
        var categoryName = (await _categoryService.GetCategoryByIdAsync(categoryMapping?.CategoryId ?? 0))?.Name;
        var sku = product != null ? await _productService.FormatSkuAsync(product, item.AttributesXml) : string.Empty;
        var quantity = product != null ? (int?)item.Quantity : null;
        var (productPrice, _, _, _) = await _priceCalculationService.GetFinalPriceAsync(product, customer, store, includeDiscounts: false);
        var (price, _) = await _taxService.GetProductPriceAsync(product, productPrice);
        var currentCurrency = await _workContext.GetWorkingCurrencyAsync();
        var priceValue = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(price, currentCurrency);
        var currency = currentCurrency?.CurrencyCode;

        var eventObject = new ConversionsEventCustomData
                {
                    ContentCategory = categoryName,
                    ContentIds = [sku],
                    ContentName = product?.Name,
                    ContentType = "product",
                    Contents =
                    [
                    new
                    {
                    id = sku,
                    quantity = quantity,
                    item_price = priceValue
                }
                ],
            Currency = currency,
            Value = priceValue
        };

        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = eventName,
                EventTime = new DateTimeOffset(item.CreatedOnUtc).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(customer),
                CustomData = eventObject,
                StoreId = item.StoreId
            }
            ]
        };
    }

    /// <summary>
    /// Prepare purchase event model
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PreparePurchaseModelAsync(Order order)
    {
        ArgumentNullException.ThrowIfNull(order);

        //check whether the purchase was initiated by the customer
        var customer = await _workContext.GetCurrentCustomerAsync();
        if (order.CustomerId != customer.Id)
            throw new NopException("Purchase was not initiated by customer");

        //prepare event object
        var currency = await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);
        var contentsProperties = await (await _orderService.GetOrderItemsAsync(order.Id)).SelectAwait(async item =>
        {
            var product = await _productService.GetProductByIdAsync(item.ProductId);
            var sku = product != null ? await _productService.FormatSkuAsync(product, item.AttributesXml) : string.Empty;
            var quantity = product != null ? (int?)item.Quantity : null;
            return new { id = sku, quantity = quantity };
        }).Cast<object>().ToListAsync();
        var eventObject = new ConversionsEventCustomData
        {
            ContentType = "product",
            Contents = contentsProperties,
            Currency = currency?.CurrencyCode,
            Value = order.OrderTotal
        };

        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.PURCHASE,
                EventTime = new DateTimeOffset(order.CreatedOnUtc).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(customer),
                CustomData = eventObject,
                StoreId = order.StoreId
            }
            ]
        };
    }

    /// <summary>
    /// Prepare view content event model
    /// </summary>
    /// <param name="productDetails">Product details model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PrepareViewContentModelAsync(ProductDetailsModel productDetails)
    {
        ArgumentNullException.ThrowIfNull(productDetails);

        //prepare event object
        var product = await _productService.GetProductByIdAsync(productDetails.Id);
        var categoryMapping = (await _categoryService.GetProductCategoriesByProductIdAsync(product?.Id ?? 0)).FirstOrDefault();
        var categoryName = (await _categoryService.GetCategoryByIdAsync(categoryMapping?.CategoryId ?? 0))?.Name;
        var sku = productDetails.Sku;
        var priceValue = productDetails.ProductPrice.PriceValue;
        var currency = (await _workContext.GetWorkingCurrencyAsync())?.CurrencyCode;

        var eventObject = new ConversionsEventCustomData
        {
            ContentCategory = categoryName,
            ContentIds = [sku],
            ContentName = product?.Name,
            ContentType = "product",
            Currency = currency,
            Value = priceValue
        };

        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.VIEW_CONTENT,
                EventTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(),
                CustomData = eventObject
            }
            ]
        };
    }

    /// <summary>
    /// Prepare initiate checkout event model
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PrepareInitiateCheckoutModelAsync()
    {
        //prepare event object
        var customer = await _workContext.GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();
        var cart = await _shoppingCartService.GetShoppingCartAsync(customer, ShoppingCartType.ShoppingCart, store.Id);
        var (price, _, _, _, _, _) = await _orderTotalCalculationService.GetShoppingCartTotalAsync(cart, false, false);
        var currentCurrency = await _workContext.GetWorkingCurrencyAsync();
        var priceValue = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(price ?? 0, currentCurrency);
        var currency = currentCurrency?.CurrencyCode;

        var contentsProperties = await cart.SelectAwait(async item =>
        {
            var product = await _productService.GetProductByIdAsync(item.ProductId);
            var sku = product != null ? await _productService.FormatSkuAsync(product, item.AttributesXml) : string.Empty;
            var quantity = product != null ? (int?)item.Quantity : null;
            return new { id = sku, quantity = quantity };
        }).Cast<object>().ToListAsync();

        var eventObject = new ConversionsEventCustomData
        {
            ContentType = "product",
            Contents = contentsProperties,
            Currency = currency,
            Value = priceValue
        };

        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.INITIATE_CHECKOUT,
                EventTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(customer),
                CustomData = eventObject
            }
            ]
        };
    }

    /// <summary>
    /// Prepare page view event model
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PreparePageViewModelAsync()
    {
        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.PAGE_VIEW,
                EventTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(),
                CustomData = new ConversionsEventCustomData()
            }
            ]
        };
    }

    /// <summary>
    /// Prepare search event model
    /// </summary>
    /// <param name="searchTerm">Search term</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PrepareSearchModelAsync(string searchTerm)
    {
        //prepare event object
        var eventObject = new ConversionsEventCustomData
        {
            SearchString = JavaScriptEncoder.Default.Encode(searchTerm)
        };

        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.SEARCH,
                EventTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(),
                CustomData = eventObject
            }
            ]
        };
    }

    /// <summary>
    /// Prepare contact event model
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PrepareContactModelAsync()
    {
        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.CONTACT,
                EventTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(),
                CustomData = new ConversionsEventCustomData()
            }
            ]
        };
    }

    /// <summary>
    /// Prepare complete registration event model
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// </returns>
    protected async Task<ConversionsEvent> PrepareCompleteRegistrationModelAsync()
    {
        //prepare event object
        var eventObject = new ConversionsEventCustomData
        {
            Status = true.ToString()
        };

        return new ConversionsEvent
            {
                Data =
                [
                new ConversionsEventDatum
                {
                EventName = FacebookPixelDefaults.COMPLETE_REGISTRATION,
                EventTime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
                EventSourceUrl = _webHelper.GetThisPageUrl(true),
                ActionSource = "website",
                UserData = await PrepareUserDataAsync(),
                CustomData = eventObject
            }
            ]
        };
    }

    #endregion

    #region Methods

    #region Scripts

    /// <summary>
    /// Prepare script to track events and store it into the session value
    /// </summary>
    /// <param name="conversionsEvent">Conversions event</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task PrepareEventScriptAsync(ConversionsEvent conversionsEvent)
    {
        await HandleFunctionAsync(async () =>
        {
            //get current stored events 
            var events = await _httpContextAccessor.HttpContext.Session
                             .GetAsync<IList<TrackedEvent>>(FacebookPixelDefaults.TrackedEventsSessionValue)
                         ?? new List<TrackedEvent>();

            var store = await _storeContext.GetCurrentStoreAsync();
            foreach (var conversionsEventData in conversionsEvent.Data)
            {
                conversionsEventData.StoreId ??= store.Id;
                var activeEvent = events.FirstOrDefault(trackedEvent =>
                    trackedEvent.EventName == conversionsEventData.EventName &&
                    trackedEvent.CustomerId == conversionsEventData.UserData?.Id &&
                    trackedEvent.StoreId == conversionsEventData.StoreId);
                if (activeEvent is null)
                {
                    activeEvent = new TrackedEvent
                    {
                        EventName = conversionsEventData.EventName,
                        CustomerId = conversionsEventData.UserData?.Id ?? 0,
                        StoreId = conversionsEventData.StoreId ?? 0,
                        IsCustomEvent = conversionsEventData.IsCustomEvent
                    };
                    events.Add(activeEvent);
                }

                activeEvent.EventObjects.Add(FormatCustomData(conversionsEventData.CustomData));
            }

            //update events in the session value
            await _httpContextAccessor.HttpContext.Session.SetAsync(FacebookPixelDefaults.TrackedEventsSessionValue, events);

            return true;
        });
    }

    /// <summary>
    /// Prepare Facebook Pixel script
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    public async Task<string> PrepareScriptAsync()
    {
        return await HandleFunctionAsync(async () =>
        {
            //get the enabled configurations
            var store = await _storeContext.GetCurrentStoreAsync();
            var configurations = await (await GetConfigurationsAsync(store.Id)).WhereAwait(async configuration =>
            {
                if (!configuration.PixelScriptEnabled)
                    return false;

                if (!configuration.DisableForUsersNotAcceptingCookieConsent)
                    return true;

                //don't display Pixel for users who not accepted Cookie Consent
                var cookieConsentAccepted = await _genericAttributeService.GetAttributeAsync<bool>(await _workContext.GetCurrentCustomerAsync(),
                    NopCustomerDefaults.EuCookieLawAcceptedAttribute, store.Id);
                return cookieConsentAccepted;
            }).ToListAsync();
            if (!configurations.Any())
                return string.Empty;

            //base script
            return $@"
    <!-- Facebook Pixel Code -->
    <script>

        !function (f, b, e, v, n, t, s) {{
            if (f.fbq) return;
            n = f.fbq = function () {{
                n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments)
            }};
            if (!f._fbq) f._fbq = n;
            n.push = n;
            n.loaded = !0;
            n.version = '2.0';
            n.agent = '{FacebookPixelDefaults.AgentId}';
            n.queue = [];
            t = b.createElement(e);
            t.async = !0;
            t.src = v;
            s = b.getElementsByTagName(e)[0];
            s.parentNode.insertBefore(t, s)
        }}(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
        {await PrepareScriptsAsync(configurations)}
    </script>
    <!-- End Facebook Pixel Code -->";
        });
    }

    /// <summary>
    /// Prepare Facebook Pixel script
    /// </summary>
    /// <param name="widgetZone">Widget zone to place script</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// </returns>
    public async Task<string> PrepareCustomEventsScriptAsync(string widgetZone)
    {
        return await HandleFunctionAsync(async () =>
        {
            var customEvents = await (await GetConfigurationsAsync()).SelectManyAwait(async configuration => await GetCustomEventsAsync(configuration.Id, widgetZone)).ToListAsync();
            foreach (var customEvent in customEvents)
                await PrepareTrackedEventScriptAsync(customEvent.EventName, string.Empty, isCustomEvent: true);

            return string.Empty;
        });
    }

    #endregion

    #region Conversions API

    /// <summary>
    /// Send add to cart events
    /// </summary>
    /// <param name="shoppingCartItem">Shopping cart item</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendAddToCartEventAsync(ShoppingCartItem shoppingCartItem)
    {
        await HandleFunctionAsync(async () =>
        {
            var eventName = shoppingCartItem.ShoppingCartTypeId == (int)ShoppingCartType.ShoppingCart
                ? FacebookPixelDefaults.ADD_TO_CART
                : FacebookPixelDefaults.ADD_TO_WISHLIST;

            return await HandleEventAsync(() => PrepareAddToCartEventModelAsync(shoppingCartItem), eventName, shoppingCartItem.StoreId);
        });
    }

    /// <summary>
    /// Send purchase events
    /// </summary>
    /// <param name="order">Order</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendPurchaseEventAsync(Order order)
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PreparePurchaseModelAsync(order), FacebookPixelDefaults.PURCHASE, order.StoreId));
    }

    /// <summary>
    /// Send view content events
    /// </summary>
    /// <param name="productDetailsModel">Product details model</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendViewContentEventAsync(ProductDetailsModel productDetailsModel)
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PrepareViewContentModelAsync(productDetailsModel), FacebookPixelDefaults.VIEW_CONTENT));
    }

    /// <summary>
    /// Send initiate checkout events
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendInitiateCheckoutEventAsync()
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PrepareInitiateCheckoutModelAsync(), FacebookPixelDefaults.INITIATE_CHECKOUT));
    }

    /// <summary>
    /// Send page view events
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendPageViewEventAsync()
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PreparePageViewModelAsync(), FacebookPixelDefaults.PAGE_VIEW));
    }

    /// <summary>
    /// Send search events
    /// </summary>
    /// <param name="searchTerm">Search term</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendSearchEventAsync(string searchTerm)
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PrepareSearchModelAsync(searchTerm), FacebookPixelDefaults.SEARCH));
    }

    /// <summary>
    /// Send contact events
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendContactEventAsync()
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PrepareContactModelAsync(), FacebookPixelDefaults.CONTACT));
    }

    /// <summary>
    /// Send complete registration events
    /// </summary>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SendCompleteRegistrationEventAsync()
    {
        await HandleFunctionAsync(() =>
            HandleEventAsync(() => PrepareCompleteRegistrationModelAsync(), FacebookPixelDefaults.COMPLETE_REGISTRATION));
    }

    #endregion

    #region Configuration

    /// <summary>
    /// Get configurations
    /// </summary>
    /// <param name="storeId">Store identifier; pass 0 to load all records</param>
    /// <param name="pageIndex">Page index</param>
    /// <param name="pageSize">Page size</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the paged list of configurations
    /// </returns>
    public async Task<IPagedList<FacebookPixelConfiguration>> GetPagedConfigurationsAsync(int storeId = 0, int pageIndex = 0, int pageSize = int.MaxValue)
    {
        var query = _facebookPixelConfigurationRepository.Table;

        //filter by the store
        if (storeId > 0)
            query = query.Where(configuration => configuration.StoreId == storeId);

        query = query.OrderBy(configuration => configuration.Id);

        return await query.ToPagedListAsync(pageIndex, pageSize);
    }

    /// <summary>
    /// Get a configuration by the identifier
    /// </summary>
    /// <param name="configurationId">Configuration identifier</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the configuration
    /// </returns>
    public async Task<FacebookPixelConfiguration> GetConfigurationByIdAsync(int configurationId)
    {
        if (configurationId == 0)
            return null;

        return await _staticCacheManager.GetAsync(_staticCacheManager.PrepareKeyForDefaultCache(FacebookPixelDefaults.ConfigurationCacheKey, configurationId), async () =>
            await _facebookPixelConfigurationRepository.GetByIdAsync(configurationId));
    }

    /// <summary>
    /// Insert the configuration
    /// </summary>
    /// <param name="configuration">Configuration</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task InsertConfigurationAsync(FacebookPixelConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

        await _facebookPixelConfigurationRepository.InsertAsync(configuration, false);
        await _staticCacheManager.RemoveByPrefixAsync(FacebookPixelDefaults.PrefixCacheKey);
    }

    /// <summary>
    /// Update the configuration
    /// </summary>
    /// <param name="configuration">Configuration</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task UpdateConfigurationAsync(FacebookPixelConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

        await _facebookPixelConfigurationRepository.UpdateAsync(configuration, false);
        await _staticCacheManager.RemoveByPrefixAsync(FacebookPixelDefaults.PrefixCacheKey);
    }

    /// <summary>
    /// Delete the configuration
    /// </summary>
    /// <param name="configuration">Configuration</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task DeleteConfigurationAsync(FacebookPixelConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

        await _facebookPixelConfigurationRepository.DeleteAsync(configuration, false);
        await _staticCacheManager.RemoveByPrefixAsync(FacebookPixelDefaults.PrefixCacheKey);
    }

    /// <summary>
    /// Get configuration custom events
    /// </summary>
    /// <param name="configurationId">Configuration identifier</param>
    /// <param name="widgetZone">Widget zone name; pass null to load all records</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the list of custom events
    /// </returns>
    public async Task<IList<CustomEvent>> GetCustomEventsAsync(int configurationId, string widgetZone = null)
    {
        var cachedCustomEvents = await _staticCacheManager.GetAsync(_staticCacheManager.PrepareKeyForDefaultCache(FacebookPixelDefaults.CustomEventsCacheKey, configurationId), async () =>
        {
            //load configuration custom events
            var configuration = await GetConfigurationByIdAsync(configurationId);
            var customEventsValue = configuration?.CustomEvents ?? string.Empty;
            var customEvents = JsonConvert.DeserializeObject<List<CustomEvent>>(customEventsValue) ?? new List<CustomEvent>();
            return customEvents;
        });

        //filter by the widget zone
        if (!string.IsNullOrEmpty(widgetZone))
            cachedCustomEvents = cachedCustomEvents.Where(customEvent => customEvent.WidgetZones?.Contains(widgetZone) ?? false).ToList();

        return cachedCustomEvents;
    }

    /// <summary>
    /// Save configuration custom events
    /// </summary>
    /// <param name="configurationId">Configuration identifier</param>
    /// <param name="eventName">Event name</param>
    /// <param name="widgetZones">Widget zones names</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public async Task SaveCustomEventsAsync(int configurationId, string eventName, IList<string> widgetZones)
    {
        if (string.IsNullOrEmpty(eventName))
            return;

        var configuration = await GetConfigurationByIdAsync(configurationId);
        if (configuration == null)
            return;

        //load configuration custom events
        var customEventsValue = configuration.CustomEvents ?? string.Empty;
        var customEvents = JsonConvert.DeserializeObject<List<CustomEvent>>(customEventsValue) ?? new List<CustomEvent>();

        //try to get an event by the passed name
        var customEvent = customEvents
            .FirstOrDefault(customEvent => eventName.Equals(customEvent.EventName, StringComparison.InvariantCultureIgnoreCase));
        if (customEvent == null)
        {
            //create new one if not exist
            customEvent = new CustomEvent { EventName = eventName };
            customEvents.Add(customEvent);
        }

        //update widget zones of this event
        customEvent.WidgetZones = widgetZones ?? new List<string>();

        //or delete an event
        if (!customEvent.WidgetZones.Any())
            customEvents.Remove(customEvent);

        //update configuration 
        configuration.CustomEvents = JsonConvert.SerializeObject(customEvents);
        await UpdateConfigurationAsync(configuration);
        await _staticCacheManager.RemoveByPrefixAsync(FacebookPixelDefaults.PrefixCacheKey);
        await _staticCacheManager.RemoveByPrefixAsync(WidgetModelDefaults.WidgetPrefixCacheKey);
    }

    /// <summary>
    /// Get used widget zones for all custom events
    /// </summary>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the list of widget zones names
    /// </returns>
    public async Task<IList<string>> GetCustomEventsWidgetZonesAsync()
    {
        return await _staticCacheManager.GetAsync(_staticCacheManager.PrepareKeyForDefaultCache(FacebookPixelDefaults.WidgetZonesCacheKey), async () =>
        {
            //load custom events and their widget zones
            var configurations = await GetConfigurationsAsync();
            var customEvents = await configurations.SelectManyAwait(async configuration => await GetCustomEventsAsync(configuration.Id)).ToListAsync();
            var widgetZones = await customEvents.SelectMany(customEvent => customEvent.WidgetZones).Distinct().ToListAsync();

            return widgetZones;
        });
    }

    #endregion

    #endregion
}