From d776ce7cb2e51026ea47c797fa5baef43a00e8d8 Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Fri, 22 Sep 2017 11:25:32 -0400 Subject: [PATCH] Order History (#49) * working on creating and viewing orders. * Working on wiring up listing of orders * List orders page works as expected. Needed to support ThenInclude scenarios. Currently using strings. --- .../OrderAggregate/CatalogItemOrdered.cs | 12 ++- .../Entities/OrderAggregate/Order.cs | 40 ++++---- .../Entities/OrderAggregate/OrderItem.cs | 22 ++++ src/ApplicationCore/Interfaces/IAppLogger.cs | 1 + .../Interfaces/IOrderRepository.cs | 12 +++ .../Interfaces/IOrderService.cs | 10 ++ src/ApplicationCore/Interfaces/IRepository.cs | 1 + .../Interfaces/ISpecification.cs | 1 + .../Interfaces/IUriComposer.cs | 6 +- .../BasketWithItemsSpecification.cs | 3 + .../CatalogFilterSpecification.cs | 2 + .../CustomerOrdersWithItemsSpecification.cs | 35 +++++++ src/Infrastructure/Data/CatalogContext.cs | 20 +++- src/Infrastructure/Data/EfRepository.cs | 21 ++-- src/Infrastructure/Data/OrderRepository.cs | 31 ++++++ src/Infrastructure/Infrastructure.csproj | 5 +- src/Infrastructure/Logging/LoggerAdapter.cs | 5 + src/Infrastructure/Services/OrderService.cs | 40 ++++++++ src/Web/Controllers/AccountController.cs | 6 ++ src/Web/Controllers/BasketController.cs | 21 ++-- src/Web/Controllers/OrderController.cs | 96 ++++++++++++++++++ src/Web/Interfaces/IBasketService.cs | 5 +- src/Web/Services/BasketService.cs | 4 +- src/Web/Startup.cs | 15 ++- src/Web/ViewModels/OrderItemViewModel.cs | 23 +++++ src/Web/ViewModels/OrderViewModel.cs | 21 ++++ src/Web/ViewModels/RegisterViewModel.cs | 3 +- src/Web/Views/Order/Detail.cshtml | 88 ++++++++++++++++ src/Web/Views/Order/Index.cshtml | 39 +++++++ .../Shared/Components/Basket/Default.cshtml | 2 +- src/Web/Views/Shared/_Layout.cshtml | 2 +- src/Web/compilerconfig.json | 8 +- .../wwwroot/css/orders/orders.component.css | 50 +++++++++ .../css/orders/orders.component.min.css | 1 + .../wwwroot/css/orders/orders.component.scss | 91 +++++++++++++++++ src/Web/wwwroot/images/brand_dark.png | Bin 5239 -> 0 bytes tests/FunctionalTests/FunctionalTests.csproj | 7 +- .../IntegrationTests/IntegrationTests.csproj | 5 + tests/UnitTests/UnitTests.csproj | 4 + 39 files changed, 698 insertions(+), 60 deletions(-) create mode 100644 src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs create mode 100644 src/ApplicationCore/Interfaces/IOrderRepository.cs create mode 100644 src/ApplicationCore/Interfaces/IOrderService.cs create mode 100644 src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs create mode 100644 src/Infrastructure/Data/OrderRepository.cs create mode 100644 src/Infrastructure/Services/OrderService.cs create mode 100644 src/Web/Controllers/OrderController.cs create mode 100644 src/Web/ViewModels/OrderItemViewModel.cs create mode 100644 src/Web/ViewModels/OrderViewModel.cs create mode 100644 src/Web/Views/Order/Detail.cshtml create mode 100644 src/Web/Views/Order/Index.cshtml create mode 100644 src/Web/wwwroot/css/orders/orders.component.css create mode 100644 src/Web/wwwroot/css/orders/orders.component.min.css create mode 100644 src/Web/wwwroot/css/orders/orders.component.scss delete mode 100644 src/Web/wwwroot/images/brand_dark.png diff --git a/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs index bba2327..481c6b2 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs @@ -6,7 +6,17 @@ /// public class CatalogItemOrdered // ValueObject { - public string CatalogItemId { get; private set; } + public CatalogItemOrdered(int catalogItemId, string productName, string pictureUri) + { + CatalogItemId = catalogItemId; + ProductName = productName; + PictureUri = pictureUri; + } + private CatalogItemOrdered() + { + // required by EF + } + public int CatalogItemId { get; private set; } public string ProductName { get; private set; } public string PictureUri { get; private set; } } diff --git a/src/ApplicationCore/Entities/OrderAggregate/Order.cs b/src/ApplicationCore/Entities/OrderAggregate/Order.cs index 4f4de52..55a1807 100644 --- a/src/ApplicationCore/Entities/OrderAggregate/Order.cs +++ b/src/ApplicationCore/Entities/OrderAggregate/Order.cs @@ -2,20 +2,31 @@ using Microsoft.eShopWeb.ApplicationCore.Entities; using System; using System.Collections.Generic; -using System.Text; namespace ApplicationCore.Entities.OrderAggregate { public class Order : BaseEntity, IAggregateRoot { + private Order() + { + } + + public Order(string buyerId, Address shipToAddress, List items) + { + ShipToAddress = shipToAddress; + _orderItems = items; + BuyerId = buyerId; + } + public string BuyerId { get; private set; } + public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now; public Address ShipToAddress { get; private set; } // DDD Patterns comment // Using a private collection field, better for DDD Aggregate's encapsulation // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection, - // but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour. - private readonly List _orderItems; + // but only through the method Order.AddOrderItem() which includes behavior. + private readonly List _orderItems = new List(); public IReadOnlyCollection OrderItems => _orderItems; // Using List<>.AsReadOnly() @@ -23,22 +34,15 @@ namespace ApplicationCore.Entities.OrderAggregate // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance) //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx - } - - public class OrderItem : BaseEntity - { - public CatalogItemOrdered ItemOrdered { get; private set; } - public decimal UnitPrice { get; private set; } - public int Units { get; private set; } - - protected OrderItem() + public decimal Total() { + var total = 0m; + foreach (var item in _orderItems) + { + total += item.UnitPrice * item.Units; + } + return total; } - public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) - { - ItemOrdered = itemOrdered; - UnitPrice = unitPrice; - Units = units; - } + } } diff --git a/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs new file mode 100644 index 0000000..d63824b --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs @@ -0,0 +1,22 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; + +namespace ApplicationCore.Entities.OrderAggregate +{ + + public class OrderItem : BaseEntity + { + public CatalogItemOrdered ItemOrdered { get; private set; } + public decimal UnitPrice { get; private set; } + public int Units { get; private set; } + + protected OrderItem() + { + } + public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) + { + ItemOrdered = itemOrdered; + UnitPrice = unitPrice; + Units = units; + } + } +} diff --git a/src/ApplicationCore/Interfaces/IAppLogger.cs b/src/ApplicationCore/Interfaces/IAppLogger.cs index 2f3f0bc..53036cd 100644 --- a/src/ApplicationCore/Interfaces/IAppLogger.cs +++ b/src/ApplicationCore/Interfaces/IAppLogger.cs @@ -6,6 +6,7 @@ /// public interface IAppLogger { + void LogInformation(string message, params object[] args); void LogWarning(string message, params object[] args); } } diff --git a/src/ApplicationCore/Interfaces/IOrderRepository.cs b/src/ApplicationCore/Interfaces/IOrderRepository.cs new file mode 100644 index 0000000..8eb9713 --- /dev/null +++ b/src/ApplicationCore/Interfaces/IOrderRepository.cs @@ -0,0 +1,12 @@ +using ApplicationCore.Entities.OrderAggregate; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + + public interface IOrderRepository : IRepository, IAsyncRepository + { + Order GetByIdWithItems(int id); + Task GetByIdWithItemsAsync(int id); + } +} diff --git a/src/ApplicationCore/Interfaces/IOrderService.cs b/src/ApplicationCore/Interfaces/IOrderService.cs new file mode 100644 index 0000000..3c37ab1 --- /dev/null +++ b/src/ApplicationCore/Interfaces/IOrderService.cs @@ -0,0 +1,10 @@ +using ApplicationCore.Entities.OrderAggregate; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + public interface IOrderService + { + Task CreateOrderAsync(int basketId, Address shippingAddress); + } +} diff --git a/src/ApplicationCore/Interfaces/IRepository.cs b/src/ApplicationCore/Interfaces/IRepository.cs index 91f670f..a448152 100644 --- a/src/ApplicationCore/Interfaces/IRepository.cs +++ b/src/ApplicationCore/Interfaces/IRepository.cs @@ -1,5 +1,6 @@ using Microsoft.eShopWeb.ApplicationCore.Entities; using System.Collections.Generic; +using System.Threading.Tasks; namespace ApplicationCore.Interfaces { diff --git a/src/ApplicationCore/Interfaces/ISpecification.cs b/src/ApplicationCore/Interfaces/ISpecification.cs index 6cff096..5b11734 100644 --- a/src/ApplicationCore/Interfaces/ISpecification.cs +++ b/src/ApplicationCore/Interfaces/ISpecification.cs @@ -8,6 +8,7 @@ namespace ApplicationCore.Interfaces { Expression> Criteria { get; } List>> Includes { get; } + List IncludeStrings { get; } void AddInclude(Expression> includeExpression); } } diff --git a/src/ApplicationCore/Interfaces/IUriComposer.cs b/src/ApplicationCore/Interfaces/IUriComposer.cs index 2e4c4ea..ccd7812 100644 --- a/src/ApplicationCore/Interfaces/IUriComposer.cs +++ b/src/ApplicationCore/Interfaces/IUriComposer.cs @@ -1,9 +1,5 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using System.Collections.Generic; - -namespace ApplicationCore.Interfaces +namespace ApplicationCore.Interfaces { - public interface IUriComposer { string ComposePicUri(string uriTemplate); diff --git a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs index a70c36e..a44fb77 100644 --- a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs +++ b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -3,6 +3,7 @@ using Microsoft.eShopWeb.ApplicationCore.Entities; using System; using System.Linq.Expressions; using System.Collections.Generic; +using ApplicationCore.Entities.OrderAggregate; namespace ApplicationCore.Specifications { @@ -28,6 +29,8 @@ namespace ApplicationCore.Specifications public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + public void AddInclude(Expression> includeExpression) { Includes.Add(includeExpression); diff --git a/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs b/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs index 30d414e..54e1f86 100644 --- a/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs +++ b/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs @@ -24,6 +24,8 @@ namespace ApplicationCore.Specifications public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + public void AddInclude(Expression> includeExpression) { Includes.Add(includeExpression); diff --git a/src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs b/src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs new file mode 100644 index 0000000..abfa9b6 --- /dev/null +++ b/src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs @@ -0,0 +1,35 @@ +using ApplicationCore.Interfaces; +using System; +using System.Linq.Expressions; +using System.Collections.Generic; +using ApplicationCore.Entities.OrderAggregate; + +namespace ApplicationCore.Specifications +{ + public class CustomerOrdersWithItemsSpecification : ISpecification + { + private readonly string _buyerId; + + public CustomerOrdersWithItemsSpecification(string buyerId) + { + _buyerId = buyerId; + AddInclude(o => o.OrderItems); + AddInclude("OrderItems.ItemOrdered"); + } + + public Expression> Criteria => o => o.BuyerId == _buyerId; + + public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + + public void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + + public void AddInclude(string includeString) + { + IncludeStrings.Add(includeString); + } + } +} diff --git a/src/Infrastructure/Data/CatalogContext.cs b/src/Infrastructure/Data/CatalogContext.cs index da13128..ec33708 100644 --- a/src/Infrastructure/Data/CatalogContext.cs +++ b/src/Infrastructure/Data/CatalogContext.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.EntityFrameworkCore.Metadata; +using ApplicationCore.Entities.OrderAggregate; namespace Infrastructure.Data { @@ -20,6 +21,8 @@ namespace Infrastructure.Data public DbSet CatalogItems { get; set; } public DbSet CatalogBrands { get; set; } public DbSet CatalogTypes { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderItems { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -27,6 +30,8 @@ namespace Infrastructure.Data builder.Entity(ConfigureCatalogBrand); builder.Entity(ConfigureCatalogType); builder.Entity(ConfigureCatalogItem); + builder.Entity(ConfigureOrder); + builder.Entity(ConfigureOrderItem); } private void ConfigureBasket(EntityTypeBuilder builder) @@ -36,7 +41,7 @@ namespace Infrastructure.Data navigation.SetPropertyAccessMode(PropertyAccessMode.Field); } - void ConfigureCatalogItem(EntityTypeBuilder builder) + private void ConfigureCatalogItem(EntityTypeBuilder builder) { builder.ToTable("Catalog"); @@ -63,7 +68,7 @@ namespace Infrastructure.Data .HasForeignKey(ci => ci.CatalogTypeId); } - void ConfigureCatalogBrand(EntityTypeBuilder builder) + private void ConfigureCatalogBrand(EntityTypeBuilder builder) { builder.ToTable("CatalogBrand"); @@ -78,7 +83,7 @@ namespace Infrastructure.Data .HasMaxLength(100); } - void ConfigureCatalogType(EntityTypeBuilder builder) + private void ConfigureCatalogType(EntityTypeBuilder builder) { builder.ToTable("CatalogType"); @@ -92,5 +97,14 @@ namespace Infrastructure.Data .IsRequired() .HasMaxLength(100); } + private void ConfigureOrder(EntityTypeBuilder builder) + { + builder.OwnsOne(o => o.ShipToAddress); + } + private void ConfigureOrderItem(EntityTypeBuilder builder) + { + builder.OwnsOne(i => i.ItemOrdered); + } + } } diff --git a/src/Infrastructure/Data/EfRepository.cs b/src/Infrastructure/Data/EfRepository.cs index 24bdc0c..cdc84fa 100644 --- a/src/Infrastructure/Data/EfRepository.cs +++ b/src/Infrastructure/Data/EfRepository.cs @@ -14,19 +14,19 @@ namespace Infrastructure.Data /// public class EfRepository : IRepository, IAsyncRepository where T : BaseEntity { - private readonly CatalogContext _dbContext; + protected readonly CatalogContext _dbContext; public EfRepository(CatalogContext dbContext) { _dbContext = dbContext; } - public T GetById(int id) + public virtual T GetById(int id) { return _dbContext.Set().Find(id); } - public async Task GetByIdAsync(int id) + public virtual async Task GetByIdAsync(int id) { return await _dbContext.Set().FindAsync(id); } @@ -45,8 +45,11 @@ namespace Infrastructure.Data { var queryableResultWithIncludes = spec.Includes .Aggregate(_dbContext.Set().AsQueryable(), - (current, include) => current.Include(include)); - return queryableResultWithIncludes + (current, include) => current.Include(include)); + var secondaryResult = spec.IncludeStrings + .Aggregate(queryableResultWithIncludes, + (current, include) => current.Include(include)); + return secondaryResult .Where(spec.Criteria) .AsEnumerable(); } @@ -54,8 +57,12 @@ namespace Infrastructure.Data { var queryableResultWithIncludes = spec.Includes .Aggregate(_dbContext.Set().AsQueryable(), - (current, include) => current.Include(include)); - return await queryableResultWithIncludes + (current, include) => current.Include(include)); + var secondaryResult = spec.IncludeStrings + .Aggregate(queryableResultWithIncludes, + (current, include) => current.Include(include)); + + return await secondaryResult .Where(spec.Criteria) .ToListAsync(); } diff --git a/src/Infrastructure/Data/OrderRepository.cs b/src/Infrastructure/Data/OrderRepository.cs new file mode 100644 index 0000000..35c5465 --- /dev/null +++ b/src/Infrastructure/Data/OrderRepository.cs @@ -0,0 +1,31 @@ +using ApplicationCore.Entities.OrderAggregate; +using ApplicationCore.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading.Tasks; + +namespace Infrastructure.Data +{ + public class OrderRepository : EfRepository, IOrderRepository + { + public OrderRepository(CatalogContext dbContext) : base(dbContext) + { + } + + public Order GetByIdWithItems(int id) + { + return _dbContext.Orders + .Include(o => o.OrderItems) + .Include("OrderItems.ItemOrdered") + .FirstOrDefault(); + } + + public Task GetByIdWithItemsAsync(int id) + { + return _dbContext.Orders + .Include(o => o.OrderItems) + .Include("OrderItems.ItemOrdered") + .FirstOrDefaultAsync(); + } + } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index ad865bf..4497c83 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -3,6 +3,10 @@ netcoreapp2.0 + + + 2.0.0 + @@ -17,7 +21,6 @@ - \ No newline at end of file diff --git a/src/Infrastructure/Logging/LoggerAdapter.cs b/src/Infrastructure/Logging/LoggerAdapter.cs index b79e843..93b5447 100644 --- a/src/Infrastructure/Logging/LoggerAdapter.cs +++ b/src/Infrastructure/Logging/LoggerAdapter.cs @@ -10,9 +10,14 @@ namespace Infrastructure.Logging { _logger = loggerFactory.CreateLogger(); } + public void LogWarning(string message, params object[] args) { _logger.LogWarning(message, args); } + public void LogInformation(string message, params object[] args) + { + _logger.LogInformation(message, args); + } } } diff --git a/src/Infrastructure/Services/OrderService.cs b/src/Infrastructure/Services/OrderService.cs new file mode 100644 index 0000000..d776285 --- /dev/null +++ b/src/Infrastructure/Services/OrderService.cs @@ -0,0 +1,40 @@ +using ApplicationCore.Interfaces; +using ApplicationCore.Entities.OrderAggregate; +using System.Threading.Tasks; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; + +namespace Infrastructure.Services +{ + public class OrderService : IOrderService + { + private readonly IAsyncRepository _orderRepository; + private readonly IAsyncRepository _basketRepository; + private readonly IAsyncRepository _itemRepository; + + public OrderService(IAsyncRepository basketRepository, + IAsyncRepository itemRepository, + IAsyncRepository orderRepository) + { + _orderRepository = orderRepository; + _basketRepository = basketRepository; + _itemRepository = itemRepository; + } + + public async Task CreateOrderAsync(int basketId, Address shippingAddress) + { + var basket = await _basketRepository.GetByIdAsync(basketId); + var items = new List(); + foreach (var item in basket.Items) + { + var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId); + var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri); + var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity); + items.Add(orderItem); + } + var order = new Order(basket.BuyerId, shippingAddress, items); + + await _orderRepository.AddAsync(order); + } + } +} diff --git a/src/Web/Controllers/AccountController.cs b/src/Web/Controllers/AccountController.cs index 1c7b974..b78e110 100644 --- a/src/Web/Controllers/AccountController.cs +++ b/src/Web/Controllers/AccountController.cs @@ -36,6 +36,12 @@ namespace Microsoft.eShopWeb.Controllers await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); ViewData["ReturnUrl"] = returnUrl; + if (!String.IsNullOrEmpty(returnUrl) && + returnUrl.ToLower().Contains("checkout")) + { + ViewData["ReturnUrl"] = "/Basket/Index"; + } + return View(); } diff --git a/src/Web/Controllers/BasketController.cs b/src/Web/Controllers/BasketController.cs index 55e58fa..a1ce980 100644 --- a/src/Web/Controllers/BasketController.cs +++ b/src/Web/Controllers/BasketController.cs @@ -8,6 +8,8 @@ using Infrastructure.Identity; using System; using Web; using System.Collections.Generic; +using ApplicationCore.Entities.OrderAggregate; +using Microsoft.AspNetCore.Authorization; namespace Microsoft.eShopWeb.Controllers { @@ -19,8 +21,10 @@ namespace Microsoft.eShopWeb.Controllers private readonly IUriComposer _uriComposer; private readonly SignInManager _signInManager; private readonly IAppLogger _logger; + private readonly IOrderService _orderService; public BasketController(IBasketService basketService, + IOrderService orderService, IUriComposer uriComposer, SignInManager signInManager, IAppLogger logger) @@ -29,6 +33,7 @@ namespace Microsoft.eShopWeb.Controllers _uriComposer = uriComposer; _signInManager = signInManager; _logger = logger; + _orderService = orderService; } [HttpGet] @@ -65,19 +70,15 @@ namespace Microsoft.eShopWeb.Controllers } [HttpPost] - public async Task Checkout(List model) + [Authorize] + public async Task Checkout(Dictionary items) { - // TODO: Get model binding working with collection of items - var basket = await GetBasketViewModelAsync(); - //await _basketService.SetQuantities(basket.Id, quantities); + var basketViewModel = await GetBasketViewModelAsync(); + await _basketService.SetQuantities(basketViewModel.Id, items); - foreach (var item in basket.Items) - { - _logger.LogWarning($"Id: {item.Id}; Qty: {item.Quantity}"); - } - // redirect to OrdersController + await _orderService.CreateOrderAsync(basketViewModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); - await _basketService.Checkout(basket.Id); + await _basketService.DeleteBasketAsync(basketViewModel.Id); return View("Checkout"); } diff --git a/src/Web/Controllers/OrderController.cs b/src/Web/Controllers/OrderController.cs new file mode 100644 index 0000000..65ad091 --- /dev/null +++ b/src/Web/Controllers/OrderController.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.eShopWeb.ViewModels; +using System; +using ApplicationCore.Entities.OrderAggregate; +using ApplicationCore.Interfaces; +using System.Linq; +using ApplicationCore.Specifications; + +namespace Microsoft.eShopWeb.Controllers +{ + [Authorize] + [Route("[controller]/[action]")] + public class OrderController : Controller + { + private readonly IOrderRepository _orderRepository; + + public OrderController(IOrderRepository orderRepository) { + _orderRepository = orderRepository; + } + + public async Task Index() + { + var orders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name)); + + var viewModel = orders + .Select(o => new OrderViewModel() + { + OrderDate = o.OrderDate, + OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel() + { + Discount = 0, + PictureUrl = oi.ItemOrdered.PictureUri, + ProductId = oi.ItemOrdered.CatalogItemId, + ProductName = oi.ItemOrdered.ProductName, + UnitPrice = oi.UnitPrice, + Units = oi.Units + }).ToList(), + OrderNumber = o.Id, + ShippingAddress = o.ShipToAddress, + Status = "Pending", + Total = o.Total() + + }); + return View(viewModel); + } + + [HttpGet("{orderId}")] + public async Task Detail(int orderId) + { + var order = await _orderRepository.GetByIdWithItemsAsync(orderId); + var viewModel = new OrderViewModel() + { + OrderDate = order.OrderDate, + OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel() + { + Discount = 0, + PictureUrl = oi.ItemOrdered.PictureUri, + ProductId = oi.ItemOrdered.CatalogItemId, + ProductName = oi.ItemOrdered.ProductName, + UnitPrice = oi.UnitPrice, + Units = oi.Units + }).ToList(), + OrderNumber = order.Id, + ShippingAddress = order.ShipToAddress, + Status = "Pending", + Total = order.Total() + }; + return View(viewModel); + } + + private OrderViewModel GetOrder() + { + var order = new OrderViewModel() + { + OrderDate = DateTimeOffset.Now.AddDays(-1), + OrderNumber = 12354, + Status = "Submitted", + Total = 123.45m, + ShippingAddress = new Address("123 Main St.", "Kent", "OH", "United States", "44240") + }; + + order.OrderItems.Add(new OrderItemViewModel() + { + ProductId = 1, + PictureUrl = "", + ProductName = "Something", + UnitPrice = 5.05m, + Units = 2 + }); + + return order; + } + } +} diff --git a/src/Web/Interfaces/IBasketService.cs b/src/Web/Interfaces/IBasketService.cs index e04e551..c76e42a 100644 --- a/src/Web/Interfaces/IBasketService.cs +++ b/src/Web/Interfaces/IBasketService.cs @@ -1,5 +1,4 @@ -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ViewModels; +using Microsoft.eShopWeb.ViewModels; using System.Collections.Generic; using System.Threading.Tasks; @@ -11,6 +10,6 @@ namespace ApplicationCore.Interfaces Task TransferBasketAsync(string anonymousId, string userName); Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity); Task SetQuantities(int basketId, Dictionary quantities); - Task Checkout(int basketId); + Task DeleteBasketAsync(int basketId); } } diff --git a/src/Web/Services/BasketService.cs b/src/Web/Services/BasketService.cs index 1900be6..2065987 100644 --- a/src/Web/Services/BasketService.cs +++ b/src/Web/Services/BasketService.cs @@ -98,12 +98,10 @@ namespace Web.Services await _basketRepository.UpdateAsync(basket); } - public async Task Checkout(int basketId) + public async Task DeleteBasketAsync(int basketId) { var basket = await _basketRepository.GetByIdAsync(basketId); - // TODO: Actually Process the order - await _basketRepository.DeleteAsync(basket); } diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index ec3d628..87c90de 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -1,8 +1,10 @@ -using ApplicationCore.Interfaces; +using ApplicationCore.Entities.OrderAggregate; +using ApplicationCore.Interfaces; using ApplicationCore.Services; using Infrastructure.Data; using Infrastructure.Identity; using Infrastructure.Logging; +using Infrastructure.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -12,6 +14,7 @@ using Microsoft.eShopWeb.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System; using System.Text; using Web.Services; @@ -53,12 +56,22 @@ namespace Microsoft.eShopWeb .AddEntityFrameworkStores() .AddDefaultTokenProviders(); + services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = true; + options.ExpireTimeSpan = TimeSpan.FromHours(1); + options.LoginPath = "/Account/Signin"; + options.LogoutPath = "/Account/Signout"; + }); + services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); services.AddMemoryCache(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.Configure(Configuration); services.AddSingleton(new UriComposer(Configuration.Get())); diff --git a/src/Web/ViewModels/OrderItemViewModel.cs b/src/Web/ViewModels/OrderItemViewModel.cs new file mode 100644 index 0000000..84819da --- /dev/null +++ b/src/Web/ViewModels/OrderItemViewModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.eShopWeb.ViewModels +{ + + public class OrderItemViewModel + { + public int ProductId { get; set; } + + public string ProductName { get; set; } + + public decimal UnitPrice { get; set; } + + public decimal Discount { get; set; } + + public int Units { get; set; } + + public string PictureUrl { get; set; } + } + +} diff --git a/src/Web/ViewModels/OrderViewModel.cs b/src/Web/ViewModels/OrderViewModel.cs new file mode 100644 index 0000000..caebd89 --- /dev/null +++ b/src/Web/ViewModels/OrderViewModel.cs @@ -0,0 +1,21 @@ +using ApplicationCore.Entities.OrderAggregate; +using System; +using System.Collections.Generic; + +namespace Microsoft.eShopWeb.ViewModels +{ + + public class OrderViewModel + { + public int OrderNumber { get; set; } + public DateTimeOffset OrderDate { get; set; } + public decimal Total { get; set; } + public string Status { get; set; } + + public Address ShippingAddress { get; set; } + + public List OrderItems { get; set; } = new List(); + + } + +} diff --git a/src/Web/ViewModels/RegisterViewModel.cs b/src/Web/ViewModels/RegisterViewModel.cs index 1724461..be6e2c3 100644 --- a/src/Web/ViewModels/RegisterViewModel.cs +++ b/src/Web/ViewModels/RegisterViewModel.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.ViewModels { diff --git a/src/Web/Views/Order/Detail.cshtml b/src/Web/Views/Order/Detail.cshtml new file mode 100644 index 0000000..9cc7656 --- /dev/null +++ b/src/Web/Views/Order/Detail.cshtml @@ -0,0 +1,88 @@ +@using Microsoft.eShopWeb.ViewModels +@model OrderViewModel +@{ + ViewData["Title"] = "My Order History"; +} +@{ + ViewData["Title"] = "Order Detail"; +} + +
+
+
+
+
Order number
+
Date
+
Total
+
Status
+
+ +
+
@Model.OrderNumber
+
@Model.OrderDate
+
$@Model.Total
+
@Model.Status
+
+
+ + @*
+
+
Description
+
+ +
+
@Model.Description
+
+
*@ + +
+
+
Shipping Address
+
+ +
+
@Model.ShippingAddress.Street
+
+ +
+
@Model.ShippingAddress.City
+
+ +
+
@Model.ShippingAddress.Country
+
+
+ +
+
+
ORDER DETAILS
+
+ + @for (int i = 0; i < Model.OrderItems.Count; i++) + { + var item = Model.OrderItems[i]; +
+
+ +
+
@item.ProductName
+
$ @item.UnitPrice.ToString("N2")
+
@item.Units
+
$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")
+
+ } +
+ +
+
+
+
TOTAL
+
+ +
+
+
$ @Model.Total
+
+
+
+
diff --git a/src/Web/Views/Order/Index.cshtml b/src/Web/Views/Order/Index.cshtml new file mode 100644 index 0000000..838ff58 --- /dev/null +++ b/src/Web/Views/Order/Index.cshtml @@ -0,0 +1,39 @@ +@using Microsoft.eShopWeb.ViewModels +@model IEnumerable +@{ + ViewData["Title"] = "My Order History"; +} + +
+
+

@ViewData["Title"]

+
+
Order number
+
Date
+
Total
+
Status
+
+
+ @if (Model != null && Model.Any()) + { + @foreach (var item in Model) + { +
+
@Html.DisplayFor(modelItem => item.OrderNumber)
+
@Html.DisplayFor(modelItem => item.OrderDate)
+
$ @Html.DisplayFor(modelItem => item.Total)
+
@Html.DisplayFor(modelItem => item.Status)
+
+ Detail +
+
+ @if (item.Status.ToLower() == "submitted") + { + Cancel + } +
+
+ } + } +
+
diff --git a/src/Web/Views/Shared/Components/Basket/Default.cshtml b/src/Web/Views/Shared/Components/Basket/Default.cshtml index c919cf5..56d4fd0 100644 --- a/src/Web/Views/Shared/Components/Basket/Default.cshtml +++ b/src/Web/Views/Shared/Components/Basket/Default.cshtml @@ -6,7 +6,7 @@
diff --git a/src/Web/Views/Shared/_Layout.cshtml b/src/Web/Views/Shared/_Layout.cshtml index 03a2be6..f533790 100644 --- a/src/Web/Views/Shared/_Layout.cshtml +++ b/src/Web/Views/Shared/_Layout.cshtml @@ -20,6 +20,7 @@ +