add ordering completed process
This commit is contained in:
		
							parent
							
								
									06d5164532
								
							
						
					
					
						commit
						1baca5bdec
					
				| @ -0,0 +1,13 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | ||||
| 
 | ||||
| public class CompleteOrderCommand : IRequest<bool> | ||||
| { | ||||
| 
 | ||||
|     [DataMember] | ||||
|     public int OrderNumber { get; private set; } | ||||
| 
 | ||||
|     public CompleteOrderCommand(int orderNumber) | ||||
|     { | ||||
|         OrderNumber = orderNumber; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | ||||
| 
 | ||||
| // Regular CommandHandler | ||||
| public class CompleteOrderCommandHandler : IRequestHandler<CompleteOrderCommand, bool> | ||||
| { | ||||
|     private readonly IOrderRepository _orderRepository; | ||||
| 
 | ||||
|     public CompleteOrderCommandHandler(IOrderRepository orderRepository) | ||||
|     { | ||||
|         _orderRepository = orderRepository; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Handler which processes the command when | ||||
|     /// administrator executes complete order from app | ||||
|     /// </summary> | ||||
|     /// <param name="command"></param> | ||||
|     /// <returns></returns> | ||||
|     public async Task<bool> Handle(CompleteOrderCommand command, CancellationToken cancellationToken) | ||||
|     { | ||||
|         var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber); | ||||
|         if (orderToUpdate == null) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         orderToUpdate.SetCompletedStatus(); | ||||
|         return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // Use for Idempotency in Command process | ||||
| public class CompleteOrderIdentifiedCommandHandler : IdentifiedCommandHandler<CompleteOrderCommand, bool> | ||||
| { | ||||
|     public CompleteOrderIdentifiedCommandHandler( | ||||
|         IMediator mediator, | ||||
|         IRequestManager requestManager, | ||||
|         ILogger<IdentifiedCommandHandler<CompleteOrderCommand, bool>> logger) | ||||
|         : base(mediator, requestManager, logger) | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     protected override bool CreateResultForDuplicateRequest() | ||||
|     { | ||||
|         return true; // Ignore duplicate requests for processing order. | ||||
|     } | ||||
| } | ||||
| @ -70,6 +70,11 @@ public abstract class IdentifiedCommandHandler<T, R> : IRequestHandler<Identifie | ||||
|                         commandId = $"{shipOrderCommand.OrderNumber}"; | ||||
|                         break; | ||||
| 
 | ||||
|                     case CompleteOrderCommand completeOrderCommand: | ||||
|                         idProperty = nameof(completeOrderCommand.OrderNumber); | ||||
|                         commandId = $"{completeOrderCommand.OrderNumber}"; | ||||
|                         break; | ||||
| 
 | ||||
|                     default: | ||||
|                         idProperty = "Id?"; | ||||
|                         commandId = "n/a"; | ||||
|  | ||||
| @ -0,0 +1,29 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; | ||||
| 
 | ||||
| public partial class OrderCompletedDomainEventHandler : INotificationHandler<OrderCompletedDomainEvent> | ||||
| { | ||||
|     private readonly IOrderRepository _orderRepository; | ||||
|     private readonly IBuyerRepository _buyerRepository; | ||||
|     private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; | ||||
|     private readonly ILogger _logger; | ||||
| 
 | ||||
|     public OrderCompletedDomainEventHandler(IOrderRepository orderRepository, ILogger<OrderCompletedDomainEventHandler> logger, IBuyerRepository buyerRepository, IOrderingIntegrationEventService orderingIntegrationEventService) | ||||
|     { | ||||
|         _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); | ||||
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||||
|         _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository)); | ||||
|         _orderingIntegrationEventService = orderingIntegrationEventService; | ||||
|     } | ||||
| 
 | ||||
|     public async Task Handle(OrderCompletedDomainEvent domainEvent, CancellationToken cancellationToken) | ||||
|     { | ||||
|         OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.Order.Id, nameof(OrderStatus.Completed), OrderStatus.Completed.Id); | ||||
| 
 | ||||
|         var order = await _orderRepository.GetAsync(domainEvent.Order.Id); | ||||
|         var buyer = await _buyerRepository.FindByIdAsync(order.GetBuyerId.Value.ToString()); | ||||
| 
 | ||||
|         var integrationEvent = new OrderStatusChangedToCompletedIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.Name); | ||||
| 
 | ||||
|         await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,15 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents.Events; | ||||
| 
 | ||||
| public record OrderStatusChangedToCompletedIntegrationEvent : IntegrationEvent | ||||
| { | ||||
|     public int OrderId { get; } | ||||
|     public string OrderStatus { get; } | ||||
|     public string BuyerName { get; } | ||||
| 
 | ||||
|     public OrderStatusChangedToCompletedIntegrationEvent(int orderId, string orderStatus, string buyerName) | ||||
|     { | ||||
|         OrderId = orderId; | ||||
|         OrderStatus = orderStatus; | ||||
|         BuyerName = buyerName; | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,8 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents; | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.IntegrationEvents; | ||||
| 
 | ||||
| public class OrderingIntegrationEventService : IOrderingIntegrationEventService | ||||
| { | ||||
|  | ||||
| @ -0,0 +1,11 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Validations; | ||||
| 
 | ||||
| public class CompleteOrderCommandValidator : AbstractValidator<CompleteOrderCommand> | ||||
| { | ||||
|     public CompleteOrderCommandValidator(ILogger<CompleteOrderCommandValidator> logger) | ||||
|     { | ||||
|         RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); | ||||
| 
 | ||||
|         logger.LogTrace("INSTANCE CREATED - {ClassName}", GetType().Name); | ||||
|     } | ||||
| } | ||||
| @ -1,7 +1,9 @@ | ||||
| using CardType = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries.CardType; | ||||
| using Order = Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries.Order; | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; | ||||
| 
 | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers; | ||||
| using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions; | ||||
| using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | ||||
| using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; | ||||
| using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; | ||||
| 
 | ||||
| [Route("api/v1/[controller]")]
 | ||||
| [Authorize] | ||||
| @ -85,11 +87,42 @@ public class OrdersController : ControllerBase | ||||
|         return Ok(); | ||||
|     } | ||||
| 
 | ||||
|     [Route("complete")] | ||||
|     [HttpPut] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status400BadRequest)] | ||||
|     public async Task<IActionResult> CompleteOrderAsync([FromBody] CompleteOrderCommand command, [FromHeader(Name = "x-requestid")] string requestId) | ||||
|     { | ||||
|         bool commandResult = false; | ||||
| 
 | ||||
|         if (Guid.TryParse(requestId, out Guid guid) && guid != Guid.Empty) | ||||
|         { | ||||
|             var requestCompleteOrder = new IdentifiedCommand<CompleteOrderCommand, bool>(command, guid); | ||||
| 
 | ||||
|             _logger.LogInformation( | ||||
|                 "Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})", | ||||
|                 requestCompleteOrder.GetGenericTypeName(), | ||||
|                 nameof(requestCompleteOrder.Command.OrderNumber), | ||||
|                 requestCompleteOrder.Command.OrderNumber, | ||||
|                 requestCompleteOrder); | ||||
| 
 | ||||
|             commandResult = await _mediator.Send(requestCompleteOrder); | ||||
|         } | ||||
| 
 | ||||
|         if (!commandResult) | ||||
|         { | ||||
|             return BadRequest(); | ||||
|         } | ||||
| 
 | ||||
|         return Ok(); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     [Route("{orderId:int}")] | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(typeof(Order), StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<Order>> GetOrderAsync(int orderId) | ||||
|     public async Task<ActionResult> GetOrderAsync(int orderId) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
| @ -97,7 +130,7 @@ public class OrdersController : ControllerBase | ||||
|             //var order customer = await _mediator.Send(new GetOrderByIdQuery(orderId)); | ||||
|             var order = await _orderQueries.GetOrderAsync(orderId); | ||||
| 
 | ||||
|             return order; | ||||
|             return Ok(order); | ||||
|         } | ||||
|         catch | ||||
|         { | ||||
| @ -106,7 +139,7 @@ public class OrdersController : ControllerBase | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(typeof(IEnumerable<OrderSummary>), StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(typeof(IEnumerable<OrderSummary>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<IEnumerable<OrderSummary>>> GetOrdersAsync() | ||||
|     { | ||||
|         var userid = _identityService.GetUserIdentity(); | ||||
| @ -117,7 +150,7 @@ public class OrdersController : ControllerBase | ||||
| 
 | ||||
|     [Route("cardtypes")] | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(typeof(IEnumerable<CardType>), StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(typeof(IEnumerable<CardType>), (int)HttpStatusCode.OK)] | ||||
|     public async Task<ActionResult<IEnumerable<CardType>>> GetCardTypesAsync() | ||||
|     { | ||||
|         var cardTypes = await _orderQueries.GetCardTypesAsync(); | ||||
|  | ||||
| @ -1,5 +1,8 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.API.Extensions; | ||||
| 
 | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Models; | ||||
| 
 | ||||
| public static class BasketItemExtensions | ||||
| { | ||||
|     public static IEnumerable<OrderItemDTO> ToOrderItemsDTO(this IEnumerable<BasketItem> basketItems) | ||||
|  | ||||
| @ -1,6 +1,13 @@ | ||||
| global using System.Data.Common; | ||||
| global using System; | ||||
| global using System.Collections.Generic; | ||||
| global using System.Data.Common; | ||||
| global using System.Data.SqlClient; | ||||
| global using System.IO; | ||||
| global using System.Linq; | ||||
| global using System.Net; | ||||
| global using System.Runtime.Serialization; | ||||
| global using System.Threading; | ||||
| global using System.Threading.Tasks; | ||||
| global using Azure.Identity; | ||||
| global using Dapper; | ||||
| global using FluentValidation; | ||||
| @ -8,6 +15,9 @@ global using Google.Protobuf.Collections; | ||||
| global using Grpc.Core; | ||||
| global using MediatR; | ||||
| global using Microsoft.AspNetCore.Authorization; | ||||
| global using Microsoft.AspNetCore.Builder; | ||||
| global using Microsoft.AspNetCore.Hosting; | ||||
| global using Microsoft.AspNetCore.Http; | ||||
| global using Microsoft.AspNetCore.Mvc; | ||||
| global using Microsoft.EntityFrameworkCore; | ||||
| global using Microsoft.EntityFrameworkCore.Design; | ||||
| @ -36,6 +46,9 @@ global using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories; | ||||
| global using Microsoft.Extensions.Configuration; | ||||
| global using Microsoft.Extensions.DependencyInjection; | ||||
| global using Microsoft.Extensions.Logging; | ||||
| global using Microsoft.Extensions.Options; | ||||
| global using Polly; | ||||
| global using Polly.Retry; | ||||
|  | ||||
| @ -26,6 +26,7 @@ services.AddSingleton<IValidator<CancelOrderCommand>, CancelOrderCommandValidato | ||||
| services.AddSingleton<IValidator<CreateOrderCommand>, CreateOrderCommandValidator>(); | ||||
| services.AddSingleton<IValidator<IdentifiedCommand<CreateOrderCommand, bool>>, IdentifiedCommandValidator>(); | ||||
| services.AddSingleton<IValidator<ShipOrderCommand>, ShipOrderCommandValidator>(); | ||||
| services.AddSingleton<IValidator<CompleteOrderCommand>, CompleteOrderCommandValidator>(); | ||||
| 
 | ||||
| services.AddScoped<IOrderQueries>(sp => new OrderQueries(builder.Configuration.GetConnectionString("OrderingDB"))); | ||||
| services.AddScoped<IBuyerRepository, BuyerRepository>(); | ||||
|  | ||||
| @ -5,3 +5,4 @@ StockConfirmed | ||||
| Paid | ||||
| Shipped | ||||
| Cancelled | ||||
| Completed | ||||
| 
 | 
| @ -156,6 +156,18 @@ public class Order | ||||
|         AddDomainEvent(new OrderCancelledDomainEvent(this)); | ||||
|     } | ||||
| 
 | ||||
|     public void SetCompletedStatus() | ||||
|     { | ||||
|         if (_orderStatusId != OrderStatus.Paid.Id) | ||||
|         { | ||||
|             StatusChangeException(OrderStatus.Completed); | ||||
|         } | ||||
| 
 | ||||
|         _orderStatusId = OrderStatus.Completed.Id; | ||||
|         _description = "The order was completed."; | ||||
|         AddDomainEvent(new OrderCompletedDomainEvent(this)); | ||||
|     } | ||||
| 
 | ||||
|     public void SetCancelledStatusWhenStockIsRejected(IEnumerable<int> orderStockRejectedItems) | ||||
|     { | ||||
|         if (_orderStatusId == OrderStatus.AwaitingValidation.Id) | ||||
|  | ||||
| @ -11,6 +11,7 @@ public class OrderStatus | ||||
|     public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant()); | ||||
|     public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant()); | ||||
|     public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant()); | ||||
|     public static OrderStatus Completed = new OrderStatus(7, nameof(Completed).ToLowerInvariant()); | ||||
| 
 | ||||
|     public OrderStatus(int id, string name) | ||||
|         : base(id, name) | ||||
|  | ||||
| @ -0,0 +1,11 @@ | ||||
| namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; | ||||
| 
 | ||||
| public class OrderCompletedDomainEvent : INotification | ||||
| { | ||||
|     public Order Order { get; } | ||||
| 
 | ||||
|     public OrderCompletedDomainEvent(Order order) | ||||
|     { | ||||
|         Order = order; | ||||
|     } | ||||
| } | ||||
| @ -1,7 +1,12 @@ | ||||
| global using System.Reflection; | ||||
| global using global::Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; | ||||
| global using global::Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions; | ||||
| global using MediatR; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Events; | ||||
| global using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; | ||||
| global using System.Collections.Generic; | ||||
| global using System.Linq; | ||||
| global using System.Reflection; | ||||
| global using System.Threading.Tasks; | ||||
| global using System.Threading; | ||||
| global using System; | ||||
| @ -50,6 +50,7 @@ public class OrderingScenarioBase | ||||
|     { | ||||
|         public static string CancelOrder = "api/v1/orders/cancel"; | ||||
|         public static string ShipOrder = "api/v1/orders/ship"; | ||||
|         public static string CompleteOrder = "api/v1/orders/complete"; | ||||
|     } | ||||
| 
 | ||||
|     private class AuthStartupFilter : IStartupFilter | ||||
|  | ||||
| @ -48,6 +48,20 @@ namespace Ordering.FunctionalTests | ||||
|             Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public async Task Complete_order_no_order_created_bad_request_response() | ||||
|         { | ||||
|             using var server = CreateServer(); | ||||
|             var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, "application/json") | ||||
|             { | ||||
|                 Headers = { { "x-requestid", Guid.NewGuid().ToString() } } | ||||
|             }; | ||||
|             var response = await server.CreateClient() | ||||
|                 .PutAsync(Put.CompleteOrder, content); | ||||
| 
 | ||||
|             Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); | ||||
|         } | ||||
| 
 | ||||
|         string BuildOrder() | ||||
|         { | ||||
|             var order = new | ||||
|  | ||||
| @ -79,6 +79,37 @@ public class OrdersWebApiTest | ||||
|         Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult.StatusCode); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Complete_order_with_requestId_success() | ||||
|     { | ||||
|         //Arrange | ||||
|         _mediatorMock.Setup(x => x.Send(It.IsAny<IdentifiedCommand<CompleteOrderCommand, bool>>(), default)) | ||||
|             .Returns(Task.FromResult(true)); | ||||
| 
 | ||||
|         //Act | ||||
|         var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object, _loggerMock.Object); | ||||
|         var actionResult = await orderController.CompleteOrderAsync(new CompleteOrderCommand(1), Guid.NewGuid().ToString()) as OkResult; | ||||
| 
 | ||||
|         //Assert | ||||
|         Assert.Equal((int)System.Net.HttpStatusCode.OK, actionResult.StatusCode); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Complete_order_bad_request() | ||||
|     { | ||||
|         //Arrange | ||||
|         _mediatorMock.Setup(x => x.Send(It.IsAny<IdentifiedCommand<CreateOrderCommand, bool>>(), default)) | ||||
|             .Returns(Task.FromResult(true)); | ||||
| 
 | ||||
|         //Act | ||||
|         var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object, _loggerMock.Object); | ||||
|         var actionResult = await orderController.CompleteOrderAsync(new CompleteOrderCommand(1), string.Empty) as BadRequestResult; | ||||
| 
 | ||||
|         //Assert | ||||
|         Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult.StatusCode); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|     public async Task Get_orders_success() | ||||
|     { | ||||
| @ -110,10 +141,10 @@ public class OrdersWebApiTest | ||||
| 
 | ||||
|         //Act | ||||
|         var orderController = new OrdersController(_mediatorMock.Object, _orderQueriesMock.Object, _identityServiceMock.Object, _loggerMock.Object); | ||||
|         var actionResult = await orderController.GetOrderAsync(fakeOrderId); | ||||
|         var actionResult = await orderController.GetOrderAsync(fakeOrderId) as OkObjectResult; | ||||
| 
 | ||||
|         //Assert | ||||
|         Assert.Same(actionResult.Value, fakeDynamicResult); | ||||
|         Assert.Equal(actionResult.StatusCode, (int)System.Net.HttpStatusCode.OK); | ||||
|     } | ||||
| 
 | ||||
|     [Fact] | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user