diff --git a/src/ApplicationCore/Entities/BasketAggregate/BasketItem.cs b/src/ApplicationCore/Entities/BasketAggregate/BasketItem.cs index 6b6dc39..18f6203 100644 --- a/src/ApplicationCore/Entities/BasketAggregate/BasketItem.cs +++ b/src/ApplicationCore/Entities/BasketAggregate/BasketItem.cs @@ -1,4 +1,7 @@ -namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate +using Ardalis.GuardClauses; +using System; + +namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate { public class BasketItem : BaseEntity { @@ -11,17 +14,21 @@ public BasketItem(int catalogItemId, int quantity, decimal unitPrice) { CatalogItemId = catalogItemId; - Quantity = quantity; UnitPrice = unitPrice; + SetQuantity(quantity); } public void AddQuantity(int quantity) { + Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue); + Quantity += quantity; } - public void SetNewQuantity(int quantity) + public void SetQuantity(int quantity) { + Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue); + Quantity = quantity; } } diff --git a/src/ApplicationCore/Exceptions/EmptyBasketOnCheckoutException.cs b/src/ApplicationCore/Exceptions/EmptyBasketOnCheckoutException.cs new file mode 100644 index 0000000..c2713a9 --- /dev/null +++ b/src/ApplicationCore/Exceptions/EmptyBasketOnCheckoutException.cs @@ -0,0 +1,24 @@ +using System; + +namespace Microsoft.eShopWeb.ApplicationCore.Exceptions +{ + public class EmptyBasketOnCheckoutException : Exception + { + public EmptyBasketOnCheckoutException() + : base($"Basket cannot have 0 items on checkout") + { + } + + protected EmptyBasketOnCheckoutException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) + { + } + + public EmptyBasketOnCheckoutException(string message) : base(message) + { + } + + public EmptyBasketOnCheckoutException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/ApplicationCore/Exceptions/GuardExtensions.cs b/src/ApplicationCore/Exceptions/GuardExtensions.cs index 2acbf00..55f27b3 100644 --- a/src/ApplicationCore/Exceptions/GuardExtensions.cs +++ b/src/ApplicationCore/Exceptions/GuardExtensions.cs @@ -1,5 +1,7 @@ using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using System.Collections.Generic; +using System.Linq; namespace Ardalis.GuardClauses { @@ -10,5 +12,11 @@ namespace Ardalis.GuardClauses if (basket == null) throw new BasketNotFoundException(basketId); } + + public static void EmptyBasketOnCheckout(this IGuardClause guardClause, IReadOnlyCollection basketItems) + { + if (!basketItems.Any()) + throw new EmptyBasketOnCheckoutException(); + } } } \ No newline at end of file diff --git a/src/ApplicationCore/Services/BasketService.cs b/src/ApplicationCore/Services/BasketService.cs index 979624a..22dbd0f 100644 --- a/src/ApplicationCore/Services/BasketService.cs +++ b/src/ApplicationCore/Services/BasketService.cs @@ -55,12 +55,13 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services Guard.Against.Null(quantities, nameof(quantities)); var basket = await _basketRepository.GetByIdAsync(basketId); Guard.Against.NullBasket(basketId, basket); + foreach (var item in basket.Items) { if (quantities.TryGetValue(item.Id.ToString(), out var quantity)) { if (_logger != null) _logger.LogInformation($"Updating quantity of item ID:{item.Id} to {quantity}."); - item.SetNewQuantity(quantity); + item.SetQuantity(quantity); } } basket.RemoveEmptyItems(); diff --git a/src/ApplicationCore/Services/OrderService.cs b/src/ApplicationCore/Services/OrderService.cs index ef035ed..3bd63a9 100644 --- a/src/ApplicationCore/Services/OrderService.cs +++ b/src/ApplicationCore/Services/OrderService.cs @@ -33,6 +33,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); Guard.Against.NullBasket(basketId, basket); + Guard.Against.EmptyBasketOnCheckout(basket.Items); var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray()); var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification); diff --git a/src/Web/Pages/Basket/BasketItemViewModel.cs b/src/Web/Pages/Basket/BasketItemViewModel.cs index 129f0e2..a30a608 100644 --- a/src/Web/Pages/Basket/BasketItemViewModel.cs +++ b/src/Web/Pages/Basket/BasketItemViewModel.cs @@ -1,4 +1,6 @@ -namespace Microsoft.eShopWeb.Web.Pages.Basket +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.eShopWeb.Web.Pages.Basket { public class BasketItemViewModel { @@ -7,7 +9,10 @@ public string ProductName { get; set; } public decimal UnitPrice { get; set; } public decimal OldUnitPrice { get; set; } + + [Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")] public int Quantity { get; set; } + public string PictureUrl { get; set; } } } diff --git a/src/Web/Pages/Basket/Checkout.cshtml.cs b/src/Web/Pages/Basket/Checkout.cshtml.cs index 7193e39..6b973ed 100644 --- a/src/Web/Pages/Basket/Checkout.cshtml.cs +++ b/src/Web/Pages/Basket/Checkout.cshtml.cs @@ -3,11 +3,13 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Web.Interfaces; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Pages.Basket @@ -19,16 +21,19 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket private readonly IOrderService _orderService; private string _username = null; private readonly IBasketViewModelService _basketViewModelService; + private readonly IAppLogger _logger; public CheckoutModel(IBasketService basketService, IBasketViewModelService basketViewModelService, SignInManager signInManager, - IOrderService orderService) + IOrderService orderService, + IAppLogger logger) { _basketService = basketService; _signInManager = signInManager; _orderService = orderService; _basketViewModelService = basketViewModelService; + _logger = logger; } public BasketViewModel BasketModel { get; set; } = new BasketViewModel(); @@ -44,15 +49,28 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket } } - public async Task OnPost(Dictionary items) + public async Task OnPost(IEnumerable items) { - await SetBasketModelAsync(); + try + { + await SetBasketModelAsync(); - await _basketService.SetQuantities(BasketModel.Id, items); + if (!ModelState.IsValid) + { + return BadRequest(); + } - await _orderService.CreateOrderAsync(BasketModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); - - await _basketService.DeleteBasketAsync(BasketModel.Id); + var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity); + await _basketService.SetQuantities(BasketModel.Id, updateModel); + await _orderService.CreateOrderAsync(BasketModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); + await _basketService.DeleteBasketAsync(BasketModel.Id); + } + catch (EmptyBasketOnCheckoutException emptyBasketOnCheckoutException) + { + //Redirect to Empty Basket page + _logger.LogWarning(emptyBasketOnCheckoutException.Message); + return RedirectToPage("/Basket/Index"); + } return RedirectToPage(); } diff --git a/src/Web/Pages/Basket/Index.cshtml b/src/Web/Pages/Basket/Index.cshtml index 8f3b5b6..8369aed 100644 --- a/src/Web/Pages/Basket/Index.cshtml +++ b/src/Web/Pages/Basket/Index.cshtml @@ -23,31 +23,29 @@
Cost
+
@for (int i = 0; i < Model.BasketModel.Items.Count; i++) { var item = Model.BasketModel.Items[i]; -
-
-
- -
-
@item.ProductName
-
$ @item.UnitPrice.ToString("N2")
-
- - -
-
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
-
-
- -
-
- @*
- @item.ProductId -
*@ +
+
+
+ +
+
@item.ProductName
+
$ @item.UnitPrice.ToString("N2")
+
+ + +
+
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
+
+
- } +
+
@*
+ @item.ProductId +
*@ }
@@ -77,16 +75,15 @@ asp-page-handler="Update"> [ Update ] - @{ - var data = new Dictionary - { + @{ var data = new Dictionary + { { Constants.BASKET_ID, Model.BasketModel.Id.ToString() }, - }; - } - + }; } + + +
diff --git a/src/Web/Pages/Basket/Index.cshtml.cs b/src/Web/Pages/Basket/Index.cshtml.cs index 9221aea..797b90e 100644 --- a/src/Web/Pages/Basket/Index.cshtml.cs +++ b/src/Web/Pages/Basket/Index.cshtml.cs @@ -8,6 +8,7 @@ using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.ViewModels; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Microsoft.eShopWeb.Web.Pages.Basket @@ -50,10 +51,17 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket return RedirectToPage(); } - public async Task OnPostUpdate(Dictionary items) + public async Task OnPostUpdate(IEnumerable items) { await SetBasketModelAsync(); - await _basketService.SetQuantities(BasketModel.Id, items); + + if (!ModelState.IsValid) + { + return; + } + + var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity); + await _basketService.SetQuantities(BasketModel.Id, updateModel); await SetBasketModelAsync(); } diff --git a/tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketAddItem.cs b/tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketAddItem.cs index 7f788a4..4b7aaff 100644 --- a/tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketAddItem.cs +++ b/tests/UnitTests/ApplicationCore/Entities/BasketTests/BasketAddItem.cs @@ -1,4 +1,5 @@ using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; +using System; using System.Linq; using Xunit; @@ -53,8 +54,8 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests var firstItem = basket.Items.Single(); Assert.Equal(1, firstItem.Quantity); - } - + } + [Fact] public void RemoveEmptyItems() { @@ -63,6 +64,23 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests basket.RemoveEmptyItems(); Assert.Equal(0, basket.Items.Count); + } + + [Fact] + public void CantAddItemWithNegativeQuantity() + { + var basket = new Basket(_buyerId); + + Assert.Throws(() => basket.AddItem(_testCatalogItemId, _testUnitPrice, -1)); + } + + [Fact] + public void CantModifyQuantityToNegativeNumber() + { + var basket = new Basket(_buyerId); + basket.AddItem(_testCatalogItemId, _testUnitPrice); + + Assert.Throws(() => basket.AddItem(_testCatalogItemId, _testUnitPrice, -2)); } } }