Prevent negative item orders (#392)

* Pulling changes over from previous branch

* Adding exception and guard clause
This commit is contained in:
Eric Fleming
2020-06-12 21:06:23 -04:00
committed by GitHub
parent 0af21d22f5
commit 248b8ed632
10 changed files with 133 additions and 46 deletions

View File

@@ -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 public class BasketItem : BaseEntity
{ {
@@ -11,17 +14,21 @@
public BasketItem(int catalogItemId, int quantity, decimal unitPrice) public BasketItem(int catalogItemId, int quantity, decimal unitPrice)
{ {
CatalogItemId = catalogItemId; CatalogItemId = catalogItemId;
Quantity = quantity;
UnitPrice = unitPrice; UnitPrice = unitPrice;
SetQuantity(quantity);
} }
public void AddQuantity(int quantity) public void AddQuantity(int quantity)
{ {
Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue);
Quantity += quantity; Quantity += quantity;
} }
public void SetNewQuantity(int quantity) public void SetQuantity(int quantity)
{ {
Guard.Against.OutOfRange(quantity, nameof(quantity), 0, int.MaxValue);
Quantity = quantity; Quantity = quantity;
} }
} }

View File

@@ -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)
{
}
}
}

View File

@@ -1,5 +1,7 @@
using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using System.Collections.Generic;
using System.Linq;
namespace Ardalis.GuardClauses namespace Ardalis.GuardClauses
{ {
@@ -10,5 +12,11 @@ namespace Ardalis.GuardClauses
if (basket == null) if (basket == null)
throw new BasketNotFoundException(basketId); throw new BasketNotFoundException(basketId);
} }
public static void EmptyBasketOnCheckout(this IGuardClause guardClause, IReadOnlyCollection<BasketItem> basketItems)
{
if (!basketItems.Any())
throw new EmptyBasketOnCheckoutException();
}
} }
} }

View File

@@ -55,12 +55,13 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
Guard.Against.Null(quantities, nameof(quantities)); Guard.Against.Null(quantities, nameof(quantities));
var basket = await _basketRepository.GetByIdAsync(basketId); var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket); Guard.Against.NullBasket(basketId, basket);
foreach (var item in basket.Items) foreach (var item in basket.Items)
{ {
if (quantities.TryGetValue(item.Id.ToString(), out var quantity)) if (quantities.TryGetValue(item.Id.ToString(), out var quantity))
{ {
if (_logger != null) _logger.LogInformation($"Updating quantity of item ID:{item.Id} to {quantity}."); if (_logger != null) _logger.LogInformation($"Updating quantity of item ID:{item.Id} to {quantity}.");
item.SetNewQuantity(quantity); item.SetQuantity(quantity);
} }
} }
basket.RemoveEmptyItems(); basket.RemoveEmptyItems();

View File

@@ -33,6 +33,7 @@ namespace Microsoft.eShopWeb.ApplicationCore.Services
var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec); var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);
Guard.Against.NullBasket(basketId, basket); Guard.Against.NullBasket(basketId, basket);
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray()); var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification); var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);

View File

@@ -1,4 +1,6 @@
namespace Microsoft.eShopWeb.Web.Pages.Basket using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.Pages.Basket
{ {
public class BasketItemViewModel public class BasketItemViewModel
{ {
@@ -7,7 +9,10 @@
public string ProductName { get; set; } public string ProductName { get; set; }
public decimal UnitPrice { get; set; } public decimal UnitPrice { get; set; }
public decimal OldUnitPrice { get; set; } public decimal OldUnitPrice { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")]
public int Quantity { get; set; } public int Quantity { get; set; }
public string PictureUrl { get; set; } public string PictureUrl { get; set; }
} }
} }

View File

@@ -3,11 +3,13 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity; using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Interfaces; using Microsoft.eShopWeb.Web.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Basket namespace Microsoft.eShopWeb.Web.Pages.Basket
@@ -19,16 +21,19 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket
private readonly IOrderService _orderService; private readonly IOrderService _orderService;
private string _username = null; private string _username = null;
private readonly IBasketViewModelService _basketViewModelService; private readonly IBasketViewModelService _basketViewModelService;
private readonly IAppLogger<CheckoutModel> _logger;
public CheckoutModel(IBasketService basketService, public CheckoutModel(IBasketService basketService,
IBasketViewModelService basketViewModelService, IBasketViewModelService basketViewModelService,
SignInManager<ApplicationUser> signInManager, SignInManager<ApplicationUser> signInManager,
IOrderService orderService) IOrderService orderService,
IAppLogger<CheckoutModel> logger)
{ {
_basketService = basketService; _basketService = basketService;
_signInManager = signInManager; _signInManager = signInManager;
_orderService = orderService; _orderService = orderService;
_basketViewModelService = basketViewModelService; _basketViewModelService = basketViewModelService;
_logger = logger;
} }
public BasketViewModel BasketModel { get; set; } = new BasketViewModel(); public BasketViewModel BasketModel { get; set; } = new BasketViewModel();
@@ -44,15 +49,28 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket
} }
} }
public async Task<IActionResult> OnPost(Dictionary<string, int> items) public async Task<IActionResult> OnPost(IEnumerable<BasketItemViewModel> 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")); var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity);
await _basketService.SetQuantities(BasketModel.Id, updateModel);
await _basketService.DeleteBasketAsync(BasketModel.Id); 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(); return RedirectToPage();
} }

