Skip to content
This repository was archived by the owner on Nov 17, 2023. It is now read-only.

Complete order async #2134

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
30 changes: 30 additions & 0 deletions src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,36 @@ public async Task<IActionResult> CancelOrderAsync([FromBody] CancelOrderCommand
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)]
Expand Down
1 change: 1 addition & 0 deletions src/Services/Ordering/Ordering.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

// 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>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,19 @@ public void SetCancelledStatus()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ 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)
{
}

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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public static class Put
{
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ public async Task Get_get_all_stored_orders_and_response_ok_status_code()
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()
{
Expand All @@ -29,8 +43,7 @@ public async Task Cancel_order_no_order_created_bad_request_response()
};
var response = await server.CreateClient()
.PutAsync(Put.CancelOrder, content);

var s = await response.Content.ReadAsStringAsync();

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 });
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}

2 changes: 2 additions & 0 deletions src/Services/Ordering/Ordering.SignalrHub/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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>();
Expand All @@ -24,6 +25,7 @@
eventBus.Subscribe<OrderStatusChangedToStockConfirmedIntegrationEvent, OrderStatusChangedToStockConfirmedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, OrderStatusChangedToCancelledIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToCompletedIntegrationEvent, OrderStatusChangedToCompletedIntegrationEventHandler>();
eventBus.Subscribe<OrderStatusChangedToSubmittedIntegrationEvent, OrderStatusChangedToSubmittedIntegrationEventHandler>();

await app.RunAsync();
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@ public async Task Cancel_order_bad_request()
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()
{
Expand Down
2 changes: 1 addition & 1 deletion src/docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/eShopOnContainers-ServicesAndWebApps.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down