public static void ApplyRewards(this CartAggregate aggregate, ICollection <PromotionReward> rewards)
        {
            var shoppingCart = aggregate.Cart;

            shoppingCart.Discounts?.Clear();
            shoppingCart.DiscountAmount = 0M;

            var cartRewards = rewards.OfType <CartSubtotalReward>();

            foreach (var reward in cartRewards.Where(reward => reward.IsValid))
            {
                //When a discount is applied to the cart subtotal, the tax calculation has already been applied, and is reflected in the tax subtotal.
                //Therefore, a discount applying to the cart subtotal will occur after tax.
                //For instance, if the cart subtotal is $100, and $15 is the tax subtotal, a cart - wide discount of 10 % will yield a total of $105($100 subtotal – $10 discount + $15 tax on the original $100).
                var discount = new Discount
                {
                    Coupon         = reward.Coupon,
                    Currency       = shoppingCart.Currency,
                    Description    = reward.Promotion?.Description,
                    DiscountAmount = reward.GetRewardAmount(shoppingCart.SubTotal, 1),
                    PromotionId    = reward.PromotionId ?? reward.Promotion?.Id,
                };
                if (shoppingCart.Discounts == null)
                {
                    shoppingCart.Discounts = new List <Discount>();
                }
                shoppingCart.Discounts.Add(discount);
                shoppingCart.DiscountAmount += discount.DiscountAmount;
            }

            // remove the (added) gifts, if corresponding valid reward is missing
            foreach (var lineItem in aggregate.GiftItems.ToList() ?? Enumerable.Empty <LineItem>())
            {
                if (!rewards.OfType <GiftReward>().Any(re => re.IsValid && lineItem.EqualsReward(re)))
                {
                    shoppingCart.Items.Remove(lineItem);
                }
            }

            var lineItemRewards = rewards.OfType <CatalogItemAmountReward>();

            foreach (var lineItem in aggregate.LineItems ?? Enumerable.Empty <LineItem>())
            {
                lineItem.ApplyRewards(shoppingCart.Currency, lineItemRewards);
            }

            var shipmentRewards = rewards.OfType <ShipmentReward>();

            foreach (var shipment in shoppingCart.Shipments ?? Enumerable.Empty <Shipment>())
            {
                shipment.ApplyRewards(shoppingCart.Currency, shipmentRewards);
            }

            var paymentRewards = rewards.OfType <PaymentReward>();

            foreach (var payment in shoppingCart.Payments ?? Enumerable.Empty <Payment>())
            {
                payment.ApplyRewards(shoppingCart.Currency, paymentRewards);
            }
        }
예제 #2
0
        public ItemQtyAdjustmentValidator(CartAggregate cartAggr)
        {
            RuleFor(x => x.NewQuantity).GreaterThan(0);
            RuleFor(x => x.LineItemId).NotNull();
            RuleFor(x => x.CartProduct).NotNull();
            RuleFor(x => x).Custom((qtyAdjust, context) =>
            {
                var lineItem = cartAggr.Cart?.Items?.FirstOrDefault(x => x.Id.EqualsInvariant(qtyAdjust.LineItemId));
                if (lineItem == null)
                {
                    context.AddFailure(CartErrorDescriber.LineItemWithGivenIdNotFound(new LineItem {
                        Id = qtyAdjust.LineItemId
                    }));
                }
                else if (lineItem.IsReadOnly)
                {
                    context.AddFailure(CartErrorDescriber.LineItemIsReadOnly(lineItem));
                }

                if (qtyAdjust.CartProduct != null && !new ProductIsAvailableSpecification().IsSatisfiedBy(qtyAdjust.CartProduct, qtyAdjust.NewQuantity))
                {
                    context.AddFailure(CartErrorDescriber.ProductQtyInsufficientError(qtyAdjust.CartProduct, qtyAdjust.NewQuantity, qtyAdjust.CartProduct.AvailableQuantity));
                }
            });
        }
예제 #3
0
        public async Task <DefaultResponse> ExecuteAsync(CreateCartCommand command)
        {
            var cartCreator = new CartAggregate(command.UserId);

            _repository.Add(cartCreator);
            await _unitOfWork.CommitAsync();

            return(new DefaultResponse(cartCreator.AggregateId));
        }
