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); } }
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)); } }); }
public async Task <DefaultResponse> ExecuteAsync(CreateCartCommand command) { var cartCreator = new CartAggregate(command.UserId); _repository.Add(cartCreator); await _unitOfWork.CommitAsync(); return(new DefaultResponse(cartCreator.AggregateId)); }
public async Task GetAvailablePaymentMethodsAsync_AggregateIsNull_ShouldReturnEmptyResult() { // Arrange CartAggregate cartAggregate = null; // Act var result = await service.GetAvailablePaymentMethodsAsync(cartAggregate); // Assert result.Should().BeEmpty(); }
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()); }
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 }); }
/// <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(); }
/// <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); } }
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)); } } }); }); }
/// <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); }
protected async virtual Task <CartAggregate> SaveCartAsync(CartAggregate cartAggregate) { await CartRepository.SaveAsync(cartAggregate); return(cartAggregate); }
/// <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) };