Beispiel #1
0
        public virtual async Task <ShoppingCartSubTotal> GetShoppingCartSubTotalAsync(IList <OrganizedShoppingCartItem> cart, bool?includeTax = null)
        {
            Guard.NotNull(cart, nameof(cart));

            var includingTax = includeTax ?? (_workContext.TaxDisplayType == TaxDisplayType.IncludingTax);
            var result       = new ShoppingCartSubTotal();

            if (!(cart?.Any() ?? false))
            {
                return(result);
            }

            var customer = cart.GetCustomer();
            var subTotalExclTaxWithoutDiscount = new Money(_primaryCurrency);
            var subTotalInclTaxWithoutDiscount = new Money(_primaryCurrency);

            foreach (var cartItem in cart)
            {
                if (cartItem.Item.Product == null)
                {
                    continue;
                }

                var     item = cartItem.Item;
                decimal taxRate = decimal.Zero;
                Money   itemExclTax, itemInclTax;

                await _productAttributeMaterializer.MergeWithCombinationAsync(item.Product, item.AttributeSelection);

                if (_primaryCurrency.RoundOrderItemsEnabled)
                {
                    // Gross > Net RoundFix.
                    var unitPrice = await _priceCalculationService.GetUnitPriceAsync(cartItem, true);

                    // Adaption to eliminate rounding issues.
                    (itemExclTax, taxRate) = await _taxService.GetProductPriceAsync(item.Product, unitPrice, false, customer : customer);

                    itemExclTax            = _primaryCurrency.AsMoney(itemExclTax.Amount * item.Quantity);
                    (itemInclTax, taxRate) = await _taxService.GetProductPriceAsync(item.Product, unitPrice, true, customer : customer);

                    itemInclTax = _primaryCurrency.AsMoney(itemInclTax.Amount * item.Quantity);
                }
                else
                {
                    var itemSubTotal = await _priceCalculationService.GetSubTotalAsync(cartItem, true);

                    (itemExclTax, taxRate) = await _taxService.GetProductPriceAsync(item.Product, itemSubTotal, false, customer : customer);

                    (itemInclTax, taxRate) = await _taxService.GetProductPriceAsync(item.Product, itemSubTotal, true, customer : customer);
                }

                subTotalExclTaxWithoutDiscount += itemExclTax;
                subTotalInclTaxWithoutDiscount += itemInclTax;

                result.TaxRates.Add(taxRate, itemInclTax.Amount - itemExclTax.Amount);
            }

            // Checkout attributes.
            if (customer != null)
            {
                var values = await _checkoutAttributeMaterializer.MaterializeCheckoutAttributeValuesAsync(customer.GenericAttributes.CheckoutAttributes);

                foreach (var value in values)
                {
                    var(attributePriceExclTax, _) = await _taxService.GetCheckoutAttributePriceAsync(value, false, customer);

                    var(attributePriceInclTax, taxRate) = await _taxService.GetCheckoutAttributePriceAsync(value, true, customer);

                    subTotalExclTaxWithoutDiscount += attributePriceExclTax.Amount;
                    subTotalInclTaxWithoutDiscount += attributePriceInclTax.Amount;

                    result.TaxRates.Add(taxRate, attributePriceInclTax.Amount - attributePriceExclTax.Amount);
                }
            }


            // Subtotal without discount.
            result.SubTotalWithoutDiscount = _primaryCurrency.AsMoney(includingTax ? subTotalInclTaxWithoutDiscount.Amount : subTotalExclTaxWithoutDiscount.Amount, true, true);

            // We calculate discount amount on order subtotal excl tax (discount first).
            var(discountAmountExclTax, appliedDiscount) = await this.GetOrderSubtotalDiscountAsync(subTotalExclTaxWithoutDiscount, customer);

            result.AppliedDiscount = appliedDiscount;

            if (subTotalExclTaxWithoutDiscount < discountAmountExclTax)
            {
                discountAmountExclTax = subTotalExclTaxWithoutDiscount;
            }

            var discountAmountInclTax = discountAmountExclTax;

            // Subtotal with discount (excl tax).
            var subTotalExclTaxWithDiscount = subTotalExclTaxWithoutDiscount - discountAmountExclTax;
            var subTotalInclTaxWithDiscount = subTotalExclTaxWithDiscount;

            // Add tax for shopping items & checkout attributes.
            var tempTaxRates = new Dictionary <decimal, decimal>(result.TaxRates);

            foreach (var kvp in tempTaxRates)
            {
                var taxRate   = kvp.Key;
                var taxAmount = kvp.Value;

                if (taxAmount != decimal.Zero)
                {
                    // Discount the tax amount that applies to subtotal items.
                    if (subTotalExclTaxWithoutDiscount > decimal.Zero)
                    {
                        var discountTax = result.TaxRates[taxRate] * (discountAmountExclTax.Amount / subTotalExclTaxWithoutDiscount.Amount);
                        discountAmountInclTax   += discountTax;
                        taxAmount                = _primaryCurrency.RoundIfEnabledFor(result.TaxRates[taxRate] - discountTax);
                        result.TaxRates[taxRate] = taxAmount;
                    }

                    // Subtotal with discount (incl tax).
                    subTotalInclTaxWithDiscount += taxAmount;
                }
            }

            discountAmountInclTax = _primaryCurrency.AsMoney(discountAmountInclTax.Amount);

            result.DiscountAmount       = _primaryCurrency.AsMoney(includingTax ? discountAmountInclTax.Amount : discountAmountExclTax.Amount, false);
            result.SubTotalWithDiscount = _primaryCurrency.AsMoney(includingTax ? subTotalInclTaxWithDiscount.Amount : subTotalExclTaxWithDiscount.Amount, true, true);

            return(result);
        }
        /// <summary>
        /// Add order items to the request query parameters
        /// </summary>
        /// <param name="parameters">Query parameters</param>
        /// <param name="postProcessPaymentRequest">Payment info required for an order processing</param>
        private async Task AddItemsParametersAsync(IDictionary <string, string> parameters, PostProcessPaymentRequest postProcessPaymentRequest)
        {
            //upload order items
            parameters.Add("cmd", "_cart");
            parameters.Add("upload", "1");

            var cartTotal        = decimal.Zero;
            var roundedCartTotal = decimal.Zero;
            var itemCount        = 1;

            //add shopping cart items
            foreach (var item in await _orderService.GetOrderItemsAsync(postProcessPaymentRequest.Order.Id))
            {
                var roundedItemPrice = Math.Round(item.UnitPriceExclTax, 2);

                var product = await _productService.GetProductByIdAsync(item.ProductId);

                //add query parameters
                parameters.Add($"item_name_{itemCount}", product.Name);
                parameters.Add($"amount_{itemCount}", roundedItemPrice.ToString("0.00", CultureInfo.InvariantCulture));
                parameters.Add($"quantity_{itemCount}", item.Quantity.ToString());

                cartTotal        += item.PriceExclTax;
                roundedCartTotal += roundedItemPrice * item.Quantity;
                itemCount++;
            }

            //add checkout attributes as order items
            var checkoutAttributeValues = _checkoutAttributeParser.ParseCheckoutAttributeValues(postProcessPaymentRequest.Order.CheckoutAttributesXml);
            var customer = await _customerService.GetCustomerByIdAsync(postProcessPaymentRequest.Order.CustomerId);

            await foreach (var(attribute, values) in checkoutAttributeValues)
            {
                await foreach (var attributeValue in values)
                {
                    var(attributePrice, _) = await _taxService.GetCheckoutAttributePriceAsync(attribute, attributeValue, false, customer);

                    var roundedAttributePrice = Math.Round(attributePrice, 2);

                    //add query parameters
                    if (attribute == null)
                    {
                        continue;
                    }

                    parameters.Add($"item_name_{itemCount}", attribute.Name);
                    parameters.Add($"amount_{itemCount}", roundedAttributePrice.ToString("0.00", CultureInfo.InvariantCulture));
                    parameters.Add($"quantity_{itemCount}", "1");

                    cartTotal        += attributePrice;
                    roundedCartTotal += roundedAttributePrice;
                    itemCount++;
                }
            }

            //add shipping fee as a separate order item, if it has price
            var roundedShippingPrice = Math.Round(postProcessPaymentRequest.Order.OrderShippingExclTax, 2);

            if (roundedShippingPrice > decimal.Zero)
            {
                parameters.Add($"item_name_{itemCount}", "Shipping fee");
                parameters.Add($"amount_{itemCount}", roundedShippingPrice.ToString("0.00", CultureInfo.InvariantCulture));
                parameters.Add($"quantity_{itemCount}", "1");

                cartTotal        += postProcessPaymentRequest.Order.OrderShippingExclTax;
                roundedCartTotal += roundedShippingPrice;
                itemCount++;
            }

            //add payment method additional fee as a separate order item, if it has price
            var roundedPaymentMethodPrice = Math.Round(postProcessPaymentRequest.Order.PaymentMethodAdditionalFeeExclTax, 2);

            if (roundedPaymentMethodPrice > decimal.Zero)
            {
                parameters.Add($"item_name_{itemCount}", "Payment method fee");
                parameters.Add($"amount_{itemCount}", roundedPaymentMethodPrice.ToString("0.00", CultureInfo.InvariantCulture));
                parameters.Add($"quantity_{itemCount}", "1");

                cartTotal        += postProcessPaymentRequest.Order.PaymentMethodAdditionalFeeExclTax;
                roundedCartTotal += roundedPaymentMethodPrice;
                itemCount++;
            }

            //add tax as a separate order item, if it has positive amount
            var roundedTaxAmount = Math.Round(postProcessPaymentRequest.Order.OrderTax, 2);

            if (roundedTaxAmount > decimal.Zero)
            {
                parameters.Add($"item_name_{itemCount}", "Tax amount");
                parameters.Add($"amount_{itemCount}", roundedTaxAmount.ToString("0.00", CultureInfo.InvariantCulture));
                parameters.Add($"quantity_{itemCount}", "1");

                cartTotal        += postProcessPaymentRequest.Order.OrderTax;
                roundedCartTotal += roundedTaxAmount;
            }

            if (cartTotal > postProcessPaymentRequest.Order.OrderTotal)
            {
                //get the difference between what the order total is and what it should be and use that as the "discount"
                var discountTotal = Math.Round(cartTotal - postProcessPaymentRequest.Order.OrderTotal, 2);
                roundedCartTotal -= discountTotal;

                //gift card or rewarded point amount applied to cart in nopCommerce - shows in PayPal as "discount"
                parameters.Add("discount_amount_cart", discountTotal.ToString("0.00", CultureInfo.InvariantCulture));
            }

            //save order total that actually sent to PayPal (used for PDT order total validation)
            await _genericAttributeService.SaveAttributeAsync(postProcessPaymentRequest.Order, PayPalHelper.OrderTotalSentToPayPal, roundedCartTotal);
        }
        /// <summary>
        /// Formats attributes
        /// </summary>
        /// <param name="attributesXml">Attributes in XML format</param>
        /// <param name="customer">Customer</param>
        /// <param name="separator">Separator</param>
        /// <param name="htmlEncode">A value indicating whether to encode (HTML) values</param>
        /// <param name="renderPrices">A value indicating whether to render prices</param>
        /// <param name="allowHyperlinks">A value indicating whether to HTML hyperlink tags could be rendered (if required)</param>
        /// <returns>
        /// A task that represents the asynchronous operation
        /// The task result contains the attributes
        /// </returns>
        public virtual async Task <string> FormatAttributesAsync(string attributesXml,
                                                                 Customer customer,
                                                                 string separator     = "<br />",
                                                                 bool htmlEncode      = true,
                                                                 bool renderPrices    = true,
                                                                 bool allowHyperlinks = true)
        {
            var result = new StringBuilder();

            var attributes = await _checkoutAttributeParser.ParseCheckoutAttributesAsync(attributesXml);

            for (var i = 0; i < attributes.Count; i++)
            {
                var attribute = attributes[i];
                var valuesStr = _checkoutAttributeParser.ParseValues(attributesXml, attribute.Id);
                for (var j = 0; j < valuesStr.Count; j++)
                {
                    var valueStr           = valuesStr[j];
                    var formattedAttribute = string.Empty;
                    if (!attribute.ShouldHaveValues())
                    {
                        //no values
                        if (attribute.AttributeControlType == AttributeControlType.MultilineTextbox)
                        {
                            //multiline textbox
                            var attributeName = await _localizationService.GetLocalizedAsync(attribute, a => a.Name, (await _workContext.GetWorkingLanguageAsync()).Id);

                            //encode (if required)
                            if (htmlEncode)
                            {
                                attributeName = WebUtility.HtmlEncode(attributeName);
                            }
                            formattedAttribute = $"{attributeName}: {HtmlHelper.FormatText(valueStr, false, true, false, false, false, false)}";
                            //we never encode multiline textbox input
                        }
                        else if (attribute.AttributeControlType == AttributeControlType.FileUpload)
                        {
                            //file upload
                            Guid.TryParse(valueStr, out var downloadGuid);
                            var download = await _downloadService.GetDownloadByGuidAsync(downloadGuid);

                            if (download != null)
                            {
                                string attributeText;
                                var    fileName = $"{download.Filename ?? download.DownloadGuid.ToString()}{download.Extension}";
                                //encode (if required)
                                if (htmlEncode)
                                {
                                    fileName = WebUtility.HtmlEncode(fileName);
                                }
                                if (allowHyperlinks)
                                {
                                    //hyperlinks are allowed
                                    var downloadLink = $"{_webHelper.GetStoreLocation(false)}download/getfileupload/?downloadId={download.DownloadGuid}";
                                    attributeText = $"<a href=\"{downloadLink}\" class=\"fileuploadattribute\">{fileName}</a>";
                                }
                                else
                                {
                                    //hyperlinks aren't allowed
                                    attributeText = fileName;
                                }

                                var attributeName = await _localizationService.GetLocalizedAsync(attribute, a => a.Name, (await _workContext.GetWorkingLanguageAsync()).Id);

                                //encode (if required)
                                if (htmlEncode)
                                {
                                    attributeName = WebUtility.HtmlEncode(attributeName);
                                }
                                formattedAttribute = $"{attributeName}: {attributeText}";
                            }
                        }
                        else
                        {
                            //other attributes (textbox, datepicker)
                            formattedAttribute = $"{await _localizationService.GetLocalizedAsync(attribute, a => a.Name, (await _workContext.GetWorkingLanguageAsync()).Id)}: {valueStr}";
                            //encode (if required)
                            if (htmlEncode)
                            {
                                formattedAttribute = WebUtility.HtmlEncode(formattedAttribute);
                            }
                        }
                    }
                    else
                    {
                        if (int.TryParse(valueStr, out var attributeValueId))
                        {
                            var attributeValue = await _checkoutAttributeService.GetCheckoutAttributeValueByIdAsync(attributeValueId);

                            if (attributeValue != null)
                            {
                                formattedAttribute = $"{await _localizationService.GetLocalizedAsync(attribute, a => a.Name, (await _workContext.GetWorkingLanguageAsync()).Id)}: {await _localizationService.GetLocalizedAsync(attributeValue, a => a.Name, (await _workContext.GetWorkingLanguageAsync()).Id)}";
                                if (renderPrices)
                                {
                                    var priceAdjustmentBase = (await _taxService.GetCheckoutAttributePriceAsync(attribute, attributeValue, customer)).price;
                                    var priceAdjustment     = await _currencyService.ConvertFromPrimaryStoreCurrencyAsync(priceAdjustmentBase, await _workContext.GetWorkingCurrencyAsync());

                                    if (priceAdjustmentBase > 0)
                                    {
                                        formattedAttribute += string.Format(
                                            await _localizationService.GetResourceAsync("FormattedAttributes.PriceAdjustment"),
                                            "+", await _priceFormatter.FormatPriceAsync(priceAdjustment), string.Empty);
                                    }
                                }
                            }

                            //encode (if required)
                            if (htmlEncode)
                            {
                                formattedAttribute = WebUtility.HtmlEncode(formattedAttribute);
                            }
                        }
                    }

                    if (string.IsNullOrEmpty(formattedAttribute))
                    {
                        continue;
                    }

                    if (i != 0 || j != 0)
                    {
                        result.Append(separator);
                    }
                    result.Append(formattedAttribute);
                }
            }

            return(result.ToString());
        }