예제 #4
0
        public async Task GetAvailablePaymentMethodsAsync_AggregateIsNull_ShouldReturnEmptyResult()
        {
            // Arrange
            CartAggregate cartAggregate = null;

            // Act
            var result = await service.GetAvailablePaymentMethodsAsync(cartAggregate);

            // Assert
            result.Should().BeEmpty();
        }
예제 #5
0
        public async Task <IEnumerable <GiftItem> > GetAvailableGiftsAsync(CartAggregate cartAggr)
        {
            var promotionEvalResult = await cartAggr.EvaluatePromotionsAsync();

            var giftRewards = promotionEvalResult.Rewards
                              .OfType <GiftReward>()
                              .Where(reward => reward.IsValid)
                              // .Distinct() is needed as multiplied gifts would be returned otherwise.
                              .Distinct()
                              .ToArray();

            var productIds = giftRewards.Select(x => x.ProductId).Distinct().Where(x => !x.IsNullOrEmpty()).ToArray();

            var productsByIds = (await _cartProductService.GetCartProductsByIdsAsync(cartAggr, productIds)).ToDictionary(x => x.Id);

            var availableProductsIds = productsByIds.Values
                                       .Where(x => (x.Product.IsActive ?? false) &&
                                              (x.Product.IsBuyable ?? false) &&
                                              x.Price != null &&
                                              (!(x.Product.TrackInventory ?? false) || x.AvailableQuantity >= giftRewards
                                               .FirstOrDefault(y => y.ProductId == x.Product.Id)?.Quantity))
                                       .Select(x => x.Product.Id)
                                       .ToHashSet();

            return(giftRewards
                   .Where(x => x.ProductId.IsNullOrEmpty() || availableProductsIds.Contains(x.ProductId))
                   .Select(reward =>
            {
                var result = _mapper.Map <GiftItem>(reward);

                // if reward has assigned product, add data from product
                if (!reward.ProductId.IsNullOrEmpty() && productsByIds.ContainsKey(reward.ProductId))
                {
                    var product = productsByIds[reward.ProductId];
                    result.CatalogId = product.Product.CatalogId;
                    result.CategoryId ??= product.Product.CategoryId;
                    result.ProductId = product.Product.Id;
                    result.Sku = product.Product.Code;
                    result.ImageUrl ??= product.Product.ImgSrc;
                    result.MeasureUnit ??= product.Product.MeasureUnit;
                    result.Name ??= product.Product.Name;
                }

                var giftInCart = cartAggr.GiftItems.FirstOrDefault(x => x.EqualsReward(result));
                // non-null LineItemId indicates that this GiftItem was added to the cart
                result.LineItemId = giftInCart?.Id;

                // CacheKey as Id
                result.Id = result.GetCacheKey();
                return result;
            }).ToList());
        }
예제 #6
0
        protected CartAggregate GetValidCartAggregate()
        {
            var cart = GetCart();

            var aggregate = new CartAggregate(
                _marketingPromoEvaluatorMock.Object,
                _shoppingCartTotalsCalculatorMock.Object,
                _taxProviderSearchServiceMock.Object,
                _mapperMock.Object);

            aggregate.GrabCart(cart, new Store(), GetMember(), GetCurrency());

            return(aggregate);
        }
        public async Task <CartValidationContext> CreateValidationContextAsync(CartAggregate cartAggr)
        {
            var availPaymentsTask      = _availMethods.GetAvailablePaymentMethodsAsync(cartAggr);
            var availShippingRatesTask = _availMethods.GetAvailableShippingRatesAsync(cartAggr);
            var cartProductsTask       = _cartProducts.GetCartProductsByIdsAsync(cartAggr, cartAggr.Cart.Items.Select(x => x.ProductId).ToArray());
            await Task.WhenAll(availPaymentsTask, availShippingRatesTask, cartProductsTask);

            return(new CartValidationContext
            {
                AllCartProducts = cartProductsTask.Result,
                AvailPaymentMethods = availPaymentsTask.Result,
                AvailShippingRates = availShippingRatesTask.Result
            });
        }
