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

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.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<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));
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();

View File

@@ -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);

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

View File

@@ -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<CheckoutModel> _logger;
public CheckoutModel(IBasketService basketService,
IBasketViewModelService basketViewModelService,
SignInManager<ApplicationUser> signInManager,
IOrderService orderService)
IOrderService orderService,
IAppLogger<CheckoutModel> 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<IActionResult> OnPost(Dictionary<string, int> items)
public async Task<IActionResult> OnPost(IEnumerable<BasketItemViewModel> items)
{
try
{
await SetBasketModelAsync();
await _basketService.SetQuantities(BasketModel.Id, items);
if (!ModelState.IsValid)
{
return BadRequest();
}
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();
}

View File

@@ -23,6 +23,7 @@
<section class="esh-basket-title col-xs-2">Cost</section>
</article>
<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++)
{
var item = Model.BasketModel.Items[i];
@@ -34,20 +35,17 @@
<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">
<input type="hidden" name="@("Items[" + i + "].Key")" value="@item.Id" />
<input type="number" class="esh-basket-input" min="0" name="@("Items[" + i + "].Value")" value="@item.Quantity" />
<input type="hidden" name="@("Items[" + i + "].Id")" value="@item.Id" />
<input type="number" class="esh-basket-input" min="0" name="@("Items[" + i + "].Quantity")" value="@item.Quantity" />
</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 class="row">
</div>
</article>
@*<div class="esh-catalog-item col-md-4">
</article> @*<div class="esh-catalog-item col-md-4">
@item.ProductId
</div>*@
}
</div>*@ }
<div class="container">
<article class="esh-basket-titles esh-basket-titles--clean row">
@@ -77,16 +75,15 @@
asp-page-handler="Update">
[ Update ]
</button>
@{
var data = new Dictionary<string, string>
@{ var data = new Dictionary<string, string>
{
{ Constants.BASKET_ID, Model.BasketModel.Id.ToString() },
};
}
}; }
<input type="submit" asp-page="Checkout"
class="btn esh-basket-checkout"
asp-all-route-data=data
value="[ Checkout ]" name="action" />
</section>
</div>
</div>

View File

@@ -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<string, int> items)
public async Task OnPostUpdate(IEnumerable<BasketItemViewModel> 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();
}

View File

@@ -1,4 +1,5 @@
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using System;
using System.Linq;
using Xunit;
@@ -64,5 +65,22 @@ namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.BasketTests
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));
}
}
}