Beispiel #4
0
        public async Task <string> FormatAttributesAsync(
            CheckoutAttributeSelection selection,
            Customer customer    = null,
            string serapator     = "<br />",
            bool htmlEncode      = true,
            bool renderPrices    = true,
            bool allowHyperlinks = true)
        {
            Guard.NotNull(selection, nameof(selection));

            customer ??= _workContext.CurrentCustomer;

            using var psb = StringBuilderPool.Instance.Get(out var sb);
            var attributesList = await _checkoutAttributeMaterializer.MaterializeCheckoutAttributesAsync(selection);

            if (attributesList.IsNullOrEmpty())
            {
                return(null);
            }

            var attributeValues = attributesList
                                  .Where(x => x.IsListTypeAttribute)
                                  .SelectMany(x => x.CheckoutAttributeValues);

            var language = _workContext.WorkingLanguage;

            for (var i = 0; i < attributesList.Count; i++)
            {
                var currentAttribute       = attributesList[i];
                var currentAttributeValues = selection.GetAttributeValues(currentAttribute.Id).ToList();

                for (var j = 0; j < currentAttributeValues.Count; j++)
                {
                    var currentValue = currentAttributeValues[j].ToString();
                    var attributeStr = string.Empty;
                    if (!currentAttribute.IsListTypeAttribute)
                    {
                        if (currentAttribute.AttributeControlType is AttributeControlType.MultilineTextbox)
                        {
                            // Multiline textbox input gets never encoded
                            var attributeName = currentAttribute.GetLocalized(a => a.Name, language).ToString();
                            if (htmlEncode)
                            {
                                attributeName = HttpUtility.HtmlEncode(attributeName);
                            }

                            attributeStr = string.Format(
                                "{0}: {1}",
                                attributeName,
                                HtmlUtils.ConvertPlainTextToHtml(currentValue.EmptyNull().Replace(":", "").HtmlEncode()));
                        }
                        else if (currentAttribute.AttributeControlType is AttributeControlType.FileUpload)
                        {
                            Guid.TryParse(currentValue, out var downloadGuid);

                            var download = await _db.Downloads
                                           .Include(x => x.MediaFile)
                                           .Where(x => x.DownloadGuid == downloadGuid)
                                           .FirstOrDefaultAsync();

                            if (download?.MediaFile != null)
                            {
                                // TODO: (ms) (core) add a method for getting URL (use routing because it handles all SEO friendly URLs) ?
                                //var genratedUrl = _mediaService.GenerateFileDownloadUrl(download.MediaFileId, 0);
                                var attributeText = string.Empty;
                                var fileName      = download.MediaFile.Name;
                                if (htmlEncode)
                                {
                                    fileName = HttpUtility.HtmlEncode(fileName);
                                }

                                if (allowHyperlinks)
                                {
                                    var downloadLink = string.Format(
                                        "{0}download/getfileupload/?downloadId={1}",
                                        _webHelper.GetStoreLocation(false),
                                        download.DownloadGuid);

                                    attributeText = string.Format(
                                        "<a href=\"{0}\" class=\"fileuploadattribute\">{1}</a>",
                                        downloadLink,
                                        fileName);
                                }
                                else
                                {
                                    attributeText = fileName;
                                }

                                var attributeName = currentAttribute.GetLocalized(a => a.Name, language).ToString();
                                if (htmlEncode)
                                {
                                    attributeName = HttpUtility.HtmlEncode(attributeName);
                                }

                                attributeStr = string.Format("{0}: {1}", attributeName, attributeText);
                            }
                        }
                        else
                        {
                            // Other attributes (textbox, datepicker...)
                            attributeStr = string.Format(
                                "{0}: {1}",
                                currentAttribute.GetLocalized(a => a.Name, language),
                                currentValue);

                            if (htmlEncode)
                            {
                                attributeStr = HttpUtility.HtmlEncode(attributeStr);
                            }
                        }
                    }
                    else
                    {
                        if (int.TryParse(currentValue, out var id))
                        {
                            var attributeValue = attributeValues.Where(x => x.Id == id).FirstOrDefault();
                            if (attributeValue != null)
                            {
                                attributeStr = string.Format(
                                    "{0}: {1}",
                                    currentAttribute.GetLocalized(x => x.Name, language),
                                    attributeValue.GetLocalized(x => x.Name, language));

                                if (renderPrices)
                                {
                                    var(priceAdjustment, _) = await _taxService.GetCheckoutAttributePriceAsync(attributeValue, customer : customer);

                                    var priceAdjustmentConverted = _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustment.Amount, _workContext.WorkingCurrency);
                                    if (priceAdjustment > 0)
                                    {
                                        attributeStr += string.Format(" [+{0}]", _currencyService.CreateMoney(priceAdjustmentConverted).ToString());
                                    }
                                }
                            }

                            if (htmlEncode)
                            {
                                attributeStr = HttpUtility.HtmlEncode(attributeStr);
                            }
                        }
                    }

                    if (attributeStr.HasValue())
                    {
                        if (i != 0 || j != 0)
                        {
                            sb.Append(serapator);
                        }

                        sb.Append(attributeStr);
                    }
                }
            }

            return(sb.ToString());
        }
        public override async Task <(decimal discountAmount, List <Discount> appliedDiscounts, decimal subTotalWithoutDiscount, decimal subTotalWithDiscount, SortedDictionary <decimal, decimal> taxRates)> GetShoppingCartSubTotalAsync(
            IList <ShoppingCartItem> cart,
            bool includingTax)
        {
            var discountAmount          = decimal.Zero;
            var appliedDiscounts        = new List <Discount>();
            var subTotalWithoutDiscount = decimal.Zero;
            var subTotalWithDiscount    = decimal.Zero;
            var taxRates = new SortedDictionary <decimal, decimal>();

            if (!cart.Any())
            {
                return(discountAmount, appliedDiscounts, subTotalWithoutDiscount, subTotalWithDiscount, taxRates);
            }

            //get the customer
            var customer = await _customerService.GetShoppingCartCustomerAsync(cart);

            //sub totals
            var subTotalExclTaxWithoutDiscount = decimal.Zero;
            var subTotalInclTaxWithoutDiscount = decimal.Zero;

            foreach (var shoppingCartItem in cart)
            {
                var sciSubTotal = (await _shoppingCartService.GetSubTotalAsync(shoppingCartItem, true)).subTotal;
                var product     = await _productService.GetProductByIdAsync(shoppingCartItem.ProductId);

                var(sciExclTax, taxRate) = await _taxService.GetProductPriceAsync(product, sciSubTotal, false, customer);

                // custom
                var(_, sciInclTax) = await _warrantyTaxService.CalculateWarrantyTaxAsync(shoppingCartItem, customer, sciExclTax);

                subTotalExclTaxWithoutDiscount += sciExclTax;
                subTotalInclTaxWithoutDiscount += sciInclTax;

                //tax rates
                var sciTax = sciInclTax - sciExclTax;
                if (taxRate <= decimal.Zero || sciTax <= decimal.Zero)
                {
                    continue;
                }

                if (!taxRates.ContainsKey(taxRate))
                {
                    taxRates.Add(taxRate, sciTax);
                }
                else
                {
                    taxRates[taxRate] = taxRates[taxRate] + sciTax;
                }
            }

            //checkout attributes
            if (customer != null)
            {
                var checkoutAttributesXml = await _genericAttributeService.GetAttributeAsync <string>(customer, NopCustomerDefaults.CheckoutAttributes, (await _storeContext.GetCurrentStoreAsync()).Id);

                var attributeValues = _checkoutAttributeParser.ParseCheckoutAttributeValues(checkoutAttributesXml);
                if (attributeValues != null)
                {
                    await foreach (var(attribute, values) in attributeValues)
                    {
                        await foreach (var attributeValue in values)
                        {
                            var(caExclTax, taxRate) = await _taxService.GetCheckoutAttributePriceAsync(attribute, attributeValue, false, customer);

                            var(caInclTax, _) = await _taxService.GetCheckoutAttributePriceAsync(attribute, attributeValue, true, customer);

                            subTotalExclTaxWithoutDiscount += caExclTax;
                            subTotalInclTaxWithoutDiscount += caInclTax;

                            //tax rates
                            var caTax = caInclTax - caExclTax;
                            if (taxRate <= decimal.Zero || caTax <= decimal.Zero)
                            {
                                continue;
                            }

                            if (!taxRates.ContainsKey(taxRate))
                            {
                                taxRates.Add(taxRate, caTax);
                            }
                            else
                            {
                                taxRates[taxRate] = taxRates[taxRate] + caTax;
                            }
                        }
                    }
                }
            }

            //subtotal without discount
            subTotalWithoutDiscount = includingTax ? subTotalInclTaxWithoutDiscount : subTotalExclTaxWithoutDiscount;
            if (subTotalWithoutDiscount < decimal.Zero)
            {
                subTotalWithoutDiscount = decimal.Zero;
            }

            if (_shoppingCartSettings.RoundPricesDuringCalculation)
            {
                subTotalWithoutDiscount = await _priceCalculationService.RoundPriceAsync(subTotalWithoutDiscount);
            }

            //We calculate discount amount on order subtotal excl tax (discount first)
            //calculate discount amount ('Applied to order subtotal' discount)
            decimal discountAmountExclTax;

            (discountAmountExclTax, appliedDiscounts) = await GetOrderSubtotalDiscountAsync(customer, subTotalExclTaxWithoutDiscount);

            if (subTotalExclTaxWithoutDiscount < discountAmountExclTax)
            {
                discountAmountExclTax = subTotalExclTaxWithoutDiscount;
            }
            var discountAmountInclTax = discountAmountExclTax;
            //subtotal with discount (excl tax)
            var subTotalExclTaxWithDiscount = subTotalExclTaxWithoutDiscount - discountAmountExclTax;
            var subTotalInclTaxWithDiscount = subTotalExclTaxWithDiscount;

            //add tax for shopping items & checkout attributes
            var tempTaxRates = new Dictionary <decimal, decimal>(taxRates);

            foreach (var kvp in tempTaxRates)
            {
                var taxRate  = kvp.Key;
                var taxValue = kvp.Value;

                if (taxValue == decimal.Zero)
                {
                    continue;
                }

                //discount the tax amount that applies to subtotal items
                if (subTotalExclTaxWithoutDiscount > decimal.Zero)
                {
                    var discountTax = taxRates[taxRate] * (discountAmountExclTax / subTotalExclTaxWithoutDiscount);
                    discountAmountInclTax += discountTax;
                    taxValue = taxRates[taxRate] - discountTax;
                    if (_shoppingCartSettings.RoundPricesDuringCalculation)
                    {
                        taxValue = await _priceCalculationService.RoundPriceAsync(taxValue);
                    }
                    taxRates[taxRate] = taxValue;
                }

                //subtotal with discount (incl tax)
                subTotalInclTaxWithDiscount += taxValue;
            }

            if (_shoppingCartSettings.RoundPricesDuringCalculation)
            {
                discountAmountInclTax = await _priceCalculationService.RoundPriceAsync(discountAmountInclTax);

                discountAmountExclTax = await _priceCalculationService.RoundPriceAsync(discountAmountExclTax);
            }

            if (includingTax)
            {
                subTotalWithDiscount = subTotalInclTaxWithDiscount;
                discountAmount       = discountAmountInclTax;
            }
            else
            {
                subTotalWithDiscount = subTotalExclTaxWithDiscount;
                discountAmount       = discountAmountExclTax;
            }

            if (subTotalWithDiscount < decimal.Zero)
            {
                subTotalWithDiscount = decimal.Zero;
            }

            if (_shoppingCartSettings.RoundPricesDuringCalculation)
            {
                subTotalWithDiscount = await _priceCalculationService.RoundPriceAsync(subTotalWithDiscount);
            }

            return(discountAmount, appliedDiscounts, subTotalWithoutDiscount, subTotalWithDiscount, taxRates);
        }
        public override async Task MapAsync(IEnumerable <OrganizedShoppingCartItem> from, ShoppingCartModel to, dynamic parameters = null)
        {
            Guard.NotNull(from, nameof(from));
            Guard.NotNull(to, nameof(to));

            if (!from.Any())
            {
                return;
            }

            await base.MapAsync(from, to, null);

            var store    = _services.StoreContext.CurrentStore;
            var customer = _services.WorkContext.CurrentCustomer;
            var currency = _services.WorkContext.WorkingCurrency;

            var isEditable = parameters?.IsEditable == true;
            var validateCheckoutAttributes        = parameters?.ValidateCheckoutAttributes == true;
            var prepareEstimateShippingIfEnabled  = parameters?.PrepareEstimateShippingIfEnabled == true;
            var setEstimateShippingDefaultAddress = parameters?.SetEstimateShippingDefaultAddress == true;
            var prepareAndDisplayOrderReviewData  = parameters?.PrepareAndDisplayOrderReviewData == true;

            #region Simple properties

            to.MediaDimensions             = _mediaSettings.CartThumbPictureSize;
            to.DeliveryTimesPresentation   = _shoppingCartSettings.DeliveryTimesInShoppingCart;
            to.DisplayBasePrice            = _shoppingCartSettings.ShowBasePrice;
            to.DisplayWeight               = _shoppingCartSettings.ShowWeight;
            to.DisplayMoveToWishlistButton = await _services.Permissions.AuthorizeAsync(Permissions.Cart.AccessWishlist);

            to.TermsOfServiceEnabled         = _orderSettings.TermsOfServiceEnabled;
            to.DisplayCommentBox             = _shoppingCartSettings.ShowCommentBox;
            to.DisplayEsdRevocationWaiverBox = _shoppingCartSettings.ShowEsdRevocationWaiverBox;
            to.IsEditable = isEditable;

            var measure = await _db.MeasureWeights.FindByIdAsync(_measureSettings.BaseWeightId, false);

            if (measure != null)
            {
                to.MeasureUnitName = measure.GetLocalized(x => x.Name);
            }

            to.CheckoutAttributeInfo = HtmlUtils.ConvertPlainTextToTable(
                HtmlUtils.ConvertHtmlToPlainText(
                    await _checkoutAttributeFormatter.FormatAttributesAsync(customer.GenericAttributes.CheckoutAttributes, customer)));

            // Gift card and gift card boxes.
            to.DiscountBox.Display = _shoppingCartSettings.ShowDiscountBox;
            var discountCouponCode = customer.GenericAttributes.DiscountCouponCode;
            var discount           = await _db.Discounts
                                     .AsNoTracking()
                                     .Where(x => x.CouponCode == discountCouponCode)
                                     .FirstOrDefaultAsync();

            if (discount != null &&
                discount.RequiresCouponCode &&
                await _discountService.IsDiscountValidAsync(discount, customer))
            {
                to.DiscountBox.CurrentCode = discount.CouponCode;
            }

            to.GiftCardBox.Display = _shoppingCartSettings.ShowGiftCardBox;

            // Reward points.
            if (_rewardPointsSettings.Enabled && !from.IncludesMatchingItems(x => x.IsRecurring) && !customer.IsGuest())
            {
                var rewardPointsBalance    = customer.GetRewardPointsBalance();
                var rewardPointsAmountBase = _orderCalculationService.ConvertRewardPointsToAmount(rewardPointsBalance);
                var rewardPointsAmount     = _currencyService.ConvertFromPrimaryCurrency(rewardPointsAmountBase.Amount, currency);

                if (rewardPointsAmount > decimal.Zero)
                {
                    to.RewardPoints.DisplayRewardPoints = true;
                    to.RewardPoints.RewardPointsAmount  = rewardPointsAmount.ToString(true);
                    to.RewardPoints.RewardPointsBalance = rewardPointsBalance;
                    to.RewardPoints.UseRewardPoints     = customer.GenericAttributes.UseRewardPointsDuringCheckout;
                }
            }

            // Cart warnings.
            var warnings    = new List <string>();
            var cartIsValid = await _shoppingCartValidator.ValidateCartItemsAsync(from, warnings, validateCheckoutAttributes, customer.GenericAttributes.CheckoutAttributes);

            if (!cartIsValid)
            {
                to.Warnings.AddRange(warnings);
            }

            #endregion

            #region Checkout attributes

            var checkoutAttributes = await _checkoutAttributeMaterializer.GetValidCheckoutAttributesAsync(from);

            foreach (var attribute in checkoutAttributes)
            {
                var caModel = new ShoppingCartModel.CheckoutAttributeModel
                {
                    Id                   = attribute.Id,
                    Name                 = attribute.GetLocalized(x => x.Name),
                    TextPrompt           = attribute.GetLocalized(x => x.TextPrompt),
                    IsRequired           = attribute.IsRequired,
                    AttributeControlType = attribute.AttributeControlType
                };

                if (attribute.IsListTypeAttribute)
                {
                    var caValues = await _db.CheckoutAttributeValues
                                   .AsNoTracking()
                                   .Where(x => x.CheckoutAttributeId == attribute.Id)
                                   .ToListAsync();

                    // Prepare each attribute with image and price
                    foreach (var caValue in caValues)
                    {
                        var pvaValueModel = new ShoppingCartModel.CheckoutAttributeValueModel
                        {
                            Id            = caValue.Id,
                            Name          = caValue.GetLocalized(x => x.Name),
                            IsPreSelected = caValue.IsPreSelected,
                            Color         = caValue.Color
                        };

                        if (caValue.MediaFileId.HasValue && caValue.MediaFile != null)
                        {
                            pvaValueModel.ImageUrl = _mediaService.GetUrl(caValue.MediaFile, _mediaSettings.VariantValueThumbPictureSize, null, false);
                        }

                        caModel.Values.Add(pvaValueModel);

                        // Display price if allowed.
                        if (await _services.Permissions.AuthorizeAsync(Permissions.Catalog.DisplayPrice))
                        {
                            var priceAdjustmentBase = await _taxService.GetCheckoutAttributePriceAsync(caValue);

                            var priceAdjustment = _currencyService.ConvertFromPrimaryCurrency(priceAdjustmentBase.Price.Amount, currency);

                            if (priceAdjustmentBase.Price > decimal.Zero)
                            {
                                pvaValueModel.PriceAdjustment = "+" + priceAdjustmentBase.Price.ToString();
                            }
                            else if (priceAdjustmentBase.Price < decimal.Zero)
                            {
                                pvaValueModel.PriceAdjustment = "-" + priceAdjustmentBase.Price.ToString();
                            }
                        }
                    }
                }

                // Set already selected attributes.
                var selectedCheckoutAttributes = customer.GenericAttributes.CheckoutAttributes;
                switch (attribute.AttributeControlType)
                {
                case AttributeControlType.DropdownList:
                case AttributeControlType.RadioList:
                case AttributeControlType.Boxes:
                case AttributeControlType.Checkboxes:
                    if (selectedCheckoutAttributes.AttributesMap.Any())
                    {
                        // Clear default selection.
                        foreach (var item in caModel.Values)
                        {
                            item.IsPreSelected = false;
                        }

                        // Select new values.
                        var selectedCaValues = await _checkoutAttributeMaterializer.MaterializeCheckoutAttributeValuesAsync(selectedCheckoutAttributes);

                        foreach (var caValue in selectedCaValues)
                        {
                            foreach (var item in caModel.Values)
                            {
                                if (caValue.Id == item.Id)
                                {
                                    item.IsPreSelected = true;
                                }
                            }
                        }
                    }
                    break;

                case AttributeControlType.TextBox:
                case AttributeControlType.MultilineTextbox:
                    if (selectedCheckoutAttributes.AttributesMap.Any())
                    {
                        var enteredText = selectedCheckoutAttributes.AttributesMap
                                          .Where(x => x.Key == attribute.Id)
                                          .SelectMany(x => x.Value)
                                          .FirstOrDefault()
                                          .ToString();

                        if (enteredText.HasValue())
                        {
                            caModel.TextValue = enteredText;
                        }
                    }
                    break;

                case AttributeControlType.Datepicker:
                {
                    // Keep in mind my that the code below works only in the current culture.
                    var enteredDate = selectedCheckoutAttributes.AttributesMap
                                      .Where(x => x.Key == attribute.Id)
                                      .SelectMany(x => x.Value)
                                      .FirstOrDefault()
                                      .ToString();

                    if (enteredDate.HasValue() &&
                        DateTime.TryParseExact(enteredDate, "D", CultureInfo.CurrentCulture, DateTimeStyles.None, out var selectedDate))
                    {
                        caModel.SelectedDay   = selectedDate.Day;
                        caModel.SelectedMonth = selectedDate.Month;
                        caModel.SelectedYear  = selectedDate.Year;
                    }
                }
                break;

                case AttributeControlType.FileUpload:
                    if (selectedCheckoutAttributes.AttributesMap.Any())
                    {
                        var FileValue = selectedCheckoutAttributes.AttributesMap
                                        .Where(x => x.Key == attribute.Id)
                                        .SelectMany(x => x.Value)
                                        .FirstOrDefault()
                                        .ToString();

                        if (FileValue.HasValue() && caModel.UploadedFileGuid.HasValue() && Guid.TryParse(caModel.UploadedFileGuid, out var guid))
                        {
                            var download = await _db.Downloads
                                           .Include(x => x.MediaFile)
                                           .FirstOrDefaultAsync(x => x.DownloadGuid == guid);

                            if (download != null && !download.UseDownloadUrl && download.MediaFile != null)
                            {
                                caModel.UploadedFileName = download.MediaFile.Name;
                            }
                        }
                    }
                    break;

                default:
                    break;
                }

                to.CheckoutAttributes.Add(caModel);
            }

            #endregion

            #region Estimate shipping

            if (prepareEstimateShippingIfEnabled)
            {
                to.EstimateShipping.Enabled = _shippingSettings.EstimateShippingEnabled &&
                                              from.Any() &&
                                              from.IncludesMatchingItems(x => x.IsShippingEnabled);

                if (to.EstimateShipping.Enabled)
                {
                    // Countries.
                    var defaultEstimateCountryId = setEstimateShippingDefaultAddress && customer.ShippingAddress != null
                        ? customer.ShippingAddress.CountryId
                        : to.EstimateShipping.CountryId;

                    var countriesForShipping = await _db.Countries
                                               .AsNoTracking()
                                               .ApplyStoreFilter(store.Id)
                                               .Where(x => x.AllowsShipping)
                                               .ToListAsync();

                    foreach (var countries in countriesForShipping)
                    {
                        to.EstimateShipping.AvailableCountries.Add(new SelectListItem
                        {
                            Text     = countries.GetLocalized(x => x.Name),
                            Value    = countries.Id.ToString(),
                            Selected = countries.Id == defaultEstimateCountryId
                        });
                    }

                    // States.
                    var states = defaultEstimateCountryId.HasValue
                        ? await _db.StateProvinces.AsNoTracking().ApplyCountryFilter(defaultEstimateCountryId.Value).ToListAsync()
                        : new();

                    if (states.Any())
                    {
                        var defaultEstimateStateId = setEstimateShippingDefaultAddress && customer.ShippingAddress != null
                            ? customer.ShippingAddress.StateProvinceId
                            : to.EstimateShipping.StateProvinceId;

                        foreach (var s in states)
                        {
                            to.EstimateShipping.AvailableStates.Add(new SelectListItem
                            {
                                Text     = s.GetLocalized(x => x.Name),
                                Value    = s.Id.ToString(),
                                Selected = s.Id == defaultEstimateStateId
                            });
                        }
                    }
                    else
                    {
                        to.EstimateShipping.AvailableStates.Add(new SelectListItem {
                            Text = T("Address.OtherNonUS"), Value = "0"
                        });
                    }

                    if (setEstimateShippingDefaultAddress && customer.ShippingAddress != null)
                    {
                        to.EstimateShipping.ZipPostalCode = customer.ShippingAddress.ZipPostalCode;
                    }
                }
            }

            #endregion

            #region Cart items

            foreach (var cartItem in from)
            {
                var model = new ShoppingCartModel.ShoppingCartItemModel();

                await cartItem.MapAsync(model);

                to.AddItems(model);
            }

            #endregion

            #region Order review data

            if (prepareAndDisplayOrderReviewData)
            {
                var checkoutState = _httpContextAccessor.HttpContext?.GetCheckoutState();

                to.OrderReviewData.Display = true;

                // Billing info.
                var billingAddress = customer.BillingAddress;
                if (billingAddress != null)
                {
                    await MapperFactory.MapAsync(billingAddress, to.OrderReviewData.BillingAddress);
                }

                // Shipping info.
                if (from.IsShippingRequired())
                {
                    to.OrderReviewData.IsShippable = true;

                    var shippingAddress = customer.ShippingAddress;
                    if (shippingAddress != null)
                    {
                        await MapperFactory.MapAsync(shippingAddress, to.OrderReviewData.ShippingAddress);
                    }

                    // Selected shipping method.
                    var shippingOption = customer.GenericAttributes.SelectedShippingOption;
                    if (shippingOption != null)
                    {
                        to.OrderReviewData.ShippingMethod = shippingOption.Name;
                    }

                    if (checkoutState != null && checkoutState.CustomProperties.ContainsKey("HasOnlyOneActiveShippingMethod"))
                    {
                        to.OrderReviewData.DisplayShippingMethodChangeOption = !(bool)checkoutState.CustomProperties.Get("HasOnlyOneActiveShippingMethod");
                    }
                }

                if (checkoutState != null && checkoutState.CustomProperties.ContainsKey("HasOnlyOneActivePaymentMethod"))
                {
                    to.OrderReviewData.DisplayPaymentMethodChangeOption = !(bool)checkoutState.CustomProperties.Get("HasOnlyOneActivePaymentMethod");
                }

                var selectedPaymentMethodSystemName = customer.GenericAttributes.SelectedPaymentMethod;
                var paymentMethod = await _paymentService.LoadPaymentMethodBySystemNameAsync(selectedPaymentMethodSystemName);

                // TODO: (ms) (core) Wait for PluginMediator.GetLocalizedFriendlyName implementation
                //model.OrderReviewData.PaymentMethod = paymentMethod != null ? _pluginMediator.GetLocalizedFriendlyName(paymentMethod.Metadata) : "";
                to.OrderReviewData.PaymentSummary            = checkoutState.PaymentSummary;
                to.OrderReviewData.IsPaymentSelectionSkipped = checkoutState.IsPaymentSelectionSkipped;
            }

            #endregion

            var paymentTypes        = new PaymentMethodType[] { PaymentMethodType.Button, PaymentMethodType.StandardAndButton };
            var boundPaymentMethods = await _paymentService.LoadActivePaymentMethodsAsync(
                customer,
                from.ToList(),
                store.Id,
                paymentTypes,
                false);

            var bpmModel = new ButtonPaymentMethodModel();

            foreach (var boundPaymentMethod in boundPaymentMethods)
            {
                if (from.IncludesMatchingItems(x => x.IsRecurring) && boundPaymentMethod.Value.RecurringPaymentType == RecurringPaymentType.NotSupported)
                {
                    continue;
                }

                var widgetInvoker = boundPaymentMethod.Value.GetPaymentInfoWidget();
                bpmModel.Items.Add(widgetInvoker);
            }

            to.ButtonPaymentMethods = bpmModel;
        }