diff --git a/src/HelloShop.OrderingService/Commands/Orders/CancelOrderCommand.cs b/src/HelloShop.OrderingService/Commands/Orders/CancelOrderCommand.cs new file mode 100644 index 0000000..c644b29 --- /dev/null +++ b/src/HelloShop.OrderingService/Commands/Orders/CancelOrderCommand.cs @@ -0,0 +1,9 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using MediatR; + +namespace HelloShop.OrderingService.Commands.Orders +{ + public record CancelOrderCommand(int OrderNumber) : IRequest; +} \ No newline at end of file diff --git a/src/HelloShop.OrderingService/Commands/Orders/CancelOrderCommandHandler.cs b/src/HelloShop.OrderingService/Commands/Orders/CancelOrderCommandHandler.cs new file mode 100644 index 0000000..f913d2e --- /dev/null +++ b/src/HelloShop.OrderingService/Commands/Orders/CancelOrderCommandHandler.cs @@ -0,0 +1,32 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using HelloShop.OrderingService.Entities.Orders; +using HelloShop.OrderingService.Infrastructure; +using HelloShop.OrderingService.Services; +using MediatR; + +namespace HelloShop.OrderingService.Commands.Orders +{ + public class CancelOrderCommandHandler(OrderingServiceDbContext dbContext) : IRequestHandler + { + public async Task Handle(CancelOrderCommand request, CancellationToken cancellationToken) + { + var orderToUpdate = await dbContext.Set().FindAsync([request.OrderNumber], cancellationToken); + + if (orderToUpdate == null) + { + return false; + } + + orderToUpdate.OrderStatus = OrderStatus.Cancelled; + + return await dbContext.SaveChangesAsync(cancellationToken) > 0; + } + } + + public class CancelOrderIdentifiedCommandHandler(IMediator mediator, IClientRequestManager requestManager) : IdentifiedCommandHandler(mediator, requestManager) + { + protected override bool CreateResultForDuplicateRequest() => true; + } +} diff --git a/src/HelloShop.OrderingService/Commands/Orders/ShipOrderCommand.cs b/src/HelloShop.OrderingService/Commands/Orders/ShipOrderCommand.cs new file mode 100644 index 0000000..e8529ba --- /dev/null +++ b/src/HelloShop.OrderingService/Commands/Orders/ShipOrderCommand.cs @@ -0,0 +1,9 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using MediatR; + +namespace HelloShop.OrderingService.Commands.Orders +{ + public record ShipOrderCommand(int OrderNumber) : IRequest; +} diff --git a/src/HelloShop.OrderingService/Commands/Orders/ShipOrderCommandHandler.cs b/src/HelloShop.OrderingService/Commands/Orders/ShipOrderCommandHandler.cs new file mode 100644 index 0000000..836528c --- /dev/null +++ b/src/HelloShop.OrderingService/Commands/Orders/ShipOrderCommandHandler.cs @@ -0,0 +1,32 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using HelloShop.OrderingService.Entities.Orders; +using HelloShop.OrderingService.Infrastructure; +using HelloShop.OrderingService.Services; +using MediatR; + +namespace HelloShop.OrderingService.Commands.Orders +{ + public class ShipOrderCommandHandler(OrderingServiceDbContext dbContext) : IRequestHandler + { + public async Task Handle(ShipOrderCommand request, CancellationToken cancellationToken) + { + var orderToUpdate = await dbContext.Set().FindAsync([request.OrderNumber], cancellationToken: cancellationToken); + + if (orderToUpdate == null) + { + return false; + } + + orderToUpdate.OrderStatus = OrderStatus.Shipped; + + return await dbContext.SaveChangesAsync(cancellationToken) > 0; + } + } + + public class ShipOrderIdentifiedCommandHandler(IMediator mediator, IClientRequestManager requestManager) : IdentifiedCommandHandler(mediator, requestManager) + { + protected override bool CreateResultForDuplicateRequest() => true; + } +} diff --git a/src/HelloShop.OrderingService/Controllers/OrdersController.cs b/src/HelloShop.OrderingService/Controllers/OrdersController.cs index cfaab74..87104f4 100644 --- a/src/HelloShop.OrderingService/Controllers/OrdersController.cs +++ b/src/HelloShop.OrderingService/Controllers/OrdersController.cs @@ -7,59 +7,95 @@ using HelloShop.OrderingService.Commands.Orders; using HelloShop.OrderingService.Models.Orders; using MediatR; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.ComponentModel.DataAnnotations; using System.Security.Claims; -namespace HelloShop.OrderingService.Controllers +namespace HelloShop.OrderingService.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class OrdersController(ILogger logger, IMediator mediator, IMapper mapper) : ControllerBase { - [Route("api/[controller]")] - [ApiController] - [Authorize] - public class OrdersController(ILogger logger, IMediator mediator, IMapper mapper) : ControllerBase + [HttpPost] + public async Task CreateOrder([FromHeader(Name = "x-request-id")] Guid requestId, CreateOrderRequest request) { - [HttpPost] - public async Task CreateOrder([FromHeader(Name = "x-request-id")] Guid requestId, CreateOrderRequest request) + using (logger.BeginScope(new List> { new("IdentifiedCommandId", requestId) })) { - using (logger.BeginScope(new List> { new("IdentifiedCommandId", requestId) })) + string? nameIdentifier = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + string? userName = User.FindFirst(ClaimTypes.Name)?.Value; + + if (!int.TryParse(nameIdentifier, out int userId) || string.IsNullOrWhiteSpace(userName)) { - string? nameIdentifier = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; - string? userName = User.FindFirst(ClaimTypes.Name)?.Value; + throw new InvalidOperationException("User id or name not found in claims."); + } - if (!int.TryParse(nameIdentifier, out int userId) || string.IsNullOrWhiteSpace(userName)) + CreateOrderCommand createOrderCommand = mapper.Map(request, opts => opts.AfterMap((src, dest) => + { + dest.UserId = userId; + dest.UserName = userName; + })); + + var createOrderIdentifiedCommand = new IdentifiedCommand(createOrderCommand, requestId); + + logger.LogInformation("Sending create order command."); + + try + { + var result = await mediator.Send(createOrderIdentifiedCommand); + return result ? Ok() : Problem(detail: "Create order failed to process.", statusCode: 500); + + } + catch (ApplicationException ex) when (ex.InnerException is FluentValidation.ValidationException validationException) + { + logger.LogWarning("Validation error in create order command."); + + ModelStateDictionary modelState = validationException.Errors.Aggregate(ModelState, (acc, error) => { - throw new InvalidOperationException("User id or name not found in claims."); - } + acc.AddModelError(error.PropertyName, error.ErrorMessage); + return acc; + }); - CreateOrderCommand createOrderCommand = mapper.Map(request, opts => opts.AfterMap((src, dest) => - { - dest.UserId = userId; - dest.UserName = userName; - })); - - var createOrderIdentifiedCommand = new IdentifiedCommand(createOrderCommand, requestId); - - logger.LogInformation("Sending create order command."); - - try - { - var result = await mediator.Send(createOrderIdentifiedCommand); - return result ? Ok() : Problem(detail: "Create order failed to process.", statusCode: 500); - - } - catch (ApplicationException ex) when (ex.InnerException is FluentValidation.ValidationException validationException) - { - logger.LogWarning("Validation error in create order command."); - - ModelStateDictionary modelState = validationException.Errors.Aggregate(ModelState, (acc, error) => - { - acc.AddModelError(error.PropertyName, error.ErrorMessage); - return acc; - }); - - return ValidationProblem(modelState); - } + return ValidationProblem(modelState); } } } + + + [HttpPut("Cancel/{id}")] + public async Task CancelOrder([FromHeader(Name = "x-request-id")] Guid requestId, int id) + { + CancelOrderCommand cancelOrderCommand = new(id); + + var requestCancelOrder = new IdentifiedCommand(cancelOrderCommand, requestId); + + var commandResult = await mediator.Send(requestCancelOrder); + + if (!commandResult) + { + return Problem(detail: "Cancel order failed to process.", statusCode: 500); + } + + return Ok(); + } + + [HttpPut("Ship/{id}")] + public async Task ShipOrder([FromHeader(Name = "x-request-id")] Guid requestId, int id) + { + ShipOrderCommand shipOrderCommand = new(id); + + var requestShipOrder = new IdentifiedCommand(shipOrderCommand, requestId); + + var commandResult = await mediator.Send(requestShipOrder); + + if (!commandResult) + { + return Problem(detail: "Ship order failed to process.", statusCode: 500); + } + + return Ok(); + } } diff --git a/src/HelloShop.OrderingService/Extensions/Extensions.cs b/src/HelloShop.OrderingService/Extensions/Extensions.cs index 4a7d5bd..ed4b93c 100644 --- a/src/HelloShop.OrderingService/Extensions/Extensions.cs +++ b/src/HelloShop.OrderingService/Extensions/Extensions.cs @@ -30,6 +30,8 @@ namespace HelloShop.OrderingService.Extensions options.AddBehavior(typeof(LoggingBehavior<,>)); options.AddBehavior(typeof(ValidatorBehavior<,>)); }); + + builder.Services.AddModelMapper().AddModelValidator(); } public static WebApplication MapApplicationEndpoints(this WebApplication app) diff --git a/src/HelloShop.OrderingService/Validations/Commands/CancelOrderCommandValidator.cs b/src/HelloShop.OrderingService/Validations/Commands/CancelOrderCommandValidator.cs new file mode 100644 index 0000000..a2375a3 --- /dev/null +++ b/src/HelloShop.OrderingService/Validations/Commands/CancelOrderCommandValidator.cs @@ -0,0 +1,16 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using FluentValidation; +using HelloShop.OrderingService.Commands.Orders; + +namespace HelloShop.OrderingService.Validations.Commands +{ + public class CancelOrderCommandValidator : AbstractValidator + { + public CancelOrderCommandValidator() + { + RuleFor(x => x.OrderNumber).GreaterThan(0); + } + } +} diff --git a/src/HelloShop.OrderingService/Validations/Commands/ShipOrderCommandValidator.cs b/src/HelloShop.OrderingService/Validations/Commands/ShipOrderCommandValidator.cs new file mode 100644 index 0000000..f23fc8b --- /dev/null +++ b/src/HelloShop.OrderingService/Validations/Commands/ShipOrderCommandValidator.cs @@ -0,0 +1,16 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using FluentValidation; +using HelloShop.OrderingService.Commands.Orders; + +namespace HelloShop.OrderingService.Validations.Commands +{ + public class ShipOrderCommandValidator : AbstractValidator + { + public ShipOrderCommandValidator() + { + RuleFor(order => order.OrderNumber).GreaterThan(0); + } + } +}