Merge ddd99393692f5b30f723ced3adf904073da5b8da into 06d5164532902a076813894c965d6c99122d39f2
This commit is contained in:
commit
12f4d37055
@ -0,0 +1,16 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands;
|
||||
|
||||
public class CompleteOrderCommand : IRequest<bool>
|
||||
{
|
||||
|
||||
[DataMember]
|
||||
public int OrderNumber { get; set; }
|
||||
public CompleteOrderCommand()
|
||||
{
|
||||
|
||||
}
|
||||
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
|
||||
/// customer 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.
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.DomainEventHandlers;
|
||||
|
||||
public partial class OrderCompletedDomainEventHandler
|
||||
: INotificationHandler<OrderCompletedDomainEvent>
|
||||
{
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly IBuyerRepository _buyerRepository;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
|
||||
|
||||
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.Cancelled), OrderStatus.Cancelled.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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -55,6 +55,36 @@ 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("ship")]
|
||||
[HttpPut]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
|
@ -23,6 +23,7 @@ services.AddMediatR(cfg =>
|
||||
|
||||
// Register the command validators for the validator behavior (validators based on FluentValidation library)
|
||||
services.AddSingleton<IValidator<CancelOrderCommand>, CancelOrderCommandValidator>();
|
||||
services.AddSingleton<IValidator<CompleteOrderCommand>, CompleteOrderCommandValidator>();
|
||||
services.AddSingleton<IValidator<CreateOrderCommand>, CreateOrderCommandValidator>();
|
||||
services.AddSingleton<IValidator<IdentifiedCommand<CreateOrderCommand, bool>>, IdentifiedCommandValidator>();
|
||||
services.AddSingleton<IValidator<ShipOrderCommand>, ShipOrderCommandValidator>();
|
||||
|
@ -156,6 +156,19 @@ public class Order
|
||||
AddDomainEvent(new OrderCancelledDomainEvent(this));
|
||||
}
|
||||
|
||||
public void SetCompletedStatus()
|
||||
{
|
||||
// make sure it is shipped before completing
|
||||
if (_orderStatusId == OrderStatus.Shipped.Id)
|
||||
{
|
||||
StatusChangeException(OrderStatus.Completed);
|
||||
}
|
||||
|
||||
_orderStatusId = OrderStatus.Completed.Id;
|
||||
_description = $"The order is completed.";
|
||||
AddDomainEvent(new OrderCompletedDomainEvent(this)); // a postponed way to raise domain events
|
||||
}
|
||||
|
||||
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)
|
||||
@ -18,7 +19,7 @@ public class OrderStatus
|
||||
}
|
||||
|
||||
public static IEnumerable<OrderStatus> List() =>
|
||||
new[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled };
|
||||
new[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled, Completed };
|
||||
|
||||
public static OrderStatus FromName(string name)
|
||||
{
|
||||
|
@ -0,0 +1,12 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
|
||||
|
||||
public class OrderCompletedDomainEvent : INotification
|
||||
{
|
||||
public Order Order { get; }
|
||||
|
||||
public OrderCompletedDomainEvent(Order order)
|
||||
{
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -19,6 +19,20 @@ namespace Ordering.FunctionalTests
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_order_no_order_created_bad_request_response()
|
||||
{
|
||||
@ -29,8 +43,7 @@ namespace Ordering.FunctionalTests
|
||||
};
|
||||
var response = await server.CreateClient()
|
||||
.PutAsync(Put.CancelOrder, content);
|
||||
|
||||
var s = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.IntegrationEvents.EventHandling;
|
||||
|
||||
public class OrderStatusChangedToCompletedIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToCompletedIntegrationEvent>
|
||||
{
|
||||
private readonly IHubContext<NotificationsHub> _hubContext;
|
||||
private readonly ILogger<OrderStatusChangedToCompletedIntegrationEventHandler> _logger;
|
||||
|
||||
public OrderStatusChangedToCompletedIntegrationEventHandler(
|
||||
IHubContext<NotificationsHub> hubContext,
|
||||
ILogger<OrderStatusChangedToCompletedIntegrationEventHandler> logger)
|
||||
{
|
||||
_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
|
||||
public async Task Handle(OrderStatusChangedToCompletedIntegrationEvent @event)
|
||||
{
|
||||
using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) }))
|
||||
{
|
||||
_logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event);
|
||||
|
||||
await _hubContext.Clients
|
||||
.Group(@event.BuyerName)
|
||||
.SendAsync("UpdatedOrderState", new { OrderId = @event.OrderId, Status = @event.OrderStatus });
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
namespace Microsoft.eShopOnContainers.Services.Ordering.SignalrHub.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;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ builder.Services.AddSignalR(builder.Configuration);
|
||||
|
||||
builder.Services.AddSingleton<IIntegrationEventHandler<OrderStatusChangedToAwaitingValidationIntegrationEvent>, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>();
|
||||
builder.Services.AddSingleton<IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>, OrderStatusChangedToCancelledIntegrationEventHandler>();
|
||||
builder.Services.AddSingleton<IIntegrationEventHandler<OrderStatusChangedToCompletedIntegrationEvent>, OrderStatusChangedToCompletedIntegrationEventHandler>();
|
||||
builder.Services.AddSingleton<IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>, OrderStatusChangedToPaidIntegrationEventHandler>();
|
||||
builder.Services.AddSingleton<IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent>, OrderStatusChangedToShippedIntegrationEventHandler>();
|
||||
builder.Services.AddSingleton<IIntegrationEventHandler<OrderStatusChangedToStockConfirmedIntegrationEvent>, OrderStatusChangedToStockConfirmedIntegrationEventHandler>();
|
||||
@ -24,6 +25,7 @@ eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedT
|
||||
eventBus.Subscribe<OrderStatusChangedToStockConfirmedIntegrationEvent, OrderStatusChangedToStockConfirmedIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, OrderStatusChangedToCancelledIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStatusChangedToCompletedIntegrationEvent, OrderStatusChangedToCompletedIntegrationEventHandler>();
|
||||
eventBus.Subscribe<OrderStatusChangedToSubmittedIntegrationEvent, OrderStatusChangedToSubmittedIntegrationEventHandler>();
|
||||
|
||||
await app.RunAsync();
|
||||
|
@ -48,6 +48,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<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), string.Empty) as BadRequestResult;
|
||||
|
||||
//Assert
|
||||
Assert.Equal((int)System.Net.HttpStatusCode.BadRequest, actionResult.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ship_order_with_requestId_success()
|
||||
{
|
||||
|
@ -260,7 +260,7 @@ services:
|
||||
|
||||
webspa:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ASPNETCORE_URLS=http://0.0.0.0:80
|
||||
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
|
||||
- PurchaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5121
|
||||
|
@ -443,8 +443,8 @@ Global
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x64.Build.0 = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.ActiveCfg = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.AppStore|x86.Build.0 = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.ActiveCfg = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|ARM.Build.0 = Debug|Any CPU
|
||||
{F16E3C6A-1C94-4EAB-BE91-099618060B68}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
|
Loading…
x
Reference in New Issue
Block a user