/// <summary>
        /// Gets allowed discounts applied to manufacturers
        /// </summary>
        /// <param name="product">Product</param>
        /// <param name="customer">Customer</param>
        /// <returns>Discounts</returns>
        protected virtual IList<Discount> GetAllowedDiscountsAppliedToManufacturers(Product product, Customer customer)
        {
            var allowedDiscounts = new List<Discount>();
            if (_catalogSettings.IgnoreDiscounts)
                return allowedDiscounts;

            foreach (var discount in _discountService.GetAllDiscounts(DiscountType.AssignedToManufacturers))
            {
                //load identifier of categories with this discount applied to
                var cacheKey = string.Format(PriceCacheEventConsumer.DISCOUNT_MANUFACTURER_IDS_MODEL_KEY,
                    discount.Id,
                    string.Join(",", customer.GetCustomerRoleIds()),
                    _storeContext.CurrentStore.Id);
                var appliedToManufacturerIds = _cacheManager.Get(cacheKey,
                    () => discount.AppliedToManufacturers.Select(x => x.Id).ToList());

                //compare with manufacturers of this product
                if (appliedToManufacturerIds.Any())
                {
                    //load identifier of categories with this discount applied to
                    var cacheKey2 = string.Format(PriceCacheEventConsumer.DISCOUNT_PRODUCT_MANUFACTURER_IDS_MODEL_KEY,
                        product.Id,
                        string.Join(",", customer.GetCustomerRoleIds()),
                        _storeContext.CurrentStore.Id);
                    var manufacturerIds = _cacheManager.Get(cacheKey2, () =>
                        _manufacturerService
                        .GetProductManufacturersByProductId(product.Id)
                        .Select(x => x.ManufacturerId)
                        .ToList());
                    foreach (var id in manufacturerIds)
                    {
                        if (appliedToManufacturerIds.Contains(id))
                        {
                            if (_discountService.ValidateDiscount(discount, customer).IsValid &&
                                discount.DiscountType == DiscountType.AssignedToManufacturers &&
                                !allowedDiscounts.ContainsDiscount(discount))
                                allowedDiscounts.Add(discount);
                        }
                    }
                }
            }

            return allowedDiscounts;
        }
        /// <summary>
        /// Gets the final price
        /// </summary>
        /// <param name="product">Product</param>
        /// <param name="customer">The customer</param>
        /// <param name="overriddenProductPrice">Overridden product price. If specified, then it'll be used instead of a product price. For example, used with product attribute combinations</param>
        /// <param name="additionalCharge">Additional charge</param>
        /// <param name="includeDiscounts">A value indicating whether include discounts or not for final price computation</param>
        /// <param name="quantity">Shopping cart item quantity</param>
        /// <param name="rentalStartDate">Rental period start date (for rental products)</param>
        /// <param name="rentalEndDate">Rental period end date (for rental products)</param>
        /// <param name="discountAmount">Applied discount amount</param>
        /// <param name="appliedDiscounts">Applied discounts</param>
        /// <returns>Final price</returns>
        public virtual decimal GetFinalPrice(Product product, 
            Customer customer,
            decimal? overriddenProductPrice,
            decimal additionalCharge, 
            bool includeDiscounts,
            int quantity,
            DateTime? rentalStartDate,
            DateTime? rentalEndDate,
            out decimal discountAmount,
            out List<Discount> appliedDiscounts)
        {
            if (product == null)
                throw new ArgumentNullException("product");

            discountAmount = decimal.Zero;
            appliedDiscounts = new List<Discount>();

            var cacheKey = string.Format(PriceCacheEventConsumer.PRODUCT_PRICE_MODEL_KEY,
                product.Id,
                overriddenProductPrice.HasValue ? overriddenProductPrice.Value.ToString(CultureInfo.InvariantCulture) : null,
                additionalCharge.ToString(CultureInfo.InvariantCulture),
                includeDiscounts, 
                quantity,
                string.Join(",", customer.GetCustomerRoleIds()),
                _storeContext.CurrentStore.Id);
             var cacheTime = _catalogSettings.CacheProductPrices ? 60 : 0;
            //we do not cache price for rental products
            //otherwise, it can cause memory leaks (to store all possible date period combinations)
            if (product.IsRental)
                cacheTime = 0;
            var cachedPrice = _cacheManager.Get(cacheKey, cacheTime, () =>
            {
                var result = new ProductPriceForCaching();

                //initial price
                decimal price = overriddenProductPrice.HasValue ? overriddenProductPrice.Value : product.Price;

                //special price
                var specialPrice = product.GetSpecialPrice();
                if (specialPrice.HasValue)
                    price = specialPrice.Value;

                //tier prices
                if (product.HasTierPrices)
                {
                    decimal? tierPrice = GetMinimumTierPrice(product, customer, quantity);
                    if (tierPrice.HasValue)
                        price = Math.Min(price, tierPrice.Value);
                }

                //additional charge
                price = price + additionalCharge;

                //rental products
                if (product.IsRental)
                    if (rentalStartDate.HasValue && rentalEndDate.HasValue)
                        price = price * product.GetRentalPeriods(rentalStartDate.Value, rentalEndDate.Value);

                if (includeDiscounts)
                {
                    //discount
                    List<Discount> tmpAppliedDiscounts;
                    decimal tmpDiscountAmount = GetDiscountAmount(product, customer, price, out tmpAppliedDiscounts);
                    price = price - tmpDiscountAmount;

                    if (tmpAppliedDiscounts != null)
                    {
                        result.AppliedDiscountIds = tmpAppliedDiscounts.Select(x=>x.Id).ToList();
                        result.AppliedDiscountAmount = tmpDiscountAmount;
                    }
                }

                if (price < decimal.Zero)
                    price = decimal.Zero;

                result.Price = price;
                return result;
            });

            if (includeDiscounts)
            {
                //Discount instance cannnot be cached between requests (when "catalogSettings.CacheProductPrices" is "true)
                //This is limitation of Entity Framework
                //That's why we load it here after working with cache
                foreach (var appliedDiscountId in cachedPrice.AppliedDiscountIds)
                {
                    var appliedDiscount = _discountService.GetDiscountById(appliedDiscountId);
                    if (appliedDiscount != null)
                        appliedDiscounts.Add(appliedDiscount);
                }
                if (appliedDiscounts.Any())
                {
                    discountAmount = cachedPrice.AppliedDiscountAmount;
                }
            }

            return cachedPrice.Price;
        }
        /// <summary>
        /// Gets allowed discounts applied to categories
        /// </summary>
        /// <param name="product">Product</param>
        /// <param name="customer">Customer</param>
        /// <returns>Discounts</returns>
        protected virtual IList<Discount> GetAllowedDiscountsAppliedToCategories(Product product, Customer customer)
        {
            var allowedDiscounts = new List<Discount>();
            if (_catalogSettings.IgnoreDiscounts)
                return allowedDiscounts;

            foreach (var discount in _discountService.GetAllDiscounts(DiscountType.AssignedToCategories))
            {
                //load identifier of categories with this discount applied to
                var cacheKey = string.Format(PriceCacheEventConsumer.DISCOUNT_CATEGORY_IDS_MODEL_KEY,
                    discount.Id,
                    string.Join(",", customer.GetCustomerRoleIds()),
                    _storeContext.CurrentStore.Id);
                var appliedToCategoryIds = _cacheManager.Get(cacheKey, () =>
                {
                    var categoryIds = new List<int>();
                    foreach (var category in discount.AppliedToCategories)
                    {
                        if (!categoryIds.Contains(category.Id))
                            categoryIds.Add(category.Id);
                        if (discount.AppliedToSubCategories)
                        {
                            //include subcategories
                            foreach (var childCategoryId in _categoryService
                                .GetAllCategoriesByParentCategoryId(category.Id, false, true)
                                .Select(x => x.Id))
                            {
                                if (!categoryIds.Contains(childCategoryId))
                                    categoryIds.Add(childCategoryId);
                            }
                        }
                    }
                    return categoryIds;
                });

                //compare with categories of this product
                if (appliedToCategoryIds.Any())
                {
                    //load identifier of categories with this discount applied to
                    var cacheKey2 = string.Format(PriceCacheEventConsumer.DISCOUNT_PRODUCT_CATEGORY_IDS_MODEL_KEY,
                        product.Id,
                        string.Join(",", customer.GetCustomerRoleIds()),
                        _storeContext.CurrentStore.Id);
                    var categoryIds = _cacheManager.Get(cacheKey2, () =>
                        _categoryService
                        .GetProductCategoriesByProductId(product.Id)
                        .Select(x => x.CategoryId)
                        .ToList());
                    foreach (var id in categoryIds)
                    {
                        if (appliedToCategoryIds.Contains(id))
                        {
                            if (_discountService.ValidateDiscount(discount, customer).IsValid &&
                                discount.DiscountType == DiscountType.AssignedToCategories &&
                                !allowedDiscounts.ContainsDiscount(discount))
                                allowedDiscounts.Add(discount);
                        }
                    }
                }
            }

            return allowedDiscounts;
        }