예제 #8
0
        /// <summary>
        /// Load <see cref="CartProduct"/>s with all dependencies
        /// </summary>
        /// <param name="aggregate">Cart aggregate</param>
        /// <param name="ids">Product ids</param>
        /// <returns>List of <see cref="CartProduct"/>s</returns>
        public async Task <IList <CartProduct> > GetCartProductsByIdsAsync(CartAggregate aggregate, IEnumerable <string> ids)
        {
            if (aggregate is null || ids.IsNullOrEmpty())
            {
                return(new List <CartProduct>());
            }

            var products = await GetProductsByIdsAsync(ids);

            var cartProducts = await GetCartProductsAsync(products);

            await Task.WhenAll(LoadDependencies(aggregate, cartProducts));

            return(cartProducts);
        }
        public async Task Handle_CreateOrder_EnsureCartDeleted()
        {
            // Arrange
            var cart = new ShoppingCart()
            {
                Name       = "default",
                Currency   = "USD",
                CustomerId = Guid.NewGuid().ToString(),
            };

            var cartService  = new Mock <ShoppingCartService>(null, null, null, null);
            var deleteCalled = false;

            cartService.Setup(x => x.GetByIdAsync(It.IsAny <string>(), It.IsAny <string>()))
            .ReturnsAsync(cart);
            cartService.Setup(x => x.DeleteAsync(It.IsAny <IEnumerable <string> >(), It.IsAny <bool>()))
            .Returns(() =>
            {
                deleteCalled = true;
                return(Task.CompletedTask);
            });

            var customerAggrRep = new Mock <ICustomerOrderAggregateRepository>();

            customerAggrRep.Setup(x => x.CreateOrderFromCart(It.IsAny <ShoppingCart>()))
            .ReturnsAsync(new CustomerOrderAggregate(null, null));

            var cartAggr = new CartAggregate(null, null, null, null, null, null);

            cartAggr.GrabCart(cart, new Store(), new Contact(), new Currency());
            var cartAggrRep = new Mock <ICartAggregateRepository>();

            cartAggrRep.Setup(x => x.GetCartForShoppingCartAsync(It.IsAny <ShoppingCart>(), null))
            .ReturnsAsync(cartAggr);

            var contextFactory = new Mock <ICartValidationContextFactory>();

            contextFactory.Setup(x => x.CreateValidationContextAsync(It.IsAny <CartAggregate>()))
            .ReturnsAsync(new CartValidationContext());

            // Take action
            var handler = new CreateOrderFromCartCommandHandler(cartService.Object, customerAggrRep.Object, cartAggrRep.Object, contextFactory.Object);
            await handler.Handle(new CreateOrderFromCartCommand(""), CancellationToken.None);

            // Assert
            deleteCalled.Should().BeTrue();
        }
        public CartAggregateTests()
        {
            aggregate = new CartAggregate(
                _marketingPromoEvaluatorMock.Object,
                _shoppingCartTotalsCalculatorMock.Object,
                _taxProviderSearchServiceMock.Object,
                _mapperMock.Object);

            var cart     = GetCart();
            var member   = GetMember();
            var store    = GetStore();
            var currency = GetCurrency();

            aggregate.GrabCart(cart, store, member, currency);

            aggregate.RecalculateAsync().GetAwaiter().GetResult();
        }
예제 #11
0
        /// <summary>
        /// Evaluate prices and apply them to <see cref="CartProduct"/>s
        /// </summary>
        /// <param name="aggregate">Cart aggregate</param>
        /// <param name="products">List of <see cref="CartProduct"/>s</param>
        protected virtual async Task ApplyPricesToCartProductAsync(CartAggregate aggregate, List <CartProduct> products)
        {
            if (products.IsNullOrEmpty())
            {
                return;
            }

            var pricesEvalContext = _mapper.Map <PriceEvaluationContext>(aggregate);

            pricesEvalContext.ProductIds = products.Select(x => x.Id).ToArray();

            var evalPricesTask = await _pricingEvaluatorService.EvaluateProductPricesAsync(pricesEvalContext);

            foreach (var cartProduct in products)
            {
                cartProduct.ApplyPrices(evalPricesTask, aggregate.Currency);
            }
        }
