Prevent negative item orders (#392)
* Pulling changes over from previous branch * Adding exception and guard clause
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user