Webiant Logo Webiant Logo
  1. No results found.

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

ProductModelFactory.cs

using System.ComponentModel;
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Tax;
using Nop.Core.Domain.Vendors;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Configuration;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Discounts;
using Nop.Services.FilterLevels;
using Nop.Services.Helpers;
using Nop.Services.Localization;
using Nop.Services.Media;
using Nop.Services.Orders;
using Nop.Services.Seo;
using Nop.Services.Shipping;
using Nop.Services.Stores;
using Nop.Web.Areas.Admin.Infrastructure.Mapper.Extensions;
using Nop.Web.Areas.Admin.Models.Catalog;
using Nop.Web.Areas.Admin.Models.Common;
using Nop.Web.Areas.Admin.Models.Orders;
using Nop.Web.Framework.Extensions;
using Nop.Web.Framework.Factories;
using Nop.Web.Framework.Models.Extensions;

namespace Nop.Web.Areas.Admin.Factories;

/// <summary>
/// Represents the product model factory implementation
/// </summary>
public partial class ProductModelFactory : IProductModelFactory
{
    #region Fields

    protected readonly CatalogSettings _catalogSettings;
    protected readonly CurrencySettings _currencySettings;
    protected readonly IAddressService _addressService;
    protected readonly IBaseAdminModelFactory _baseAdminModelFactory;
    protected readonly ICategoryService _categoryService;
    protected readonly ICurrencyService _currencyService;
    protected readonly ICustomerService _customerService;
    protected readonly IDateTimeHelper _dateTimeHelper;
    protected readonly IDiscountService _discountService;
    protected readonly IDiscountSupportedModelFactory _discountSupportedModelFactory;
    protected readonly IFilterLevelValueService _filterLevelValueService;
    protected readonly ILanguageService _languageService;
    protected readonly ILocalizationService _localizationService;
    protected readonly ILocalizedModelFactory _localizedModelFactory;
    protected readonly IManufacturerService _manufacturerService;
    protected readonly IMeasureService _measureService;
    protected readonly IOrderService _orderService;
    protected readonly IPictureService _pictureService;
    protected readonly IPriceFormatter _priceFormatter;
    protected readonly IProductAttributeFormatter _productAttributeFormatter;
    protected readonly IProductAttributeParser _productAttributeParser;
    protected readonly IProductAttributeService _productAttributeService;
    protected readonly IProductService _productService;
    protected readonly IProductTagService _productTagService;
    protected readonly IProductTemplateService _productTemplateService;
    protected readonly ISettingModelFactory _settingModelFactory;
    protected readonly ISettingService _settingService;
    protected readonly IShipmentService _shipmentService;
    protected readonly IShoppingCartService _shoppingCartService;
    protected readonly ISpecificationAttributeService _specificationAttributeService;
    protected readonly IStoreMappingSupportedModelFactory _storeMappingSupportedModelFactory;
    protected readonly IStoreContext _storeContext;
    protected readonly IStoreService _storeService;
    protected readonly IUrlRecordService _urlRecordService;
    protected readonly IVideoService _videoService;
    protected readonly IWarehouseService _warehouseService;
    protected readonly IWorkContext _workContext;
    protected readonly MeasureSettings _measureSettings;
    protected readonly NopHttpClient _nopHttpClient;
    protected readonly TaxSettings _taxSettings;
    protected readonly VendorSettings _vendorSettings;

    #endregion

    #region Ctor

    public ProductModelFactory(CatalogSettings catalogSettings,
        CurrencySettings currencySettings,
        IAddressService addressService,
        IBaseAdminModelFactory baseAdminModelFactory,
        ICategoryService categoryService,
        ICurrencyService currencyService,
        ICustomerService customerService,
        IDateTimeHelper dateTimeHelper,
        IDiscountService discountService,
        IDiscountSupportedModelFactory discountSupportedModelFactory,
        IFilterLevelValueService filterLevelValueService,
        ILanguageService languageService,
        ILocalizationService localizationService,
        ILocalizedModelFactory localizedModelFactory,
        IManufacturerService manufacturerService,
        IMeasureService measureService,
        IOrderService orderService,
        IPictureService pictureService,
        IPriceFormatter priceFormatter,
        IProductAttributeFormatter productAttributeFormatter,
        IProductAttributeParser productAttributeParser,
        IProductAttributeService productAttributeService,
        IProductService productService,
        IProductTagService productTagService,
        IProductTemplateService productTemplateService,
        ISettingModelFactory settingModelFactory,
        ISettingService settingService,
        IShipmentService shipmentService,
        IShoppingCartService shoppingCartService,
        ISpecificationAttributeService specificationAttributeService,
        IStoreMappingSupportedModelFactory storeMappingSupportedModelFactory,
        IStoreContext storeContext,
        IStoreService storeService,
        IUrlRecordService urlRecordService,
        IVideoService videoService,
        IWarehouseService warehouseService,
        IWorkContext workContext,
        MeasureSettings measureSettings,
        NopHttpClient nopHttpClient,
        TaxSettings taxSettings,
        VendorSettings vendorSettings)
    {
        _catalogSettings = catalogSettings;
        _currencySettings = currencySettings;
        _addressService = addressService;
        _baseAdminModelFactory = baseAdminModelFactory;
        _categoryService = categoryService;
        _currencyService = currencyService;
        _customerService = customerService;
        _dateTimeHelper = dateTimeHelper;
        _discountService = discountService;
        _discountSupportedModelFactory = discountSupportedModelFactory;
        _filterLevelValueService = filterLevelValueService;
        _languageService = languageService;
        _localizationService = localizationService;
        _localizedModelFactory = localizedModelFactory;
        _manufacturerService = manufacturerService;
        _measureService = measureService;
        _orderService = orderService;
        _pictureService = pictureService;
        _priceFormatter = priceFormatter;
        _productAttributeFormatter = productAttributeFormatter;
        _productAttributeParser = productAttributeParser;
        _productAttributeService = productAttributeService;
        _productService = productService;
        _productTagService = productTagService;
        _productTemplateService = productTemplateService;
        _settingModelFactory = settingModelFactory;
        _settingService = settingService;
        _shipmentService = shipmentService;
        _shoppingCartService = shoppingCartService;
        _specificationAttributeService = specificationAttributeService;
        _storeMappingSupportedModelFactory = storeMappingSupportedModelFactory;
        _storeContext = storeContext;
        _storeService = storeService;
        _urlRecordService = urlRecordService;
        _videoService = videoService;
        _warehouseService = warehouseService;
        _workContext = workContext;
        _measureSettings = measureSettings;
        _nopHttpClient = nopHttpClient;
        _taxSettings = taxSettings;
        _vendorSettings = vendorSettings;
    }

    #endregion

    #region Utilities

    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task<string> GetSpecificationAttributeNameAsync(SpecificationAttribute specificationAttribute)
    {
        var name = specificationAttribute.Name;

        if (specificationAttribute.SpecificationAttributeGroupId.HasValue)
        {
            var group = await _specificationAttributeService.GetSpecificationAttributeGroupByIdAsync(specificationAttribute.SpecificationAttributeGroupId.Value);
            if (group != null)
                name = string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.SpecificationAttributes.NameFormat"), group.Name, name);
        }

        return name;
    }

    /// <summary>
    /// Prepare copy product model
    /// </summary>
    /// <param name="model">Copy product model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the copy product model
    /// </returns>
    protected virtual async Task<CopyProductModel> PrepareCopyProductModelAsync(CopyProductModel model, Product product)
    {
        ArgumentNullException.ThrowIfNull(model);

        model.Id = product.Id;
        model.Name = string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Copy.Name.New"), product.Name);
        model.Published = true;
        model.CopyMultimedia = true;

        return model;
    }

    /// <summary>
    /// Prepare product warehouse inventory models
    /// </summary>
    /// <param name="models">List of product warehouse inventory models</param>
    /// <param name="product">Product</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task PrepareProductWarehouseInventoryModelsAsync(IList<ProductWarehouseInventoryModel> models, Product product)
    {
        ArgumentNullException.ThrowIfNull(models);

        foreach (var warehouse in await _warehouseService.GetAllWarehousesAsync())
        {
            var model = new ProductWarehouseInventoryModel
            {
                WarehouseId = warehouse.Id,
                WarehouseName = warehouse.Name
            };

            if (product != null)
            {
                var productWarehouseInventory = (await _productService.GetAllProductWarehouseInventoryRecordsAsync(product.Id))?.FirstOrDefault(inventory => inventory.WarehouseId == warehouse.Id);
                if (productWarehouseInventory != null)
                {
                    model.WarehouseUsed = true;
                    model.StockQuantity = productWarehouseInventory.StockQuantity;
                    model.ReservedQuantity = productWarehouseInventory.ReservedQuantity;
                    model.PlannedQuantity = await _shipmentService.GetQuantityInShipmentsAsync(product, productWarehouseInventory.WarehouseId, true, true);
                }
            }

            models.Add(model);
        }
    }

    /// <summary>
    /// Prepare product attribute mapping validation rules string
    /// </summary>
    /// <param name="attributeMapping">Product attribute mapping</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the validation rules string
    /// </returns>
    protected virtual async Task<string> PrepareProductAttributeMappingValidationRulesStringAsync(ProductAttributeMapping attributeMapping)
    {
        if (!attributeMapping.ValidationRulesAllowed())
            return string.Empty;

        var validationRules = new StringBuilder(string.Empty);
        if (attributeMapping.ValidationMinLength.HasValue)
        {
            validationRules.AppendFormat("{0}: {1}<br />",
                await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.MinLength"),
                attributeMapping.ValidationMinLength);
        }

        if (attributeMapping.ValidationMaxLength.HasValue)
        {
            validationRules.AppendFormat("{0}: {1}<br />",
                await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.MaxLength"),
                attributeMapping.ValidationMaxLength);
        }

        if (!string.IsNullOrEmpty(attributeMapping.ValidationFileAllowedExtensions))
        {
            validationRules.AppendFormat("{0}: {1}<br />",
                await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.FileAllowedExtensions"),
                WebUtility.HtmlEncode(attributeMapping.ValidationFileAllowedExtensions));
        }

        if (attributeMapping.ValidationFileMaximumSize.HasValue)
        {
            validationRules.AppendFormat("{0}: {1}<br />",
                await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.FileMaximumSize"),
                attributeMapping.ValidationFileMaximumSize);
        }

        if (!string.IsNullOrEmpty(attributeMapping.DefaultValue))
        {
            validationRules.AppendFormat("{0}: {1}<br />",
                await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.ValidationRules.DefaultValue"),
                WebUtility.HtmlEncode(attributeMapping.DefaultValue));
        }

        return validationRules.ToString();
    }