예제 #12
0
        public async Task <IEnumerable <CartProduct> > GetCartProductsByIdsAsync(CartAggregate cartAggr, string[] ids)
        {
            if (cartAggr == null)
            {
                throw new ArgumentNullException(nameof(cartAggr));
            }
            if (ids == null)
            {
                throw new ArgumentNullException(nameof(ids));
            }

            var result   = new List <CartProduct>();
            var products = await _productService.GetByIdsAsync(ids, (ItemResponseGroup.ItemAssets | ItemResponseGroup.ItemInfo | ItemResponseGroup.Outlines | ItemResponseGroup.Seo).ToString());

            if (!products.IsNullOrEmpty())
            {
                var loadInventoriesTask = _inventorySearchService.SearchInventoriesAsync(new InventorySearchCriteria
                {
                    ProductIds = ids,
                    //Do not use int.MaxValue use only 10 items per requested product
                    //TODO: Replace to pagination load
                    Take = Math.Min(ids.Length * 10, 500)
                });

                var pricesEvalContext = _mapper.Map <PriceEvaluationContext>(cartAggr);
                pricesEvalContext.ProductIds = ids;
                var evalPricesTask = _pricingService.EvaluateProductPricesAsync(pricesEvalContext);

                await Task.WhenAll(loadInventoriesTask, evalPricesTask);

                foreach (var product in products)
                {
                    var cartProduct = new CartProduct(product);
                    //Apply inventories
                    cartProduct.ApplyInventories(loadInventoriesTask.Result.Results, cartAggr.Store);

                    //Apply prices
                    cartProduct.ApplyPrices(evalPricesTask.Result, cartAggr.Currency);
                    result.Add(cartProduct);
                }
            }
            return(result);
        }
 public ChangeCartItemPriceValidator(CartAggregate cartAggr)
 {
     RuleFor(x => x.NewPrice).GreaterThanOrEqualTo(0);
     RuleFor(x => x.LineItemId).NotNull().NotEmpty();
     RuleSet("strict", () =>
     {
         RuleFor(x => x).Custom((newPriceRequest, context) =>
         {
             var lineItem = cartAggr.Cart.Items.FirstOrDefault(x => x.Id == newPriceRequest.LineItemId);
             if (lineItem != null)
             {
                 var newSalePrice = newPriceRequest.NewPrice;
                 if (lineItem.SalePrice > newSalePrice)
                 {
                     context.AddFailure(CartErrorDescriber.UnableToSetLessPrice(lineItem));
                 }
             }
         });
     });
 }
