From 9105043888ae13527fa62f46a53638f066836f20 Mon Sep 17 00:00:00 2001 From: gbpadial Date: Wed, 18 May 2022 10:35:12 -0300 Subject: [PATCH] =?UTF-8?q?Teste=20C#=20para=20Belezanaweb=20(Botic=C3=A1r?= =?UTF-8?q?io)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O teste foi desenvolvido utilizando .Net Core 3.1, CQRS, FluentValidation e AutoMapper. Base de dados em Memória. Swagger não incluso. --- src/Belezanaweb.API/Belezanaweb.API.csproj | 17 +++ .../Controllers/ProductController.cs | 45 ++++++ src/Belezanaweb.API/Program.cs | 20 +++ src/Belezanaweb.API/Startup.cs | 56 +++++++ .../appsettings.Development.json | 9 ++ src/Belezanaweb.API/appsettings.json | 10 ++ .../Belezanaweb.Application.Core.csproj | 19 +++ .../Commands/IRequestBase.cs | 9 ++ .../Commands/RequestBase.cs | 13 ++ .../Commands/Response.cs | 50 ++++++ .../GlobalExceptionHandlerMiddleware.cs | 74 +++++++++ ...obalExceptionHandlerMiddlewareExtension.cs | 21 +++ .../Queries/IQuery.cs | 8 + .../Belezanaweb.Application.csproj | 19 +++ .../Products/Commands/AlterProductCommand.cs | 13 ++ .../Products/Commands/BaseProductCommand.cs | 12 ++ .../Products/Commands/CreateProductCommand.cs | 13 ++ .../Products/Commands/DeleteProductCommand.cs | 21 +++ .../Products/DTOs/InventoryDTO.cs | 11 ++ .../Products/DTOs/WarehouseDTO.cs | 13 ++ .../Handlers/AlterProductCommandHandler.cs | 42 ++++++ .../Handlers/CreateProductCommandHandler.cs | 41 +++++ .../Handlers/DeleteProductCommandHandler.cs | 33 ++++ .../Handlers/GetProductBySkuQueryHandler.cs | 36 +++++ .../Products/Handlers/ProductHandlerBase.cs | 37 +++++ .../Products/Queries/GetProductBySkuQuery.cs | 23 +++ .../Products/ViewModels/InventoryViewModel.cs | 10 ++ .../Products/ViewModels/ProductViewModel.cs | 14 ++ .../Products/ViewModels/WarehouseViewModel.cs | 9 ++ .../Profiles/Products/InventoryProfile.cs | 22 +++ .../Profiles/Products/ProductProfile.cs | 21 +++ .../Profiles/Products/WarehouseProfile.cs | 17 +++ .../Products/AlterProductValidator.cs | 11 ++ .../Products/BaseProductValidator.cs | 55 +++++++ .../Products/CreateProductValidator.cs | 13 ++ .../Products/DeleteProductValidator.cs | 15 ++ .../Products/GetProductBySkuValidator.cs | 15 ++ src/Belezanaweb.Core/Belezanaweb.Core.csproj | 11 ++ src/Belezanaweb.Core/Enums/EnumExtensions.cs | 27 ++++ .../Exceptions/BusinessException.cs | 11 ++ .../Exceptions/ValidatorException.cs | 26 ++++ .../Belezanaweb.Domain.Core.csproj | 7 + .../Entities/EntityBase.cs | 10 ++ .../Entities/IEntity.cs | 6 + .../Repositories/IRepository.cs | 15 ++ .../Belezanaweb.Domain.csproj | 11 ++ .../Products/Entities/Inventory.cs | 9 ++ .../Products/Entities/Product.cs | 11 ++ .../Products/Entities/Warehouse.cs | 9 ++ .../Products/Enums/WarehouseType.cs | 13 ++ .../Repositories/IProductRepository.cs | 9 ++ .../Belezanaweb.Infra.Data.csproj | 11 ++ .../DbContexts/InMemoryDbContext.cs | 15 ++ .../Products/ProductRepository.cs | 38 +++++ .../Belezanaweb.Infra.IoC.csproj | 20 +++ src/Belezanaweb.Infra.IoC/IoC.cs | 24 +++ src/Belezanaweb.Solution.sln | 88 +++++++++++ .../Belezanaweb.Application.Tests.csproj | 24 +++ .../Handlers/ProductCommandHandlerTest.cs | 142 ++++++++++++++++++ .../Resources/AlterProductCommand.json | 18 +++ .../Resources/CreateProductCommand.json | 18 +++ 61 files changed, 1440 insertions(+) create mode 100644 src/Belezanaweb.API/Belezanaweb.API.csproj create mode 100644 src/Belezanaweb.API/Controllers/ProductController.cs create mode 100644 src/Belezanaweb.API/Program.cs create mode 100644 src/Belezanaweb.API/Startup.cs create mode 100644 src/Belezanaweb.API/appsettings.Development.json create mode 100644 src/Belezanaweb.API/appsettings.json create mode 100644 src/Belezanaweb.Application.Core/Belezanaweb.Application.Core.csproj create mode 100644 src/Belezanaweb.Application.Core/Commands/IRequestBase.cs create mode 100644 src/Belezanaweb.Application.Core/Commands/RequestBase.cs create mode 100644 src/Belezanaweb.Application.Core/Commands/Response.cs create mode 100644 src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddleware.cs create mode 100644 src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddlewareExtension.cs create mode 100644 src/Belezanaweb.Application.Core/Queries/IQuery.cs create mode 100644 src/Belezanaweb.Application/Belezanaweb.Application.csproj create mode 100644 src/Belezanaweb.Application/Products/Commands/AlterProductCommand.cs create mode 100644 src/Belezanaweb.Application/Products/Commands/BaseProductCommand.cs create mode 100644 src/Belezanaweb.Application/Products/Commands/CreateProductCommand.cs create mode 100644 src/Belezanaweb.Application/Products/Commands/DeleteProductCommand.cs create mode 100644 src/Belezanaweb.Application/Products/DTOs/InventoryDTO.cs create mode 100644 src/Belezanaweb.Application/Products/DTOs/WarehouseDTO.cs create mode 100644 src/Belezanaweb.Application/Products/Handlers/AlterProductCommandHandler.cs create mode 100644 src/Belezanaweb.Application/Products/Handlers/CreateProductCommandHandler.cs create mode 100644 src/Belezanaweb.Application/Products/Handlers/DeleteProductCommandHandler.cs create mode 100644 src/Belezanaweb.Application/Products/Handlers/GetProductBySkuQueryHandler.cs create mode 100644 src/Belezanaweb.Application/Products/Handlers/ProductHandlerBase.cs create mode 100644 src/Belezanaweb.Application/Products/Queries/GetProductBySkuQuery.cs create mode 100644 src/Belezanaweb.Application/Products/ViewModels/InventoryViewModel.cs create mode 100644 src/Belezanaweb.Application/Products/ViewModels/ProductViewModel.cs create mode 100644 src/Belezanaweb.Application/Products/ViewModels/WarehouseViewModel.cs create mode 100644 src/Belezanaweb.Application/Profiles/Products/InventoryProfile.cs create mode 100644 src/Belezanaweb.Application/Profiles/Products/ProductProfile.cs create mode 100644 src/Belezanaweb.Application/Profiles/Products/WarehouseProfile.cs create mode 100644 src/Belezanaweb.Application/Validators/Products/AlterProductValidator.cs create mode 100644 src/Belezanaweb.Application/Validators/Products/BaseProductValidator.cs create mode 100644 src/Belezanaweb.Application/Validators/Products/CreateProductValidator.cs create mode 100644 src/Belezanaweb.Application/Validators/Products/DeleteProductValidator.cs create mode 100644 src/Belezanaweb.Application/Validators/Products/GetProductBySkuValidator.cs create mode 100644 src/Belezanaweb.Core/Belezanaweb.Core.csproj create mode 100644 src/Belezanaweb.Core/Enums/EnumExtensions.cs create mode 100644 src/Belezanaweb.Core/Exceptions/BusinessException.cs create mode 100644 src/Belezanaweb.Core/Exceptions/ValidatorException.cs create mode 100644 src/Belezanaweb.Domain.Core/Belezanaweb.Domain.Core.csproj create mode 100644 src/Belezanaweb.Domain.Core/Entities/EntityBase.cs create mode 100644 src/Belezanaweb.Domain.Core/Entities/IEntity.cs create mode 100644 src/Belezanaweb.Domain.Core/Repositories/IRepository.cs create mode 100644 src/Belezanaweb.Domain/Belezanaweb.Domain.csproj create mode 100644 src/Belezanaweb.Domain/Products/Entities/Inventory.cs create mode 100644 src/Belezanaweb.Domain/Products/Entities/Product.cs create mode 100644 src/Belezanaweb.Domain/Products/Entities/Warehouse.cs create mode 100644 src/Belezanaweb.Domain/Products/Enums/WarehouseType.cs create mode 100644 src/Belezanaweb.Domain/Products/Repositories/IProductRepository.cs create mode 100644 src/Belezanaweb.Infra.Data/Belezanaweb.Infra.Data.csproj create mode 100644 src/Belezanaweb.Infra.Data/DbContexts/InMemoryDbContext.cs create mode 100644 src/Belezanaweb.Infra.Data/Repositories/Products/ProductRepository.cs create mode 100644 src/Belezanaweb.Infra.IoC/Belezanaweb.Infra.IoC.csproj create mode 100644 src/Belezanaweb.Infra.IoC/IoC.cs create mode 100644 src/Belezanaweb.Solution.sln create mode 100644 tests/Belezanaweb.Application.Tests/Belezanaweb.Application.Tests.csproj create mode 100644 tests/Belezanaweb.Application.Tests/Products/Handlers/ProductCommandHandlerTest.cs create mode 100644 tests/Belezanaweb.Application.Tests/Resources/AlterProductCommand.json create mode 100644 tests/Belezanaweb.Application.Tests/Resources/CreateProductCommand.json diff --git a/src/Belezanaweb.API/Belezanaweb.API.csproj b/src/Belezanaweb.API/Belezanaweb.API.csproj new file mode 100644 index 00000000..2a98b58a --- /dev/null +++ b/src/Belezanaweb.API/Belezanaweb.API.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + diff --git a/src/Belezanaweb.API/Controllers/ProductController.cs b/src/Belezanaweb.API/Controllers/ProductController.cs new file mode 100644 index 00000000..61a04c46 --- /dev/null +++ b/src/Belezanaweb.API/Controllers/ProductController.cs @@ -0,0 +1,45 @@ +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Application.Products.Queries; +using Belezanaweb.Application.Products.ViewModels; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; + +namespace Belezanaweb.API.Controllers +{ + [Route("api/[controller]")] + public class ProductController : ControllerBase + { + private readonly IMediator _mediator; + + public ProductController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpGet("{sku:long}")] + public async Task> GetProductBySkuAsync([FromRoute] long sku) + { + return await _mediator.Send(new GetProductBySkuQuery(sku)); + } + + [HttpPost] + public async Task CreateProductAsync([FromBody] CreateProductCommand command) + { + return await _mediator.Send(command); + } + + [HttpPut] + public async Task AlterProductAsync([FromBody] AlterProductCommand command) + { + return await _mediator.Send(command); + } + + [HttpDelete("{sku:long}")] + public async Task DeleteProductAsync([FromRoute] long sku) + { + return await _mediator.Send(new DeleteProductCommand(sku)); + } + } +} diff --git a/src/Belezanaweb.API/Program.cs b/src/Belezanaweb.API/Program.cs new file mode 100644 index 00000000..f3df621d --- /dev/null +++ b/src/Belezanaweb.API/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Belezanaweb.API +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Belezanaweb.API/Startup.cs b/src/Belezanaweb.API/Startup.cs new file mode 100644 index 00000000..12cf21c3 --- /dev/null +++ b/src/Belezanaweb.API/Startup.cs @@ -0,0 +1,56 @@ +using Belezanaweb.Application.Core.Middlewares; +using Belezanaweb.Infra.IoC; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + + +namespace Belezanaweb.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + IoC.Load(services); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + app.UseGlobalExceptionHandlerMiddleware(); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/Belezanaweb.API/appsettings.Development.json b/src/Belezanaweb.API/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/src/Belezanaweb.API/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Belezanaweb.API/appsettings.json b/src/Belezanaweb.API/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/src/Belezanaweb.API/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Belezanaweb.Application.Core/Belezanaweb.Application.Core.csproj b/src/Belezanaweb.Application.Core/Belezanaweb.Application.Core.csproj new file mode 100644 index 00000000..0acc8815 --- /dev/null +++ b/src/Belezanaweb.Application.Core/Belezanaweb.Application.Core.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/src/Belezanaweb.Application.Core/Commands/IRequestBase.cs b/src/Belezanaweb.Application.Core/Commands/IRequestBase.cs new file mode 100644 index 00000000..0fe98f79 --- /dev/null +++ b/src/Belezanaweb.Application.Core/Commands/IRequestBase.cs @@ -0,0 +1,9 @@ +using FluentValidation.Results; + +namespace Belezanaweb.Application.Core.Commands +{ + public interface IRequestBase + { + ValidationResult ValidationResult { get; } + } +} diff --git a/src/Belezanaweb.Application.Core/Commands/RequestBase.cs b/src/Belezanaweb.Application.Core/Commands/RequestBase.cs new file mode 100644 index 00000000..e14ac8a8 --- /dev/null +++ b/src/Belezanaweb.Application.Core/Commands/RequestBase.cs @@ -0,0 +1,13 @@ +using FluentValidation.Results; +using MediatR; +using System.Text.Json.Serialization; + +namespace Belezanaweb.Application.Core.Commands +{ + public abstract class RequestBase : IRequestBase, IRequest + { + [JsonIgnore] + public ValidationResult ValidationResult { get; set; } + public abstract bool IsValid(); + } +} diff --git a/src/Belezanaweb.Application.Core/Commands/Response.cs b/src/Belezanaweb.Application.Core/Commands/Response.cs new file mode 100644 index 00000000..a4132840 --- /dev/null +++ b/src/Belezanaweb.Application.Core/Commands/Response.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace Belezanaweb.Application.Core.Commands +{ + public class BaseResponse where TRequest : class + { + [JsonProperty("success")] + public bool Success { get; set; } + [JsonProperty("errorMessages")] + public string[] ErrorMessages { get; set; } + [JsonProperty("data")] + public TRequest Data { get; set; } + } + + public class Response : BaseResponse where TRequest : class + { + public Response(string errorMessage) + { + base.ErrorMessages = new[] { errorMessage } ; + Success = false; + Data = default; + } + + public Response(TRequest data) + { + Data = data; + Success = true; + } + } + + public class Response : BaseResponse + { + public Response(string errorMessage) + { + ErrorMessages = new[] { errorMessage }; + Success = false; + } + + public Response(string[] errorMessages) + { + ErrorMessages = errorMessages; + Success = false; + } + + public Response() + { + Success = true; + } + } +} diff --git a/src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddleware.cs b/src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddleware.cs new file mode 100644 index 00000000..bf01fedb --- /dev/null +++ b/src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddleware.cs @@ -0,0 +1,74 @@ +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Core.Exceptions; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; + +namespace Belezanaweb.Application.Core.Middlewares +{ + public class GlobalExceptionHandlerMiddleware : IMiddleware + { + public GlobalExceptionHandlerMiddleware() + { + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (BusinessException ex) + { + await HandleExceptionAsync(context, ex, (int)HttpStatusCode.BadRequest); + } + catch (ValidatorException ex) + { + await HandleExceptionAsync(context, ex); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex, (int)HttpStatusCode.InternalServerError); + } + } + + private Task HandleExceptionAsync(HttpContext context, ValidatorException payload) + { + ConfigureErrorResponse(context, (int)HttpStatusCode.BadRequest); + + var text = string.Empty; + if (payload != null) + { + var errors = new List(); + foreach (Exception ex in payload.Exceptions) + { + errors.Add(ex.Message); + } + text = JsonConvert.SerializeObject(new Response(errors.ToArray())); + } + return context.Response.WriteAsync(text); + } + + private Task HandleExceptionAsync(HttpContext context, Exception payload, int statusCode) + { + ConfigureErrorResponse(context, statusCode); + + var text = string.Empty; + if (payload != null) + { + text = JsonConvert.SerializeObject(new Response(payload.Message)); + } + return context.Response.WriteAsync(text); + } + + private void ConfigureErrorResponse(HttpContext context, int statusCode) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + } + + } +} diff --git a/src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddlewareExtension.cs b/src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddlewareExtension.cs new file mode 100644 index 00000000..ee95a10c --- /dev/null +++ b/src/Belezanaweb.Application.Core/Middlewares/GlobalExceptionHandlerMiddlewareExtension.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Belezanaweb.Application.Core.Middlewares +{ + public static class GlobalExceptionHandlerMiddlewareExtension + { + public static IServiceCollection AddGlobalExceptionHandlerMiddleware(this IServiceCollection services) + { + return services.AddTransient(); + } + + public static void UseGlobalExceptionHandlerMiddleware(this IApplicationBuilder app) + { + app.UseMiddleware(); + } + } +} diff --git a/src/Belezanaweb.Application.Core/Queries/IQuery.cs b/src/Belezanaweb.Application.Core/Queries/IQuery.cs new file mode 100644 index 00000000..2d09f82e --- /dev/null +++ b/src/Belezanaweb.Application.Core/Queries/IQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Belezanaweb.Application.Core.Queries +{ + public interface IQuery : IRequest + { + } +} diff --git a/src/Belezanaweb.Application/Belezanaweb.Application.csproj b/src/Belezanaweb.Application/Belezanaweb.Application.csproj new file mode 100644 index 00000000..8e80fab1 --- /dev/null +++ b/src/Belezanaweb.Application/Belezanaweb.Application.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/src/Belezanaweb.Application/Products/Commands/AlterProductCommand.cs b/src/Belezanaweb.Application/Products/Commands/AlterProductCommand.cs new file mode 100644 index 00000000..e789d768 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Commands/AlterProductCommand.cs @@ -0,0 +1,13 @@ +using Belezanaweb.Application.Validators.Products; + +namespace Belezanaweb.Application.Products.Commands +{ + public class AlterProductCommand : BaseProductCommand + { + public override bool IsValid() + { + ValidationResult = new AlterProductValidator().Validate(this); + return ValidationResult.IsValid; + } + } +} diff --git a/src/Belezanaweb.Application/Products/Commands/BaseProductCommand.cs b/src/Belezanaweb.Application/Products/Commands/BaseProductCommand.cs new file mode 100644 index 00000000..644e67ae --- /dev/null +++ b/src/Belezanaweb.Application/Products/Commands/BaseProductCommand.cs @@ -0,0 +1,12 @@ +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.DTOs; + +namespace Belezanaweb.Application.Products.Commands +{ + public abstract class BaseProductCommand : RequestBase + { + public long Sku { get; set; } + public string Name { get; set; } + public InventoryDTO Inventory { get; set; } + } +} diff --git a/src/Belezanaweb.Application/Products/Commands/CreateProductCommand.cs b/src/Belezanaweb.Application/Products/Commands/CreateProductCommand.cs new file mode 100644 index 00000000..eccbdea9 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Commands/CreateProductCommand.cs @@ -0,0 +1,13 @@ +using Belezanaweb.Application.Validators.Products; + +namespace Belezanaweb.Application.Products.Commands +{ + public class CreateProductCommand : BaseProductCommand + { + public override bool IsValid() + { + ValidationResult = new CreateProductValidator().Validate(this); + return ValidationResult.IsValid; + } + } +} diff --git a/src/Belezanaweb.Application/Products/Commands/DeleteProductCommand.cs b/src/Belezanaweb.Application/Products/Commands/DeleteProductCommand.cs new file mode 100644 index 00000000..0670e142 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Commands/DeleteProductCommand.cs @@ -0,0 +1,21 @@ +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Validators.Products; + +namespace Belezanaweb.Application.Products.Commands +{ + public class DeleteProductCommand : RequestBase + { + public DeleteProductCommand(long sku) + { + Sku = sku; + } + + public long Sku { get; } + + public override bool IsValid() + { + ValidationResult = new DeleteProductValidator().Validate(this); + return ValidationResult.IsValid; + } + } +} diff --git a/src/Belezanaweb.Application/Products/DTOs/InventoryDTO.cs b/src/Belezanaweb.Application/Products/DTOs/InventoryDTO.cs new file mode 100644 index 00000000..01dda015 --- /dev/null +++ b/src/Belezanaweb.Application/Products/DTOs/InventoryDTO.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Belezanaweb.Application.Products.DTOs +{ + public class InventoryDTO + { + public IEnumerable Warehouses { get; set; } + } +} diff --git a/src/Belezanaweb.Application/Products/DTOs/WarehouseDTO.cs b/src/Belezanaweb.Application/Products/DTOs/WarehouseDTO.cs new file mode 100644 index 00000000..90fb877f --- /dev/null +++ b/src/Belezanaweb.Application/Products/DTOs/WarehouseDTO.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Belezanaweb.Application.Products.DTOs +{ + public class WarehouseDTO + { + public string Locality { get; set; } + public int Quantity { get; set; } + public string Type { get; set; } + } +} diff --git a/src/Belezanaweb.Application/Products/Handlers/AlterProductCommandHandler.cs b/src/Belezanaweb.Application/Products/Handlers/AlterProductCommandHandler.cs new file mode 100644 index 00000000..cfa58154 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Handlers/AlterProductCommandHandler.cs @@ -0,0 +1,42 @@ +using AutoMapper; +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Core.Exceptions; +using Belezanaweb.Domain.Products.Entity; +using Belezanaweb.Domain.Products.Repositories; +using MediatR; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Belezanaweb.Application.Products.Handlers +{ + public class AlterProductCommandHandler : ProductHandlerBase, IRequestHandler + { + private readonly IProductRepository _productRepository; + private readonly IMapper _mapper; + + public AlterProductCommandHandler(IProductRepository productRepository, IMapper mapper) + : base(productRepository) + { + _productRepository = productRepository; + _mapper = mapper; + } + + public Task Handle(AlterProductCommand request, CancellationToken cancellationToken) + { + if (!request.IsValid()) + throw new ValidatorException(request.ValidationResult); + + Product existingProduct = base.GetProductBySku(request.Sku); + + var product = _mapper.Map(request); + product.UpdatedAt = DateTime.UtcNow; + product.CreatedAt = existingProduct.CreatedAt; + + _productRepository.Update(product); + + return Task.FromResult(new Response()); + } + } +} diff --git a/src/Belezanaweb.Application/Products/Handlers/CreateProductCommandHandler.cs b/src/Belezanaweb.Application/Products/Handlers/CreateProductCommandHandler.cs new file mode 100644 index 00000000..dd46fa2d --- /dev/null +++ b/src/Belezanaweb.Application/Products/Handlers/CreateProductCommandHandler.cs @@ -0,0 +1,41 @@ +using AutoMapper; +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Core.Exceptions; +using Belezanaweb.Domain.Products.Entity; +using Belezanaweb.Domain.Products.Repositories; +using MediatR; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Belezanaweb.Application.Products.Handlers +{ + public class CreateProductCommandHandler : ProductHandlerBase, IRequestHandler + { + private readonly IProductRepository _productRepository; + private readonly IMapper _mapper; + + public CreateProductCommandHandler(IProductRepository productRepository, IMapper mapper) + : base(productRepository) + { + _productRepository = productRepository; + _mapper = mapper; + } + + public Task Handle(CreateProductCommand request, CancellationToken cancellationToken) + { + if (!request.IsValid()) + throw new ValidatorException(request.ValidationResult); + + base.CheckIfProductExists(request.Sku); + + var product = _mapper.Map(request); + product.CreatedAt = DateTime.UtcNow; + _productRepository.Insert(product); + + + return Task.FromResult(new Response()); + } + } +} diff --git a/src/Belezanaweb.Application/Products/Handlers/DeleteProductCommandHandler.cs b/src/Belezanaweb.Application/Products/Handlers/DeleteProductCommandHandler.cs new file mode 100644 index 00000000..24a5d6e3 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Handlers/DeleteProductCommandHandler.cs @@ -0,0 +1,33 @@ +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Core.Exceptions; +using Belezanaweb.Domain.Products.Repositories; +using MediatR; +using System.Threading; +using System.Threading.Tasks; + +namespace Belezanaweb.Application.Products.Handlers +{ + public class DeleteProductCommandHandler : ProductHandlerBase, IRequestHandler + { + private readonly IProductRepository _productRepository; + + public DeleteProductCommandHandler(IProductRepository productRepository) + : base(productRepository) + { + _productRepository = productRepository; + } + + public Task Handle(DeleteProductCommand request, CancellationToken cancellationToken) + { + if (!request.IsValid()) + throw new ValidatorException(request.ValidationResult); + + var existingProduct = base.GetProductBySku(request.Sku); + + _productRepository.Delete(existingProduct); + + return Task.FromResult(new Response()); + } + } +} diff --git a/src/Belezanaweb.Application/Products/Handlers/GetProductBySkuQueryHandler.cs b/src/Belezanaweb.Application/Products/Handlers/GetProductBySkuQueryHandler.cs new file mode 100644 index 00000000..6dc6e43e --- /dev/null +++ b/src/Belezanaweb.Application/Products/Handlers/GetProductBySkuQueryHandler.cs @@ -0,0 +1,36 @@ +using AutoMapper; +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.Queries; +using Belezanaweb.Application.Products.ViewModels; +using Belezanaweb.Core.Exceptions; +using Belezanaweb.Domain.Products.Entity; +using Belezanaweb.Domain.Products.Repositories; +using MediatR; +using System.Threading; +using System.Threading.Tasks; + +namespace Belezanaweb.Application.Products.Handlers +{ + public class GetProductBySkuQueryHandler : ProductHandlerBase, IRequestHandler> + { + private readonly IProductRepository _productRepository; + private readonly IMapper _mapper; + + public GetProductBySkuQueryHandler(IProductRepository productRepository, IMapper mapper) + : base(productRepository) + { + _productRepository = productRepository; + _mapper = mapper; + } + + public Task> Handle(GetProductBySkuQuery request, CancellationToken cancellationToken) + { + if (!request.IsValid()) + throw new ValidatorException(request.ValidationResult); + + Product product = base.GetProductBySku(request.Sku); + + return Task.FromResult(new Response(_mapper.Map(product))); + } + } +} diff --git a/src/Belezanaweb.Application/Products/Handlers/ProductHandlerBase.cs b/src/Belezanaweb.Application/Products/Handlers/ProductHandlerBase.cs new file mode 100644 index 00000000..452d0793 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Handlers/ProductHandlerBase.cs @@ -0,0 +1,37 @@ +using Belezanaweb.Core.Exceptions; +using Belezanaweb.Domain.Products.Entity; +using Belezanaweb.Domain.Products.Repositories; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Belezanaweb.Application.Products.Handlers +{ + public abstract class ProductHandlerBase + { + protected readonly IProductRepository productRepository; + + public ProductHandlerBase(IProductRepository productRepository) + { + this.productRepository = productRepository; + } + + protected Product GetProductBySku(long sku) + { + Product existingProduct = productRepository.Get(sku); + if (existingProduct == null) + throw new BusinessException($"Produto com SKU {sku} não existe na base."); + + return existingProduct; + } + + protected void CheckIfProductExists(long sku) + { + Product existingProduct = productRepository.Get(sku); + if (existingProduct != null) + throw new BusinessException($"Produto já existe para o Sku {sku}."); + } + + + } +} diff --git a/src/Belezanaweb.Application/Products/Queries/GetProductBySkuQuery.cs b/src/Belezanaweb.Application/Products/Queries/GetProductBySkuQuery.cs new file mode 100644 index 00000000..9765a236 --- /dev/null +++ b/src/Belezanaweb.Application/Products/Queries/GetProductBySkuQuery.cs @@ -0,0 +1,23 @@ +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.ViewModels; +using Belezanaweb.Application.Validators.Products; +using MediatR; + +namespace Belezanaweb.Application.Products.Queries +{ + public class GetProductBySkuQuery : RequestBase> + { + public GetProductBySkuQuery(long sku) + { + Sku = sku; + } + + public long Sku { get; } + + public override bool IsValid() + { + ValidationResult = new GetProductBySkuValidator().Validate(this); + return ValidationResult.IsValid; + } + } +} diff --git a/src/Belezanaweb.Application/Products/ViewModels/InventoryViewModel.cs b/src/Belezanaweb.Application/Products/ViewModels/InventoryViewModel.cs new file mode 100644 index 00000000..aa9c05b8 --- /dev/null +++ b/src/Belezanaweb.Application/Products/ViewModels/InventoryViewModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Belezanaweb.Application.Products.ViewModels +{ + public class InventoryViewModel + { + public int Quantity { get; set; } + public IEnumerable Warehouses { get; set; } + } +} diff --git a/src/Belezanaweb.Application/Products/ViewModels/ProductViewModel.cs b/src/Belezanaweb.Application/Products/ViewModels/ProductViewModel.cs new file mode 100644 index 00000000..73ebdfde --- /dev/null +++ b/src/Belezanaweb.Application/Products/ViewModels/ProductViewModel.cs @@ -0,0 +1,14 @@ +using Belezanaweb.Domain.Products.Entity; +using System.Collections.Generic; + +namespace Belezanaweb.Application.Products.ViewModels +{ + public class ProductViewModel + { + public long Sku { get; set; } + public string Name { get; set; } + public bool IsMarketable { get; set; } + public InventoryViewModel Inventory { get; set; } + + } +} diff --git a/src/Belezanaweb.Application/Products/ViewModels/WarehouseViewModel.cs b/src/Belezanaweb.Application/Products/ViewModels/WarehouseViewModel.cs new file mode 100644 index 00000000..75138e73 --- /dev/null +++ b/src/Belezanaweb.Application/Products/ViewModels/WarehouseViewModel.cs @@ -0,0 +1,9 @@ +namespace Belezanaweb.Application.Products.ViewModels +{ + public class WarehouseViewModel + { + public string Locality { get; set; } + public int Quantity { get; set; } + public string Type { get; set; } + } +} diff --git a/src/Belezanaweb.Application/Profiles/Products/InventoryProfile.cs b/src/Belezanaweb.Application/Profiles/Products/InventoryProfile.cs new file mode 100644 index 00000000..becda719 --- /dev/null +++ b/src/Belezanaweb.Application/Profiles/Products/InventoryProfile.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Belezanaweb.Application.Products.DTOs; +using Belezanaweb.Application.Products.ViewModels; +using Belezanaweb.Domain.Products.Entity; +using System.Linq; + +namespace Belezanaweb.Application.Profiles.Products +{ + public class InventoryProfile : Profile + { + public InventoryProfile() + { + CreateMap() + .ForMember(dest => dest.Warehouses, opt => opt.MapFrom(src => src.Warehouses)); + + CreateMap() + .ForMember(dest => dest.Warehouses, opt => opt.MapFrom(src => src.Warehouses)) + .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Warehouses.Sum(w => w.Quantity))); + + } + } +} diff --git a/src/Belezanaweb.Application/Profiles/Products/ProductProfile.cs b/src/Belezanaweb.Application/Profiles/Products/ProductProfile.cs new file mode 100644 index 00000000..32bece39 --- /dev/null +++ b/src/Belezanaweb.Application/Profiles/Products/ProductProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Application.Products.ViewModels; +using Belezanaweb.Domain.Products.Entity; +using System.Linq; + +namespace Belezanaweb.Application.Profiles.Products +{ + public class ProductProfile : Profile + { + public ProductProfile() + { + CreateMap() + .ForMember(dest => dest.Inventory, opt => opt.MapFrom(src => src.Inventory)); + + CreateMap() + .ForMember(dest => dest.Inventory, opt => opt.MapFrom(src => src.Inventory)) + .ForMember(dest => dest.IsMarketable, opt => opt.MapFrom(src => src.Inventory.Warehouses.Any(w => w.Quantity > 0))); + } + } +} diff --git a/src/Belezanaweb.Application/Profiles/Products/WarehouseProfile.cs b/src/Belezanaweb.Application/Profiles/Products/WarehouseProfile.cs new file mode 100644 index 00000000..ab4df512 --- /dev/null +++ b/src/Belezanaweb.Application/Profiles/Products/WarehouseProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Belezanaweb.Application.Products.DTOs; +using Belezanaweb.Application.Products.ViewModels; +using Belezanaweb.Domain.Products.Entity; + +namespace Belezanaweb.Application.Profiles.Products +{ + public class WarehouseProfile : Profile + { + public WarehouseProfile() + { + CreateMap(); + + CreateMap(); + } + } +} diff --git a/src/Belezanaweb.Application/Validators/Products/AlterProductValidator.cs b/src/Belezanaweb.Application/Validators/Products/AlterProductValidator.cs new file mode 100644 index 00000000..f9145f38 --- /dev/null +++ b/src/Belezanaweb.Application/Validators/Products/AlterProductValidator.cs @@ -0,0 +1,11 @@ +using Belezanaweb.Domain.Products.Repositories; + +namespace Belezanaweb.Application.Validators.Products +{ + public class AlterProductValidator : BaseProductValidator + { + public AlterProductValidator() : base() + { + } + } +} diff --git a/src/Belezanaweb.Application/Validators/Products/BaseProductValidator.cs b/src/Belezanaweb.Application/Validators/Products/BaseProductValidator.cs new file mode 100644 index 00000000..2bc9f2a6 --- /dev/null +++ b/src/Belezanaweb.Application/Validators/Products/BaseProductValidator.cs @@ -0,0 +1,55 @@ +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Application.Products.DTOs; +using Belezanaweb.Core.Enums; +using Belezanaweb.Domain.Products.Enums; +using FluentValidation; +using System; +using System.Linq; + +namespace Belezanaweb.Application.Validators.Products +{ + public abstract class BaseProductValidator : AbstractValidator + { + public BaseProductValidator() + { + RuleFor(p => p.Sku) + .NotEmpty() + .WithMessage("Sku não pode ser nulo."); + + RuleFor(p => p.Name) + .NotEmpty() + .WithMessage("Nome precisa de um valor."); + + RuleFor(p => p.Inventory) + .NotEmpty() + .WithMessage("O produto precisa de inventário."); + + RuleForEach(p => p.Inventory.Warehouses) + .SetValidator(new WarehouseValidator()); + } + } + + class WarehouseValidator : AbstractValidator + { + public WarehouseValidator() + { + RuleFor(w => w.Locality) + .NotEmpty() + .WithMessage("A localidade do warehouse deve ser informada."); + + RuleFor(w => w.Type) + .NotEmpty() + .WithMessage("O tipo do warehouse deve ser informado.") + .Must(IsOfTypeWarehouseType) + .WithMessage("Typo de warehouse inválido."); + } + + private bool IsOfTypeWarehouseType(string type) + { + return Enum.GetValues(typeof(WarehouseType)) + .Cast().Any(x => x.GetDescription() == type); + } + + } + +} diff --git a/src/Belezanaweb.Application/Validators/Products/CreateProductValidator.cs b/src/Belezanaweb.Application/Validators/Products/CreateProductValidator.cs new file mode 100644 index 00000000..d49743eb --- /dev/null +++ b/src/Belezanaweb.Application/Validators/Products/CreateProductValidator.cs @@ -0,0 +1,13 @@ +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Domain.Products.Repositories; +using FluentValidation; + +namespace Belezanaweb.Application.Validators.Products +{ + public class CreateProductValidator : BaseProductValidator + { + public CreateProductValidator() : base() + { + } + } +} diff --git a/src/Belezanaweb.Application/Validators/Products/DeleteProductValidator.cs b/src/Belezanaweb.Application/Validators/Products/DeleteProductValidator.cs new file mode 100644 index 00000000..b2936def --- /dev/null +++ b/src/Belezanaweb.Application/Validators/Products/DeleteProductValidator.cs @@ -0,0 +1,15 @@ +using Belezanaweb.Application.Products.Commands; +using FluentValidation; + +namespace Belezanaweb.Application.Validators.Products +{ + public class DeleteProductValidator : AbstractValidator + { + public DeleteProductValidator() + { + RuleFor(p => p.Sku) + .NotEmpty() + .WithMessage("Sku não pode ser nulo."); + } + } +} diff --git a/src/Belezanaweb.Application/Validators/Products/GetProductBySkuValidator.cs b/src/Belezanaweb.Application/Validators/Products/GetProductBySkuValidator.cs new file mode 100644 index 00000000..d767f0f8 --- /dev/null +++ b/src/Belezanaweb.Application/Validators/Products/GetProductBySkuValidator.cs @@ -0,0 +1,15 @@ +using Belezanaweb.Application.Products.Queries; +using FluentValidation; + +namespace Belezanaweb.Application.Validators.Products +{ + public class GetProductBySkuValidator : AbstractValidator + { + public GetProductBySkuValidator() + { + RuleFor(p => p.Sku) + .NotEmpty() + .WithMessage("Sku não pode ser nulo."); + } + } +} diff --git a/src/Belezanaweb.Core/Belezanaweb.Core.csproj b/src/Belezanaweb.Core/Belezanaweb.Core.csproj new file mode 100644 index 00000000..93a2398f --- /dev/null +++ b/src/Belezanaweb.Core/Belezanaweb.Core.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/Belezanaweb.Core/Enums/EnumExtensions.cs b/src/Belezanaweb.Core/Enums/EnumExtensions.cs new file mode 100644 index 00000000..e9640785 --- /dev/null +++ b/src/Belezanaweb.Core/Enums/EnumExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel; + +namespace Belezanaweb.Core.Enums +{ + public static class EnumExtensions + { + public static string GetDescription(this T enumValue) + where T : struct, IConvertible + { + if (!typeof(T).IsEnum) + return null; + + var description = enumValue.ToString(); + var fieldInfo = enumValue.GetType().GetField(enumValue.ToString()); + + if (fieldInfo != null) + { + var attrs = fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), true); + if (attrs != null && attrs.Length > 0) + description = ((DescriptionAttribute)attrs[0]).Description; + } + + return description; + } + } +} diff --git a/src/Belezanaweb.Core/Exceptions/BusinessException.cs b/src/Belezanaweb.Core/Exceptions/BusinessException.cs new file mode 100644 index 00000000..36b1c801 --- /dev/null +++ b/src/Belezanaweb.Core/Exceptions/BusinessException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Belezanaweb.Core.Exceptions +{ + public class BusinessException : Exception + { + public BusinessException(string errorMessage) : base(errorMessage) + { + } + } +} diff --git a/src/Belezanaweb.Core/Exceptions/ValidatorException.cs b/src/Belezanaweb.Core/Exceptions/ValidatorException.cs new file mode 100644 index 00000000..68112293 --- /dev/null +++ b/src/Belezanaweb.Core/Exceptions/ValidatorException.cs @@ -0,0 +1,26 @@ +using FluentValidation.Results; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Belezanaweb.Core.Exceptions +{ + public class ValidatorException : Exception + { + public List Exceptions { get; set; } + + public ValidatorException(ValidationResult validation) : this(validation, null) + { + } + + public ValidatorException(ValidationResult validation, Exception innerException) : base(null, innerException) + { + Exceptions = new List(); + + foreach (var error in validation.Errors) + { + Exceptions.Add(new Exception(error.ErrorMessage)); + } + } + } +} diff --git a/src/Belezanaweb.Domain.Core/Belezanaweb.Domain.Core.csproj b/src/Belezanaweb.Domain.Core/Belezanaweb.Domain.Core.csproj new file mode 100644 index 00000000..cb631906 --- /dev/null +++ b/src/Belezanaweb.Domain.Core/Belezanaweb.Domain.Core.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/src/Belezanaweb.Domain.Core/Entities/EntityBase.cs b/src/Belezanaweb.Domain.Core/Entities/EntityBase.cs new file mode 100644 index 00000000..fb8e4b30 --- /dev/null +++ b/src/Belezanaweb.Domain.Core/Entities/EntityBase.cs @@ -0,0 +1,10 @@ +using System; + +namespace Belezanaweb.Domain.Core.Entities +{ + public class EntityBase : IEntity + { + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + } +} diff --git a/src/Belezanaweb.Domain.Core/Entities/IEntity.cs b/src/Belezanaweb.Domain.Core/Entities/IEntity.cs new file mode 100644 index 00000000..d94b41ef --- /dev/null +++ b/src/Belezanaweb.Domain.Core/Entities/IEntity.cs @@ -0,0 +1,6 @@ +namespace Belezanaweb.Domain.Core.Entities +{ + public interface IEntity + { + } +} diff --git a/src/Belezanaweb.Domain.Core/Repositories/IRepository.cs b/src/Belezanaweb.Domain.Core/Repositories/IRepository.cs new file mode 100644 index 00000000..3a2b4640 --- /dev/null +++ b/src/Belezanaweb.Domain.Core/Repositories/IRepository.cs @@ -0,0 +1,15 @@ +using Belezanaweb.Domain.Core.Entities; +using System; +using System.Collections.Generic; + +namespace Belezanaweb.Domain.Core.Repositories +{ + public interface IRepository where T : IEntity + { + IEnumerable GetList(int skip, int take); + void Insert(T entity); + T Get(long id); + void Delete(T entity); + void Update(T entity); + } +} diff --git a/src/Belezanaweb.Domain/Belezanaweb.Domain.csproj b/src/Belezanaweb.Domain/Belezanaweb.Domain.csproj new file mode 100644 index 00000000..1da46633 --- /dev/null +++ b/src/Belezanaweb.Domain/Belezanaweb.Domain.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/Belezanaweb.Domain/Products/Entities/Inventory.cs b/src/Belezanaweb.Domain/Products/Entities/Inventory.cs new file mode 100644 index 00000000..6a63c4c3 --- /dev/null +++ b/src/Belezanaweb.Domain/Products/Entities/Inventory.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Belezanaweb.Domain.Products.Entity +{ + public class Inventory + { + public ICollection Warehouses { get; set; } + } +} \ No newline at end of file diff --git a/src/Belezanaweb.Domain/Products/Entities/Product.cs b/src/Belezanaweb.Domain/Products/Entities/Product.cs new file mode 100644 index 00000000..45ee39ed --- /dev/null +++ b/src/Belezanaweb.Domain/Products/Entities/Product.cs @@ -0,0 +1,11 @@ +using Belezanaweb.Domain.Core.Entities; + +namespace Belezanaweb.Domain.Products.Entity +{ + public class Product : EntityBase + { + public long Sku { get; set; } + public string Name { get; set; } + public Inventory Inventory { get; set; } + } +} diff --git a/src/Belezanaweb.Domain/Products/Entities/Warehouse.cs b/src/Belezanaweb.Domain/Products/Entities/Warehouse.cs new file mode 100644 index 00000000..5a31927a --- /dev/null +++ b/src/Belezanaweb.Domain/Products/Entities/Warehouse.cs @@ -0,0 +1,9 @@ +namespace Belezanaweb.Domain.Products.Entity +{ + public class Warehouse + { + public string Locality { get; set; } + public int Quantity { get; set; } + public string Type { get; set; } + } +} diff --git a/src/Belezanaweb.Domain/Products/Enums/WarehouseType.cs b/src/Belezanaweb.Domain/Products/Enums/WarehouseType.cs new file mode 100644 index 00000000..58f6583e --- /dev/null +++ b/src/Belezanaweb.Domain/Products/Enums/WarehouseType.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; + +namespace Belezanaweb.Domain.Products.Enums +{ + public enum WarehouseType + { + [Description("ECOMMERCE")] + Ecommerce = 1, + + [Description("PHYSICAL_STORE")] + PhysicalStore = 2 + } +} diff --git a/src/Belezanaweb.Domain/Products/Repositories/IProductRepository.cs b/src/Belezanaweb.Domain/Products/Repositories/IProductRepository.cs new file mode 100644 index 00000000..085f71e1 --- /dev/null +++ b/src/Belezanaweb.Domain/Products/Repositories/IProductRepository.cs @@ -0,0 +1,9 @@ +using Belezanaweb.Domain.Core.Repositories; +using Belezanaweb.Domain.Products.Entity; + +namespace Belezanaweb.Domain.Products.Repositories +{ + public interface IProductRepository : IRepository + { + } +} diff --git a/src/Belezanaweb.Infra.Data/Belezanaweb.Infra.Data.csproj b/src/Belezanaweb.Infra.Data/Belezanaweb.Infra.Data.csproj new file mode 100644 index 00000000..a24afc99 --- /dev/null +++ b/src/Belezanaweb.Infra.Data/Belezanaweb.Infra.Data.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/Belezanaweb.Infra.Data/DbContexts/InMemoryDbContext.cs b/src/Belezanaweb.Infra.Data/DbContexts/InMemoryDbContext.cs new file mode 100644 index 00000000..b4a2a668 --- /dev/null +++ b/src/Belezanaweb.Infra.Data/DbContexts/InMemoryDbContext.cs @@ -0,0 +1,15 @@ +using Belezanaweb.Domain.Products.Entity; +using System.Collections.Generic; + +namespace Belezanaweb.Infra.Data.DbContexts +{ + public static class InMemoryDbContext + { + private static IList _products = new List(); + + public static IList Products + { + get => _products; + } + } +} diff --git a/src/Belezanaweb.Infra.Data/Repositories/Products/ProductRepository.cs b/src/Belezanaweb.Infra.Data/Repositories/Products/ProductRepository.cs new file mode 100644 index 00000000..a734a114 --- /dev/null +++ b/src/Belezanaweb.Infra.Data/Repositories/Products/ProductRepository.cs @@ -0,0 +1,38 @@ +using Belezanaweb.Domain.Products.Entity; +using Belezanaweb.Domain.Products.Repositories; +using Belezanaweb.Infra.Data.DbContexts; +using System.Collections.Generic; +using System.Linq; + +namespace Belezanaweb.Infra.Data.Repositories.Products +{ + public class ProductRepository : IProductRepository + { + public void Delete(Product entity) + { + InMemoryDbContext.Products.Remove(entity); + } + + public Product Get(long id) + { + return InMemoryDbContext.Products.FirstOrDefault(p => p.Sku == id); + } + + public IEnumerable GetList(int skip, int take) + { + return InMemoryDbContext.Products.Skip(skip).Take(take); + } + + public void Insert(Product entity) + { + InMemoryDbContext.Products.Add(entity); + } + + public void Update(Product entity) + { + var item = InMemoryDbContext.Products.Single(p => p.Sku == entity.Sku); + var index = InMemoryDbContext.Products.IndexOf(item); + InMemoryDbContext.Products[index] = entity; + } + } +} diff --git a/src/Belezanaweb.Infra.IoC/Belezanaweb.Infra.IoC.csproj b/src/Belezanaweb.Infra.IoC/Belezanaweb.Infra.IoC.csproj new file mode 100644 index 00000000..538641f0 --- /dev/null +++ b/src/Belezanaweb.Infra.IoC/Belezanaweb.Infra.IoC.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + diff --git a/src/Belezanaweb.Infra.IoC/IoC.cs b/src/Belezanaweb.Infra.IoC/IoC.cs new file mode 100644 index 00000000..c0cab310 --- /dev/null +++ b/src/Belezanaweb.Infra.IoC/IoC.cs @@ -0,0 +1,24 @@ +using Belezanaweb.Application.Core.Middlewares; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Application.Profiles.Products; +using Belezanaweb.Application.Validators; +using Belezanaweb.Application.Validators.Products; +using Belezanaweb.Domain.Products.Repositories; +using Belezanaweb.Infra.Data.Repositories.Products; +using FluentValidation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Belezanaweb.Infra.IoC +{ + public static class IoC + { + public static void Load(IServiceCollection services) + { + services.AddGlobalExceptionHandlerMiddleware(); + services.AddMediatR(typeof(BaseProductCommand).Assembly); + services.AddScoped(); + services.AddAutoMapper(typeof(InventoryProfile).Assembly); + } + } +} diff --git a/src/Belezanaweb.Solution.sln b/src/Belezanaweb.Solution.sln new file mode 100644 index 00000000..81573318 --- /dev/null +++ b/src/Belezanaweb.Solution.sln @@ -0,0 +1,88 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32421.90 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{2B869BFC-7A6D-4CE9-B382-AF7468410674}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C1240B14-8D26-4B28-AA13-16B0CF0B5FBE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Domain", "Belezanaweb.Domain\Belezanaweb.Domain.csproj", "{8ABF50BC-D574-487F-B9C6-BC0875A203A2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Application", "Belezanaweb.Application\Belezanaweb.Application.csproj", "{AD4C7577-C3B9-4F34-9756-27F83AAA5A1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Infra.IoC", "Belezanaweb.Infra.IoC\Belezanaweb.Infra.IoC.csproj", "{D36EECD9-7EAD-479E-B222-8EC798258FBC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Domain.Core", "Belezanaweb.Domain.Core\Belezanaweb.Domain.Core.csproj", "{54B80C5A-CBD6-4432-A912-1F776A2C1C28}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Core", "Belezanaweb.Core\Belezanaweb.Core.csproj", "{5166B82B-CF2B-43A0-9EB4-3E5362C17AC6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.API", "Belezanaweb.API\Belezanaweb.API.csproj", "{ED206A91-7449-44EF-8FFC-921A9B849A3D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Infra.Data", "Belezanaweb.Infra.Data\Belezanaweb.Infra.Data.csproj", "{770B7A23-AFFE-4AA4-BF45-73E1BB45225D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Application.Core", "Belezanaweb.Application.Core\Belezanaweb.Application.Core.csproj", "{B00C2B31-8A16-40CF-A78C-7D832C9599A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Belezanaweb.Application.Tests", "..\tests\Belezanaweb.Application.Tests\Belezanaweb.Application.Tests.csproj", "{162C2357-F587-4881-AF29-D9BA6E96E995}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8ABF50BC-D574-487F-B9C6-BC0875A203A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8ABF50BC-D574-487F-B9C6-BC0875A203A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ABF50BC-D574-487F-B9C6-BC0875A203A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8ABF50BC-D574-487F-B9C6-BC0875A203A2}.Release|Any CPU.Build.0 = Release|Any CPU + {AD4C7577-C3B9-4F34-9756-27F83AAA5A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD4C7577-C3B9-4F34-9756-27F83AAA5A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD4C7577-C3B9-4F34-9756-27F83AAA5A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD4C7577-C3B9-4F34-9756-27F83AAA5A1B}.Release|Any CPU.Build.0 = Release|Any CPU + {D36EECD9-7EAD-479E-B222-8EC798258FBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D36EECD9-7EAD-479E-B222-8EC798258FBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D36EECD9-7EAD-479E-B222-8EC798258FBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D36EECD9-7EAD-479E-B222-8EC798258FBC}.Release|Any CPU.Build.0 = Release|Any CPU + {54B80C5A-CBD6-4432-A912-1F776A2C1C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54B80C5A-CBD6-4432-A912-1F776A2C1C28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54B80C5A-CBD6-4432-A912-1F776A2C1C28}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54B80C5A-CBD6-4432-A912-1F776A2C1C28}.Release|Any CPU.Build.0 = Release|Any CPU + {5166B82B-CF2B-43A0-9EB4-3E5362C17AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5166B82B-CF2B-43A0-9EB4-3E5362C17AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5166B82B-CF2B-43A0-9EB4-3E5362C17AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5166B82B-CF2B-43A0-9EB4-3E5362C17AC6}.Release|Any CPU.Build.0 = Release|Any CPU + {ED206A91-7449-44EF-8FFC-921A9B849A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED206A91-7449-44EF-8FFC-921A9B849A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED206A91-7449-44EF-8FFC-921A9B849A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED206A91-7449-44EF-8FFC-921A9B849A3D}.Release|Any CPU.Build.0 = Release|Any CPU + {770B7A23-AFFE-4AA4-BF45-73E1BB45225D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {770B7A23-AFFE-4AA4-BF45-73E1BB45225D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {770B7A23-AFFE-4AA4-BF45-73E1BB45225D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {770B7A23-AFFE-4AA4-BF45-73E1BB45225D}.Release|Any CPU.Build.0 = Release|Any CPU + {B00C2B31-8A16-40CF-A78C-7D832C9599A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B00C2B31-8A16-40CF-A78C-7D832C9599A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B00C2B31-8A16-40CF-A78C-7D832C9599A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B00C2B31-8A16-40CF-A78C-7D832C9599A6}.Release|Any CPU.Build.0 = Release|Any CPU + {162C2357-F587-4881-AF29-D9BA6E96E995}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {162C2357-F587-4881-AF29-D9BA6E96E995}.Debug|Any CPU.Build.0 = Debug|Any CPU + {162C2357-F587-4881-AF29-D9BA6E96E995}.Release|Any CPU.ActiveCfg = Release|Any CPU + {162C2357-F587-4881-AF29-D9BA6E96E995}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8ABF50BC-D574-487F-B9C6-BC0875A203A2} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {AD4C7577-C3B9-4F34-9756-27F83AAA5A1B} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {D36EECD9-7EAD-479E-B222-8EC798258FBC} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {54B80C5A-CBD6-4432-A912-1F776A2C1C28} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {5166B82B-CF2B-43A0-9EB4-3E5362C17AC6} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {ED206A91-7449-44EF-8FFC-921A9B849A3D} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {770B7A23-AFFE-4AA4-BF45-73E1BB45225D} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {B00C2B31-8A16-40CF-A78C-7D832C9599A6} = {2B869BFC-7A6D-4CE9-B382-AF7468410674} + {162C2357-F587-4881-AF29-D9BA6E96E995} = {C1240B14-8D26-4B28-AA13-16B0CF0B5FBE} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D0DA56E9-ADD3-4F2C-8320-7AAF5F2F1471} + EndGlobalSection +EndGlobal diff --git a/tests/Belezanaweb.Application.Tests/Belezanaweb.Application.Tests.csproj b/tests/Belezanaweb.Application.Tests/Belezanaweb.Application.Tests.csproj new file mode 100644 index 00000000..1f22f4a0 --- /dev/null +++ b/tests/Belezanaweb.Application.Tests/Belezanaweb.Application.Tests.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/tests/Belezanaweb.Application.Tests/Products/Handlers/ProductCommandHandlerTest.cs b/tests/Belezanaweb.Application.Tests/Products/Handlers/ProductCommandHandlerTest.cs new file mode 100644 index 00000000..04b815a4 --- /dev/null +++ b/tests/Belezanaweb.Application.Tests/Products/Handlers/ProductCommandHandlerTest.cs @@ -0,0 +1,142 @@ +using AutoMapper; +using Belezanaweb.Application.Core.Commands; +using Belezanaweb.Application.Products.Commands; +using Belezanaweb.Application.Products.DTOs; +using Belezanaweb.Application.Products.Handlers; +using Belezanaweb.Application.Products.Queries; +using Belezanaweb.Core.Enums; +using Belezanaweb.Core.Exceptions; +using Belezanaweb.Domain.Products.Enums; +using Belezanaweb.Domain.Products.Repositories; +using Belezanaweb.Infra.IoC; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using NUnit.Framework; +using System.IO; +using System.Threading.Tasks; + +namespace Belezanaweb.Application.Tests +{ + public class Tests + { + private IServiceCollection _services; + private IProductRepository _productRepository; + private IMapper _mapper; + + [OneTimeSetUp] + public void Setup() + { + _services = new ServiceCollection(); + IoC.Load(_services); + var provider = _services.BuildServiceProvider(); + _productRepository = provider.GetRequiredService(); + _mapper = provider.GetRequiredService(); + } + + [Test(Description = "Cria um novo produto sem problemas.")] + public async Task CreateNewProduct() + { + var response = await CreateProductWithSkuAsync(12345678); + Assert.True(response.Success); + } + + [Test(Description = "Tenta criar um produto com SKU já existente, gerando BusinessException")] + public async Task CreateNewProductWithSku() + { + var response = await CreateProductWithSkuAsync(888); + Assert.True(response.Success); + Assert.CatchAsync(async () => await CreateProductWithSkuAsync(888)); + } + + [Test(Description = "Tenta criar um produto sem informar um SKU, gerando ValidatorException")] + public void CreateNewProductWithoutSku() + { + Assert.CatchAsync(async () => await CreateProductWithSkuAsync(0)); + } + + [Test(Description = "Obtém o produto gravado sem problemas")] + public async Task GetProductByValidSku() + { + var createResponse = await CreateProductWithSkuAsync(111); + Assert.True(createResponse.Success); + + var request = new GetProductBySkuQuery(111); + var queryHandler = new GetProductBySkuQueryHandler(_productRepository, _mapper); + var response = await queryHandler.Handle(request, new System.Threading.CancellationToken()); + + Assert.True(response.Success); + Assert.True(response.Data.IsMarketable); + Assert.AreEqual(15, response.Data.Inventory.Quantity); + } + + [Test(Description = "Tenta obter produto com SKU inexistente, gerando BusinessException")] + public void GetProductByInvalidSku() + { + long sku = 1; + var command = new GetProductBySkuQuery(sku); + + var handler = new GetProductBySkuQueryHandler(_productRepository, _mapper); + Assert.CatchAsync(async () => await handler.Handle(command, new System.Threading.CancellationToken())); + } + + [Test(Description = "Altera o estoque do produto cadastrado")] + public async Task SetWarehouseQuantity() + { + var createResponse = await CreateProductWithSkuAsync(555); + Assert.True(createResponse.Success); + + var command = GetCommand("AlterProductCommand"); + command.Sku = 555; + var handler = new AlterProductCommandHandler(_productRepository, _mapper); + var response = await handler.Handle(command, new System.Threading.CancellationToken()); + + Assert.True(response.Success); + } + + [Test(Description = "Cria um produto e logo exclúi")] + public async Task DeleteProduct() + { + var response = await CreateProductWithSkuAsync(999); + Assert.True(response.Success); + + var deleteCommand = new DeleteProductCommand(999); + var deleteHandler = new DeleteProductCommandHandler(_productRepository); + var deleteResponse = await deleteHandler.Handle(deleteCommand, new System.Threading.CancellationToken()); + Assert.True(deleteResponse.Success); + } + + [Test(Description = "Tenta excluir um produto não existente, gerando BusinessException")] + public void DeleteInvalidProduct() + { + var command = new DeleteProductCommand(987); + var handler = new DeleteProductCommandHandler(_productRepository); + Assert.CatchAsync(async () => await handler.Handle(command, new System.Threading.CancellationToken())); + } + + [Test(Description = "Tenta alterar um produto não existente na base, gerando BusinessException")] + public void TryToAlterInvalidProduct() + { + var command = GetCommand("AlterProductCommand"); + command.Sku = 12312; + + var handler = new AlterProductCommandHandler(_productRepository, _mapper); + Assert.CatchAsync(async () => await handler.Handle(command, new System.Threading.CancellationToken())); + + } + + private T GetCommand(string fileName) where T : IRequestBase + { + JsonSerializer jsonSerializer = new JsonSerializer(); + using TextReader file = File.OpenText(@$"../../../Resources/{fileName}.json"); + return (T)jsonSerializer.Deserialize(file, typeof(T)); + } + + private async Task CreateProductWithSkuAsync(long sku) + { + var command = GetCommand("CreateProductCommand"); + command.Sku = sku; + var handler = new CreateProductCommandHandler(_productRepository, _mapper); + return await handler.Handle(command, new System.Threading.CancellationToken()); + } + } +} \ No newline at end of file diff --git a/tests/Belezanaweb.Application.Tests/Resources/AlterProductCommand.json b/tests/Belezanaweb.Application.Tests/Resources/AlterProductCommand.json new file mode 100644 index 00000000..5160dbdd --- /dev/null +++ b/tests/Belezanaweb.Application.Tests/Resources/AlterProductCommand.json @@ -0,0 +1,18 @@ +{ + "sku": 43264, + "name": "L'Oréal Professionnel Expert Absolut Repair Cortex Lipidium - Máscara de Reconstrução 500g", + "inventory": { + "warehouses": [ + { + "locality": "SP", + "quantity": 4, + "type": "ECOMMERCE" + }, + { + "locality": "MOEMA", + "quantity": 0, + "type": "PHYSICAL_STORE" + } + ] + } +} diff --git a/tests/Belezanaweb.Application.Tests/Resources/CreateProductCommand.json b/tests/Belezanaweb.Application.Tests/Resources/CreateProductCommand.json new file mode 100644 index 00000000..929dc661 --- /dev/null +++ b/tests/Belezanaweb.Application.Tests/Resources/CreateProductCommand.json @@ -0,0 +1,18 @@ +{ + "sku": 43264, + "name": "L'Oréal Professionnel Expert Absolut Repair Cortex Lipidium - Máscara de Reconstrução 500g", + "inventory": { + "warehouses": [ + { + "locality": "SP", + "quantity": 12, + "type": "ECOMMERCE" + }, + { + "locality": "MOEMA", + "quantity": 3, + "type": "PHYSICAL_STORE" + } + ] + } +}