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
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user