diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 000000000..fe1152bdb --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 000000000..9491a2fda --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/src/ProjInv.API/Controllers/InvestidorController.cs b/src/ProjInv.API/Controllers/InvestidorController.cs new file mode 100644 index 000000000..6d6f7c0f3 --- /dev/null +++ b/src/ProjInv.API/Controllers/InvestidorController.cs @@ -0,0 +1,151 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using ProjInv.Application.UseCases.Investidor.Commands; + +namespace ProjInv.API.Controllers +{ + [ApiController] + [Route("api/Investidor")] + public class InvestidorController : Controller + { + private readonly IMediator _mediator; + private readonly Serilog.ILogger _logger; + + public InvestidorController(IMediator mediator, Serilog.ILogger logger) + { + _mediator = mediator; + _logger = logger; + } + + [HttpPost] + public async Task CriarInvestidor([FromBody] CriarInvestidorCommand request) + { + try + { + var result = await _mediator.Send(request, CancellationToken.None); + return Ok(new + { + hasError = false, + message = "Investidor criado com sucesso.", + data = result + }); + } + catch (ArgumentException ex) + { + _logger.Warning(ex, "Erro de validacao ao criar investidor"); + return BadRequest(new + { + hasError = true, + message = ex.Message + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao criar investidor"); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao.", + }); + } + } + + [HttpGet] + public async Task GetAllInvestidores([FromQuery] GetAllInvestidorCommand request, CancellationToken cancellationToken) + { + try + { + var result = await _mediator.Send(request, cancellationToken); + return Ok(new + { + hasError = false, + message = "Investidores listados com sucesso.", + data = result + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao listar investidores"); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + + [HttpPut("{id}")] + public async Task UpdateInvestidor([FromBody] UpdateInvestidorCommand request, CancellationToken cancellationToken) + { + try + { + var result = await _mediator.Send(request, cancellationToken); + return Ok(new + { + hasError = false, + message = "Investidor atualizado com sucesso.", + data = result + }); + } + catch (KeyNotFoundException ex) + { + _logger.Warning(ex, "Investidor {InvestidorId} nao encontrado", request.Id); + return NotFound(new + { + hasError = true, + message = ex.Message + }); + } + catch (ArgumentException ex) + { + _logger.Warning(ex, "Erro de validacao ao atualizar investidor {InvestidorId}", request.Id); + return BadRequest(new + { + hasError = true, + message = ex.Message + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao atualizar investidor {InvestidorId}", request.Id); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + + [HttpDelete("{id}")] + public async Task DeleteInvestidor(Guid id) + { + try + { + await _mediator.Send(new DeleteInvestidorCommand { Id = id }, CancellationToken.None); + return Ok(new + { + hasError = false, + message = "Investidor excluido com sucesso." + }); + } + catch (KeyNotFoundException ex) + { + _logger.Warning(ex, "Investidor {InvestidorId} nao encontrado", id); + return NotFound(new + { + hasError = true, + message = ex.Message + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao excluir investidor {InvestidorId}", id); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + } +} diff --git a/src/ProjInv.API/Controllers/InvestimentoController.cs b/src/ProjInv.API/Controllers/InvestimentoController.cs new file mode 100644 index 000000000..9d366416a --- /dev/null +++ b/src/ProjInv.API/Controllers/InvestimentoController.cs @@ -0,0 +1,186 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class InvestimentoController : Controller + { + private readonly IMediator _mediator; + private readonly Serilog.ILogger _logger; + + public InvestimentoController(IMediator mediator, Serilog.ILogger logger) + { + _mediator = mediator; + _logger = logger; + } + + [HttpPost] + public async Task CriarInvestimento([FromBody] CriarInvestimentoCommand request, CancellationToken cancellationToken) + { + try + { + var result = await _mediator.Send(request, cancellationToken); + return Ok(new + { + hasError = false, + message = "Investimento criado com sucesso.", + data = result + }); + } + catch (ArgumentException ex) + { + _logger.Warning(ex, "Erro de validacao ao criar investimento"); + return BadRequest(new + { + hasError = true, + message = ex.Message + }); + } + catch (KeyNotFoundException ex) + { + _logger.Warning(ex, "Investidor nao encontrado"); + return NotFound(new + { + hasError = true, + message = ex.Message + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao criar investimento"); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + + [HttpGet("{id}")] + public async Task GetInvestimentoById(Guid id, CancellationToken cancellationToken) + { + try + { + var result = await _mediator.Send(new VisualizarInvestimentoCommand { Id = id }, cancellationToken); + return Ok(new + { + hasError = false, + message = "Investimento encontrado com sucesso.", + data = result + }); + } + catch (KeyNotFoundException ex) + { + _logger.Warning(ex, "Investimento {InvestimentoId} nao encontrado", id); + return NotFound(new + { + hasError = true, + message = ex.Message + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao buscar investimento {InvestimentoId}", id); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + + [HttpPost("{id}/retirar")] + public async Task RetirarInvestimento(Guid id, [FromBody] RetirarInvestimentoCommand request, CancellationToken cancellationToken) + { + try + { + if (id != request.InvestimentoId && request.InvestimentoId != Guid.Empty) + { + return BadRequest(new + { + hasError = true, + message = "ID do investimento na URL difere do ID no corpo da requisicao." + }); + } + + request.InvestimentoId = id; + var result = await _mediator.Send(request, cancellationToken); + return Ok(new + { + hasError = false, + message = "Retirada realizada com sucesso.", + data = result + }); + } + catch (KeyNotFoundException ex) + { + _logger.Warning(ex, "Investimento {InvestimentoId} nao encontrado", id); + return NotFound(new + { + hasError = true, + message = ex.Message + }); + } + catch (InvalidOperationException ex) + { + _logger.Warning(ex, "Operacao invalida ao retirar investimento {InvestimentoId}", id); + return BadRequest(new + { + hasError = true, + message = ex.Message + }); + } + catch (ArgumentException ex) + { + _logger.Warning(ex, "Erro de validacao ao retirar investimento {InvestimentoId}", id); + return BadRequest(new + { + hasError = true, + message = ex.Message + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao processar retirada do investimento {InvestimentoId}", id); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + + [HttpGet("/api/investidor/{investidorId}/investimentos")] + public async Task ListarInvestimentos(Guid investidorId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10, CancellationToken cancellationToken = default) + { + try + { + var command = new ListarInvestimentosCommand + { + InvestidorId = investidorId, + Page = page, + PageSize = pageSize + }; + var result = await _mediator.Send(command, cancellationToken); + return Ok(new + { + hasError = false, + message = "Investimentos listados com sucesso.", + data = result + }); + } + catch (Exception ex) + { + _logger.Error(ex, "Erro ao listar investimentos do investidor {InvestidorId}", investidorId); + return StatusCode(500, new + { + hasError = true, + message = "Erro interno ao processar a requisicao." + }); + } + } + } +} diff --git a/src/ProjInv.API/Dockerfile b/src/ProjInv.API/Dockerfile new file mode 100644 index 000000000..13d432c1f --- /dev/null +++ b/src/ProjInv.API/Dockerfile @@ -0,0 +1,33 @@ +# Acesse https://aka.ms/customizecontainer para saber como personalizar seu contêiner de depuração e como o Visual Studio usa este Dockerfile para criar suas imagens para uma depuração mais rápida. + +# Esta fase é usada durante a execução no VS no modo rápido (Padrão para a configuração de Depuração) +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# Esta fase é usada para compilar o projeto de serviço +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["ProjInv.API/ProjInv.API.csproj", "ProjInv.API/"] +COPY ["ProjInv.Application/ProjInv.Application.csproj", "ProjInv.Application/"] +COPY ["ProjInv.Domain/ProjInv.Domain.csproj", "ProjInv.Domain/"] +COPY ["ProjInv.Infrastructure/ProjInv.Infrastructure.csproj", "ProjInv.Infrastructure/"] +RUN dotnet restore "./ProjInv.API/ProjInv.API.csproj" +COPY . . +WORKDIR "/src/ProjInv.API" +RUN dotnet build "./ProjInv.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Esta fase é usada para publicar o projeto de serviço a ser copiado para a fase final +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ProjInv.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Esta fase é usada na produção ou quando executada no VS no modo normal (padrão quando não está usando a configuração de Depuração) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ProjInv.API.dll"] \ No newline at end of file diff --git a/src/ProjInv.API/Program.cs b/src/ProjInv.API/Program.cs new file mode 100644 index 000000000..a3d9d8ea4 --- /dev/null +++ b/src/ProjInv.API/Program.cs @@ -0,0 +1,95 @@ +using FluentValidation; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; +using ProjInv.Domain.Interfaces; +using ProjInv.Infrastructure.Data; +using ProjInv.Infrastructure.Repositories; +using System.Text.Json; + +using Serilog; +using ProjInv.Application.UseCases.Investidor; +using ProjInv.Application.UseCases.Investidor.Handles; + +var builder = WebApplication.CreateBuilder(args); + +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + +builder.Host.UseSerilog(); + +builder.Services.AddControllers(); + +if (builder.Environment.IsEnvironment("Testing")) +{ + builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("InMemoryDbForTesting")); +} +else +{ + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); +} + +builder.Services.AddHealthChecks() + .AddDbContextCheck("database"); + +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Investimentos API", + Version = "v1" + }); +}); + +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(GetAllInvestidorHandler).Assembly)); +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CriarInvestidorHandler).Assembly)); +builder.Services.AddValidatorsFromAssembly(typeof(CriarInvestidorHandler).Assembly); + +builder.Services.AddSingleton(Log.Logger); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.MapHealthChecks("/health", new HealthCheckOptions +{ + ResponseWriter = async (context, report) => + { + context.Response.ContentType = "application/json; charset=utf-8"; + var response = new + { + status = report.Status.ToString(), + totalDuration = report.TotalDuration, + checks = report.Entries.Select(e => new + { + name = e.Key, + status = e.Value.Status.ToString(), + description = e.Value.Description, + exception = e.Value.Exception?.Message, + duration = e.Value.Duration + }) + }; + await context.Response.WriteAsync( + JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true }) + ); + } +}); + +app.Run(); diff --git a/src/ProjInv.API/ProjInv.API.csproj b/src/ProjInv.API/ProjInv.API.csproj new file mode 100644 index 000000000..b822c032b --- /dev/null +++ b/src/ProjInv.API/ProjInv.API.csproj @@ -0,0 +1,34 @@ + + + + net9.0 + enable + enable + d3d8812d-baec-44ca-a155-93299ea9b1a5 + Linux + ..\docker-compose.dcproj + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/src/ProjInv.API/ProjInv.API.http b/src/ProjInv.API/ProjInv.API.http new file mode 100644 index 000000000..54572ff15 --- /dev/null +++ b/src/ProjInv.API/ProjInv.API.http @@ -0,0 +1,6 @@ +@ProjInv.API_HostAddress = http://localhost:5266 + +GET {{ProjInv.API_HostAddress}}/health +Accept: application/json + +### diff --git a/src/ProjInv.API/Properties/launchSettings.json b/src/ProjInv.API/Properties/launchSettings.json new file mode 100644 index 000000000..b94aaf7f4 --- /dev/null +++ b/src/ProjInv.API/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5266" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7052;http://localhost:5266" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/src/ProjInv.API/appsettings.Development.json b/src/ProjInv.API/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/src/ProjInv.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/ProjInv.API/appsettings.json b/src/ProjInv.API/appsettings.json new file mode 100644 index 000000000..7b3a0b997 --- /dev/null +++ b/src/ProjInv.API/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=ProjInvestimento;User Id=sa;Password=YourStrong@Passw0rd;MultipleActiveResultSets=True;TrustServerCertificate=True" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/ProjInv.API/libman.json b/src/ProjInv.API/libman.json new file mode 100644 index 000000000..8ed1410ec --- /dev/null +++ b/src/ProjInv.API/libman.json @@ -0,0 +1,5 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/src/ProjInv.Application/DTOs/Responses/Investidor/CriarInvestidorResponseDto.cs b/src/ProjInv.Application/DTOs/Responses/Investidor/CriarInvestidorResponseDto.cs new file mode 100644 index 000000000..1b847287f --- /dev/null +++ b/src/ProjInv.Application/DTOs/Responses/Investidor/CriarInvestidorResponseDto.cs @@ -0,0 +1,8 @@ +namespace ProjInv.Application.DTOs.Responses.Investidor +{ + public class InvestidorResponseDto + { + public Guid Id { get; set; } + public string Nome { get; set; } = string.Empty; + } +} diff --git a/src/ProjInv.Application/DTOs/Responses/Investidor/GetAllInvestidorResponseDto.cs b/src/ProjInv.Application/DTOs/Responses/Investidor/GetAllInvestidorResponseDto.cs new file mode 100644 index 000000000..7be4eccd0 --- /dev/null +++ b/src/ProjInv.Application/DTOs/Responses/Investidor/GetAllInvestidorResponseDto.cs @@ -0,0 +1,7 @@ +namespace ProjInv.Application.DTOs.Responses.Investidor +{ + public class GetAllInvestidorResponseDto + { + public List Investidores { get; set; } + } +} diff --git a/src/ProjInv.Application/DTOs/Responses/Investidor/InvestidorDto.cs b/src/ProjInv.Application/DTOs/Responses/Investidor/InvestidorDto.cs new file mode 100644 index 000000000..d590ff79a --- /dev/null +++ b/src/ProjInv.Application/DTOs/Responses/Investidor/InvestidorDto.cs @@ -0,0 +1,8 @@ +namespace ProjInv.Application.DTOs.Responses.Investidor +{ + public class InvestidorDto + { + public Guid Id { get; set; } + public string Nome { get; set; } = string.Empty; + } +} diff --git a/src/ProjInv.Application/DTOs/Responses/Investimento/InvestimentoResponseDto.cs b/src/ProjInv.Application/DTOs/Responses/Investimento/InvestimentoResponseDto.cs new file mode 100644 index 000000000..49f8bd65d --- /dev/null +++ b/src/ProjInv.Application/DTOs/Responses/Investimento/InvestimentoResponseDto.cs @@ -0,0 +1,13 @@ +namespace ProjInv.Application.DTOs.Responses.Investimento +{ + public class InvestimentoResponseDto + { + public Guid Id { get; set; } + public Guid InvestidorId { get; set; } + public decimal ValorInicial { get; set; } + public DateTime DataCriacao { get; set; } + public decimal SaldoEsperado { get; set; } + public bool FoiRetirado { get; set; } + public RetiradaDto? Retirada { get; set; } + } +} diff --git a/src/ProjInv.Application/DTOs/Responses/Investimento/ListarInvestimentosResponseDto.cs b/src/ProjInv.Application/DTOs/Responses/Investimento/ListarInvestimentosResponseDto.cs new file mode 100644 index 000000000..1ba76024b --- /dev/null +++ b/src/ProjInv.Application/DTOs/Responses/Investimento/ListarInvestimentosResponseDto.cs @@ -0,0 +1,10 @@ +namespace ProjInv.Application.DTOs.Responses.Investimento +{ + public class ListarInvestimentosResponseDto + { + public IEnumerable Investimentos { get; set; } = new List(); + public int TotalCount { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } +} diff --git a/src/ProjInv.Application/DTOs/Responses/Investimento/RetiradaDto.cs b/src/ProjInv.Application/DTOs/Responses/Investimento/RetiradaDto.cs new file mode 100644 index 000000000..122309413 --- /dev/null +++ b/src/ProjInv.Application/DTOs/Responses/Investimento/RetiradaDto.cs @@ -0,0 +1,11 @@ +namespace ProjInv.Application.DTOs.Responses.Investimento +{ + public class RetiradaDto + { + public Guid Id { get; set; } + public DateTime DataRetirada { get; set; } + public decimal ValorBruto { get; set; } + public decimal Impostos { get; set; } + public decimal ValorLiquido { get; set; } + } +} diff --git a/src/ProjInv.Application/ProjInv.Application.csproj b/src/ProjInv.Application/ProjInv.Application.csproj new file mode 100644 index 000000000..39d86ee3a --- /dev/null +++ b/src/ProjInv.Application/ProjInv.Application.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/src/ProjInv.Application/UseCases/Investidor/Commands/CriarInvestidorCommand.cs b/src/ProjInv.Application/UseCases/Investidor/Commands/CriarInvestidorCommand.cs new file mode 100644 index 000000000..f4e5e8bcf --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Commands/CriarInvestidorCommand.cs @@ -0,0 +1,10 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investidor; + +namespace ProjInv.Application.UseCases.Investidor.Commands +{ + public class CriarInvestidorCommand : IRequest + { + public string Nome { get; set; } = string.Empty; + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Commands/DeleteInvestidorCommand.cs b/src/ProjInv.Application/UseCases/Investidor/Commands/DeleteInvestidorCommand.cs new file mode 100644 index 000000000..a6c4d9d11 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Commands/DeleteInvestidorCommand.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace ProjInv.Application.UseCases.Investidor.Commands +{ + public class DeleteInvestidorCommand : IRequest + { + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/src/ProjInv.Application/UseCases/Investidor/Commands/GetAllInvestidorCommand.cs b/src/ProjInv.Application/UseCases/Investidor/Commands/GetAllInvestidorCommand.cs new file mode 100644 index 000000000..e44e2031e --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Commands/GetAllInvestidorCommand.cs @@ -0,0 +1,11 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investidor; + +namespace ProjInv.Application.UseCases.Investidor.Commands +{ + public class GetAllInvestidorCommand : IRequest + { + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Commands/UpdateInvestidorCommand.cs b/src/ProjInv.Application/UseCases/Investidor/Commands/UpdateInvestidorCommand.cs new file mode 100644 index 000000000..a27553b5e --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Commands/UpdateInvestidorCommand.cs @@ -0,0 +1,17 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investidor; + +namespace ProjInv.Application.UseCases.Investidor.Commands +{ + public class UpdateInvestidorCommand : IRequest + { + public Guid Id { get; set; } + public string Nome { get; set; } = string.Empty; + + public UpdateInvestidorCommand(Guid id, string nome) + { + Id = id; + Nome = nome; + } + } +} \ No newline at end of file diff --git a/src/ProjInv.Application/UseCases/Investidor/Handles/CriarInvestidorHandler.cs b/src/ProjInv.Application/UseCases/Investidor/Handles/CriarInvestidorHandler.cs new file mode 100644 index 000000000..ddeea7bea --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Handles/CriarInvestidorHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; + +using FluentValidation; +using ProjInv.Application.DTOs.Responses.Investidor; +using ProjInv.Application.UseCases.Investidor.Commands; +using Serilog; + +namespace ProjInv.Application.UseCases.Investidor.Handles +{ + public class CriarInvestidorHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + private readonly ILogger _logger; + + public CriarInvestidorHandler( + IUnitOfWork uow, + IValidator validator, + ILogger logger) + { + _uow = uow; + _validator = validator; + _logger = logger; + } + + public async Task Handle(CriarInvestidorCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + _logger.Error("Falha ao validar o CriarInvestidorCommand: {Errors}", errors); + throw new ArgumentException(errors); + } + + await _uow.BeginTransactionAsync(); + + try + { + var investidor = new Domain.Entities.Investidor(request.Nome); + await _uow.Investidores.AddAsync(investidor, cancellationToken); + await _uow.CommitAsync(); + + return new InvestidorResponseDto + { + Id = investidor.Id, + Nome = investidor.Nome + }; + } + catch + { + await _uow.RollbackAsync(); + _logger.Error("Erro ao criar investidor"); + throw; + } + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Handles/DeleteInvestidorHandler.cs b/src/ProjInv.Application/UseCases/Investidor/Handles/DeleteInvestidorHandler.cs new file mode 100644 index 000000000..492f9a37f --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Handles/DeleteInvestidorHandler.cs @@ -0,0 +1,60 @@ +using FluentValidation; +using MediatR; +using ProjInv.Application.UseCases.Investidor.Commands; +using ProjInv.Domain.Interfaces; +using Serilog; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace ProjInv.Application.UseCases.Investidor.Handles +{ + public class DeleteInvestidorHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + private readonly ILogger _logger; + + public DeleteInvestidorHandler( + IUnitOfWork uow, + IValidator validator, + ILogger logger) + { + _uow = uow; + _validator = validator; + _logger = logger; + } + + public async Task Handle(DeleteInvestidorCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + _logger.Error("Falha ao validar o DeleteInvestidorCommand: {Errors}", errors); + throw new ArgumentException(errors); + } + + await _uow.BeginTransactionAsync(); + + try + { + var investidor = await _uow.Investidores.GetByIdAsync(request.Id, cancellationToken); + if (investidor == null) + { + _logger.Error("Investidor no encontrado para Remoo."); + throw new KeyNotFoundException("Investidor nao encontrado."); + } + + await _uow.Investidores.DeleteAsync(request.Id, cancellationToken); + await _uow.CommitAsync(); + + return Unit.Value; + } + catch + { + await _uow.RollbackAsync(); + _logger.Error("Erro ao deletar o Investidor: {Id}", request.Id); + throw; + } + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Handles/GetAllInvestidoresHandler.cs b/src/ProjInv.Application/UseCases/Investidor/Handles/GetAllInvestidoresHandler.cs new file mode 100644 index 000000000..7fca1a3e5 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Handles/GetAllInvestidoresHandler.cs @@ -0,0 +1,50 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses; +using ProjInv.Domain.Interfaces; + +using FluentValidation; +using ProjInv.Application.DTOs.Responses.Investidor; +using ProjInv.Application.UseCases.Investidor.Commands; +using Serilog; + +namespace ProjInv.Application.UseCases.Investidor +{ + public class GetAllInvestidorHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + private readonly ILogger _logger; + + public GetAllInvestidorHandler( + IUnitOfWork uow, + IValidator validator, + ILogger logger) + { + _uow = uow; + _validator = validator; + _logger = logger; + } + + public async Task Handle(GetAllInvestidorCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + _logger.Error("Falha ao validar GetAllInvestidorCommand: {Errors}", errors); + throw new ArgumentException(errors); + } + + var investidores = await _uow.Investidores.GetAllAsync(request.Page, request.PageSize, cancellationToken); + + return new GetAllInvestidorResponseDto + { + Investidores = investidores.Select(x => new InvestidorDto + { + Id = x.Id, + Nome = x.Nome + }).ToList() + }; + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Handles/UpdateInvestidorHandler.cs b/src/ProjInv.Application/UseCases/Investidor/Handles/UpdateInvestidorHandler.cs new file mode 100644 index 000000000..3f6c50e75 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Handles/UpdateInvestidorHandler.cs @@ -0,0 +1,65 @@ +using FluentValidation; +using MediatR; +using ProjInv.Application.DTOs.Responses.Investidor; +using ProjInv.Application.UseCases.Investidor.Commands; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Application.UseCases.Investidor.Handles +{ + public class UpdateInvestidorHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + private readonly ILogger _logger; + + public UpdateInvestidorHandler( + IUnitOfWork uow, + IValidator validator, + ILogger logger) + { + _uow = uow; + _validator = validator; + _logger = logger; + } + + public async Task Handle(UpdateInvestidorCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + _logger.Error("Falha ao validar UpdateInvestidorCommand: {Errors}", errors); + throw new ArgumentException(errors); + } + + await _uow.BeginTransactionAsync(); + + try + { + var investidor = await _uow.Investidores.GetByIdAsync(request.Id, cancellationToken); + if (investidor == null) + { + _logger.Error("Investidor com Id {Id} nao encontrado para atualizacao.", request.Id); + throw new KeyNotFoundException("Investidor nao encontrado."); + } + + investidor.UpdateNome(request.Nome); + await _uow.Investidores.UpdateAsync(investidor, cancellationToken); + await _uow.CommitAsync(); + + return new InvestidorResponseDto + { + Id = investidor.Id, + Nome = investidor.Nome + }; + } + catch + { + await _uow.RollbackAsync(); + _logger.Error("Erro ao atualizar Investidor com Id {Id}.", request.Id); + throw; + } + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Validators/CriarInvestidorCommandValidator.cs b/src/ProjInv.Application/UseCases/Investidor/Validators/CriarInvestidorCommandValidator.cs new file mode 100644 index 000000000..acf7b9f28 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Validators/CriarInvestidorCommandValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investidor.Commands; + +namespace ProjInv.Application.UseCases.Investidor.Validators +{ + public class CriarInvestidorCommandValidator : AbstractValidator + { + public CriarInvestidorCommandValidator() + { + RuleFor(x => x.Nome) + .NotEmpty().WithMessage("O nome do investidor e obrigatorio.") + .Length(3, 200).WithMessage("O nome deve ter entre 3 e 200 caracteres."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Validators/DeleteInvestidorCommandValidator.cs b/src/ProjInv.Application/UseCases/Investidor/Validators/DeleteInvestidorCommandValidator.cs new file mode 100644 index 000000000..bc3658d43 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Validators/DeleteInvestidorCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investidor.Commands; + +namespace ProjInv.Application.UseCases.Investidor.Validators +{ + public class DeleteInvestidorCommandValidator : AbstractValidator + { + public DeleteInvestidorCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("O ID do investidor e obrigatorio."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Validators/GetAllInvestidorCommandValidator.cs b/src/ProjInv.Application/UseCases/Investidor/Validators/GetAllInvestidorCommandValidator.cs new file mode 100644 index 000000000..886d25f51 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Validators/GetAllInvestidorCommandValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investidor.Commands; + +namespace ProjInv.Application.UseCases.Investidor.Validators +{ + public class GetAllInvestidorCommandValidator : AbstractValidator + { + public GetAllInvestidorCommandValidator() + { + RuleFor(x => x.Page) + .GreaterThan(0).WithMessage("A pagina deve ser maior que zero."); + + RuleFor(x => x.PageSize) + .GreaterThan(0).WithMessage("O tamanho da pagina deve ser maior que zero."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investidor/Validators/UpdateInvestidorCommandValidator.cs b/src/ProjInv.Application/UseCases/Investidor/Validators/UpdateInvestidorCommandValidator.cs new file mode 100644 index 000000000..d536171f8 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investidor/Validators/UpdateInvestidorCommandValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investidor.Commands; + +namespace ProjInv.Application.UseCases.Investidor.Validators +{ + public class UpdateInvestidorCommandValidator : AbstractValidator + { + public UpdateInvestidorCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("O ID do investidor e obrigatorio."); + + RuleFor(x => x.Nome) + .NotEmpty().WithMessage("O nome do investidor e obrigatorio.") + .Length(3, 200).WithMessage("O nome deve ter entre 3 e 200 caracteres."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Commands/CriarInvestimentoCommand.cs b/src/ProjInv.Application/UseCases/Investimento/Commands/CriarInvestimentoCommand.cs new file mode 100644 index 000000000..b828c1e7e --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Commands/CriarInvestimentoCommand.cs @@ -0,0 +1,12 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investimento; + +namespace ProjInv.Application.UseCases.Investimento.Commands +{ + public class CriarInvestimentoCommand : IRequest + { + public Guid InvestidorId { get; set; } + public decimal ValorInicial { get; set; } + public DateTime DataCriacao { get; set; } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Commands/ListarInvestimentosCommand.cs b/src/ProjInv.Application/UseCases/Investimento/Commands/ListarInvestimentosCommand.cs new file mode 100644 index 000000000..566cd90a2 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Commands/ListarInvestimentosCommand.cs @@ -0,0 +1,12 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investimento; + +namespace ProjInv.Application.UseCases.Investimento.Commands +{ + public class ListarInvestimentosCommand : IRequest + { + public Guid InvestidorId { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Commands/RetirarInvestimentoCommand.cs b/src/ProjInv.Application/UseCases/Investimento/Commands/RetirarInvestimentoCommand.cs new file mode 100644 index 000000000..5cff5d7f3 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Commands/RetirarInvestimentoCommand.cs @@ -0,0 +1,11 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investimento; + +namespace ProjInv.Application.UseCases.Investimento.Commands +{ + public class RetirarInvestimentoCommand : IRequest + { + public Guid InvestimentoId { get; set; } + public DateTime DataRetirada { get; set; } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Commands/VisualizarInvestimentoCommand.cs b/src/ProjInv.Application/UseCases/Investimento/Commands/VisualizarInvestimentoCommand.cs new file mode 100644 index 000000000..4fbf36ab8 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Commands/VisualizarInvestimentoCommand.cs @@ -0,0 +1,10 @@ +using MediatR; +using ProjInv.Application.DTOs.Responses.Investimento; + +namespace ProjInv.Application.UseCases.Investimento.Commands +{ + public class VisualizarInvestimentoCommand : IRequest + { + public Guid Id { get; set; } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Handles/CriarInvestimentoHandler.cs b/src/ProjInv.Application/UseCases/Investimento/Handles/CriarInvestimentoHandler.cs new file mode 100644 index 000000000..9668ca9a4 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Handles/CriarInvestimentoHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using ProjInv.Domain.Interfaces; +using Serilog; +using FluentValidation; +using ProjInv.Application.DTOs.Responses.Investimento; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Handles +{ + public class CriarInvestimentoHandler : IRequestHandler + { + private readonly IUnitOfWork _unitOfWork; + private readonly IValidator _validator; + private readonly ILogger _logger; + + public CriarInvestimentoHandler( + IUnitOfWork unitOfWork, + IValidator validator, + ILogger logger) + { + _unitOfWork = unitOfWork; + _validator = validator; + _logger = logger; + } + + public async Task Handle(CriarInvestimentoCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + throw new ArgumentException(errors); + } + + _logger.Information("Criando investimento para investidor {InvestidorId} com valor {Valor}", request.InvestidorId, request.ValorInicial); + + var investidor = await _unitOfWork.Investidores.GetByIdAsync(request.InvestidorId, cancellationToken); + if (investidor == null) + { + _logger.Warning("Investidor {InvestidorId} nao encontrado", request.InvestidorId); + throw new KeyNotFoundException($"Investidor com ID {request.InvestidorId} nao encontrado."); + } + + var investimento = new Domain.Entities.Investimento(request.InvestidorId, request.ValorInicial, request.DataCriacao); + await _unitOfWork.Investimentos.AddAsync(investimento, cancellationToken); + + await _unitOfWork.CommitAsync(); + + return new InvestimentoResponseDto + { + Id = investimento.Id, + InvestidorId = investimento.InvestidorId, + ValorInicial = investimento.ValorInicial, + DataCriacao = investimento.DataCriacao, + SaldoEsperado = investimento.CalcularSaldoEsperado(), + FoiRetirado = false + }; + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Handles/ListarInvestimentosHandler.cs b/src/ProjInv.Application/UseCases/Investimento/Handles/ListarInvestimentosHandler.cs new file mode 100644 index 000000000..eeb705ad9 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Handles/ListarInvestimentosHandler.cs @@ -0,0 +1,69 @@ +using MediatR; +using ProjInv.Domain.Interfaces; + +using FluentValidation; +using ProjInv.Application.DTOs.Responses.Investimento; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Handles +{ + public class ListarInvestimentosHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + + public ListarInvestimentosHandler(IUnitOfWork uow, IValidator validator) + { + _uow = uow; + _validator = validator; + } + + public async Task Handle(ListarInvestimentosCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + throw new ArgumentException(errors); + } + + var investimentos = await _uow.Investimentos.GetByInvestidorIdAsync(request.InvestidorId, request.Page, request.PageSize, cancellationToken); + var totalCount = await _uow.Investimentos.CountByInvestidorIdAsync(request.InvestidorId, cancellationToken); + + var investimentosDto = investimentos.Select(i => + { + RetiradaDto? retiradaDto = null; + if (i.FoiRetirado) + { + retiradaDto = new RetiradaDto + { + Id = i.Retirada!.Id, + DataRetirada = i.Retirada.DataRetirada, + ValorBruto = i.Retirada.ValorBruto, + Impostos = i.Retirada.Impostos, + ValorLiquido = i.Retirada.ValorLiquido + }; + } + + return new InvestimentoResponseDto + { + Id = i.Id, + InvestidorId = i.InvestidorId, + ValorInicial = i.ValorInicial, + DataCriacao = i.DataCriacao, + SaldoEsperado = i.CalcularSaldoEsperado(), + FoiRetirado = i.FoiRetirado, + Retirada = retiradaDto + }; + }).ToList(); + + return new ListarInvestimentosResponseDto + { + Investimentos = investimentosDto, + TotalCount = totalCount, + Page = request.Page, + PageSize = request.PageSize + }; + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Handles/RetirarInvestimentoHandler.cs b/src/ProjInv.Application/UseCases/Investimento/Handles/RetirarInvestimentoHandler.cs new file mode 100644 index 000000000..9099bd792 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Handles/RetirarInvestimentoHandler.cs @@ -0,0 +1,96 @@ +using MediatR; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +using FluentValidation; +using ProjInv.Application.DTOs.Responses.Investimento; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Handles +{ + public class RetirarInvestimentoHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + private readonly ILogger _logger; + + public RetirarInvestimentoHandler( + IUnitOfWork uow, + IValidator validator, + ILogger logger) + { + _uow = uow; + _validator = validator; + _logger = logger; + } + + public async Task Handle(RetirarInvestimentoCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + throw new ArgumentException(errors); + } + + _logger.Information("Processando retirada do investimento {InvestimentoId}", request.InvestimentoId); + + await _uow.BeginTransactionAsync(); + + try + { + var investimento = await _uow.Investimentos.GetByIdAsync(request.InvestimentoId, cancellationToken); + if (investimento == null) + { + _logger.Warning("Investimento {InvestimentoId} nao encontrado", request.InvestimentoId); + throw new KeyNotFoundException($"Investimento com ID {request.InvestimentoId} nao encontrado."); + } + + if (investimento.FoiRetirado) + { + _logger.Warning("Investimento {InvestimentoId} ja foi retirado", request.InvestimentoId); + throw new InvalidOperationException("Este investimento ja foi retirado."); + } + + if (request.DataRetirada < investimento.DataCriacao) + { + throw new ArgumentException("A data de retirada nao pode ser anterior a data de criacao do investimento."); + } + + if (request.DataRetirada > DateTime.UtcNow.Date) + { + throw new ArgumentException("A data de retirada nao pode ser no futuro."); + } + + var valorBruto = investimento.CalcularSaldoEsperado(request.DataRetirada); + var impostos = investimento.CalcularImpostos(request.DataRetirada); + var valorLiquido = valorBruto - impostos; + + var retirada = new Retirada(request.InvestimentoId, request.DataRetirada, valorBruto, impostos, valorLiquido); + await _uow.Retiradas.AddAsync(retirada, cancellationToken); + + await _uow.Investimentos.UpdateAsync(investimento, cancellationToken); + + await _uow.CommitAsync(); + + _logger.Information("Retirada {RetiradaId} criada com sucesso para investimento {InvestimentoId}", retirada.Id, request.InvestimentoId); + + return new RetiradaDto + { + Id = retirada.Id, + DataRetirada = retirada.DataRetirada, + ValorBruto = retirada.ValorBruto, + Impostos = retirada.Impostos, + ValorLiquido = retirada.ValorLiquido + }; + } + catch + { + await _uow.RollbackAsync(); + _logger.Error("Erro ao processar retirada do investimento {InvestimentoId}", request.InvestimentoId); + throw; + } + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Handles/VisualizarInvestimentoHandler.cs b/src/ProjInv.Application/UseCases/Investimento/Handles/VisualizarInvestimentoHandler.cs new file mode 100644 index 000000000..efc539075 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Handles/VisualizarInvestimentoHandler.cs @@ -0,0 +1,61 @@ +using MediatR; +using ProjInv.Domain.Interfaces; + +using FluentValidation; +using ProjInv.Application.DTOs.Responses.Investimento; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Handles +{ + public class VisualizarInvestimentoHandler : IRequestHandler + { + private readonly IUnitOfWork _uow; + private readonly IValidator _validator; + + public VisualizarInvestimentoHandler(IUnitOfWork uow, IValidator validator) + { + _uow = uow; + _validator = validator; + } + + public async Task Handle(VisualizarInvestimentoCommand request, CancellationToken cancellationToken) + { + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var errors = string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage)); + throw new ArgumentException(errors); + } + + var investimento = await _uow.Investimentos.GetByIdAsync(request.Id, cancellationToken); + if (investimento == null) + { + throw new KeyNotFoundException($"Investimento com ID {request.Id} nao encontrado."); + } + + RetiradaDto? retiradaDto = null; + if (investimento.FoiRetirado && investimento.Retirada != null) + { + retiradaDto = new RetiradaDto + { + Id = investimento.Retirada.Id, + DataRetirada = investimento.Retirada.DataRetirada, + ValorBruto = investimento.Retirada.ValorBruto, + Impostos = investimento.Retirada.Impostos, + ValorLiquido = investimento.Retirada.ValorLiquido + }; + } + + return new InvestimentoResponseDto + { + Id = investimento.Id, + InvestidorId = investimento.InvestidorId, + ValorInicial = investimento.ValorInicial, + DataCriacao = investimento.DataCriacao, + SaldoEsperado = investimento.CalcularSaldoEsperado(), + FoiRetirado = investimento.FoiRetirado, + Retirada = retiradaDto + }; + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Validators/CriarInvestimentoCommandValidator.cs b/src/ProjInv.Application/UseCases/Investimento/Validators/CriarInvestimentoCommandValidator.cs new file mode 100644 index 000000000..d362dfeea --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Validators/CriarInvestimentoCommandValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Validators +{ + public class CriarInvestimentoCommandValidator : AbstractValidator + { + public CriarInvestimentoCommandValidator() + { + RuleFor(x => x.InvestidorId) + .NotEmpty().WithMessage("O ID do investidor e obrigatorio."); + + RuleFor(x => x.ValorInicial) + .GreaterThan(0).WithMessage("O valor inicial deve ser maior que zero."); + + RuleFor(x => x.DataCriacao) + .LessThanOrEqualTo(DateTime.UtcNow.Date).WithMessage("A data de criacao nao pode ser no futuro."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Validators/ListarInvestimentosCommandValidator.cs b/src/ProjInv.Application/UseCases/Investimento/Validators/ListarInvestimentosCommandValidator.cs new file mode 100644 index 000000000..1ac0f7ffb --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Validators/ListarInvestimentosCommandValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Validators +{ + public class ListarInvestimentosCommandValidator : AbstractValidator + { + public ListarInvestimentosCommandValidator() + { + RuleFor(x => x.InvestidorId) + .NotEmpty().WithMessage("O ID do investidor e obrigatorio."); + + RuleFor(x => x.Page) + .GreaterThan(0).WithMessage("A pagina deve ser maior que zero."); + + RuleFor(x => x.PageSize) + .GreaterThan(0).WithMessage("O tamanho da pagina deve ser maior que zero."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Validators/RetirarInvestimentoCommandValidator.cs b/src/ProjInv.Application/UseCases/Investimento/Validators/RetirarInvestimentoCommandValidator.cs new file mode 100644 index 000000000..ea5d16841 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Validators/RetirarInvestimentoCommandValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Validators +{ + public class RetirarInvestimentoCommandValidator : AbstractValidator + { + public RetirarInvestimentoCommandValidator() + { + RuleFor(x => x.InvestimentoId) + .NotEmpty().WithMessage("O ID do investimento e obrigatorio."); + + RuleFor(x => x.DataRetirada) + .LessThanOrEqualTo(DateTime.UtcNow.Date).WithMessage("A data de retirada nao pode ser no futuro."); + } + } +} diff --git a/src/ProjInv.Application/UseCases/Investimento/Validators/VisualizarInvestimentoCommandValidator.cs b/src/ProjInv.Application/UseCases/Investimento/Validators/VisualizarInvestimentoCommandValidator.cs new file mode 100644 index 000000000..79a555ab4 --- /dev/null +++ b/src/ProjInv.Application/UseCases/Investimento/Validators/VisualizarInvestimentoCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using ProjInv.Application.UseCases.Investimento.Commands; + +namespace ProjInv.Application.UseCases.Investimento.Validators +{ + public class VisualizarInvestimentoCommandValidator : AbstractValidator + { + public VisualizarInvestimentoCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty().WithMessage("O ID do investimento e obrigatorio."); + } + } +} diff --git a/src/ProjInv.Domain/Entities/Investidor.cs b/src/ProjInv.Domain/Entities/Investidor.cs new file mode 100644 index 000000000..62f6cdd63 --- /dev/null +++ b/src/ProjInv.Domain/Entities/Investidor.cs @@ -0,0 +1,23 @@ +namespace ProjInv.Domain.Entities +{ + public class Investidor + { + public Guid Id { get; private set; } + public string Nome { get; set; } = string.Empty; + private readonly List _investimentos = new(); + public IReadOnlyCollection Investimentos => _investimentos.AsReadOnly(); + + private Investidor() { } + + public Investidor(string nome) + { + Id = Guid.NewGuid(); + Nome = nome; + } + + public void UpdateNome(string nome) + { + Nome = nome; + } + } +} diff --git a/src/ProjInv.Domain/Entities/Investimento.cs b/src/ProjInv.Domain/Entities/Investimento.cs new file mode 100644 index 000000000..c21ebd986 --- /dev/null +++ b/src/ProjInv.Domain/Entities/Investimento.cs @@ -0,0 +1,100 @@ +namespace ProjInv.Domain.Entities +{ + public class Investimento + { + public Guid Id { get; private set; } + public Guid InvestidorId { get; private set; } + public decimal ValorInicial { get; private set; } + public DateTime DataCriacao { get; private set; } + + public Investidor? Investidor { get; private set; } + public Retirada? Retirada { get; private set; } + + private Investimento() { } + + public Investimento(Guid investidorId, decimal valorInicial, DateTime dataCriacao) + { + if (valorInicial <= 0) + throw new ArgumentException("O valor inicial do investimento deve ser maior que zero.", nameof(valorInicial)); + + var dataCriacaoUtc = NormalizeToUtcDate(dataCriacao); + + if (dataCriacaoUtc > DateTime.UtcNow.Date) + throw new ArgumentException("A data de criacao do investimento nao pode ser no futuro.", nameof(dataCriacao)); + + Id = Guid.NewGuid(); + InvestidorId = investidorId; + ValorInicial = valorInicial; + DataCriacao = dataCriacaoUtc; + } + + public bool FoiRetirado => Retirada != null; + + public decimal CalcularGanhos(DateTime? dataReferencia = null) + { + var dataFinal = dataReferencia.HasValue + ? NormalizeToUtcDate(dataReferencia.Value) + : DateTime.UtcNow.Date; + + if (dataFinal < DataCriacao) + throw new ArgumentException("A data de referencia nao pode ser anterior a data de criacao do investimento."); + + if (FoiRetirado && Retirada!.DataRetirada < dataFinal) + dataFinal = Retirada.DataRetirada; + + var meses = CalcularMesesEntreDatas(DataCriacao, dataFinal); + + var taxaMensal = 0.0052m; + var montante = ValorInicial * (decimal)Math.Pow((double)(1 + taxaMensal), meses); + var ganhos = montante - ValorInicial; + + return Math.Round(ganhos, 2); + } + + public decimal CalcularSaldoEsperado(DateTime? dataReferencia = null) + { + return ValorInicial + CalcularGanhos(dataReferencia); + } + public decimal CalcularImpostos(DateTime dataRetirada) + { + var ganhos = CalcularGanhos(dataRetirada); + var idade = dataRetirada - DataCriacao; + + decimal taxaImposto; + if (idade.TotalDays < 365) + taxaImposto = 0.225m; + else if (idade.TotalDays < 730) + taxaImposto = 0.185m; + else + taxaImposto = 0.15m; + + return Math.Round(ganhos * taxaImposto, 2); + } + + private int CalcularMesesEntreDatas(DateTime dataInicio, DateTime dataFim) + { + var anos = dataFim.Year - dataInicio.Year; + var meses = dataFim.Month - dataInicio.Month; + var totalMeses = (anos * 12) + meses; + + if (dataFim.Day < dataInicio.Day) + totalMeses--; + + return Math.Max(0, totalMeses); + } + + private static DateTime NormalizeToUtcDate(DateTime dt) + { + DateTime utc; + + if (dt.Kind == DateTimeKind.Utc) + utc = dt; + else if (dt.Kind == DateTimeKind.Local) + utc = dt.ToUniversalTime(); + else + utc = DateTime.SpecifyKind(dt, DateTimeKind.Local).ToUniversalTime(); + + return utc.Date; + } + } +} diff --git a/src/ProjInv.Domain/Entities/Retirada.cs b/src/ProjInv.Domain/Entities/Retirada.cs new file mode 100644 index 000000000..32fb1fa7f --- /dev/null +++ b/src/ProjInv.Domain/Entities/Retirada.cs @@ -0,0 +1,26 @@ +namespace ProjInv.Domain.Entities +{ + public class Retirada + { + public Guid Id { get; private set; } + public Guid InvestimentoId { get; private set; } + public DateTime DataRetirada { get; private set; } + public decimal ValorBruto { get; private set; } + public decimal Impostos { get; private set; } + public decimal ValorLiquido { get; private set; } + + public Investimento? Investimento { get; private set; } + + private Retirada() { } + + public Retirada(Guid investimentoId, DateTime dataRetirada, decimal valorBruto, decimal impostos, decimal valorLiquido) + { + Id = Guid.NewGuid(); + InvestimentoId = investimentoId; + DataRetirada = dataRetirada; + ValorBruto = valorBruto; + Impostos = impostos; + ValorLiquido = valorLiquido; + } + } +} diff --git a/src/ProjInv.Domain/Interfaces/IInvestidorRepository.cs b/src/ProjInv.Domain/Interfaces/IInvestidorRepository.cs new file mode 100644 index 000000000..4c6020efc --- /dev/null +++ b/src/ProjInv.Domain/Interfaces/IInvestidorRepository.cs @@ -0,0 +1,13 @@ +using ProjInv.Domain.Entities; + +namespace ProjInv.Domain.Interfaces +{ + public interface IInvestidorRepository + { + Task GetByIdAsync(Guid id, CancellationToken cancellationToken); + Task> GetAllAsync(int page, int pageSize, CancellationToken cancellationToken); + Task AddAsync(Investidor investidor, CancellationToken cancellationToken); + Task UpdateAsync(Investidor investidor, CancellationToken cancellationToken); + Task DeleteAsync(Guid id, CancellationToken cancellationToken); + } +} diff --git a/src/ProjInv.Domain/Interfaces/IInvestimentoRepository.cs b/src/ProjInv.Domain/Interfaces/IInvestimentoRepository.cs new file mode 100644 index 000000000..f4d0e3e3c --- /dev/null +++ b/src/ProjInv.Domain/Interfaces/IInvestimentoRepository.cs @@ -0,0 +1,15 @@ +using ProjInv.Domain.Entities; + +namespace ProjInv.Domain.Interfaces +{ + public interface IInvestimentoRepository + { + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetByInvestidorIdAsync(Guid investidorId, int page, int pageSize, CancellationToken cancellationToken = default); + Task CountByInvestidorIdAsync(Guid investidorId, CancellationToken cancellationToken = default); + Task AddAsync(Investimento investimento, CancellationToken cancellationToken = default); + Task UpdateAsync(Investimento investimento, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); + } +} diff --git a/src/ProjInv.Domain/Interfaces/IRetiradaRepository.cs b/src/ProjInv.Domain/Interfaces/IRetiradaRepository.cs new file mode 100644 index 000000000..ab80b6c05 --- /dev/null +++ b/src/ProjInv.Domain/Interfaces/IRetiradaRepository.cs @@ -0,0 +1,11 @@ +using ProjInv.Domain.Entities; + +namespace ProjInv.Domain.Interfaces +{ + public interface IRetiradaRepository + { + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByInvestimentoIdAsync(Guid investimentoId, CancellationToken cancellationToken = default); + Task AddAsync(Retirada retirada, CancellationToken cancellationToken = default); + } +} diff --git a/src/ProjInv.Domain/Interfaces/IUnitOfWork.cs b/src/ProjInv.Domain/Interfaces/IUnitOfWork.cs new file mode 100644 index 000000000..e6a81df5e --- /dev/null +++ b/src/ProjInv.Domain/Interfaces/IUnitOfWork.cs @@ -0,0 +1,12 @@ +namespace ProjInv.Domain.Interfaces +{ + public interface IUnitOfWork : IDisposable + { + IInvestidorRepository Investidores { get; } + IInvestimentoRepository Investimentos { get; } + IRetiradaRepository Retiradas { get; } + Task BeginTransactionAsync(); + Task CommitAsync(); + Task RollbackAsync(); + } +} diff --git a/src/ProjInv.Domain/ProjInv.Domain.csproj b/src/ProjInv.Domain/ProjInv.Domain.csproj new file mode 100644 index 000000000..168732cef --- /dev/null +++ b/src/ProjInv.Domain/ProjInv.Domain.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/src/ProjInv.Infrastructure/Data/AppDbContext.cs b/src/ProjInv.Infrastructure/Data/AppDbContext.cs new file mode 100644 index 000000000..f68bd2fe6 --- /dev/null +++ b/src/ProjInv.Infrastructure/Data/AppDbContext.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Infrastructure.Data.Configurations; + +namespace ProjInv.Infrastructure.Data +{ + public class AppDbContext : DbContext + { + public AppDbContext(DbContextOptions options): base(options) {} + + public DbSet Investidores { get; set; } + public DbSet Investimentos { get; set; } + public DbSet Retiradas { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new InvestidorConfiguration()); + modelBuilder.ApplyConfiguration(new InvestimentoConfiguration()); + modelBuilder.ApplyConfiguration(new RetiradaConfiguration()); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/src/ProjInv.Infrastructure/Data/Configurations/InvestidorConfiguration.cs b/src/ProjInv.Infrastructure/Data/Configurations/InvestidorConfiguration.cs new file mode 100644 index 000000000..27e28c230 --- /dev/null +++ b/src/ProjInv.Infrastructure/Data/Configurations/InvestidorConfiguration.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ProjInv.Domain.Entities; + +namespace ProjInv.Infrastructure.Data.Configurations +{ + public class InvestidorConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Investidores"); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.Nome) + .IsRequired() + .HasMaxLength(200); + + builder.HasMany(i => i.Investimentos) + .WithOne(inv => inv.Investidor) + .HasForeignKey(inv => inv.InvestidorId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/src/ProjInv.Infrastructure/Data/Configurations/InvestimentoConfiguration.cs b/src/ProjInv.Infrastructure/Data/Configurations/InvestimentoConfiguration.cs new file mode 100644 index 000000000..302043ca8 --- /dev/null +++ b/src/ProjInv.Infrastructure/Data/Configurations/InvestimentoConfiguration.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ProjInv.Domain.Entities; + +namespace ProjInv.Infrastructure.Data.Configurations +{ + public class InvestimentoConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Investimentos"); + + builder.HasKey(i => i.Id); + + builder.Property(i => i.InvestidorId) + .IsRequired(); + + builder.Property(i => i.ValorInicial) + .IsRequired() + .HasColumnType("decimal(18,2)"); + + builder.Property(i => i.DataCriacao) + .IsRequired(); + + builder.HasOne(i => i.Investidor) + .WithMany(inv => inv.Investimentos) + .HasForeignKey(i => i.InvestidorId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasOne(i => i.Retirada) + .WithOne(r => r.Investimento) + .HasForeignKey(r => r.InvestimentoId) + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/src/ProjInv.Infrastructure/Data/Configurations/RetiradaConfiguration.cs b/src/ProjInv.Infrastructure/Data/Configurations/RetiradaConfiguration.cs new file mode 100644 index 000000000..81d8e27e3 --- /dev/null +++ b/src/ProjInv.Infrastructure/Data/Configurations/RetiradaConfiguration.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ProjInv.Domain.Entities; + +namespace ProjInv.Infrastructure.Data.Configurations +{ + public class RetiradaConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Retiradas"); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.InvestimentoId) + .IsRequired(); + + builder.Property(r => r.DataRetirada) + .IsRequired(); + + builder.Property(r => r.ValorBruto) + .IsRequired() + .HasColumnType("decimal(18,2)"); + + builder.Property(r => r.Impostos) + .IsRequired() + .HasColumnType("decimal(18,2)"); + + builder.Property(r => r.ValorLiquido) + .IsRequired() + .HasColumnType("decimal(18,2)"); + } + } +} diff --git a/src/ProjInv.Infrastructure/Migrations/20251130001751_ImplementacaoInvestimentosCorreta.Designer.cs b/src/ProjInv.Infrastructure/Migrations/20251130001751_ImplementacaoInvestimentosCorreta.Designer.cs new file mode 100644 index 000000000..ce79ab289 --- /dev/null +++ b/src/ProjInv.Infrastructure/Migrations/20251130001751_ImplementacaoInvestimentosCorreta.Designer.cs @@ -0,0 +1,129 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ProjInv.Infrastructure.Data; + +#nullable disable + +namespace ProjInv.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251130001751_ImplementacaoInvestimentosCorreta")] + partial class ImplementacaoInvestimentosCorreta + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investidor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Investidores", (string)null); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investimento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DataCriacao") + .HasColumnType("datetime2"); + + b.Property("InvestidorId") + .HasColumnType("uniqueidentifier"); + + b.Property("ValorInicial") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("InvestidorId"); + + b.ToTable("Investimentos", (string)null); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Retirada", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DataRetirada") + .HasColumnType("datetime2"); + + b.Property("Impostos") + .HasColumnType("decimal(18,2)"); + + b.Property("InvestimentoId") + .HasColumnType("uniqueidentifier"); + + b.Property("ValorBruto") + .HasColumnType("decimal(18,2)"); + + b.Property("ValorLiquido") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("InvestimentoId") + .IsUnique(); + + b.ToTable("Retiradas", (string)null); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investimento", b => + { + b.HasOne("ProjInv.Domain.Entities.Investidor", "Investidor") + .WithMany("Investimentos") + .HasForeignKey("InvestidorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Investidor"); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Retirada", b => + { + b.HasOne("ProjInv.Domain.Entities.Investimento", "Investimento") + .WithOne("Retirada") + .HasForeignKey("ProjInv.Domain.Entities.Retirada", "InvestimentoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Investimento"); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investidor", b => + { + b.Navigation("Investimentos"); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investimento", b => + { + b.Navigation("Retirada"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ProjInv.Infrastructure/Migrations/20251130001751_ImplementacaoInvestimentosCorreta.cs b/src/ProjInv.Infrastructure/Migrations/20251130001751_ImplementacaoInvestimentosCorreta.cs new file mode 100644 index 000000000..876994dd4 --- /dev/null +++ b/src/ProjInv.Infrastructure/Migrations/20251130001751_ImplementacaoInvestimentosCorreta.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjInv.Infrastructure.Migrations +{ + /// + public partial class ImplementacaoInvestimentosCorreta : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Investidores", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Nome = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Investidores", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Investimentos", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + InvestidorId = table.Column(type: "uniqueidentifier", nullable: false), + ValorInicial = table.Column(type: "decimal(18,2)", nullable: false), + DataCriacao = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Investimentos", x => x.Id); + table.ForeignKey( + name: "FK_Investimentos_Investidores_InvestidorId", + column: x => x.InvestidorId, + principalTable: "Investidores", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Retiradas", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + InvestimentoId = table.Column(type: "uniqueidentifier", nullable: false), + DataRetirada = table.Column(type: "datetime2", nullable: false), + ValorBruto = table.Column(type: "decimal(18,2)", nullable: false), + Impostos = table.Column(type: "decimal(18,2)", nullable: false), + ValorLiquido = table.Column(type: "decimal(18,2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Retiradas", x => x.Id); + table.ForeignKey( + name: "FK_Retiradas_Investimentos_InvestimentoId", + column: x => x.InvestimentoId, + principalTable: "Investimentos", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Investimentos_InvestidorId", + table: "Investimentos", + column: "InvestidorId"); + + migrationBuilder.CreateIndex( + name: "IX_Retiradas_InvestimentoId", + table: "Retiradas", + column: "InvestimentoId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Retiradas"); + + migrationBuilder.DropTable( + name: "Investimentos"); + + migrationBuilder.DropTable( + name: "Investidores"); + } + } +} diff --git a/src/ProjInv.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/ProjInv.Infrastructure/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 000000000..6b47c2d93 --- /dev/null +++ b/src/ProjInv.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,126 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ProjInv.Infrastructure.Data; + +#nullable disable + +namespace ProjInv.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investidor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Nome") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("Investidores", (string)null); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investimento", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DataCriacao") + .HasColumnType("datetime2"); + + b.Property("InvestidorId") + .HasColumnType("uniqueidentifier"); + + b.Property("ValorInicial") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("InvestidorId"); + + b.ToTable("Investimentos", (string)null); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Retirada", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DataRetirada") + .HasColumnType("datetime2"); + + b.Property("Impostos") + .HasColumnType("decimal(18,2)"); + + b.Property("InvestimentoId") + .HasColumnType("uniqueidentifier"); + + b.Property("ValorBruto") + .HasColumnType("decimal(18,2)"); + + b.Property("ValorLiquido") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("InvestimentoId") + .IsUnique(); + + b.ToTable("Retiradas", (string)null); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investimento", b => + { + b.HasOne("ProjInv.Domain.Entities.Investidor", "Investidor") + .WithMany("Investimentos") + .HasForeignKey("InvestidorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Investidor"); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Retirada", b => + { + b.HasOne("ProjInv.Domain.Entities.Investimento", "Investimento") + .WithOne("Retirada") + .HasForeignKey("ProjInv.Domain.Entities.Retirada", "InvestimentoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Investimento"); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investidor", b => + { + b.Navigation("Investimentos"); + }); + + modelBuilder.Entity("ProjInv.Domain.Entities.Investimento", b => + { + b.Navigation("Retirada"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/ProjInv.Infrastructure/ProjInv.Infrastructure.csproj b/src/ProjInv.Infrastructure/ProjInv.Infrastructure.csproj new file mode 100644 index 000000000..02f2aa5a0 --- /dev/null +++ b/src/ProjInv.Infrastructure/ProjInv.Infrastructure.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/src/ProjInv.Infrastructure/Repositories/InvestidorRepository.cs b/src/ProjInv.Infrastructure/Repositories/InvestidorRepository.cs new file mode 100644 index 000000000..b9226f836 --- /dev/null +++ b/src/ProjInv.Infrastructure/Repositories/InvestidorRepository.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using ProjInv.Infrastructure.Data; + +namespace ProjInv.Infrastructure.Repositories +{ + public class InvestidorRepository : IInvestidorRepository + { + private readonly AppDbContext _context; + + public InvestidorRepository(AppDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + return await _context.Investidores + .Include(i => i.Investimentos) + .ThenInclude(inv => inv.Retirada) + .FirstOrDefaultAsync(i => i.Id == id, cancellationToken); + } + public async Task> GetAllAsync(int page, int pageSize, CancellationToken cancellationToken) + { + return await _context.Investidores + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task AddAsync(Investidor investidor, CancellationToken cancellationToken) + { + await _context.Investidores.AddAsync(investidor); + await _context.SaveChangesAsync(); + } + + public async Task UpdateAsync(Investidor investidor, CancellationToken cancellationToken) + { + _context.Investidores.Update(investidor); + await _context.SaveChangesAsync(); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) { + var investidor = await _context.Investidores.FindAsync(id); + if (investidor != null) + { + _context.Investidores.Remove(investidor); + await _context.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/ProjInv.Infrastructure/Repositories/InvestimentoRepository.cs b/src/ProjInv.Infrastructure/Repositories/InvestimentoRepository.cs new file mode 100644 index 000000000..8b1b217c1 --- /dev/null +++ b/src/ProjInv.Infrastructure/Repositories/InvestimentoRepository.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using ProjInv.Infrastructure.Data; + +namespace ProjInv.Infrastructure.Repositories +{ + public class InvestimentoRepository : IInvestimentoRepository + { + private readonly AppDbContext _context; + + public InvestimentoRepository(AppDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.Investimentos + .Include(i => i.Investidor) + .Include(i => i.Retirada) + .FirstOrDefaultAsync(i => i.Id == id, cancellationToken); + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _context.Investimentos + .Include(i => i.Investidor) + .Include(i => i.Retirada) + .OrderByDescending(i => i.DataCriacao) + .ToListAsync(cancellationToken); + } + + public async Task> GetByInvestidorIdAsync(Guid investidorId, int page, int pageSize, CancellationToken cancellationToken = default) + { + return await _context.Investimentos + .Where(i => i.InvestidorId == investidorId) + .Include(i => i.Retirada) + .OrderByDescending(i => i.DataCriacao) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + } + + public async Task CountByInvestidorIdAsync(Guid investidorId, CancellationToken cancellationToken = default) + { + return await _context.Investimentos + .CountAsync(i => i.InvestidorId == investidorId, cancellationToken); + } + + public async Task AddAsync(Investimento investimento, CancellationToken cancellationToken = default) + { + await _context.Investimentos.AddAsync(investimento, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateAsync(Investimento investimento, CancellationToken cancellationToken = default) + { + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var investimento = await GetByIdAsync(id, cancellationToken); + if (investimento != null) + { + _context.Investimentos.Remove(investimento); + await _context.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/ProjInv.Infrastructure/Repositories/RetiradaRepository.cs b/src/ProjInv.Infrastructure/Repositories/RetiradaRepository.cs new file mode 100644 index 000000000..79e5e6a24 --- /dev/null +++ b/src/ProjInv.Infrastructure/Repositories/RetiradaRepository.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using ProjInv.Infrastructure.Data; + +namespace ProjInv.Infrastructure.Repositories +{ + public class RetiradaRepository : IRetiradaRepository + { + private readonly AppDbContext _context; + + public RetiradaRepository(AppDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.Retiradas + .Include(r => r.Investimento) + .FirstOrDefaultAsync(r => r.Id == id, cancellationToken); + } + + public async Task GetByInvestimentoIdAsync(Guid investimentoId, CancellationToken cancellationToken = default) + { + return await _context.Retiradas + .FirstOrDefaultAsync(r => r.InvestimentoId == investimentoId, cancellationToken); + } + + public async Task AddAsync(Retirada retirada, CancellationToken cancellationToken = default) + { + await _context.Retiradas.AddAsync(retirada, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/src/ProjInv.Infrastructure/Repositories/UnitOfWork.cs b/src/ProjInv.Infrastructure/Repositories/UnitOfWork.cs new file mode 100644 index 000000000..c04831321 --- /dev/null +++ b/src/ProjInv.Infrastructure/Repositories/UnitOfWork.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Storage; +using ProjInv.Domain.Interfaces; +using ProjInv.Infrastructure.Data; + +namespace ProjInv.Infrastructure.Repositories +{ + public class UnitOfWork : IUnitOfWork + { + private readonly AppDbContext _context; + + public IInvestidorRepository Investidores { get; } + public IInvestimentoRepository Investimentos { get; } + public IRetiradaRepository Retiradas { get; } + + public UnitOfWork(AppDbContext context) + { + _context = context; + Investidores = new InvestidorRepository(context); + Investimentos = new InvestimentoRepository(context); + Retiradas = new RetiradaRepository(context); + } + + public async Task BeginTransactionAsync() + { + await _context.Database.BeginTransactionAsync(); + } + + public async Task CommitAsync() + { + await _context.SaveChangesAsync(); + await _context.Database.CommitTransactionAsync(); + } + + public async Task RollbackAsync() + { + await _context.Database.RollbackTransactionAsync(); + } + + public void Dispose() + { + _context.Dispose(); + } + } +} diff --git a/src/ProjInv.Tests/Application/CriarInvestidorHandlerTests.cs b/src/ProjInv.Tests/Application/CriarInvestidorHandlerTests.cs new file mode 100644 index 000000000..7944f51cf --- /dev/null +++ b/src/ProjInv.Tests/Application/CriarInvestidorHandlerTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investidor.Commands; +using ProjInv.Application.UseCases.Investidor.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Tests.Application +{ + public class CriarInvestidorHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly Mock _loggerMock; + private readonly CriarInvestidorHandler _handler; + + public CriarInvestidorHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _loggerMock = new Mock(); + _handler = new CriarInvestidorHandler(_uowMock.Object, _validatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task Deve_Criar_Investidor_Com_Sucesso() + { + var command = new CriarInvestidorCommand { Nome = "Lucas" }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.AddAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _uowMock.Setup(u => u.CommitAsync()) + .Returns(Task.CompletedTask); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.Nome.Should().Be(command.Nome); + result.Id.Should().NotBeEmpty(); + + _uowMock.Verify(u => u.Investidores.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + _uowMock.Verify(u => u.CommitAsync(), Times.Once); + } + } +} diff --git a/src/ProjInv.Tests/Application/CriarInvestimentoHandlerTests.cs b/src/ProjInv.Tests/Application/CriarInvestimentoHandlerTests.cs new file mode 100644 index 000000000..effaaab4d --- /dev/null +++ b/src/ProjInv.Tests/Application/CriarInvestimentoHandlerTests.cs @@ -0,0 +1,79 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investimento.Commands; +using ProjInv.Application.UseCases.Investimento.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Tests.Application +{ + public class CriarInvestimentoHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly Mock _loggerMock; + private readonly CriarInvestimentoHandler _handler; + + public CriarInvestimentoHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _loggerMock = new Mock(); + _handler = new CriarInvestimentoHandler(_uowMock.Object, _validatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task Deve_Criar_Investimento_Com_Sucesso() + { + var command = new CriarInvestimentoCommand + { + InvestidorId = Guid.NewGuid(), + ValorInicial = 1000m, + DataCriacao = DateTime.UtcNow.Date + }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetByIdAsync(command.InvestidorId, It.IsAny())) + .ReturnsAsync(new Investidor("Teste")); + + _uowMock.Setup(u => u.Investimentos.AddAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _uowMock.Setup(u => u.CommitAsync()) + .Returns(Task.CompletedTask); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.ValorInicial.Should().Be(command.ValorInicial); + result.InvestidorId.Should().Be(command.InvestidorId); + + _uowMock.Verify(u => u.Investimentos.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + _uowMock.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task Deve_Lancar_Excecao_Se_Investidor_Nao_Existir() + { + var command = new CriarInvestimentoCommand + { + InvestidorId = Guid.NewGuid(), + ValorInicial = 1000m, + DataCriacao = DateTime.UtcNow.Date + }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetByIdAsync(command.InvestidorId, It.IsAny())) + .ReturnsAsync((Investidor?)null); + + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } + } +} diff --git a/src/ProjInv.Tests/Application/DeleteInvestidorHandlerTests.cs b/src/ProjInv.Tests/Application/DeleteInvestidorHandlerTests.cs new file mode 100644 index 000000000..75ff57c04 --- /dev/null +++ b/src/ProjInv.Tests/Application/DeleteInvestidorHandlerTests.cs @@ -0,0 +1,66 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investidor.Commands; +using ProjInv.Application.UseCases.Investidor.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Tests.Application +{ + public class DeleteInvestidorHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly Mock _loggerMock; + private readonly DeleteInvestidorHandler _handler; + + public DeleteInvestidorHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _loggerMock = new Mock(); + _handler = new DeleteInvestidorHandler(_uowMock.Object, _validatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task Deve_Deletar_Investidor_Com_Sucesso() + { + var command = new DeleteInvestidorCommand { Id = Guid.NewGuid() }; + var investidor = new Investidor("Lucas"); + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetByIdAsync(command.Id, It.IsAny())) + .ReturnsAsync(investidor); + + _uowMock.Setup(u => u.Investidores.DeleteAsync(command.Id, It.IsAny())) + .Returns(Task.CompletedTask); + + _uowMock.Setup(u => u.CommitAsync()) + .Returns(Task.CompletedTask); + + await _handler.Handle(command, CancellationToken.None); + + _uowMock.Verify(u => u.Investidores.DeleteAsync(command.Id, It.IsAny()), Times.Once); + _uowMock.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task Deve_Lancar_Excecao_Se_Investidor_Nao_Existir_Delete() + { + var command = new DeleteInvestidorCommand { Id = Guid.NewGuid() }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetByIdAsync(command.Id, It.IsAny())) + .ReturnsAsync((Investidor?)null); + + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } + } +} diff --git a/src/ProjInv.Tests/Application/GetAllInvestidoresHandlerTests.cs b/src/ProjInv.Tests/Application/GetAllInvestidoresHandlerTests.cs new file mode 100644 index 000000000..3e33559c6 --- /dev/null +++ b/src/ProjInv.Tests/Application/GetAllInvestidoresHandlerTests.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investidor.Commands; +using ProjInv.Application.UseCases.Investidor; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Tests.Application +{ + public class GetAllInvestidoresHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly Mock _loggerMock; + private readonly GetAllInvestidorHandler _handler; + + public GetAllInvestidoresHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _loggerMock = new Mock(); + _handler = new GetAllInvestidorHandler(_uowMock.Object, _validatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task Deve_Listar_Investidores_Com_Sucesso() + { + var command = new GetAllInvestidorCommand { Page = 1, PageSize = 10 }; + var investidores = new List { new Investidor("Lucas"), new Investidor("Teste") }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetAllAsync(command.Page, command.PageSize, It.IsAny())) + .ReturnsAsync(investidores); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.Investidores.Should().HaveCount(2); + } + } +} diff --git a/src/ProjInv.Tests/Application/ListarInvestimentosHandlerTests.cs b/src/ProjInv.Tests/Application/ListarInvestimentosHandlerTests.cs new file mode 100644 index 000000000..397e18949 --- /dev/null +++ b/src/ProjInv.Tests/Application/ListarInvestimentosHandlerTests.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investimento.Commands; +using ProjInv.Application.UseCases.Investimento.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; + +namespace ProjInv.Tests.Application +{ + public class ListarInvestimentosHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly ListarInvestimentosHandler _handler; + + public ListarInvestimentosHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _handler = new ListarInvestimentosHandler(_uowMock.Object, _validatorMock.Object); + } + + [Fact] + public async Task Deve_Listar_Investimentos_Com_Sucesso() + { + var command = new ListarInvestimentosCommand { InvestidorId = Guid.NewGuid(), Page = 1, PageSize = 10 }; + var investimento = new Investimento(command.InvestidorId, 1000m, DateTime.UtcNow); + var investimentos = new List { investimento }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investimentos.GetByInvestidorIdAsync(command.InvestidorId, command.Page, command.PageSize, It.IsAny())) + .ReturnsAsync(investimentos); + + _uowMock.Setup(u => u.Investimentos.CountByInvestidorIdAsync(command.InvestidorId, It.IsAny())) + .ReturnsAsync(1); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.Investimentos.Should().HaveCount(1); + result.TotalCount.Should().Be(1); + } + } +} diff --git a/src/ProjInv.Tests/Application/RetirarInvestimentoHandlerTests.cs b/src/ProjInv.Tests/Application/RetirarInvestimentoHandlerTests.cs new file mode 100644 index 000000000..07cfcb6ad --- /dev/null +++ b/src/ProjInv.Tests/Application/RetirarInvestimentoHandlerTests.cs @@ -0,0 +1,89 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investimento.Commands; +using ProjInv.Application.UseCases.Investimento.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Tests.Application +{ + public class RetirarInvestimentoHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly Mock _loggerMock; + private readonly RetirarInvestimentoHandler _handler; + + public RetirarInvestimentoHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _loggerMock = new Mock(); + _handler = new RetirarInvestimentoHandler(_uowMock.Object, _validatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task Deve_Realizar_Retirada_Com_Sucesso() + { + var investimentoId = Guid.NewGuid(); + var investidorId = Guid.NewGuid(); + var dataCriacao = DateTime.UtcNow.AddMonths(-6).Date; + var investimento = new Investimento(investidorId, 1000m, dataCriacao); + + var command = new RetirarInvestimentoCommand + { + InvestimentoId = investimentoId, + DataRetirada = DateTime.UtcNow.Date + }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investimentos.GetByIdAsync(command.InvestimentoId, It.IsAny())) + .ReturnsAsync(investimento); + + _uowMock.Setup(u => u.Retiradas.AddAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _uowMock.Setup(u => u.CommitAsync()) + .Returns(Task.CompletedTask); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.ValorLiquido.Should().BeGreaterThan(0); + + _uowMock.Verify(u => u.Retiradas.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + _uowMock.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task Nao_Deve_Permitir_Retirada_Se_Ja_Retirado() + { + var investimentoId = Guid.NewGuid(); + var investidorId = Guid.NewGuid(); + var dataCriacao = DateTime.UtcNow.AddMonths(-6).Date; + var investimento = new Investimento(investidorId, 1000m, dataCriacao); + + var retirada = new Retirada(investimentoId, DateTime.UtcNow, 1000, 0, 1000); + typeof(Investimento).GetProperty("Retirada")!.SetValue(investimento, retirada); + + var command = new RetirarInvestimentoCommand + { + InvestimentoId = investimentoId, + DataRetirada = DateTime.UtcNow.Date + }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investimentos.GetByIdAsync(command.InvestimentoId, It.IsAny())) + .ReturnsAsync(investimento); + + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } + } +} diff --git a/src/ProjInv.Tests/Application/UpdateInvestidorHandlerTests.cs b/src/ProjInv.Tests/Application/UpdateInvestidorHandlerTests.cs new file mode 100644 index 000000000..bc24df17f --- /dev/null +++ b/src/ProjInv.Tests/Application/UpdateInvestidorHandlerTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investidor.Commands; +using ProjInv.Application.UseCases.Investidor.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; +using Serilog; + +namespace ProjInv.Tests.Application +{ + public class UpdateInvestidorHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly Mock _loggerMock; + private readonly UpdateInvestidorHandler _handler; + + public UpdateInvestidorHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _loggerMock = new Mock(); + _handler = new UpdateInvestidorHandler(_uowMock.Object, _validatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task Deve_Atualizar_Investidor_Com_Sucesso() + { + var investidorId = Guid.NewGuid(); + var investidor = new Investidor("Lucas"); + + var command = new UpdateInvestidorCommand(investidorId, "Lucas Updated"); + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetByIdAsync(command.Id, It.IsAny())) + .ReturnsAsync(investidor); + + _uowMock.Setup(u => u.Investidores.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + _uowMock.Setup(u => u.CommitAsync()) + .Returns(Task.CompletedTask); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.Nome.Should().Be(command.Nome); + + _uowMock.Verify(u => u.Investidores.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + _uowMock.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task Deve_Lancar_Excecao_Se_Investidor_Nao_Existir_Update() + { + var command = new UpdateInvestidorCommand(Guid.NewGuid(), "Lucas Updated"); + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investidores.GetByIdAsync(command.Id, It.IsAny())) + .ReturnsAsync((Investidor?)null); + + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } + } +} diff --git a/src/ProjInv.Tests/Application/VisualizarInvestimentoHandlerTests.cs b/src/ProjInv.Tests/Application/VisualizarInvestimentoHandlerTests.cs new file mode 100644 index 000000000..879466ac4 --- /dev/null +++ b/src/ProjInv.Tests/Application/VisualizarInvestimentoHandlerTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Moq; +using ProjInv.Application.UseCases.Investimento.Commands; +using ProjInv.Application.UseCases.Investimento.Handles; +using ProjInv.Domain.Entities; +using ProjInv.Domain.Interfaces; + +namespace ProjInv.Tests.Application +{ + public class VisualizarInvestimentoHandlerTests + { + private readonly Mock _uowMock; + private readonly Mock> _validatorMock; + private readonly VisualizarInvestimentoHandler _handler; + + public VisualizarInvestimentoHandlerTests() + { + _uowMock = new Mock(); + _validatorMock = new Mock>(); + _handler = new VisualizarInvestimentoHandler(_uowMock.Object, _validatorMock.Object); + } + + [Fact] + public async Task Deve_Visualizar_Investimento_Com_Sucesso() + { + var command = new VisualizarInvestimentoCommand { Id = Guid.NewGuid() }; + var investimento = new Investimento(Guid.NewGuid(), 1000m, DateTime.UtcNow); + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investimentos.GetByIdAsync(command.Id, It.IsAny())) + .ReturnsAsync(investimento); + + var result = await _handler.Handle(command, CancellationToken.None); + + result.Should().NotBeNull(); + result.ValorInicial.Should().Be(1000m); + } + + [Fact] + public async Task Deve_Lancar_Excecao_Se_Investimento_Nao_Existir() + { + var command = new VisualizarInvestimentoCommand { Id = Guid.NewGuid() }; + + _validatorMock.Setup(v => v.ValidateAsync(command, It.IsAny())) + .ReturnsAsync(new ValidationResult()); + + _uowMock.Setup(u => u.Investimentos.GetByIdAsync(command.Id, It.IsAny())) + .ReturnsAsync((Investimento?)null); + + await Assert.ThrowsAsync(() => _handler.Handle(command, CancellationToken.None)); + } + } +} diff --git a/src/ProjInv.Tests/Domain/InvestidorTests.cs b/src/ProjInv.Tests/Domain/InvestidorTests.cs new file mode 100644 index 000000000..658b09d19 --- /dev/null +++ b/src/ProjInv.Tests/Domain/InvestidorTests.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using ProjInv.Domain.Entities; + +namespace ProjInv.Tests.Domain +{ + public class InvestidorTests + { + [Fact] + public void Deve_Criar_Investidor_Com_Sucesso() + { + var nome = "Lucas"; + var investidor = new Investidor(nome); + + investidor.Should().NotBeNull(); + investidor.Id.Should().NotBeEmpty(); + investidor.Nome.Should().Be(nome); + investidor.Investimentos.Should().BeEmpty(); + } + + [Fact] + public void Deve_Atualizar_Nome_Investidor() + { + var investidor = new Investidor("Lucas"); + var novoNome = "Lucas Updated"; + + investidor.UpdateNome(novoNome); + + investidor.Nome.Should().Be(novoNome); + } + } +} diff --git a/src/ProjInv.Tests/Domain/InvestimentoTests.cs b/src/ProjInv.Tests/Domain/InvestimentoTests.cs new file mode 100644 index 000000000..5a937e8b4 --- /dev/null +++ b/src/ProjInv.Tests/Domain/InvestimentoTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using ProjInv.Domain.Entities; + +namespace ProjInv.Tests.Domain +{ + public class InvestimentoTests + { + [Fact] + public void Deve_Criar_Investimento_Com_Sucesso() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataCriacao = DateTime.UtcNow.Date; + + var investimento = new Investimento(investidorId, valorInicial, dataCriacao); + + investimento.Should().NotBeNull(); + investimento.InvestidorId.Should().Be(investidorId); + investimento.ValorInicial.Should().Be(valorInicial); + investimento.DataCriacao.Should().Be(dataCriacao); + } + + [Fact] + public void Nao_Deve_Criar_Investimento_Com_Valor_Negativo_Ou_Zero() + { + var investidorId = Guid.NewGuid(); + var dataCriacao = DateTime.UtcNow.Date; + + Assert.Throws(() => new Investimento(investidorId, 0, dataCriacao)); + Assert.Throws(() => new Investimento(investidorId, -100, dataCriacao)); + } + + [Fact] + public void Nao_Deve_Criar_Investimento_No_Futuro() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataFutura = DateTime.UtcNow.AddDays(1); + + Assert.Throws(() => new Investimento(investidorId, valorInicial, dataFutura)); + } + + [Fact] + public void Deve_Calcular_Ganhos_Corretamente() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataCriacao = DateTime.UtcNow.AddMonths(-1); + var investimento = new Investimento(investidorId, valorInicial, dataCriacao); + + var ganhos = investimento.CalcularGanhos(DateTime.UtcNow); + + ganhos.Should().Be(5.20m); + } + + [Fact] + public void Deve_Calcular_Saldo_Esperado_Corretamente() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataCriacao = DateTime.UtcNow.AddMonths(-1); + var investimento = new Investimento(investidorId, valorInicial, dataCriacao); + + var saldo = investimento.CalcularSaldoEsperado(DateTime.UtcNow); + + saldo.Should().Be(1005.20m); + } + } +} diff --git a/src/ProjInv.Tests/Domain/RetiradaTests.cs b/src/ProjInv.Tests/Domain/RetiradaTests.cs new file mode 100644 index 000000000..709976502 --- /dev/null +++ b/src/ProjInv.Tests/Domain/RetiradaTests.cs @@ -0,0 +1,56 @@ +using FluentAssertions; +using ProjInv.Domain.Entities; + +namespace ProjInv.Tests.Domain +{ + public class RetiradaTests + { + [Fact] + public void Deve_Calcular_Imposto_22_5_Porcento_Para_Menos_De_Um_Ano() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataCriacao = DateTime.UtcNow.AddMonths(-6); + var investimento = new Investimento(investidorId, valorInicial, dataCriacao); + var dataRetirada = DateTime.UtcNow; + + var ganhos = investimento.CalcularGanhos(dataRetirada); + var imposto = investimento.CalcularImpostos(dataRetirada); + + var impostoEsperado = Math.Round(ganhos * 0.225m, 2); + imposto.Should().Be(impostoEsperado); + } + + [Fact] + public void Deve_Calcular_Imposto_18_5_Porcento_Entre_Um_E_Dois_Anos() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataCriacao = DateTime.UtcNow.AddMonths(-18); + var investimento = new Investimento(investidorId, valorInicial, dataCriacao); + var dataRetirada = DateTime.UtcNow; + + var ganhos = investimento.CalcularGanhos(dataRetirada); + var imposto = investimento.CalcularImpostos(dataRetirada); + + var impostoEsperado = Math.Round(ganhos * 0.185m, 2); + imposto.Should().Be(impostoEsperado); + } + + [Fact] + public void Deve_Calcular_Imposto_15_Porcento_Mais_De_Dois_Anos() + { + var investidorId = Guid.NewGuid(); + var valorInicial = 1000m; + var dataCriacao = DateTime.UtcNow.AddMonths(-30); + var investimento = new Investimento(investidorId, valorInicial, dataCriacao); + var dataRetirada = DateTime.UtcNow; + + var ganhos = investimento.CalcularGanhos(dataRetirada); + var imposto = investimento.CalcularImpostos(dataRetirada); + + var impostoEsperado = Math.Round(ganhos * 0.15m, 2); + imposto.Should().Be(impostoEsperado); + } + } +} diff --git a/src/ProjInv.Tests/Infrastructure/InvestidorRepositoryTests.cs b/src/ProjInv.Tests/Infrastructure/InvestidorRepositoryTests.cs new file mode 100644 index 000000000..01029d449 --- /dev/null +++ b/src/ProjInv.Tests/Infrastructure/InvestidorRepositoryTests.cs @@ -0,0 +1,50 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Infrastructure.Data; +using ProjInv.Infrastructure.Repositories; + +namespace ProjInv.Tests.Infrastructure +{ + public class InvestidorRepositoryTests + { + private readonly DbContextOptions _options; + + public InvestidorRepositoryTests() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + } + + [Fact] + public async Task Deve_Adicionar_Investidor() + { + using var context = new AppDbContext(_options); + var repository = new InvestidorRepository(context); + var investidor = new Investidor("Lucas"); + + await repository.AddAsync(investidor, CancellationToken.None); + await context.SaveChangesAsync(); + + var saved = await context.Investidores.FirstOrDefaultAsync(); + saved.Should().NotBeNull(); + saved!.Nome.Should().Be("Lucas"); + } + + [Fact] + public async Task Deve_Obter_Investidor_Por_Id() + { + using var context = new AppDbContext(_options); + var repository = new InvestidorRepository(context); + var investidor = new Investidor("Lucas"); + await context.Investidores.AddAsync(investidor); + await context.SaveChangesAsync(); + + var result = await repository.GetByIdAsync(investidor.Id, CancellationToken.None); + + result.Should().NotBeNull(); + result!.Id.Should().Be(investidor.Id); + } + } +} diff --git a/src/ProjInv.Tests/Infrastructure/InvestimentoRepositoryTests.cs b/src/ProjInv.Tests/Infrastructure/InvestimentoRepositoryTests.cs new file mode 100644 index 000000000..f189b7e74 --- /dev/null +++ b/src/ProjInv.Tests/Infrastructure/InvestimentoRepositoryTests.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Infrastructure.Data; +using ProjInv.Infrastructure.Repositories; + +namespace ProjInv.Tests.Infrastructure +{ + public class InvestimentoRepositoryTests + { + private readonly DbContextOptions _options; + + public InvestimentoRepositoryTests() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + } + + [Fact] + public async Task Deve_Adicionar_Investimento() + { + using var context = new AppDbContext(_options); + var repository = new InvestimentoRepository(context); + var investimento = new Investimento(Guid.NewGuid(), 1000m, DateTime.UtcNow); + + await repository.AddAsync(investimento); + await context.SaveChangesAsync(); + + var saved = await context.Investimentos.FirstOrDefaultAsync(); + saved.Should().NotBeNull(); + saved!.ValorInicial.Should().Be(1000m); + } + + [Fact] + public async Task Deve_Listar_Investimentos_Por_Investidor_Paginado() + { + using var context = new AppDbContext(_options); + var repository = new InvestimentoRepository(context); + var investidorId = Guid.NewGuid(); + + for (int i = 0; i < 15; i++) + { + await context.Investimentos.AddAsync(new Investimento(investidorId, 1000 + i, DateTime.UtcNow)); + } + await context.SaveChangesAsync(); + + var result = await repository.GetByInvestidorIdAsync(investidorId, 1, 10); + + result.Should().HaveCount(10); + } + } +} diff --git a/src/ProjInv.Tests/Infrastructure/RetiradaRepositoryTests.cs b/src/ProjInv.Tests/Infrastructure/RetiradaRepositoryTests.cs new file mode 100644 index 000000000..19545559f --- /dev/null +++ b/src/ProjInv.Tests/Infrastructure/RetiradaRepositoryTests.cs @@ -0,0 +1,35 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Infrastructure.Data; +using ProjInv.Infrastructure.Repositories; + +namespace ProjInv.Tests.Infrastructure +{ + public class RetiradaRepositoryTests + { + private readonly DbContextOptions _options; + + public RetiradaRepositoryTests() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + } + + [Fact] + public async Task Deve_Adicionar_Retirada() + { + using var context = new AppDbContext(_options); + var repository = new RetiradaRepository(context); + var retirada = new Retirada(Guid.NewGuid(), DateTime.UtcNow, 1000, 100, 900); + + await repository.AddAsync(retirada); + await context.SaveChangesAsync(); + + var saved = await context.Retiradas.FirstOrDefaultAsync(); + saved.Should().NotBeNull(); + saved!.ValorBruto.Should().Be(1000); + } + } +} diff --git a/src/ProjInv.Tests/Infrastructure/UnitOfWorkTests.cs b/src/ProjInv.Tests/Infrastructure/UnitOfWorkTests.cs new file mode 100644 index 000000000..ed28ea840 --- /dev/null +++ b/src/ProjInv.Tests/Infrastructure/UnitOfWorkTests.cs @@ -0,0 +1,83 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using ProjInv.Domain.Entities; +using ProjInv.Infrastructure.Data; +using ProjInv.Infrastructure.Repositories; + +namespace ProjInv.Tests.Infrastructure +{ + public class UnitOfWorkTests + { + private readonly DbContextOptions _options; + + public UnitOfWorkTests() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) + .Options; + } + + [Fact] + public void Deve_Expor_Repositorios_Corretamente() + { + using var context = new AppDbContext(_options); + var uow = new UnitOfWork(context); + + uow.Investidores.Should().NotBeNull(); + uow.Investimentos.Should().NotBeNull(); + uow.Retiradas.Should().NotBeNull(); + } + + [Fact] + public async Task Deve_Salvar_Mudancas_Com_CommitAsync() + { + using var context = new AppDbContext(_options); + var uow = new UnitOfWork(context); + + var investidor = new Investidor("Lucas"); + await uow.Investidores.AddAsync(investidor, CancellationToken.None); + await context.SaveChangesAsync(); + + var saved = await context.Investidores.FirstOrDefaultAsync(); + saved.Should().NotBeNull(); + saved!.Nome.Should().Be("Lucas"); + } + + [Fact] + public async Task Deve_Iniciar_E_Fazer_Commit_De_Transacao() + { + using var context = new AppDbContext(_options); + var uow = new UnitOfWork(context); + + await uow.BeginTransactionAsync(); + await uow.CommitAsync(); + + context.ChangeTracker.HasChanges().Should().BeFalse(); + } + + [Fact] + public async Task Deve_Fazer_Rollback_De_Transacao() + { + using var context = new AppDbContext(_options); + var uow = new UnitOfWork(context); + + await uow.BeginTransactionAsync(); + await uow.RollbackAsync(); + + context.ChangeTracker.HasChanges().Should().BeFalse(); + } + + [Fact] + public void Deve_Fazer_Dispose_Corretamente() + { + var context = new AppDbContext(_options); + var uow = new UnitOfWork(context); + + uow.Dispose(); + + var action = () => context.Investidores.ToList(); + action.Should().Throw(); + } + } +} diff --git a/src/ProjInv.Tests/ProjInv.Tests.csproj b/src/ProjInv.Tests/ProjInv.Tests.csproj new file mode 100644 index 000000000..69c4c7c14 --- /dev/null +++ b/src/ProjInv.Tests/ProjInv.Tests.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ProjInv.slnx b/src/ProjInv.slnx new file mode 100644 index 000000000..edb79a50a --- /dev/null +++ b/src/ProjInv.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/README.md b/src/README.md new file mode 100644 index 000000000..b014a170c --- /dev/null +++ b/src/README.md @@ -0,0 +1,184 @@ +# Projeto Investimento API + +Uma API construída com .NET 9, seguindo os princípios SOLID e Clean Architecture, com logs detalhados, testes automatizados e suporte a Docker. + +## 🏗️ Arquitetura + +Este projeto implementa boas práticas de **Clean Architecture** e **S.O.L.I.D** com a seguinte estrutura: + +- **ProjInv.Domain**: Entidades, interfaces e regras de negócio +- **ProjInv.Application**: UseCases, DTOs e lógica de aplicação +- **ProjInv.Infrastructure**: Repositórios, acesso a dados e serviços externos +- **ProjInv.API**: Endpoints da API, middleware e apresentação +- **ProjInv.Tests**: Testes unitários e de integração + +## 🚀 Características + +- Clean Architecture +- SOLID +- API .NET 9 +- Entity Framework Core (SQL Server) +- Logging estruturado com Serilog +- Validação de entrada com FluentValidation/MediatR +- Middleware de tratamento global de exceções +- Testes com xUnit +- Docker e docker-compose +- Documentação Swagger + +## 📋 Endpoints da API + +### Investidores +- `POST /api/investidores` - Cria um novo investidor +- `GET /api/investidores` - Lista todos os investidores +- `GET /api/investidores/{id}` - Busca investidor por ID +- `PUT /api/investidores/{id}` - Atualiza investidor +- `DELETE /api/investidores/{id}` - Remove investidor + +### Investimentos +- `POST /api/investimentos` - Cria um novo investimento +- `GET /api/investimentos` - Lista todos os investimentos +- `GET /api/investimentos/{id}` - Busca investimento por ID +- `PUT /api/investimentos/{id}` - Atualiza investimento +- `DELETE /api/investimentos/{id}` - Remove investimento + +## 🛠️ Pré-Requisitos + +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) +- [SQL Server](https://www.microsoft.com/en-us/sql-server) ou [Docker](https://www.docker.com/) +- [Visual Studio 2022](https://visualstudio.microsoft.com/) ou [Visual Studio Code](https://code.visualstudio.com/) + +## 🚀 Iniciando + +### Usando Docker + +1. Clone o repositório + ```bash + git clone + cd backend-test/src + ``` +2. Rode com Docker Compose + ```bash + docker-compose up -d + ``` +3. Rode as migrations + ```bash + dotnet ef migrations add InitialCreate --project ProjInv.Infrastructure --startup-project ProjInv.API + dotnet ef database update --project ProjInv.Infrastructure --startup-project ProjInv.API + ``` +4. Acesse a API + - Health: http://localhost:8080/health + - Swagger: http://localhost:8080/swagger + +### Local + +1. Clone o repositório + ```bash + git clone + cd backend-test/src + ``` +2. Restaure dependências + ```bash + dotnet restore + ``` +3. Rode as migrations + ```bash + dotnet ef migrations add InitialCreate --project ProjInv.Infrastructure --startup-project ProjInv.API + dotnet ef database update --project ProjInv.Infrastructure --startup-project ProjInv.API + ``` +4. Rode a aplicação + ```bash + dotnet run --project ProjInv.API + ``` +5. Acesse a API + - Health: http://localhost:7057/health + - Swagger: https://localhost:7057/swagger + +## 📊 Database Schema + +### Investidor +- `Id` (uniqueidentifier, PK) +- `Nome` (nvarchar(200), NOT NULL) + +### Investimento +- `Id` (uniqueidentifier, PK) +- `InvestidorId` (uniqueidentifier, FK → Investidor.Id) +- `ValorInicial` (decimal(18,2), NOT NULL) +- `DataCriacao` (datetime2, NOT NULL) + +### Retirada +- `Id` (uniqueidentifier, PK) +- `InvestimentoId` (uniqueidentifier, FK → Investimento.Id) +- `DataRetirada` (datetime2, NOT NULL) +- `ValorBruto` (decimal(18,2), NOT NULL) +- `Impostos` (decimal(18,2), NOT NULL) +- `ValorLiquido` (decimal(18,2), NOT NULL) + +## 🧪 Testes + +- Rode todos os testes: + ```bash + dotnet test + ``` +- Cobertura: + ```bash + powershell -ExecutionPolicy Bypass -File run_coverage.ps1 + ``` + +## 📁 Estrutura do Projeto + +``` +backend-test/ +├── ProjInv.API/ # API (Controllers, Middleware, Configurações) +│ ├── Controllers/ # Endpoints da aplicação +│ ├── appsettings.json # Configurações da aplicação +│ ├── Dockerfile # Dockerfile da API +│ └── Program.cs # Ponto de entrada +├── ProjInv.Application/ # Camada de Aplicação (DTOs, UseCases) +│ ├── DTOs/ # Data Transfer Objects +│ │ ├── Requests/ # DTOs de requisição +│ │ └── Responses/ # DTOs de resposta +│ └── UseCases/ # Casos de Uso (Handlers, Commands, Validators) +│ ├── Investidor/ # UseCases de Investidor +│ └── Investimento/ # UseCases de Investimento +├── ProjInv.Domain/ # Camada de Domínio (Entities, Interfaces) +│ ├── Entities/ # Entidades de domínio +│ │ ├── Investidor.cs +│ │ ├── Investimento.cs +│ │ └── Retirada.cs +│ └── Interfaces/ # Interfaces de repositórios +├── ProjInv.Infrastructure/ # Camada de Infraestrutura (Data, Repositories) +│ ├── Data/ # DbContext e configuração EF Core +│ │ ├── AppDbContext.cs +│ │ └── Configurations/ # Configurações EF Core +│ ├── Migrations/ # Migrações do banco de dados +│ └── Repositories/ # Implementações dos repositórios +├── ProjInv.Tests/ # Projeto de testes +│ ├── Application/ # Testes de Handlers +│ ├── Domain/ # Testes de Entidades +│ └── Infrastructure/ # Testes de Repositórios +├── coverlet.runsettings # Configuração de cobertura +├── docker-compose.yml # Configuração Docker Compose +├── run_coverage.ps1 # Script para rodar cobertura +└── README.md # Este arquivo +``` + +## 🔧 Configuração + +- `ASPNETCORE_ENVIRONMENT`: Ambiente +- `ConnectionStrings__DefaultConnection`: String de conexão +- `ASPNETCORE_URLS`: URLs da aplicação + +## Logs + +- Serilog: console, arquivos rotativos, formato JSON +- Diretório: `logs/` + +## Docker + +- API: `8080:8080` +- SQL Server: `1433:1433` +- Volumes: `sqlserver_data`, `./logs` + +## Segurança + +- Validação com FluentValidation diff --git a/src/coverlet.runsettings b/src/coverlet.runsettings new file mode 100644 index 000000000..3fe21bae7 --- /dev/null +++ b/src/coverlet.runsettings @@ -0,0 +1,15 @@ + + + + + + + cobertura + [ProjInv.Tests]*,[*.Tests]*,[ProjInv.Infrastructure]ProjInv.Infrastructure.Migrations.* + **/*Migrations/*.cs,**/*Program.cs,**/*Startup.cs,**/*AppDbContext.cs,**/*DTOs/*.cs,**/*Exception.cs,**/*Command.cs,**/*Query.cs,**/*Dto.cs,**/*DTO.cs,**/*Validator.cs,**/*Options.cs,**/*Configuration.cs + Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute + + + + + diff --git a/src/docker-compose.dcproj b/src/docker-compose.dcproj new file mode 100644 index 000000000..c58b10901 --- /dev/null +++ b/src/docker-compose.dcproj @@ -0,0 +1,15 @@ + + + + 2.1 + Linux + 81dded9d-158b-e303-5f62-77a2896d2a5a + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/src/docker-compose.override.yml b/src/docker-compose.override.yml new file mode 100644 index 000000000..c3eb10200 --- /dev/null +++ b/src/docker-compose.override.yml @@ -0,0 +1,14 @@ +services: + projinv.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_HTTP_PORTS=8080 + - ASPNETCORE_HTTPS_PORTS=8081 + ports: + - "8080" + - "8081" + volumes: + - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro + - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro + - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro + - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro \ No newline at end of file diff --git a/src/docker-compose.yml b/src/docker-compose.yml new file mode 100644 index 000000000..c3536aed9 --- /dev/null +++ b/src/docker-compose.yml @@ -0,0 +1,41 @@ +services: + projinv.api: + build: + context: . + dockerfile: ProjInv.API/Dockerfile + image: projinv-api + container_name: projinv-api + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=ProjInvestimento;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=true; + ports: + - "8080:8080" + depends_on: + - sqlserver + networks: + - projinv-network + volumes: + - ./logs:/app/logs + restart: unless-stopped + + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: sqlserver + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=YourStrong@Passw0rd + ports: + - "1433:1433" + volumes: + - sql_data:/var/opt/mssql + networks: + - projinv-network + restart: unless-stopped + +volumes: + sql_data: + +networks: + projinv-network: + driver: bridge \ No newline at end of file diff --git a/src/launchSettings.json b/src/launchSettings.json new file mode 100644 index 000000000..2fe0a8fa5 --- /dev/null +++ b/src/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Docker Compose": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "serviceActions": { + "projinv.api": "StartDebugging" + } + } + } +} \ No newline at end of file diff --git a/src/run_coverage.ps1 b/src/run_coverage.ps1 new file mode 100644 index 000000000..584504e0e --- /dev/null +++ b/src/run_coverage.ps1 @@ -0,0 +1,26 @@ +$ErrorActionPreference = "Stop" + +Write-Host "Cleaning previous results..." -ForegroundColor Yellow +Remove-Item -Path ./TestResults -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item -Path ./CoverageReport -Recurse -Force -ErrorAction SilentlyContinue + +Write-Host "Running Tests with Coverage..." -ForegroundColor Cyan +dotnet test ProjInv.Tests/ProjInv.Tests.csproj --collect:"XPlat Code Coverage" --settings coverlet.runsettings --results-directory ./TestResults + +$reportGeneratorInstalled = Get-Command reportgenerator -ErrorAction SilentlyContinue +if ($null -eq $reportGeneratorInstalled) { + Write-Host "Installing ReportGenerator tool..." -ForegroundColor Yellow + dotnet tool install -g dotnet-reportgenerator-globaltool +} + +$latestCoverageFile = Get-ChildItem -Path ./TestResults -Recurse -Filter "coverage.cobertura.xml" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + +if ($latestCoverageFile) { + Write-Host "Generating HTML Report..." -ForegroundColor Cyan + reportgenerator -reports:$latestCoverageFile.FullName -targetdir:./CoverageReport -reporttypes:Html -assemblyfilters:"-ProjInv.Tests" -classfilters:"-Program;-*Migrations*;-*DTO*;-*Dto*;-*Exception*;-*AppDbContext*;-*TransactionType*" -filefilters:"-*Migrations*;-*Program.cs*;-*Designer.cs*;-*DTOs*" + + Write-Host "Opening Report..." -ForegroundColor Green + Start-Process "./CoverageReport/index.html" +} else { + Write-Host "No coverage file found!" -ForegroundColor Red +}