예제 #14
0
        /// <summary>
        /// Load inventories and apply them to <see cref="CartProduct"/>s
        /// </summary>
        /// <param name="aggregate">Cart aggregate</param>
        /// <param name="products">List of <see cref="CartProduct"/>s</param>
        protected virtual async Task ApplyInventoriesToCartProductAsync(CartAggregate aggregate, List <CartProduct> products)
        {
            if (products.IsNullOrEmpty())
            {
                return;
            }

            var ids = products.Select(x => x.Id).ToArray();

            var countResult = await _inventorySearchService.SearchInventoriesAsync(new InventorySearchCriteria
            {
                ProductIds = ids,
                Skip       = 0,
                Take       = DefaultPageSize
            });

            var allLoadInventories = countResult.Results.ToList();

            if (countResult.TotalCount > DefaultPageSize)
            {
                for (var i = DefaultPageSize; i < countResult.TotalCount; i += DefaultPageSize)
                {
                    var loadInventoriesTask = await _inventorySearchService.SearchInventoriesAsync(new InventorySearchCriteria
                    {
                        ProductIds = ids,
                        Skip       = i,
                        Take       = DefaultPageSize
                    });

                    allLoadInventories.AddRange(loadInventoriesTask.Results);
                }
            }

            foreach (var cartProduct in products)
            {
                cartProduct.ApplyInventories(allLoadInventories, aggregate.Store);
            }
        }
        public async Task <IEnumerable <PaymentMethod> > GetAvailablePaymentMethodsAsync(CartAggregate cartAggr)
        {
            if (cartAggr == null)
            {
                return(Enumerable.Empty <PaymentMethod>());
            }

            var criteria = new PaymentMethodsSearchCriteria
            {
                IsActive = true,
                Take     = _takeOnSearch,
                StoreId  = cartAggr.Store?.Id,
            };

            var result = await _paymentMethodsSearchService.SearchPaymentMethodsAsync(criteria);

            if (result.Results.IsNullOrEmpty())
            {
                return(Enumerable.Empty <PaymentMethod>());
            }

            var evalContext = _mapper.Map <PromotionEvaluationContext>(cartAggr);
            var promoResult = await cartAggr.EvaluatePromotionsAsync(evalContext);

            foreach (var paymentMethod in result.Results)
            {
                paymentMethod.ApplyRewards(promoResult.Rewards);
            }

            //Evaluate taxes for available payments
            var taxProvider = await GetActiveTaxProviderAsync(cartAggr.Store.Id);

            if (taxProvider != null)
            {
                var taxEvalContext = _mapper.Map <TaxEvaluationContext>(cartAggr);
                taxEvalContext.Lines.Clear();
                taxEvalContext.Lines.AddRange(result.Results.SelectMany(x => _mapper.Map <IEnumerable <TaxLine> >(x)));
                var taxRates = taxProvider.CalculateRates(taxEvalContext);
                foreach (var paymentMethod in result.Results)
                {
                    paymentMethod.ApplyTaxRates(taxRates);
                }
            }

            return(result.Results);
        }
        public async Task <IEnumerable <ShippingRate> > GetAvailableShippingRatesAsync(CartAggregate cartAggr)
        {
            if (cartAggr == null)
            {
                return(Enumerable.Empty <ShippingRate>());
            }

            //Request available shipping rates
            var shippingEvaluationContext = new ShippingEvaluationContext(cartAggr.Cart);

            var criteria = new ShippingMethodsSearchCriteria
            {
                IsActive = true,
                Take     = _takeOnSearch,
                StoreId  = cartAggr.Store?.Id
            };

            var activeAvailableShippingMethods = (await _shippingMethodsSearchService.SearchShippingMethodsAsync(criteria)).Results;

            var availableShippingRates = activeAvailableShippingMethods
                                         .SelectMany(x => x.CalculateRates(shippingEvaluationContext))
                                         .Where(x => x.ShippingMethod == null || x.ShippingMethod.IsActive)
                                         .ToArray();

            if (availableShippingRates.IsNullOrEmpty())
            {
                return(Enumerable.Empty <ShippingRate>());
            }

            //Evaluate promotions cart and apply rewards for available shipping methods
            var evalContext     = _mapper.Map <PromotionEvaluationContext>(cartAggr);
            var promoEvalResult = await cartAggr.EvaluatePromotionsAsync(evalContext);

            foreach (var shippingRate in availableShippingRates)
            {
                shippingRate.ApplyRewards(promoEvalResult.Rewards);
            }

            var taxProvider = await GetActiveTaxProviderAsync(cartAggr.Store.Id);

            if (taxProvider != null)
            {
                var taxEvalContext = _mapper.Map <TaxEvaluationContext>(cartAggr);
                taxEvalContext.Lines.Clear();
                taxEvalContext.Lines.AddRange(availableShippingRates.SelectMany(x => _mapper.Map <IEnumerable <TaxLine> >(x)));
                var taxRates = taxProvider.CalculateRates(taxEvalContext);
                foreach (var shippingRate in availableShippingRates)
                {
                    shippingRate.ApplyTaxRates(taxRates);
                }
            }

            return(availableShippingRates);
        }
예제 #17
0
        protected async virtual Task <CartAggregate> SaveCartAsync(CartAggregate cartAggregate)
        {
            await CartRepository.SaveAsync(cartAggregate);

            return(cartAggregate);
        }
예제 #18
0
 /// <summary>
 /// Load all properties for <see cref="CartProduct"/>s
 /// </summary>
 /// <param name="aggregate">Cart aggregate</param>
 /// <param name="products">List of <see cref="CartProduct"/>s</param>
 /// <returns>List of <see cref="Task"/>s</returns>
 protected virtual List <Task> LoadDependencies(CartAggregate aggregate, List <CartProduct> products) => new List <Task>
 {
     ApplyInventoriesToCartProductAsync(aggregate, products),
     ApplyPricesToCartProductAsync(aggregate, products)
 };