diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommand.cs new file mode 100644 index 000000000..549bfb19b --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommand.cs @@ -0,0 +1,13 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; + +public class CompleteOrderCommand : IRequest +{ + + [DataMember] + public int OrderNumber { get; private set; } + + public CompleteOrderCommand(int orderNumber) + { + OrderNumber = orderNumber; + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommandHandler.cs new file mode 100644 index 000000000..d6560e261 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommandHandler.cs @@ -0,0 +1,48 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; + +// Regular CommandHandler +public class CompleteOrderCommandHandler : IRequestHandler +{ + private readonly IOrderRepository _orderRepository; + + public CompleteOrderCommandHandler(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + /// + /// Handler which processes the command when + /// administrator executes complete order from app + /// + /// + /// + public async Task 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 +{ + public CompleteOrderIdentifiedCommandHandler( + IMediator mediator, + IRequestManager requestManager, + ILogger> logger) + : base(mediator, requestManager, logger) + { + } + + protected override bool CreateResultForDuplicateRequest() + { + return true; // Ignore duplicate requests for processing order. + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs index 0852bdc10..0d30d667a 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs @@ -70,6 +70,11 @@ public abstract class IdentifiedCommandHandler : IRequestHandler +{ + private readonly IOrderRepository _orderRepository; + private readonly IBuyerRepository _buyerRepository; + private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + private readonly ILogger _logger; + + public OrderCompletedDomainEventHandler(IOrderRepository orderRepository, ILogger 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); + } +} diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCompletedIntegrationEvent.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCompletedIntegrationEvent.cs new file mode 100644 index 000000000..6b4282536 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCompletedIntegrationEvent.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs index 29e9a7ab4..db3a07036 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs @@ -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 { diff --git a/src/Services/Ordering/Ordering.API/Application/Validations/CompleteOrderCommandValidator.cs b/src/Services/Ordering/Ordering.API/Application/Validations/CompleteOrderCommandValidator.cs new file mode 100644 index 000000000..5a62712b1 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Validations/CompleteOrderCommandValidator.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Validations; + +public class CompleteOrderCommandValidator : AbstractValidator +{ + public CompleteOrderCommandValidator(ILogger logger) + { + RuleFor(order => order.OrderNumber).NotEmpty().WithMessage("No orderId found"); + + logger.LogTrace("INSTANCE CREATED - {ClassName}", GetType().Name); + } +} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index df7572bb7..19bef4afc 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -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 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(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> GetOrderAsync(int orderId) + public async Task 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), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] public async Task>> GetOrdersAsync() { var userid = _identityService.GetUserIdentity(); @@ -117,7 +150,7 @@ public class OrdersController : ControllerBase [Route("cardtypes")] [HttpGet] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] public async Task>> GetCardTypesAsync() { var cardTypes = await _orderQueries.GetCardTypesAsync(); diff --git a/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs b/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs index 4e8ab1b15..918780f4a 100644 --- a/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs +++ b/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs @@ -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 ToOrderItemsDTO(this IEnumerable basketItems) diff --git a/src/Services/Ordering/Ordering.API/GlobalUsings.cs b/src/Services/Ordering/Ordering.API/GlobalUsings.cs index fc2590844..70255b36a 100644 --- a/src/Services/Ordering/Ordering.API/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.API/GlobalUsings.cs @@ -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; diff --git a/src/Services/Ordering/Ordering.API/Program.cs b/src/Services/Ordering/Ordering.API/Program.cs index 56d76de33..fd19fcb79 100644 --- a/src/Services/Ordering/Ordering.API/Program.cs +++ b/src/Services/Ordering/Ordering.API/Program.cs @@ -26,6 +26,7 @@ services.AddSingleton, CancelOrderCommandValidato services.AddSingleton, CreateOrderCommandValidator>(); services.AddSingleton>, IdentifiedCommandValidator>(); services.AddSingleton, ShipOrderCommandValidator>(); +services.AddSingleton, CompleteOrderCommandValidator>(); services.AddScoped(sp => new OrderQueries(builder.Configuration.GetConnectionString("OrderingDB"))); services.AddScoped(); diff --git a/src/Services/Ordering/Ordering.API/Setup/OrderStatus.csv b/src/Services/Ordering/Ordering.API/Setup/OrderStatus.csv index 8a3109887..6a41b9136 100644 --- a/src/Services/Ordering/Ordering.API/Setup/OrderStatus.csv +++ b/src/Services/Ordering/Ordering.API/Setup/OrderStatus.csv @@ -4,4 +4,5 @@ AwaitingValidation StockConfirmed Paid Shipped -Cancelled \ No newline at end of file +Cancelled +Completed \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs index 8e452c42f..62030e39d 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -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 orderStockRejectedItems) { if (_orderStatusId == OrderStatus.AwaitingValidation.Id) diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs index 8c3cc50fb..7ce6f9486 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs @@ -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) diff --git a/src/Services/Ordering/Ordering.Domain/Events/OrderCompletedDomainEvent.cs b/src/Services/Ordering/Ordering.Domain/Events/OrderCompletedDomainEvent.cs new file mode 100644 index 000000000..4183df760 --- /dev/null +++ b/src/Services/Ordering/Ordering.Domain/Events/OrderCompletedDomainEvent.cs @@ -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; + } +} diff --git a/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs b/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs index 866be7bcb..33d18f36e 100644 --- a/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs +++ b/src/Services/Ordering/Ordering.Domain/GlobalUsings.cs @@ -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; \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs index f3fba40e3..422fd1b3e 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs @@ -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 diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs index 85dd4ab3d..dd94b3a51 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarios.cs @@ -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 diff --git a/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs b/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs index 0235c94b6..a0bafad2a 100644 --- a/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs +++ b/src/Services/Ordering/Ordering.UnitTests/Application/OrdersWebApiTest.cs @@ -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>(), 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>(), 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]