diff --git a/src/Belezanaweb.API/Belezanaweb.API.csproj b/src/Belezanaweb.API/Belezanaweb.API.csproj new file mode 100644 index 0000000..2a98b58 --- /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 0000000..61a04c4 --- /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 0000000..f3df621 --- /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 0000000..12cf21c --- /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 0000000..8983e0f --- /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 0000000..d9d9a9b --- /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 0000000..0acc881 --- /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 0000000..0fe98f7 --- /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 0000000..e14ac8a --- /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 0000000..a413284 --- /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 0000000..bf01fed --- /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 0000000..ee95a10 --- /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 0000000..2d09f82 --- /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 0000000..8e80fab --- /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 0000000..e789d76 --- /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 0000000..644e67a --- /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 0000000..eccbdea --- /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 0000000..0670e14 --- /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 0000000..01dda01 --- /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 0000000..90fb877 --- /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 0000000..cfa5815 --- /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 0000000..dd46fa2 --- /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 0000000..24a5d6e --- /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 0000000..6dc6e43 --- /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 0000000..452d079 --- /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 0000000..9765a23 --- /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 0000000..aa9c05b --- /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 0000000..73ebdfd --- /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 0000000..75138e7 --- /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 0000000..becda71 --- /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 0000000..32bece3 --- /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 0000000..ab4df51 --- /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 0000000..f9145f3 --- /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 0000000..2bc9f2a --- /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 0000000..d49743e --- /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 0000000..b2936de --- /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 0000000..d767f0f --- /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 0000000..93a2398 --- /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 0000000..e964078 --- /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 0000000..36b1c80 --- /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 0000000..6811229 --- /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 0000000..cb63190 --- /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 0000000..fb8e4b3 --- /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 0000000..d94b41e --- /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 0000000..3a2b464 --- /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 0000000..1da4663 --- /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 0000000..6a63c4c --- /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 0000000..45ee39e --- /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 0000000..5a31927 --- /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 0000000..58f6583 --- /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 0000000..085f71e --- /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 0000000..a24afc9 --- /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 0000000..b4a2a66 --- /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 0000000..a734a11 --- /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 0000000..538641f --- /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 0000000..c0cab31 --- /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 0000000..8157331 --- /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 0000000..1f22f4a --- /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 0000000..04b815a --- /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 0000000..5160dbd --- /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 0000000..929dc66 --- /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" + } + ] + } +}