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.Infrastructure.Cache;
using Nop.Web.Models.Catalog;

namespace Nop.Plugin.Widgets.FacebookPixel.Services;

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

    /// 
    /// Get default tabs number to format scripts indentation
    /// 
    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 _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 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

    /// 
    /// Handle function and get result
    /// 
    /// Result type
    /// Function
    /// Whether to log errors
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the function result
    /// 
    protected async Task HandleFunctionAsync(Func> 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;
        }
    }

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

    /// 
    /// Prepare scripts
    /// 
    /// Enabled configurations
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    protected async Task PrepareScriptsAsync(IList configurations)
    {
        return await PrepareInitScriptAsync(configurations) +
               await PrepareUserPropertiesScriptAsync(configurations) +
               await PreparePageViewScriptAsync(configurations) +
               await PrepareTrackedEventsScriptAsync(configurations);
    }

    /// 
    /// Prepare user info (used with Advanced Matching feature)
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the user info
    /// 
    protected async Task 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())
        ]);
    }

    /// 
    /// Prepare script to init Facebook Pixel
    /// 
    /// Enabled configurations
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    protected async Task PrepareInitScriptAsync(IList 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});";
        });
    }

    /// 
    /// Prepare script to pass user properties
    /// 
    /// Enabled configurations
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    protected async Task PrepareUserPropertiesScriptAsync(IList 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});"));
    }

    /// 
    /// Prepare script to track "PageView" event
    /// 
    /// Enabled configurations
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    protected async Task PreparePageViewScriptAsync(IList 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}');"));
    }

    /// 
    /// Prepare scripts to track events
    /// 
    /// Enabled configurations
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    protected async Task PrepareTrackedEventsScriptAsync(IList configurations)
    {
        //get previously stored events and remove them from the session data
        var events = (await _httpContextAccessor.HttpContext.Session
                         .GetAsync>(FacebookPixelDefaults.TrackedEventsSessionValue))
                     ?? new List();
        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()
            };
            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});");
                });
            });
        });
    }

    /// 
    /// Prepare script to track event and store it for the further using
    /// 
    /// Event name
    /// Event object
    /// Customer identifier
    /// Store identifier
    /// Whether the event is a custom one
    /// A task that represents the asynchronous operation
    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>(FacebookPixelDefaults.TrackedEventsSessionValue)
                     ?? new List();
        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);
    }

    /// 
    /// Format event object to look pretty
    /// 
    /// Event object properties
    /// Tabs number for indentation script
    /// Script code
    protected string FormatEventObject(List<(string Name, object Value)> properties, int? tabsNumber = null)
    {
        //local function to format list of objects
        string formatObjectList(List> 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> 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)}}}";
    }

    /// 
    /// Format script to look pretty
    /// 
    /// Enabled configurations
    /// Function to get script for the passed configuration
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    protected async Task FormatScriptAsync(IList configurations, Func> 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;
    }

    /// 
    /// Format custom event data to look pretty
    /// 
    /// Custom data
    /// Script code
    protected string FormatCustomData(ConversionsEventCustomData customData)
    {
        List<(string Name, object Value)> getProperties(JObject jObject)
        {
            var result = jObject.ToObject>();
            foreach (var pair in result)
            {
                if (pair.Value is JObject nestedObject)
                    result[pair.Key] = getProperties(nestedObject);
                if (pair.Value is JArray nestedArray && nestedArray.OfType().Any())
                    result[pair.Key] = nestedArray.OfType().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 });
        }
    }

    /// 
    /// Get configurations
    /// 
    /// Store identifier; pass 0 to load all records
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of configurations
    /// 
    protected async Task> 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());
    }

    /// 
    /// Prepare Pixel script and send requests to Conversions API for the passed event
    /// 
    /// Function to prepare model
    /// Event name
    /// Store identifier; pass null to load records for the current store
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the value whether handling was successful
    /// 
    protected async Task HandleEventAsync(Func> 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;
    }

    /// 
    /// Prepare user data for conversions api
    /// 
    /// 
    /// Customer
    /// A task that represents the asynchronous operation
    /// The task result contains the user data
    /// 
    protected async Task 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
        };
    }

    /// 
    /// Prepare add to cart event model
    /// 
    /// Shopping cart item
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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
            }
            ]
        };
    }

    /// 
    /// Prepare purchase event model
    /// 
    /// Order
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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().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
            }
            ]
        };
    }

    /// 
    /// Prepare view content event model
    /// 
    /// Product details model
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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
            }
            ]
        };
    }

    /// 
    /// Prepare initiate checkout event model
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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().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
            }
            ]
        };
    }

    /// 
    /// Prepare page view event model
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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()
            }
            ]
        };
    }

    /// 
    /// Prepare search event model
    /// 
    /// Search term
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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
            }
            ]
        };
    }

    /// 
    /// Prepare contact event model
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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()
            }
            ]
        };
    }

    /// 
    /// Prepare complete registration event model
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the ConversionsEvent model
    /// 
    protected async Task 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

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

            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;
        });
    }

    /// 
    /// Prepare Facebook Pixel script
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    public async Task 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(await _workContext.GetCurrentCustomerAsync(),
                    NopCustomerDefaults.EuCookieLawAcceptedAttribute, store.Id);
                return cookieConsentAccepted;
            }).ToListAsync();
            if (!configurations.Any())
                return string.Empty;

            //base script
            return $@"
    
    
    ";
        });
    }

    /// 
    /// Prepare Facebook Pixel script
    /// 
    /// Widget zone to place script
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the script code
    /// 
    public async Task 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

    /// 
    /// Send add to cart events
    /// 
    /// Shopping cart item
    /// A task that represents the asynchronous operation
    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);
        });
    }

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

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

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

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

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

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

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

    #endregion

    #region Configuration

    /// 
    /// Get configurations
    /// 
    /// Store identifier; pass 0 to load all records
    /// Page index
    /// Page size
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the paged list of configurations
    /// 
    public async Task> 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);
    }

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

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

    /// 
    /// Insert the configuration
    /// 
    /// Configuration
    /// A task that represents the asynchronous operation
    public async Task InsertConfigurationAsync(FacebookPixelConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

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

    /// 
    /// Update the configuration
    /// 
    /// Configuration
    /// A task that represents the asynchronous operation
    public async Task UpdateConfigurationAsync(FacebookPixelConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

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

    /// 
    /// Delete the configuration
    /// 
    /// Configuration
    /// A task that represents the asynchronous operation
    public async Task DeleteConfigurationAsync(FacebookPixelConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

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

    /// 
    /// Get configuration custom events
    /// 
    /// Configuration identifier
    /// Widget zone name; pass null to load all records
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of custom events
    /// 
    public async Task> 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>(customEventsValue) ?? new List();
            return customEvents;
        });

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

        return cachedCustomEvents;
    }

    /// 
    /// Save configuration custom events
    /// 
    /// Configuration identifier
    /// Event name
    /// Widget zones names
    /// A task that represents the asynchronous operation
    public async Task SaveCustomEventsAsync(int configurationId, string eventName, IList 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>(customEventsValue) ?? new List();

        //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();

        //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);
    }

    /// 
    /// Get used widget zones for all custom events
    /// 
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the list of widget zones names
    /// 
    public async Task> 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
}