    /// <summary>
    /// Prepare product attribute condition model
    /// </summary>
    /// <param name="model">Product attribute condition model</param>
    /// <param name="productAttributeMapping">Product attribute mapping</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    protected virtual async Task PrepareProductAttributeConditionModelAsync(ProductAttributeConditionModel model,
        ProductAttributeMapping productAttributeMapping)
    {
        ArgumentNullException.ThrowIfNull(model);
        ArgumentNullException.ThrowIfNull(productAttributeMapping);

        model.ProductAttributeMappingId = productAttributeMapping.Id;
        model.EnableCondition = !string.IsNullOrEmpty(productAttributeMapping.ConditionAttributeXml);

        //pre-select attribute and values
        var selectedPva = (await _productAttributeParser
                .ParseProductAttributeMappingsAsync(productAttributeMapping.ConditionAttributeXml))
            .FirstOrDefault();

        var attributes = (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(productAttributeMapping.ProductId))
            //ignore non-combinable attributes (should have selectable values)
            .Where(x => x.CanBeUsedAsCondition())
            //ignore this attribute (it cannot depend on itself)
            .Where(x => x.Id != productAttributeMapping.Id)
            .ToList();
        foreach (var attribute in attributes)
        {
            var attributeModel = new ProductAttributeConditionModel.ProductAttributeModel
            {
                Id = attribute.Id,
                ProductAttributeId = attribute.ProductAttributeId,
                Name = (await _productAttributeService.GetProductAttributeByIdAsync(attribute.ProductAttributeId)).Name,
                TextPrompt = attribute.TextPrompt,
                IsRequired = attribute.IsRequired,
                AttributeControlType = attribute.AttributeControlType
            };

            if (attribute.ShouldHaveValues())
            {
                //values
                var attributeValues = await _productAttributeService.GetProductAttributeValuesAsync(attribute.Id);
                foreach (var attributeValue in attributeValues)
                {
                    var attributeValueModel = new ProductAttributeConditionModel.ProductAttributeValueModel
                    {
                        Id = attributeValue.Id,
                        Name = attributeValue.Name,
                        IsPreSelected = attributeValue.IsPreSelected
                    };
                    attributeModel.Values.Add(attributeValueModel);
                }

                //pre-select attribute and value
                if (selectedPva != null && attribute.Id == selectedPva.Id)
                {
                    //attribute
                    model.SelectedProductAttributeId = selectedPva.Id;

                    //values
                    switch (attribute.AttributeControlType)
                    {
                        case AttributeControlType.DropdownList:
                        case AttributeControlType.RadioList:
                        case AttributeControlType.Checkboxes:
                        case AttributeControlType.ColorSquares:
                        case AttributeControlType.ImageSquares:
                            if (!string.IsNullOrEmpty(productAttributeMapping.ConditionAttributeXml))
                            {
                                //clear default selection
                                foreach (var item in attributeModel.Values)
                                    item.IsPreSelected = false;

                                //select new values
                                var selectedValues =
                                    await _productAttributeParser.ParseProductAttributeValuesAsync(productAttributeMapping
                                        .ConditionAttributeXml);
                                foreach (var attributeValue in selectedValues)
                                foreach (var item in attributeModel.Values)
                                    if (attributeValue.Id == item.Id)
                                        item.IsPreSelected = true;
                            }

                            break;
                        case AttributeControlType.ReadonlyCheckboxes:
                        case AttributeControlType.TextBox:
                        case AttributeControlType.MultilineTextbox:
                        case AttributeControlType.Datepicker:
                        case AttributeControlType.FileUpload:
                        default:
                            //these attribute types are supported as conditions
                            break;
                    }
                }
            }

            model.ProductAttributes.Add(attributeModel);
        }
    }

    /// <summary>
    /// Prepare related product search model
    /// </summary>
    /// <param name="searchModel">Related product search model</param>
    /// <param name="product">Product</param>
    /// <returns>Related product search model</returns>
    protected virtual RelatedProductSearchModel PrepareRelatedProductSearchModel(RelatedProductSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare cross-sell product search model
    /// </summary>
    /// <param name="searchModel">Cross-sell product search model</param>
    /// <param name="product">Product</param>
    /// <returns>Cross-sell product search model</returns>
    protected virtual CrossSellProductSearchModel PrepareCrossSellProductSearchModel(CrossSellProductSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare filter level values search model
    /// </summary>
    /// <param name="searchModel">Filter level value search model</param>
    /// <param name="product">Product</param>
    /// <returns>Filter level value search model</returns>
    protected virtual FilterLevelValueSearchModel PrepareFilterLevelValuesSearchModel(FilterLevelValueSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare associated product search model
    /// </summary>
    /// <param name="searchModel">Associated product search model</param>
    /// <param name="product">Product</param>
    /// <returns>Associated product search model</returns>
    protected virtual AssociatedProductSearchModel PrepareAssociatedProductSearchModel(AssociatedProductSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product picture search model
    /// </summary>
    /// <param name="searchModel">Product picture search model</param>
    /// <param name="product">Product</param>
    /// <returns>Product picture search model</returns>
    protected virtual ProductPictureSearchModel PrepareProductPictureSearchModel(ProductPictureSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product video search model
    /// </summary>
    /// <param name="searchModel">Product video search model</param>
    /// <param name="product">Product</param>
    /// <returns>Product video search model</returns>
    protected virtual ProductVideoSearchModel PrepareProductVideoSearchModel(ProductVideoSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product order search model
    /// </summary>
    /// <param name="searchModel">Product order search model</param>
    /// <param name="product">Product</param>
    /// <returns>Product order search model</returns>
    protected virtual ProductOrderSearchModel PrepareProductOrderSearchModel(ProductOrderSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare tier price search model
    /// </summary>
    /// <param name="searchModel">Tier price search model</param>
    /// <param name="product">Product</param>
    /// <returns>Tier price search model</returns>
    protected virtual TierPriceSearchModel PrepareTierPriceSearchModel(TierPriceSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare stock quantity history search model
    /// </summary>
    /// <param name="searchModel">Stock quantity history search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the stock quantity history search model
    /// </returns>
    protected virtual async Task<StockQuantityHistorySearchModel> PrepareStockQuantityHistorySearchModelAsync(StockQuantityHistorySearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare available warehouses
        await _baseAdminModelFactory.PrepareWarehousesAsync(searchModel.AvailableWarehouses);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product attribute mapping search model
    /// </summary>
    /// <param name="searchModel">Product attribute mapping search model</param>
    /// <param name="product">Product</param>
    /// <returns>Product attribute mapping search model</returns>
    protected virtual ProductAttributeMappingSearchModel PrepareProductAttributeMappingSearchModel(ProductAttributeMappingSearchModel searchModel,
        Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product attribute value search model
    /// </summary>
    /// <param name="searchModel">Product attribute value search model</param>
    /// <param name="productAttributeMapping">Product attribute mapping</param>
    /// <returns>Product attribute value search model</returns>
    protected virtual ProductAttributeValueSearchModel PrepareProductAttributeValueSearchModel(ProductAttributeValueSearchModel searchModel,
        ProductAttributeMapping productAttributeMapping)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(productAttributeMapping);

        searchModel.ProductAttributeMappingId = productAttributeMapping.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product attribute combination search model
    /// </summary>
    /// <param name="searchModel">Product attribute combination search model</param>
    /// <param name="product">Product</param>
    /// <returns>Product attribute combination search model</returns>
    protected virtual ProductAttributeCombinationSearchModel PrepareProductAttributeCombinationSearchModel(
        ProductAttributeCombinationSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare product specification attribute search model
    /// </summary>
    /// <param name="searchModel">Product specification attribute search model</param>
    /// <param name="product">Product</param>
    /// <returns>Product specification attribute search model</returns>
    protected virtual ProductSpecificationAttributeSearchModel PrepareProductSpecificationAttributeSearchModel(
        ProductSpecificationAttributeSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        searchModel.ProductId = product.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare tagged products search model
    /// </summary>
    /// <param name="searchModel">Tagged products search model</param>
    /// <param name="productTag">Product tag</param>
    /// <returns>Related product search model</returns>
    protected virtual ProductTagProductSearchModel PrepareTaggedProductsSearchModel(ProductTagProductSearchModel searchModel, ProductTag productTag)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(productTag);

        searchModel.ProductTagId = productTag.Id;

        //prepare page parameters
        searchModel.SetGridPageSize();

        return searchModel;
    }

    #endregion

    #region Methods

    /// <summary>
    /// Prepare product search model
    /// </summary>
    /// <param name="searchModel">Product search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product search model
    /// </returns>
    public virtual async Task<ProductSearchModel> PrepareProductSearchModelAsync(ProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //a vendor should have access only to his products
        searchModel.IsLoggedInAsVendor = await _workContext.GetCurrentVendorAsync() != null;
        searchModel.AllowVendorsToImportProducts = _vendorSettings.AllowVendorsToImportProducts;

        var licenseCheckModel = new LicenseCheckModel();
        try
        {
            var result = await _nopHttpClient.GetLicenseCheckDetailsAsync();
            if (!string.IsNullOrEmpty(result))
            {
                licenseCheckModel = JsonConvert.DeserializeObject<LicenseCheckModel>(result);
                if (licenseCheckModel.DisplayWarning == false && licenseCheckModel.BlockPages == false)
                    await _settingService.SetSettingAsync($"{nameof(AdminAreaSettings)}.{nameof(AdminAreaSettings.CheckLicense)}", false);
            }
        }
        catch { }
        searchModel.LicenseCheckModel = licenseCheckModel;

        //prepare available categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(searchModel.AvailableCategories);

        //prepare available manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(searchModel.AvailableManufacturers);

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(searchModel.AvailableStores);

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(searchModel.AvailableVendors);

        //prepare available product types
        await _baseAdminModelFactory.PrepareProductTypesAsync(searchModel.AvailableProductTypes);

        //prepare available warehouses
        await _baseAdminModelFactory.PrepareWarehousesAsync(searchModel.AvailableWarehouses);

        searchModel.HideStoresList = _catalogSettings.IgnoreStoreLimitations || searchModel.AvailableStores.SelectionIsNotPossible();

        //prepare "published" filter (0 - all; 1 - published only; 2 - unpublished only)
        searchModel.AvailablePublishedOptions.Add(new SelectListItem
        {
            Value = "0",
            Text = await _localizationService.GetResourceAsync("Admin.Catalog.Products.List.SearchPublished.All")
        });
        searchModel.AvailablePublishedOptions.Add(new SelectListItem
        {
            Value = "1",
            Text = await _localizationService.GetResourceAsync("Admin.Catalog.Products.List.SearchPublished.PublishedOnly")
        });
        searchModel.AvailablePublishedOptions.Add(new SelectListItem
        {
            Value = "2",
            Text = await _localizationService.GetResourceAsync("Admin.Catalog.Products.List.SearchPublished.UnpublishedOnly")
        });

        //prepare grid
        searchModel.SetGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare paged product list model
    /// </summary>
    /// <param name="searchModel">Product search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product list model
    /// </returns>
    public virtual async Task<ProductListModel> PrepareProductListModelAsync(ProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get parameters to filter comments
        var overridePublished = searchModel.SearchPublishedId == 0 ? null : (bool?)(searchModel.SearchPublishedId == 1);
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null)
            searchModel.SearchVendorId = currentVendor.Id;
        var categoryIds = new List<int> { searchModel.SearchCategoryId };
        if (searchModel.SearchIncludeSubCategories && searchModel.SearchCategoryId > 0)
        {
            var childCategoryIds = await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: searchModel.SearchCategoryId, showHidden: true);
            categoryIds.AddRange(childCategoryIds);
        }

        //get products
        var products = await _productService.SearchProductsAsync(showHidden: true,
            categoryIds: categoryIds,
            manufacturerIds: new List<int> { searchModel.SearchManufacturerId },
            storeId: searchModel.SearchStoreId,
            vendorId: searchModel.SearchVendorId,
            warehouseId: searchModel.SearchWarehouseId,
            productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null,
            keywords: searchModel.SearchProductName,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize,
            overridePublished: overridePublished);

        var primaryStoreCurrency = await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);

        //prepare list model
        var model = await new ProductListModel().PrepareToGridAsync(searchModel, products, () =>
        {
            return products.SelectAwait(async product =>
            {
                //fill in model values from the entity
                var productModel = product.ToModel<ProductModel>();

                //little performance optimization: ensure that "FullDescription" is not returned
                productModel.FullDescription = string.Empty;

                //fill formatted price
                productModel.FormattedPrice = product.ProductType == ProductType.GroupedProduct ? null : await _priceFormatter.FormatPriceAsync(product.Price);

                productModel.PrimaryStoreCurrencyCode = primaryStoreCurrency.CurrencyCode;

                //fill in additional values (not existing in the entity)
                productModel.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);
                var defaultProductPicture = (await _pictureService.GetPicturesByProductIdAsync(product.Id, 1)).FirstOrDefault();
                (productModel.PictureThumbnailUrl, _) = await _pictureService.GetPictureUrlAsync(defaultProductPicture, 75);
                productModel.ProductTypeName = await _localizationService.GetLocalizedEnumAsync(product.ProductType);
                if (product.ProductType == ProductType.SimpleProduct && product.ManageInventoryMethod == ManageInventoryMethod.ManageStock)
                    productModel.StockQuantityStr = (await _productService.GetTotalStockQuantityAsync(product)).ToString();

                return productModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare product model
    /// </summary>
    /// <param name="model">Product model</param>
    /// <param name="product">Product</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product model
    /// </returns>
    public virtual async Task<ProductModel> PrepareProductModelAsync(ProductModel model, Product product, bool excludeProperties = false)
    {
        Func<ProductLocalizedModel, int, Task> localizedModelConfiguration = null;

        if (product != null)
        {
            //fill in model values from the entity
            if (model == null)
            {
                model = product.ToModel<ProductModel>();
                model.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);
            }

            var parentGroupedProduct = await _productService.GetProductByIdAsync(product.ParentGroupedProductId);
            if (parentGroupedProduct != null)
            {
                model.AssociatedToProductId = product.ParentGroupedProductId;
                model.AssociatedToProductName = parentGroupedProduct.Name;
            }

            model.LastStockQuantity = product.StockQuantity;

            model.SelectedProductTags = (await _productTagService.GetAllProductTagsByProductIdAsync(product.Id)).Select(tag => tag.Name).ToList();
            model.AvailableProductTags = (await _productTagService.GetAllProductTagsAsync())
                .Select(pt => new SelectListItem { Text = pt.Name, Value = pt.Name }).ToList();

            model.ProductAttributesExist = (await _productAttributeService.GetAllProductAttributesAsync()).Any();

            model.CanCreateCombinations = await (await _productAttributeService
                .GetProductAttributeMappingsByProductIdAsync(product.Id)).AnyAwaitAsync(async pam => (await _productAttributeService.GetProductAttributeValuesAsync(pam.Id)).Any());

            if (!excludeProperties)
            {
                model.SelectedCategoryIds = (await _categoryService.GetProductCategoriesByProductIdAsync(product.Id, true))
                    .Select(productCategory => productCategory.CategoryId).ToList();
                model.SelectedManufacturerIds = (await _manufacturerService.GetProductManufacturersByProductIdAsync(product.Id, true))
                    .Select(productManufacturer => productManufacturer.ManufacturerId).ToList();
            }

            //prepare copy product model
            await PrepareCopyProductModelAsync(model.CopyProductModel, product);

            //prepare nested search model
            PrepareRelatedProductSearchModel(model.RelatedProductSearchModel, product);
            PrepareCrossSellProductSearchModel(model.CrossSellProductSearchModel, product);
            PrepareFilterLevelValuesSearchModel(model.FilterLevelValueSearchModel, product);
            PrepareAssociatedProductSearchModel(model.AssociatedProductSearchModel, product);
            PrepareProductPictureSearchModel(model.ProductPictureSearchModel, product);
            PrepareProductVideoSearchModel(model.ProductVideoSearchModel, product);
            PrepareProductSpecificationAttributeSearchModel(model.ProductSpecificationAttributeSearchModel, product);
            PrepareProductOrderSearchModel(model.ProductOrderSearchModel, product);
            PrepareTierPriceSearchModel(model.TierPriceSearchModel, product);
            await PrepareStockQuantityHistorySearchModelAsync(model.StockQuantityHistorySearchModel, product);
            PrepareProductAttributeMappingSearchModel(model.ProductAttributeMappingSearchModel, product);
            PrepareProductAttributeCombinationSearchModel(model.ProductAttributeCombinationSearchModel, product);

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.Name = await _localizationService.GetLocalizedAsync(product, entity => entity.Name, languageId, false, false);
                locale.FullDescription = await _localizationService.GetLocalizedAsync(product, entity => entity.FullDescription, languageId, false, false);
                locale.ShortDescription = await _localizationService.GetLocalizedAsync(product, entity => entity.ShortDescription, languageId, false, false);
                locale.MetaKeywords = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaKeywords, languageId, false, false);
                locale.MetaDescription = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaDescription, languageId, false, false);
                locale.MetaTitle = await _localizationService.GetLocalizedAsync(product, entity => entity.MetaTitle, languageId, false, false);
                locale.SeName = await _urlRecordService.GetSeNameAsync(product, languageId, false, false);
            };
        }

        //set default values for the new model
        if (product == null)
        {
            model.MaximumCustomerEnteredPrice = 1000;
            model.MaxNumberOfDownloads = 10;
            model.RecurringCycleLength = 100;
            model.RecurringTotalCycles = 10;
            model.RentalPriceLength = 1;
            model.StockQuantity = 10000;
            model.NotifyAdminForQuantityBelow = 1;
            model.OrderMinimumQuantity = 1;
            model.OrderMaximumQuantity = 10000;
            model.TaxCategoryId = _taxSettings.DefaultTaxCategoryId;
            model.UnlimitedDownloads = true;
            model.IsShipEnabled = true;
            model.AllowCustomerReviews = true;
            model.Published = true;
            model.VisibleIndividually = true;
        }

        model.PrimaryStoreCurrencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode;
        model.BaseWeightIn = (await _measureService.GetMeasureWeightByIdAsync(_measureSettings.BaseWeightId)).Name;
        model.BaseDimensionIn = (await _measureService.GetMeasureDimensionByIdAsync(_measureSettings.BaseDimensionId)).Name;
        model.HasAvailableSpecificationAttributes =
            (await _specificationAttributeService.GetSpecificationAttributesWithOptionsAsync()).Any();

        var currentVendor = await _workContext.GetCurrentVendorAsync();
        model.IsLoggedInAsVendor = currentVendor != null;

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        //prepare editor settings
        model.ProductEditorSettingsModel = await _settingModelFactory.PrepareProductEditorSettingsModelAsync();

        //prepare available product templates
        await _baseAdminModelFactory.PrepareProductTemplatesAsync(model.AvailableProductTemplates, false);

        //prepare available product types
        var productTemplates = await _productTemplateService.GetAllProductTemplatesAsync();
        foreach (var productType in Enum.GetValues(typeof(ProductType)).OfType<ProductType>())
        {
            model.ProductsTypesSupportedByProductTemplates.Add((int)productType, new List<SelectListItem>());
            foreach (var template in productTemplates)
            {
                var list = (IList<int>)TypeDescriptor.GetConverter(typeof(List<int>)).ConvertFrom(template.IgnoredProductTypes) ?? new List<int>();
                if (string.IsNullOrEmpty(template.IgnoredProductTypes) || !list.Contains((int)productType))
                {
                    model.ProductsTypesSupportedByProductTemplates[(int)productType].Add(new SelectListItem
                    {
                        Text = template.Name,
                        Value = template.Id.ToString()
                    });
                }
            }
        }

        //prepare available delivery dates
        await _baseAdminModelFactory.PrepareDeliveryDatesAsync(model.AvailableDeliveryDates,
            defaultItemText: await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.DeliveryDate.None"));

        //prepare available product availability ranges
        await _baseAdminModelFactory.PrepareProductAvailabilityRangesAsync(model.AvailableProductAvailabilityRanges,
            defaultItemText: await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.ProductAvailabilityRange.None"));

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(model.AvailableVendors,
            defaultItemText: await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.Vendor.None"));

        //prepare available tax categories
        await _baseAdminModelFactory.PrepareTaxCategoriesAsync(model.AvailableTaxCategories);

        //prepare available warehouses
        await _baseAdminModelFactory.PrepareWarehousesAsync(model.AvailableWarehouses,
            defaultItemText: await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.Warehouse.None"));
        await PrepareProductWarehouseInventoryModelsAsync(model.ProductWarehouseInventoryModels, product);

        //prepare available base price units
        var availableMeasureWeights = (await _measureService.GetAllMeasureWeightsAsync())
            .Select(weight => new SelectListItem { Text = weight.Name, Value = weight.Id.ToString() }).ToList();
        model.AvailableBasepriceUnits = availableMeasureWeights;
        model.AvailableBasepriceBaseUnits = availableMeasureWeights;

        //prepare model categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(model.AvailableCategories, false);
        foreach (var categoryItem in model.AvailableCategories)
        {
            categoryItem.Selected = int.TryParse(categoryItem.Value, out var categoryId)
                                    && model.SelectedCategoryIds.Contains(categoryId);
        }

        //prepare model manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(model.AvailableManufacturers, false);
        foreach (var manufacturerItem in model.AvailableManufacturers)
        {
            manufacturerItem.Selected = int.TryParse(manufacturerItem.Value, out var manufacturerId)
                                        && model.SelectedManufacturerIds.Contains(manufacturerId);
        }

        //prepare model discounts
        var availableDiscounts = await _discountService.GetAllDiscountsAsync(
            discountType: DiscountType.AssignedToSkus,
            showHidden: true, isActive: null,
            vendorId: currentVendor?.Id ?? 0);

        await _discountSupportedModelFactory.PrepareModelDiscountsAsync(model, product, availableDiscounts, excludeProperties);

        //prepare model stores
        await _storeMappingSupportedModelFactory.PrepareModelStoresAsync(model, product, excludeProperties);

        await _baseAdminModelFactory.PreparePreTranslationSupportModelAsync(model);

        return model;
    }

    /// <summary>
    /// Prepare required product search model to add to the product
    /// </summary>
    /// <param name="searchModel">Required product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the required product search model to add to the product
    /// </returns>
    public virtual async Task<AddRequiredProductSearchModel> PrepareAddRequiredProductSearchModelAsync(AddRequiredProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        searchModel.IsLoggedInAsVendor = await _workContext.GetCurrentVendorAsync() != null;

        //prepare available categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(searchModel.AvailableCategories);

        //prepare available manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(searchModel.AvailableManufacturers);

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(searchModel.AvailableStores);

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(searchModel.AvailableVendors);

        //prepare available product types
        await _baseAdminModelFactory.PrepareProductTypesAsync(searchModel.AvailableProductTypes);

        //prepare page parameters
        searchModel.SetPopupGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare required product list model to add to the product
    /// </summary>
    /// <param name="searchModel">Required product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the required product list model to add to the product
    /// </returns>
    public virtual async Task<AddRequiredProductListModel> PrepareAddRequiredProductListModelAsync(AddRequiredProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //a vendor should have access only to his products
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null)
            searchModel.SearchVendorId = currentVendor.Id;

        //get products
        var products = await _productService.SearchProductsAsync(showHidden: true,
            categoryIds: new List<int> { searchModel.SearchCategoryId },
            manufacturerIds: new List<int> { searchModel.SearchManufacturerId },
            storeId: searchModel.SearchStoreId,
            vendorId: searchModel.SearchVendorId,
            productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null,
            keywords: searchModel.SearchProductName,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare grid model
        var model = await new AddRequiredProductListModel().PrepareToGridAsync(searchModel, products, () =>
        {
            return products.SelectAwait(async product =>
            {
                var productModel = product.ToModel<ProductModel>();

                productModel.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);

                return productModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged related product list model
    /// </summary>
    /// <param name="searchModel">Related product search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the related product list model
    /// </returns>
    public virtual async Task<RelatedProductListModel> PrepareRelatedProductListModelAsync(RelatedProductSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get related products
        var relatedProducts = (await _productService
            .GetRelatedProductsByProductId1Async(productId1: product.Id, showHidden: true)).ToPagedList(searchModel);

        //prepare grid model
        var model = await new RelatedProductListModel().PrepareToGridAsync(searchModel, relatedProducts, () =>
        {
            return relatedProducts.SelectAwait(async relatedProduct =>
            {
                //fill in model values from the entity
                var relatedProductModel = relatedProduct.ToModel<RelatedProductModel>();

                //fill in additional values (not existing in the entity)
                relatedProductModel.Product2Name = (await _productService.GetProductByIdAsync(relatedProduct.ProductId2))?.Name;

                return relatedProductModel;
            });
        });
        return model;
    }

    /// <summary>
    /// Prepare related product search model to add to the product
    /// </summary>
    /// <param name="searchModel">Related product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the related product search model to add to the product
    /// </returns>
    public virtual async Task<AddRelatedProductSearchModel> PrepareAddRelatedProductSearchModelAsync(AddRelatedProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        searchModel.IsLoggedInAsVendor = await _workContext.GetCurrentVendorAsync() != null;

        //prepare available categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(searchModel.AvailableCategories);

        //prepare available manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(searchModel.AvailableManufacturers);

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(searchModel.AvailableStores);

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(searchModel.AvailableVendors);

        //prepare available product types
        await _baseAdminModelFactory.PrepareProductTypesAsync(searchModel.AvailableProductTypes);

        //prepare page parameters
        searchModel.SetPopupGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare paged related product list model to add to the product
    /// </summary>
    /// <param name="searchModel">Related product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the related product list model to add to the product
    /// </returns>
    public virtual async Task<AddRelatedProductListModel> PrepareAddRelatedProductListModelAsync(AddRelatedProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //a vendor should have access only to his products
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null)
            searchModel.SearchVendorId = currentVendor.Id;

        //get products
        var products = await _productService.SearchProductsAsync(showHidden: true,
            categoryIds: new List<int> { searchModel.SearchCategoryId },
            manufacturerIds: new List<int> { searchModel.SearchManufacturerId },
            storeId: searchModel.SearchStoreId,
            vendorId: searchModel.SearchVendorId,
            productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null,
            keywords: searchModel.SearchProductName,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare grid model
        var model = await new AddRelatedProductListModel().PrepareToGridAsync(searchModel, products, () =>
        {
            return products.SelectAwait(async product =>
            {
                var productModel = product.ToModel<ProductModel>();

                productModel.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);

                return productModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged cross-sell product list model
    /// </summary>
    /// <param name="searchModel">Cross-sell product search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the cross-sell product list model
    /// </returns>
    public virtual async Task<CrossSellProductListModel> PrepareCrossSellProductListModelAsync(CrossSellProductSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get cross-sell products
        var crossSellProducts = (await _productService
            .GetCrossSellProductsByProductId1Async(productId1: product.Id, showHidden: true)).ToPagedList(searchModel);

        //prepare grid model
        var model = await new CrossSellProductListModel().PrepareToGridAsync(searchModel, crossSellProducts, () =>
        {
            return crossSellProducts.SelectAwait(async crossSellProduct =>
            {
                //fill in model values from the entity
                var crossSellProductModel = new CrossSellProductModel
                {
                    Id = crossSellProduct.Id,
                    ProductId2 = crossSellProduct.ProductId2
                };

                //fill in additional values (not existing in the entity)
                crossSellProductModel.Product2Name = (await _productService.GetProductByIdAsync(crossSellProduct.ProductId2))?.Name;

                return crossSellProductModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged filter level value list model
    /// </summary>
    /// <param name="searchModel">Filter level value search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the filter level value list model
    /// </returns>
    public virtual async Task<FilterLevelValueListModel> PrepareFilterLevelValueListModelAsync(FilterLevelValueSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get filter level values
        var filterLevelValues = (await _filterLevelValueService
            .GetFilterLevelValuesByProductIdAsync(productId: product.Id)).ToPagedList(searchModel);

        var (filterLevel1Disabled, filterLevel2Disabled, filterLevel3Disabled) = _filterLevelValueService.IsFilterLevelDisabled();

        //prepare grid model
        var model = await new FilterLevelValueListModel().PrepareToGridAsync(searchModel, filterLevelValues, () =>
        {
            return filterLevelValues.SelectAwait(filterLevelValue =>
            {
                //fill in model values from the entity
                var filterLevelValueModel = new FilterLevelValueModel
                {
                    Id = filterLevelValue.Id,
                    FilterLevel1Value = filterLevelValue.FilterLevel1Value,
                    FilterLevel2Value = filterLevelValue.FilterLevel2Value,
                    FilterLevel3Value = filterLevelValue.FilterLevel3Value,
                    FilterLevel1ValueEnabled = !filterLevel1Disabled,
                    FilterLevel2ValueEnabled = !filterLevel2Disabled,
                    FilterLevel3ValueEnabled = !filterLevel3Disabled
                };

                return new ValueTask<FilterLevelValueModel>(filterLevelValueModel);
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare cross-sell product search model to add to the product
    /// </summary>
    /// <param name="searchModel">Cross-sell product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the cross-sell product search model to add to the product
    /// </returns>
    public virtual async Task<AddCrossSellProductSearchModel> PrepareAddCrossSellProductSearchModelAsync(AddCrossSellProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        searchModel.IsLoggedInAsVendor = await _workContext.GetCurrentVendorAsync() != null;

        //prepare available categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(searchModel.AvailableCategories);

        //prepare available manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(searchModel.AvailableManufacturers);

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(searchModel.AvailableStores);

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(searchModel.AvailableVendors);

        //prepare available product types
        await _baseAdminModelFactory.PrepareProductTypesAsync(searchModel.AvailableProductTypes);

        //prepare page parameters
        searchModel.SetPopupGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare paged crossSell product list model to add to the product
    /// </summary>
    /// <param name="searchModel">CrossSell product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the crossSell product list model to add to the product
    /// </returns>
    public virtual async Task<AddCrossSellProductListModel> PrepareAddCrossSellProductListModelAsync(AddCrossSellProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //a vendor should have access only to his products
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null)
            searchModel.SearchVendorId = currentVendor.Id;

        //get products
        var products = await _productService.SearchProductsAsync(showHidden: true,
            categoryIds: new List<int> { searchModel.SearchCategoryId },
            manufacturerIds: new List<int> { searchModel.SearchManufacturerId },
            storeId: searchModel.SearchStoreId,
            vendorId: searchModel.SearchVendorId,
            productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null,
            keywords: searchModel.SearchProductName,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare grid model
        var model = await new AddCrossSellProductListModel().PrepareToGridAsync(searchModel, products, () =>
        {
            return products.SelectAwait(async product =>
            {
                var productModel = product.ToModel<ProductModel>();

                productModel.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);

                return productModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged associated product list model
    /// </summary>
    /// <param name="searchModel">Associated product search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the associated product list model
    /// </returns>
    public virtual async Task<AssociatedProductListModel> PrepareAssociatedProductListModelAsync(AssociatedProductSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        var vendor = await _workContext.GetCurrentVendorAsync();
        //get associated products
        var associatedProducts = (await _productService.GetAssociatedProductsAsync(showHidden: true,
            parentGroupedProductId: product.Id,
            vendorId: vendor?.Id ?? 0)).ToPagedList(searchModel);

        //prepare grid model
        var model = new AssociatedProductListModel().PrepareToGrid(searchModel, associatedProducts, () =>
        {
            return associatedProducts.Select(associatedProduct =>
            {
                var associatedProductModel = associatedProduct.ToModel<AssociatedProductModel>();
                associatedProductModel.ProductName = associatedProduct.Name;

                return associatedProductModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare associated product search model to add to the product
    /// </summary>
    /// <param name="searchModel">Associated product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the associated product search model to add to the product
    /// </returns>
    public virtual async Task<AddAssociatedProductSearchModel> PrepareAddAssociatedProductSearchModelAsync(AddAssociatedProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        searchModel.IsLoggedInAsVendor = await _workContext.GetCurrentVendorAsync() != null;

        //prepare available categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(searchModel.AvailableCategories);

        //prepare available manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(searchModel.AvailableManufacturers);

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(searchModel.AvailableStores);

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(searchModel.AvailableVendors);

        //prepare available product types
        await _baseAdminModelFactory.PrepareProductTypesAsync(searchModel.AvailableProductTypes);

        //prepare page parameters
        searchModel.SetPopupGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare paged associated product list model to add to the product
    /// </summary>
    /// <param name="searchModel">Associated product search model to add to the product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the associated product list model to add to the product
    /// </returns>
    public virtual async Task<AddAssociatedProductListModel> PrepareAddAssociatedProductListModelAsync(AddAssociatedProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //a vendor should have access only to his products
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null)
            searchModel.SearchVendorId = currentVendor.Id;

        //get products
        var products = await _productService.SearchProductsAsync(showHidden: true,
            categoryIds: new List<int> { searchModel.SearchCategoryId },
            manufacturerIds: new List<int> { searchModel.SearchManufacturerId },
            storeId: searchModel.SearchStoreId,
            vendorId: searchModel.SearchVendorId,
            productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null,
            keywords: searchModel.SearchProductName,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare grid model
        var model = await new AddAssociatedProductListModel().PrepareToGridAsync(searchModel, products, () =>
        {
            return products.SelectAwait(async product =>
            {
                //fill in model values from the entity
                var productModel = product.ToModel<ProductModel>();

                //fill in additional values (not existing in the entity)
                productModel.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);
                var parentGroupedProduct = await _productService.GetProductByIdAsync(product.ParentGroupedProductId);

                if (parentGroupedProduct == null)
                    return productModel;

                productModel.AssociatedToProductId = product.ParentGroupedProductId;
                productModel.AssociatedToProductName = parentGroupedProduct.Name;

                return productModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged product picture list model
    /// </summary>
    /// <param name="searchModel">Product picture search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product picture list model
    /// </returns>
    public virtual async Task<ProductPictureListModel> PrepareProductPictureListModelAsync(ProductPictureSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get product pictures
        var productPictures = (await _productService.GetProductPicturesByProductIdAsync(product.Id)).ToPagedList(searchModel);

        //prepare grid model
        var model = await new ProductPictureListModel().PrepareToGridAsync(searchModel, productPictures, () =>
        {
            return productPictures.SelectAwait(async productPicture =>
            {
                //fill in model values from the entity
                var productPictureModel = productPicture.ToModel<ProductPictureModel>();

                //fill in additional values (not existing in the entity)
                var picture = (await _pictureService.GetPictureByIdAsync(productPicture.PictureId))
                              ?? throw new Exception("Picture cannot be loaded");

                productPictureModel.PictureUrl = (await _pictureService.GetPictureUrlAsync(picture)).Url;

                productPictureModel.OverrideAltAttribute = picture.AltAttribute;
                productPictureModel.OverrideTitleAttribute = picture.TitleAttribute;

                return productPictureModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged product video list model
    /// </summary>
    /// <param name="searchModel">Product video search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product video list model
    /// </returns>
    public virtual async Task<ProductVideoListModel> PrepareProductVideoListModelAsync(ProductVideoSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get product videos
        var productVideos = (await _productService.GetProductVideosByProductIdAsync(product.Id)).ToPagedList(searchModel);

        //prepare grid model
        var model = await new ProductVideoListModel().PrepareToGridAsync(searchModel, productVideos, () =>
        {
            return productVideos.SelectAwait(async productVideo =>
            {
                //fill in model values from the entity
                var productVideoModel = productVideo.ToModel<ProductVideoModel>();

                //fill in additional values (not existing in the entity)
                var video = (await _videoService.GetVideoByIdAsync(productVideo.VideoId))
                            ?? throw new Exception("Video cannot be loaded");

                productVideoModel.VideoUrl = video.VideoUrl;

                return productVideoModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged product specification attribute list model
    /// </summary>
    /// <param name="searchModel">Product specification attribute search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product specification attribute list model
    /// </returns>
    public virtual async Task<ProductSpecificationAttributeListModel> PrepareProductSpecificationAttributeListModelAsync(
        ProductSpecificationAttributeSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get product specification attributes
        var productSpecificationAttributes = (await _specificationAttributeService
            .GetProductSpecificationAttributesAsync(product.Id)).ToPagedList(searchModel);

        //prepare grid model
        var model = await new ProductSpecificationAttributeListModel().PrepareToGridAsync(searchModel, productSpecificationAttributes, () =>
        {
            return productSpecificationAttributes.SelectAwait(async attribute =>
            {
                //fill in model values from the entity
                var productSpecificationAttributeModel = attribute.ToModel<ProductSpecificationAttributeModel>();

                var specAttributeOption = await _specificationAttributeService
                    .GetSpecificationAttributeOptionByIdAsync(attribute.SpecificationAttributeOptionId);
                var specAttribute = await _specificationAttributeService
                    .GetSpecificationAttributeByIdAsync(specAttributeOption.SpecificationAttributeId);

                //fill in additional values (not existing in the entity)
                productSpecificationAttributeModel.AttributeTypeName = await _localizationService.GetLocalizedEnumAsync(attribute.AttributeType);

                productSpecificationAttributeModel.AttributeId = specAttribute.Id;
                productSpecificationAttributeModel.AttributeName = await GetSpecificationAttributeNameAsync(specAttribute);
                var currentLanguage = await _workContext.GetWorkingLanguageAsync();

                switch (attribute.AttributeType)
                {
                    case SpecificationAttributeType.Option:
                        productSpecificationAttributeModel.ValueRaw = WebUtility.HtmlEncode(specAttributeOption.Name);
                        productSpecificationAttributeModel.SpecificationAttributeOptionId = specAttributeOption.Id;
                        break;
                    case SpecificationAttributeType.CustomText:
                        productSpecificationAttributeModel.ValueRaw = WebUtility.HtmlEncode(await _localizationService.GetLocalizedAsync(attribute, x => x.CustomValue, currentLanguage?.Id));
                        break;
                    case SpecificationAttributeType.CustomHtmlText:
                        productSpecificationAttributeModel.ValueRaw = await _localizationService
                            .GetLocalizedAsync(attribute, x => x.CustomValue, currentLanguage?.Id);
                        break;
                    case SpecificationAttributeType.Hyperlink:
                        productSpecificationAttributeModel.ValueRaw = attribute.CustomValue;
                        break;
                }

                return productSpecificationAttributeModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged product specification attribute model
    /// </summary>
    /// <param name="productId">Product id</param>
    /// <param name="specificationId">Specification attribute id</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product specification attribute model
    /// </returns>
    public virtual async Task<AddSpecificationAttributeModel> PrepareAddSpecificationAttributeModelAsync(int productId, int? specificationId)
    {
        if (!specificationId.HasValue)
        {
            return new AddSpecificationAttributeModel
            {
                AvailableAttributes = await (await _specificationAttributeService.GetSpecificationAttributesWithOptionsAsync())
                    .SelectAwait(async attributeWithOption =>
                    {
                        var attributeName = await GetSpecificationAttributeNameAsync(attributeWithOption);

                        return new SelectListItem(attributeName, attributeWithOption.Id.ToString());
                    }).ToListAsync(),
                ProductId = productId,
                Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync<AddSpecificationAttributeLocalizedModel>()
            };
        }

        var attribute = await _specificationAttributeService.GetProductSpecificationAttributeByIdAsync(specificationId.Value) 
                        ?? throw new ArgumentException("No specification attribute found with the specified id");

        //a vendor should have access only to his products
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null && (await _productService.GetProductByIdAsync(attribute.ProductId)).VendorId != currentVendor.Id)
            throw new UnauthorizedAccessException("This is not your product");

        var specAttributeOption = await _specificationAttributeService.GetSpecificationAttributeOptionByIdAsync(attribute.SpecificationAttributeOptionId);
        var specAttribute = await _specificationAttributeService.GetSpecificationAttributeByIdAsync(specAttributeOption.SpecificationAttributeId);

        var model = attribute.ToModel<AddSpecificationAttributeModel>();
        model.SpecificationId = attribute.Id;
        model.AttributeId = specAttribute.Id;
        model.AttributeTypeName = await _localizationService.GetLocalizedEnumAsync(attribute.AttributeType);
        model.AttributeName = specAttribute.Name;

        model.AvailableAttributes = await (await _specificationAttributeService.GetSpecificationAttributesWithOptionsAsync())
            .SelectAwait(async attributeWithOption =>
            {
                var attributeName = await GetSpecificationAttributeNameAsync(attributeWithOption);

                return new SelectListItem(attributeName, attributeWithOption.Id.ToString());
            })
            .ToListAsync();

        model.AvailableOptions = (await _specificationAttributeService
                .GetSpecificationAttributeOptionsBySpecificationAttributeAsync(model.AttributeId))
            .Select(option => new SelectListItem { Text = option.Name, Value = option.Id.ToString() })
            .ToList();

        switch (attribute.AttributeType)
        {
            case SpecificationAttributeType.Option:
                model.ValueRaw = WebUtility.HtmlEncode(specAttributeOption.Name);
                model.SpecificationAttributeOptionId = specAttributeOption.Id;
                break;
            case SpecificationAttributeType.CustomText:
                model.Value = WebUtility.HtmlDecode(attribute.CustomValue);
                break;
            case SpecificationAttributeType.CustomHtmlText:
                model.ValueRaw = attribute.CustomValue;
                break;
            case SpecificationAttributeType.Hyperlink:
                model.Value = attribute.CustomValue;
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(attribute.AttributeType));
        }

        model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(
            async (AddSpecificationAttributeLocalizedModel locale, int languageId) =>
            {
                switch (attribute.AttributeType)
                {
                    case SpecificationAttributeType.CustomHtmlText:
                        locale.ValueRaw = await _localizationService.GetLocalizedAsync(attribute, entity => entity.CustomValue, languageId, false, false);
                        break;
                    case SpecificationAttributeType.CustomText:
                        locale.Value = await _localizationService.GetLocalizedAsync(attribute, entity => entity.CustomValue, languageId, false, false);
                        break;
                    case SpecificationAttributeType.Option:
                        break;
                    case SpecificationAttributeType.Hyperlink:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            });

        return model;
    }

    /// <summary>
    /// Prepare product tag search model
    /// </summary>
    /// <param name="searchModel">Product tag search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product tag search model
    /// </returns>
    public virtual Task<ProductTagSearchModel> PrepareProductTagSearchModelAsync(ProductTagSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //prepare page parameters
        searchModel.SetGridPageSize();

        return Task.FromResult(searchModel);
    }

    /// <summary>
    /// Prepare paged product tag list model
    /// </summary>
    /// <param name="searchModel">Product tag search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product tag list model
    /// </returns>
    public virtual async Task<ProductTagListModel> PrepareProductTagListModelAsync(ProductTagSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get product tags
        var productTags = (await (await _productTagService.GetAllProductTagsAsync(tagName: searchModel.SearchTagName))
                .OrderByDescendingAwait(async tag => await _productTagService.GetProductCountByProductTagIdAsync(tag.Id, storeId: 0, showHidden: true)).ToListAsync())
            .ToPagedList(searchModel);

        //prepare list model
        var model = await new ProductTagListModel().PrepareToGridAsync(searchModel, productTags, () =>
        {
            return productTags.SelectAwait(async tag =>
            {
                //fill in model values from the entity
                var productTagModel = tag.ToModel<ProductTagModel>();

                //fill in additional values (not existing in the entity)
                productTagModel.ProductCount = await _productTagService.GetProductCountByProductTagIdAsync(tag.Id, storeId: 0, showHidden: true);

                return productTagModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare product tag model
    /// </summary>
    /// <param name="model">Product tag model</param>
    /// <param name="productTag">Product tag</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product tag model
    /// </returns>
    public virtual async Task<ProductTagModel> PrepareProductTagModelAsync(ProductTagModel model, ProductTag productTag, bool excludeProperties = false)
    {
        Func<ProductTagLocalizedModel, int, Task> localizedModelConfiguration = null;

        if (productTag != null)
        {
            //fill in model values from the entity
            if (model == null)
            {
                model = productTag.ToModel<ProductTagModel>();
            }

            model.ProductCount = await _productTagService.GetProductCountByProductTagIdAsync(productTag.Id, storeId: 0, showHidden: true);

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.Name = await _localizationService.GetLocalizedAsync(productTag, entity => entity.Name, languageId, false, false);
                locale.MetaKeywords = await _localizationService.GetLocalizedAsync(productTag, entity => entity.MetaKeywords, languageId, false, false);
                locale.MetaDescription = await _localizationService.GetLocalizedAsync(productTag, entity => entity.MetaDescription, languageId, false, false);
                locale.MetaTitle = await _localizationService.GetLocalizedAsync(productTag, entity => entity.MetaTitle, languageId, false, false);
            };
        }

        PrepareTaggedProductsSearchModel(model.ProductTagProductSearchModel, productTag);

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        return model;
    }

    /// <summary>
    /// Prepare tagged product list model
    /// </summary>
    /// <param name="searchModel">Product search model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains list model for the tagged products
    /// </returns>
    public virtual async Task<ProductTagProductListModel> PrepareTaggedProductListModelAsync(ProductTagProductSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //get products by tag
        var products = await _productService.SearchProductsAsync(
                productTagId: searchModel.ProductTagId,
                showHidden: true,
                pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare list model
        return new ProductTagProductListModel().PrepareToGrid(searchModel, products, () => products.Select(product => product.ToModel<ProductModel>()));
    }

    /// <summary>
    /// Prepare paged product order list model
    /// </summary>
    /// <param name="searchModel">Product order search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product order list model
    /// </returns>
    public virtual async Task<ProductOrderListModel> PrepareProductOrderListModelAsync(ProductOrderSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get orders
        var orders = await _orderService.SearchOrdersAsync(productId: searchModel.ProductId,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare grid model
        var model = await new ProductOrderListModel().PrepareToGridAsync(searchModel, orders, () =>
        {
            return orders.SelectAwait(async order =>
            {
                var billingAddress = await _addressService.GetAddressByIdAsync(order.BillingAddressId);

                //fill in model values from the entity
                var orderModel = new OrderModel
                {
                    Id = order.Id,
                    CustomerEmail = billingAddress.Email,
                    CustomOrderNumber = order.CustomOrderNumber
                };

                //convert dates to the user time
                orderModel.CreatedOn = await _dateTimeHelper.ConvertToUserTimeAsync(order.CreatedOnUtc, DateTimeKind.Utc);

                //fill in additional values (not existing in the entity)
                orderModel.StoreName = (await _storeService.GetStoreByIdAsync(order.StoreId))?.Name ?? "Deleted";
                orderModel.OrderStatus = await _localizationService.GetLocalizedEnumAsync(order.OrderStatus);
                orderModel.PaymentStatus = await _localizationService.GetLocalizedEnumAsync(order.PaymentStatus);
                orderModel.ShippingStatus = await _localizationService.GetLocalizedEnumAsync(order.ShippingStatus);

                return orderModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged tier price list model
    /// </summary>
    /// <param name="searchModel">Tier price search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ier price list model
    /// </returns>
    public virtual async Task<TierPriceListModel> PrepareTierPriceListModelAsync(TierPriceSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get tier prices
        var tierPrices = (await _productService.GetTierPricesByProductAsync(product.Id))
            .OrderBy(price => price.StoreId).ThenBy(price => price.Quantity).ThenBy(price => price.CustomerRoleId)
            .ToList().ToPagedList(searchModel);

        //prepare grid model
        var model = await new TierPriceListModel().PrepareToGridAsync(searchModel, tierPrices, () =>
        {
            return tierPrices.SelectAwait(async price =>
            {
                //fill in model values from the entity
                var tierPriceModel = price.ToModel<TierPriceModel>();

                //fill in additional values (not existing in the entity)   
                tierPriceModel.Store = price.StoreId > 0
                    ? ((await _storeService.GetStoreByIdAsync(price.StoreId))?.Name ?? "Deleted")
                    : await _localizationService.GetResourceAsync("Admin.Catalog.Products.TierPrices.Fields.Store.All");
                tierPriceModel.CustomerRoleId = price.CustomerRoleId ?? 0;
                tierPriceModel.CustomerRole = price.CustomerRoleId.HasValue
                    ? (await _customerService.GetCustomerRoleByIdAsync(price.CustomerRoleId.Value))?.Name
                    : await _localizationService.GetResourceAsync("Admin.Catalog.Products.TierPrices.Fields.CustomerRole.All");

                tierPriceModel.FormattedPrice = await _priceFormatter.FormatPriceAsync(price.Price);

                return tierPriceModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare tier price model
    /// </summary>
    /// <param name="model">Tier price model</param>
    /// <param name="product">Product</param>
    /// <param name="tierPrice">Tier price</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the ier price model
    /// </returns>
    public virtual async Task<TierPriceModel> PrepareTierPriceModelAsync(TierPriceModel model,
        Product product, TierPrice tierPrice, bool excludeProperties = false)
    {
        ArgumentNullException.ThrowIfNull(product);

        if (tierPrice != null)
        {
            //fill in model values from the entity
            if (model == null)
                model = tierPrice.ToModel<TierPriceModel>();
        }

        model.PrimaryStoreCurrencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode;

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(model.AvailableStores);

        //prepare available customer roles
        await _baseAdminModelFactory.PrepareCustomerRolesAsync(model.AvailableCustomerRoles);

        return model;
    }

    /// <summary>
    /// Prepare paged stock quantity history list model
    /// </summary>
    /// <param name="searchModel">Stock quantity history search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the stock quantity history list model
    /// </returns>
    public virtual async Task<StockQuantityHistoryListModel> PrepareStockQuantityHistoryListModelAsync(StockQuantityHistorySearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get stock quantity history
        var stockQuantityHistory = await _productService.GetStockQuantityHistoryAsync(product: product,
            warehouseId: searchModel.WarehouseId,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        var currentCustomer = await _workContext.GetCurrentCustomerAsync();
        var currentStore = await _storeContext.GetCurrentStoreAsync();

        //prepare grid model
        var model = await new StockQuantityHistoryListModel().PrepareToGridAsync(searchModel, stockQuantityHistory, () =>
        {
            return stockQuantityHistory.SelectAwait(async historyEntry =>
            {
                //fill in model values from the entity
                var stockQuantityHistoryModel = historyEntry.ToModel<StockQuantityHistoryModel>();

                //convert dates to the user time
                stockQuantityHistoryModel.CreatedOn =
                    await _dateTimeHelper.ConvertToUserTimeAsync(historyEntry.CreatedOnUtc, DateTimeKind.Utc);

                //fill in additional values (not existing in the entity)
                var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(historyEntry.CombinationId ?? 0);
                if (combination != null)
                {
                    stockQuantityHistoryModel.AttributeCombination = await _productAttributeFormatter
                        .FormatAttributesAsync(product, combination.AttributesXml, currentCustomer, currentStore, renderGiftCardAttributes: false);
                }

                stockQuantityHistoryModel.WarehouseName = historyEntry.WarehouseId.HasValue
                    ? (await _warehouseService.GetWarehouseByIdAsync(historyEntry.WarehouseId.Value))?.Name ?? "Deleted"
                    : await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.Warehouse.None");

                return stockQuantityHistoryModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged product attribute mapping list model
    /// </summary>
    /// <param name="searchModel">Product attribute mapping search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product attribute mapping list model
    /// </returns>
    public virtual async Task<ProductAttributeMappingListModel> PrepareProductAttributeMappingListModelAsync(ProductAttributeMappingSearchModel searchModel,
        Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get product attribute mappings
        var productAttributeMappings = (await _productAttributeService
            .GetProductAttributeMappingsByProductIdAsync(product.Id)).ToPagedList(searchModel);

        //prepare grid model
        var model = await new ProductAttributeMappingListModel().PrepareToGridAsync(searchModel, productAttributeMappings, () =>
        {
            return productAttributeMappings.SelectAwait(async attributeMapping =>
            {
                //fill in model values from the entity
                var productAttributeMappingModel = attributeMapping.ToModel<ProductAttributeMappingModel>();

                //fill in additional values (not existing in the entity)
                productAttributeMappingModel.ConditionString = string.Empty;

                productAttributeMappingModel.ValidationRulesString = await PrepareProductAttributeMappingValidationRulesStringAsync(attributeMapping);
                productAttributeMappingModel.ProductAttribute = (await _productAttributeService.GetProductAttributeByIdAsync(attributeMapping.ProductAttributeId))?.Name;
                productAttributeMappingModel.AttributeControlType = await _localizationService.GetLocalizedEnumAsync(attributeMapping.AttributeControlType);
                var conditionAttribute = (await _productAttributeParser
                        .ParseProductAttributeMappingsAsync(attributeMapping.ConditionAttributeXml))
                    .FirstOrDefault();
                if (conditionAttribute == null)
                    return productAttributeMappingModel;

                var conditionValue = (await _productAttributeParser
                        .ParseProductAttributeValuesAsync(attributeMapping.ConditionAttributeXml))
                    .FirstOrDefault();
                if (conditionValue != null)
                {
                    productAttributeMappingModel.ConditionString =
                        $"{WebUtility.HtmlEncode((await _productAttributeService.GetProductAttributeByIdAsync(conditionAttribute.ProductAttributeId)).Name)}: {WebUtility.HtmlEncode(conditionValue.Name)}";
                }

                return productAttributeMappingModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare product attribute mapping model
    /// </summary>
    /// <param name="model">Product attribute mapping model</param>
    /// <param name="product">Product</param>
    /// <param name="productAttributeMapping">Product attribute mapping</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product attribute mapping model
    /// </returns>
    public virtual async Task<ProductAttributeMappingModel> PrepareProductAttributeMappingModelAsync(ProductAttributeMappingModel model,
        Product product, ProductAttributeMapping productAttributeMapping, bool excludeProperties = false)
    {
        Func<ProductAttributeMappingLocalizedModel, int, Task> localizedModelConfiguration = null;

        ArgumentNullException.ThrowIfNull(product);

        if (productAttributeMapping != null)
        {
            //fill in model values from the entity
            model ??= new ProductAttributeMappingModel
            {
                Id = productAttributeMapping.Id
            };

            model.ProductAttribute = (await _productAttributeService.GetProductAttributeByIdAsync(productAttributeMapping.ProductAttributeId)).Name;
            model.AttributeControlType = await _localizationService.GetLocalizedEnumAsync(productAttributeMapping.AttributeControlType);

            if (!excludeProperties)
            {
                model.ProductAttributeId = productAttributeMapping.ProductAttributeId;
                model.TextPrompt = productAttributeMapping.TextPrompt;
                model.IsRequired = productAttributeMapping.IsRequired;
                model.AttributeControlTypeId = productAttributeMapping.AttributeControlTypeId;
                model.DisplayOrder = productAttributeMapping.DisplayOrder;
                model.ValidationMinLength = productAttributeMapping.ValidationMinLength;
                model.ValidationMaxLength = productAttributeMapping.ValidationMaxLength;
                model.ValidationFileAllowedExtensions = productAttributeMapping.ValidationFileAllowedExtensions;
                model.ValidationFileMaximumSize = productAttributeMapping.ValidationFileMaximumSize;
                model.DefaultValue = productAttributeMapping.DefaultValue;
            }

            //prepare condition attributes model
            model.ConditionAllowed = true;
            await PrepareProductAttributeConditionModelAsync(model.ConditionModel, productAttributeMapping);

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.TextPrompt = await _localizationService.GetLocalizedAsync(productAttributeMapping, entity => entity.TextPrompt, languageId, false, false);
                locale.DefaultValue = await _localizationService.GetLocalizedAsync(productAttributeMapping, entity => entity.DefaultValue, languageId, false, false);
            };

            //prepare nested search model
            PrepareProductAttributeValueSearchModel(model.ProductAttributeValueSearchModel, productAttributeMapping);
        }

        model.ProductId = product.Id;

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        //prepare available product attributes
        model.AvailableProductAttributes = (await _productAttributeService.GetAllProductAttributesAsync()).Select(productAttribute => new SelectListItem
        {
            Text = productAttribute.Name,
            Value = productAttribute.Id.ToString()
        }).ToList();

        await _baseAdminModelFactory.PreparePreTranslationSupportModelAsync(model);

        return model;
    }

    /// <summary>
    /// Prepare paged product attribute value list model
    /// </summary>
    /// <param name="searchModel">Product attribute value search model</param>
    /// <param name="productAttributeMapping">Product attribute mapping</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product attribute value list model
    /// </returns>
    public virtual async Task<ProductAttributeValueListModel> PrepareProductAttributeValueListModelAsync(ProductAttributeValueSearchModel searchModel,
        ProductAttributeMapping productAttributeMapping)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(productAttributeMapping);

        //get product attribute values
        var productAttributeValues = (await _productAttributeService
            .GetProductAttributeValuesAsync(productAttributeMapping.Id)).ToPagedList(searchModel);

        //prepare list model
        var model = await new ProductAttributeValueListModel().PrepareToGridAsync(searchModel, productAttributeValues, () =>
        {
            return productAttributeValues.SelectAwait(async value =>
            {
                //fill in model values from the entity
                var productAttributeValueModel = value.ToModel<ProductAttributeValueModel>();

                //fill in additional values (not existing in the entity)
                productAttributeValueModel.AttributeValueTypeName = await _localizationService.GetLocalizedEnumAsync(value.AttributeValueType);

                productAttributeValueModel.Name = productAttributeMapping.AttributeControlType != AttributeControlType.ColorSquares
                    ? value.Name : $"{value.Name} - {value.ColorSquaresRgb}";
                if (value.AttributeValueType == AttributeValueType.Simple)
                {
                    productAttributeValueModel.PriceAdjustmentStr = value.PriceAdjustment.ToString("G29");
                    if (value.PriceAdjustmentUsePercentage)
                        productAttributeValueModel.PriceAdjustmentStr += " %";
                    productAttributeValueModel.WeightAdjustmentStr = value.WeightAdjustment.ToString("G29");
                }

                if (value.AttributeValueType == AttributeValueType.AssociatedToProduct)
                {
                    productAttributeValueModel.AssociatedProductName = (await _productService.GetProductByIdAsync(value.AssociatedProductId))?.Name ?? string.Empty;
                }

                var valuePicture = (await _productAttributeService.GetProductAttributeValuePicturesAsync(value.Id)).FirstOrDefault();
                var pictureThumbnailUrl = await _pictureService.GetPictureUrlAsync(valuePicture?.PictureId ?? 0, 75, false);

                //little hack here. Grid is rendered wrong way with <img> without "src" attribute
                if (string.IsNullOrEmpty(pictureThumbnailUrl))
                    pictureThumbnailUrl = await _pictureService.GetDefaultPictureUrlAsync(targetSize: 1);

                productAttributeValueModel.PictureThumbnailUrl = pictureThumbnailUrl;

                return productAttributeValueModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare product attribute value model
    /// </summary>
    /// <param name="model">Product attribute value model</param>
    /// <param name="productAttributeMapping">Product attribute mapping</param>
    /// <param name="productAttributeValue">Product attribute value</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product attribute value model
    /// </returns>
    public virtual async Task<ProductAttributeValueModel> PrepareProductAttributeValueModelAsync(ProductAttributeValueModel model,
        ProductAttributeMapping productAttributeMapping, ProductAttributeValue productAttributeValue, bool excludeProperties = false)
    {
        ArgumentNullException.ThrowIfNull(productAttributeMapping);

        Func<ProductAttributeValueLocalizedModel, int, Task> localizedModelConfiguration = null;

        if (productAttributeValue != null)
        {
            //fill in model values from the entity
            model ??= new ProductAttributeValueModel
            {
                ProductAttributeMappingId = productAttributeValue.ProductAttributeMappingId,
                AttributeValueTypeId = productAttributeValue.AttributeValueTypeId,
                AttributeValueTypeName = await _localizationService.GetLocalizedEnumAsync(productAttributeValue.AttributeValueType),
                AssociatedProductId = productAttributeValue.AssociatedProductId,
                Name = productAttributeValue.Name,
                ColorSquaresRgb = productAttributeValue.ColorSquaresRgb,
                DisplayColorSquaresRgb = productAttributeMapping.AttributeControlType == AttributeControlType.ColorSquares,
                ImageSquaresPictureId = productAttributeValue.ImageSquaresPictureId,
                DisplayImageSquaresPicture = productAttributeMapping.AttributeControlType == AttributeControlType.ImageSquares,
                PriceAdjustment = productAttributeValue.PriceAdjustment,
                PriceAdjustmentUsePercentage = productAttributeValue.PriceAdjustmentUsePercentage,
                WeightAdjustment = productAttributeValue.WeightAdjustment,
                Cost = productAttributeValue.Cost,
                CustomerEntersQty = productAttributeValue.CustomerEntersQty,
                Quantity = productAttributeValue.Quantity,
                IsPreSelected = productAttributeValue.IsPreSelected,
                DisplayOrder = productAttributeValue.DisplayOrder,
                PictureIds = (await _productAttributeService.GetProductAttributeValuePicturesAsync(productAttributeValue.Id))
                    .Select(c => c.PictureId).ToList(),
            };

            model.AssociatedProductName = (await _productService.GetProductByIdAsync(productAttributeValue.AssociatedProductId))?.Name;

            //define localized model configuration action
            localizedModelConfiguration = async (locale, languageId) =>
            {
                locale.Name = await _localizationService.GetLocalizedAsync(productAttributeValue, entity => entity.Name, languageId, false, false);
            };
        }

        model.ProductAttributeMappingId = productAttributeMapping.Id;
        model.DisplayColorSquaresRgb = productAttributeMapping.AttributeControlType == AttributeControlType.ColorSquares;
        model.DisplayImageSquaresPicture = productAttributeMapping.AttributeControlType == AttributeControlType.ImageSquares;

        //set default values for the new model
        if (productAttributeValue == null)
            model.Quantity = 1;

        //prepare localized models
        if (!excludeProperties)
            model.Locales = await _localizedModelFactory.PrepareLocalizedModelsAsync(localizedModelConfiguration);

        //prepare picture models
        var productPictures = await _productService.GetProductPicturesByProductIdAsync(productAttributeMapping.ProductId);
        model.ProductPictureModels = await productPictures.SelectAwait(async productPicture => new ProductPictureModel
        {
            Id = productPicture.Id,
            ProductId = productPicture.ProductId,
            PictureId = productPicture.PictureId,
            PictureUrl = await _pictureService.GetPictureUrlAsync(productPicture.PictureId),
            DisplayOrder = productPicture.DisplayOrder
        }).ToListAsync();

        return model;
    }

    /// <summary>
    /// Prepare product model to associate to the product attribute value
    /// </summary>
    /// <param name="searchModel">Product model to associate to the product attribute value</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product model to associate to the product attribute value
    /// </returns>
    public virtual async Task<AssociateProductToAttributeValueSearchModel> PrepareAssociateProductToAttributeValueSearchModelAsync(
        AssociateProductToAttributeValueSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        searchModel.IsLoggedInAsVendor = await _workContext.GetCurrentVendorAsync() != null;

        //prepare available categories
        await _baseAdminModelFactory.PrepareCategoriesAsync(searchModel.AvailableCategories);

        //prepare available manufacturers
        await _baseAdminModelFactory.PrepareManufacturersAsync(searchModel.AvailableManufacturers);

        //prepare available stores
        await _baseAdminModelFactory.PrepareStoresAsync(searchModel.AvailableStores);

        //prepare available vendors
        await _baseAdminModelFactory.PrepareVendorsAsync(searchModel.AvailableVendors);

        //prepare available product types
        await _baseAdminModelFactory.PrepareProductTypesAsync(searchModel.AvailableProductTypes);

        //prepare page parameters
        searchModel.SetPopupGridPageSize();

        return searchModel;
    }

    /// <summary>
    /// Prepare paged product model to associate to the product attribute value
    /// </summary>
    /// <param name="searchModel">Product model to associate to the product attribute value</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product model to associate to the product attribute value
    /// </returns>
    public virtual async Task<AssociateProductToAttributeValueListModel> PrepareAssociateProductToAttributeValueListModelAsync(
        AssociateProductToAttributeValueSearchModel searchModel)
    {
        ArgumentNullException.ThrowIfNull(searchModel);

        //a vendor should have access only to his products
        var currentVendor = await _workContext.GetCurrentVendorAsync();
        if (currentVendor != null)
            searchModel.SearchVendorId = currentVendor.Id;

        //get products
        var products = await _productService.SearchProductsAsync(showHidden: true,
            categoryIds: new List<int> { searchModel.SearchCategoryId },
            manufacturerIds: new List<int> { searchModel.SearchManufacturerId },
            storeId: searchModel.SearchStoreId,
            vendorId: searchModel.SearchVendorId,
            productType: searchModel.SearchProductTypeId > 0 ? (ProductType?)searchModel.SearchProductTypeId : null,
            keywords: searchModel.SearchProductName,
            pageIndex: searchModel.Page - 1, pageSize: searchModel.PageSize);

        //prepare grid model
        var model = await new AssociateProductToAttributeValueListModel().PrepareToGridAsync(searchModel, products, () =>
        {
            //fill in model values from the entity
            return products.SelectAwait(async product =>
            {
                var productModel = product.ToModel<ProductModel>();

                productModel.SeName = await _urlRecordService.GetSeNameAsync(product, 0, true, false);

                return productModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare paged product attribute combination list model
    /// </summary>
    /// <param name="searchModel">Product attribute combination search model</param>
    /// <param name="product">Product</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product attribute combination list model
    /// </returns>
    public virtual async Task<ProductAttributeCombinationListModel> PrepareProductAttributeCombinationListModelAsync(
        ProductAttributeCombinationSearchModel searchModel, Product product)
    {
        ArgumentNullException.ThrowIfNull(searchModel);
        ArgumentNullException.ThrowIfNull(product);

        //get product attribute combinations
        var productAttributeCombinations = (await _productAttributeService
            .GetAllProductAttributeCombinationsAsync(product.Id)).ToPagedList(searchModel);

        var currentCustomer = await _workContext.GetCurrentCustomerAsync();
        var currentStore = await _storeContext.GetCurrentStoreAsync();

        //prepare grid model
        var model = await new ProductAttributeCombinationListModel().PrepareToGridAsync(searchModel, productAttributeCombinations, () =>
        {
            return productAttributeCombinations.SelectAwait(async combination =>
            {
                //fill in model values from the entity
                var productAttributeCombinationModel = combination.ToModel<ProductAttributeCombinationModel>();

                //fill in additional values (not existing in the entity)
                productAttributeCombinationModel.AttributesXml = await _productAttributeFormatter
                    .FormatAttributesAsync(product, combination.AttributesXml, currentCustomer, currentStore, "<br />", true, true, true, false);

                var combinationPicture = (await _productAttributeService.GetProductAttributeCombinationPicturesAsync(combination.Id)).FirstOrDefault();
                var pictureThumbnailUrl = await _pictureService.GetPictureUrlAsync(combinationPicture?.PictureId ?? 0, 75, false);

                //little hack here. Grid is rendered wrong way with <img> without "src" attribute
                if (string.IsNullOrEmpty(pictureThumbnailUrl))
                    pictureThumbnailUrl = await _pictureService.GetDefaultPictureUrlAsync(targetSize: 1);

                productAttributeCombinationModel.PictureThumbnailUrl = pictureThumbnailUrl;
                var warnings = (await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(currentCustomer,
                        ShoppingCartType.ShoppingCart, product,
                        attributesXml: combination.AttributesXml,
                        ignoreNonCombinableAttributes: true)
                    ).Aggregate(string.Empty, (message, warning) => $"{message}{warning}<br />");
                productAttributeCombinationModel.Warnings = new List<string> { warnings };

                return productAttributeCombinationModel;
            });
        });

        return model;
    }

    /// <summary>
    /// Prepare product attribute combination model
    /// </summary>
    /// <param name="model">Product attribute combination model</param>
    /// <param name="product">Product</param>
    /// <param name="productAttributeCombination">Product attribute combination</param>
    /// <param name="excludeProperties">Whether to exclude populating of some properties of model</param>
    /// <returns>
    /// A task that represents the asynchronous operation
    /// The task result contains the product attribute combination model
    /// </returns>
    public virtual async Task<ProductAttributeCombinationModel> PrepareProductAttributeCombinationModelAsync(ProductAttributeCombinationModel model,
        Product product, ProductAttributeCombination productAttributeCombination, bool excludeProperties = false)
    {
        ArgumentNullException.ThrowIfNull(product);

        if (productAttributeCombination != null)
        {
            //fill in model values from the entity
            model ??= new ProductAttributeCombinationModel
            {
                AllowOutOfStockOrders = productAttributeCombination.AllowOutOfStockOrders,
                AttributesXml = productAttributeCombination.AttributesXml,
                Gtin = productAttributeCombination.Gtin,
                Id = productAttributeCombination.Id,
                ManufacturerPartNumber = productAttributeCombination.ManufacturerPartNumber,
                NotifyAdminForQuantityBelow = productAttributeCombination.NotifyAdminForQuantityBelow,
                OverriddenPrice = productAttributeCombination.OverriddenPrice,
                PictureIds = (await _productAttributeService.GetProductAttributeCombinationPicturesAsync(productAttributeCombination.Id))
                    .Select(c => c.PictureId).ToList(),
                ProductId = productAttributeCombination.ProductId,
                Sku = productAttributeCombination.Sku,
                StockQuantity = productAttributeCombination.StockQuantity,
                MinStockQuantity = productAttributeCombination.MinStockQuantity
            };
        }

        model.ProductId = product.Id;

        //set default values for the new model
        if (productAttributeCombination == null)
        {
            model.ProductId = product.Id;
            model.StockQuantity = 10000;
            model.NotifyAdminForQuantityBelow = 1;
        }

        //prepare picture models
        var productPictures = await _productService.GetProductPicturesByProductIdAsync(product.Id);
        model.ProductPictureModels = await productPictures.SelectAwait(async productPicture => new ProductPictureModel
        {
            Id = productPicture.Id,
            ProductId = productPicture.ProductId,
            PictureId = productPicture.PictureId,
            PictureUrl = await _pictureService.GetPictureUrlAsync(productPicture.PictureId),
            DisplayOrder = productPicture.DisplayOrder
        }).ToListAsync();

        //prepare product attribute mappings (exclude non-combinable attributes)
        var attributes = (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id))
            .Where(productAttributeMapping => !productAttributeMapping.IsNonCombinable()).ToList();

        foreach (var attribute in attributes)
        {
            var attributeModel = new ProductAttributeCombinationModel.ProductAttributeModel
            {
                Id = attribute.Id,
                ProductAttributeId = attribute.ProductAttributeId,
                Name = (await _productAttributeService.GetProductAttributeByIdAsync(attribute.ProductAttributeId)).Name,
                TextPrompt = attribute.TextPrompt,
                IsRequired = attribute.IsRequired,
                AttributeControlType = attribute.AttributeControlType
            };

            if (attribute.ShouldHaveValues())
            {
                //values
                var attributeValues = await _productAttributeService.GetProductAttributeValuesAsync(attribute.Id);
                var preSelectedValue = _productAttributeParser.ParseValues(model.AttributesXml, attribute.Id);
                foreach (var attributeValue in attributeValues)
                {
                    var attributeValueModel = new ProductAttributeCombinationModel.ProductAttributeValueModel
                    {
                        Id = attributeValue.Id,
                        Name = attributeValue.Name,
                        IsPreSelected = preSelectedValue.Contains(attributeValue.Id.ToString())
                    };
                    attributeModel.Values.Add(attributeValueModel);
                }
            }

            model.ProductAttributes.Add(attributeModel);
        }

        return model;
    }

    #endregion
}