View File

@@ -23,31 +23,29 @@
<section class="esh-basket-title col-xs-2">Cost</section> <section class="esh-basket-title col-xs-2">Cost</section>
</article> </article>
<div class="esh-catalog-items row"> <div class="esh-catalog-items row">
<div asp-validation-summary="All" class="text-danger"></div>
@for (int i = 0; i < Model.BasketModel.Items.Count; i++) @for (int i = 0; i < Model.BasketModel.Items.Count; i++)
{ {
var item = Model.BasketModel.Items[i]; var item = Model.BasketModel.Items[i];
<article class="esh-basket-items row"> <article class="esh-basket-items row">
<div> <div>
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down"> <section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
<img class="esh-basket-image" src="@item.PictureUrl" /> <img class="esh-basket-image" src="@item.PictureUrl" />
</section> </section>
<section class="esh-basket-item esh-basket-item--middle col-xs-3">@item.ProductName</section> <section class="esh-basket-item esh-basket-item--middle col-xs-3">@item.ProductName</section>
<section class="esh-basket-item esh-basket-item--middle col-xs-2">$ @item.UnitPrice.ToString("N2")</section> <section class="esh-basket-item esh-basket-item--middle col-xs-2">$ @item.UnitPrice.ToString("N2")</section>
<section class="esh-basket-item esh-basket-item--middle col-xs-2"> <section class="esh-basket-item esh-basket-item--middle col-xs-2">
<input type="hidden" name="@("Items[" + i + "].Key")" value="@item.Id" /> <input type="hidden" name="@("Items[" + i + "].Id")" value="@item.Id" />
<input type="number" class="esh-basket-input" min="0" name="@("Items[" + i + "].Value")" value="@item.Quantity" /> <input type="number" class="esh-basket-input" min="0" name="@("Items[" + i + "].Quantity")" value="@item.Quantity" />
</section> </section>
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")</section> <section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")</section>
</div> </div>
<div class="row"> <div class="row">
</div>
</article>
@*<div class="esh-catalog-item col-md-4">
@item.ProductId
</div>*@
} </div>
</article> @*<div class="esh-catalog-item col-md-4">
@item.ProductId
</div>*@ }
<div class="container"> <div class="container">
<article class="esh-basket-titles esh-basket-titles--clean row"> <article class="esh-basket-titles esh-basket-titles--clean row">
@@ -77,16 +75,15 @@
asp-page-handler="Update"> asp-page-handler="Update">
[ Update ] [ Update ]
</button> </button>
@{ @{ var data = new Dictionary<string, string>
var data = new Dictionary<string, string> {
{
{ Constants.BASKET_ID, Model.BasketModel.Id.ToString() }, { Constants.BASKET_ID, Model.BasketModel.Id.ToString() },
}; }; }
}
<input type="submit" asp-page="Checkout" <input type="submit" asp-page="Checkout"
class="btn esh-basket-checkout" class="btn esh-basket-checkout"
asp-all-route-data=data value="[ Checkout ]" name="action" />
value="[ Checkout ]" name="action" />
</section> </section>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.ViewModels; using Microsoft.eShopWeb.Web.ViewModels;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Basket namespace Microsoft.eShopWeb.Web.Pages.Basket
@@ -50,10 +51,17 @@ namespace Microsoft.eShopWeb.Web.Pages.Basket
return RedirectToPage(); return RedirectToPage();
} }
public async Task OnPostUpdate(Dictionary<string, int> items) public async Task OnPostUpdate(IEnumerable<BasketItemViewModel> items)
{ {
await SetBasketModelAsync(); 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(); await SetBasketModelAsync();
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using System;
using System.Linq; using System.Linq;
using Xunit; using Xunit;
@@ -53,8 +54,8 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
var firstItem = basket.Items.Single(); var firstItem = basket.Items.Single();
Assert.Equal(1, firstItem.Quantity); Assert.Equal(1, firstItem.Quantity);
} }
[Fact] [Fact]
public void RemoveEmptyItems() public void RemoveEmptyItems()
{ {
@@ -63,6 +64,23 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
basket.RemoveEmptyItems(); basket.RemoveEmptyItems();
Assert.Equal(0, basket.Items.Count); Assert.Equal(0, basket.Items.Count);
}
[Fact]
public void CantAddItemWithNegativeQuantity()
{
var basket = new Basket(_buyerId);
Assert.Throws<ArgumentOutOfRangeException>(() => basket.AddItem(_testCatalogItemId, _testUnitPrice, -1));
}
[Fact]
public void CantModifyQuantityToNegativeNumber()
{
var basket = new Basket(_buyerId);
basket.AddItem(_testCatalogItemId, _testUnitPrice);
Assert.Throws<ArgumentOutOfRangeException>(() => basket.AddItem(_testCatalogItemId, _testUnitPrice, -2));
} }
} }
} }