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..c2864ae57 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommand.cs @@ -0,0 +1,12 @@ +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; + } +} \ No newline at end of file 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..bb4b093a2 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CompleteOrderCommandHandler.cs @@ -0,0 +1,47 @@ +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 + /// shipment company requests to complete order from API + /// + /// + /// + 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/DomainEventHandlers/OrderCompletedDomainEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCompletedDomainEventHandler.cs new file mode 100644 index 000000000..03f2e52bf --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/DomainEventHandlers/OrderCompletedDomainEventHandler.cs @@ -0,0 +1,29 @@ +namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers; + +public partial class OrderCompletedDomainEventHandler : INotificationHandler +{ + 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); + } +} \ No newline at end of file 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..fac75426a --- /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/IOrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs index 18807a56b..2cefc81cd 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs @@ -3,5 +3,5 @@ public interface IOrderingIntegrationEventService { Task PublishEventsThroughEventBusAsync(Guid transactionId); - Task AddAndSaveEventAsync(IntegrationEvent evt); + Task AddAndSaveEventAsync(IntegrationEvent integrationEvent); } diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs index 29e9a7ab4..ba81fdd8b 100644 --- a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs @@ -44,10 +44,10 @@ public class OrderingIntegrationEventService : IOrderingIntegrationEventService } } - public async Task AddAndSaveEventAsync(IntegrationEvent evt) + public async Task AddAndSaveEventAsync(IntegrationEvent integrationEvent) { - _logger.LogInformation("Enqueuing integration event {IntegrationEventId} to repository ({@IntegrationEvent})", evt.Id, evt); + _logger.LogInformation("Enqueuing integration event {IntegrationEventId} to repository ({@IntegrationEvent})", integrationEvent.Id, integrationEvent); - await _eventLogService.SaveEventAsync(evt, _orderingContext.GetCurrentTransaction()); + await _eventLogService.SaveEventAsync(integrationEvent, _orderingContext.GetCurrentTransaction()); } } 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..facef8566 --- /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..1256d17e9 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -138,4 +138,34 @@ public class OrdersController : ControllerBase return await _mediator.Send(createOrderDraftCommand); } + + [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(); + } } 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..cb2f49ceb 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -190,4 +190,17 @@ public class Order { return _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice()); } + + public void SetCompletedStatus() + { + if (_orderStatusId != OrderStatus.Shipped.Id) + { + StatusChangeException(OrderStatus.Completed); + } + + _orderStatusId = OrderStatus.Completed.Id; + _description = $"The order was {OrderStatus.Completed}."; + + AddDomainEvent(new OrderCompletedDomainEvent(this)); + } } diff --git a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs index 8c3cc50fb..e0b9b1350 100644 --- a/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs +++ b/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs @@ -5,12 +5,13 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.O public class OrderStatus : Enumeration { - public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant()); - public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant()); - public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant()); - 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 Submitted = new(1, nameof(Submitted).ToLowerInvariant()); + public static OrderStatus AwaitingValidation = new(2, nameof(AwaitingValidation).ToLowerInvariant()); + public static OrderStatus StockConfirmed = new(3, nameof(StockConfirmed).ToLowerInvariant()); + public static OrderStatus Paid = new(4, nameof(Paid).ToLowerInvariant()); + public static OrderStatus Shipped = new(5, nameof(Shipped).ToLowerInvariant()); + public static OrderStatus Cancelled = new(6, nameof(Cancelled).ToLowerInvariant()); + public static OrderStatus Completed = new(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..bed7ef8be --- /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; + } +} \ No newline at end of file