From 08a2049afb08578cf1488ba782881bda34a74a70 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Tue, 25 Nov 2025 21:32:22 -0300 Subject: [PATCH 01/40] chore: gitignore development --- .gitignore | 484 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..bc78471db --- /dev/null +++ b/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# 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/ +[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 +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# 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 +*.tlog +*.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 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# 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/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# 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 + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp From 5af553358c85dc4a1a50d95e91a3cce3a5859ecd Mon Sep 17 00:00:00 2001 From: inacio88 Date: Tue, 25 Nov 2025 21:39:45 -0300 Subject: [PATCH 02/40] feat: projetos base e solucao development --- api/Program.cs | 44 ++++++++++++++++++++++++++++++ api/Properties/launchSettings.json | 41 ++++++++++++++++++++++++++++ api/api.csproj | 14 ++++++++++ api/api.http | 6 ++++ api/appsettings.Development.json | 8 ++++++ api/appsettings.json | 9 ++++++ common/Class1.cs | 6 ++++ common/common.csproj | 9 ++++++ core/Class1.cs | 6 ++++ core/core.csproj | 9 ++++++ solucao.sln | 34 +++++++++++++++++++++++ 11 files changed, 186 insertions(+) create mode 100644 api/Program.cs create mode 100644 api/Properties/launchSettings.json create mode 100644 api/api.csproj create mode 100644 api/api.http create mode 100644 api/appsettings.Development.json create mode 100644 api/appsettings.json create mode 100644 common/Class1.cs create mode 100644 common/common.csproj create mode 100644 core/Class1.cs create mode 100644 core/core.csproj create mode 100644 solucao.sln diff --git a/api/Program.cs b/api/Program.cs new file mode 100644 index 000000000..00ff53936 --- /dev/null +++ b/api/Program.cs @@ -0,0 +1,44 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast") +.WithOpenApi(); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/api/Properties/launchSettings.json b/api/Properties/launchSettings.json new file mode 100644 index 000000000..c49ce4ad1 --- /dev/null +++ b/api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1265", + "sslPort": 44308 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7271;http://localhost:5285", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/api/api.csproj b/api/api.csproj new file mode 100644 index 000000000..8b2086b6a --- /dev/null +++ b/api/api.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/api/api.http b/api/api.http new file mode 100644 index 000000000..ac6b35a47 --- /dev/null +++ b/api/api.http @@ -0,0 +1,6 @@ +@api_HostAddress = http://localhost:5285 + +GET {{api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/api/appsettings.Development.json b/api/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/api/appsettings.json b/api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/common/Class1.cs b/common/Class1.cs new file mode 100644 index 000000000..1dc009212 --- /dev/null +++ b/common/Class1.cs @@ -0,0 +1,6 @@ +namespace common; + +public class Class1 +{ + +} diff --git a/common/common.csproj b/common/common.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/common/common.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/core/Class1.cs b/core/Class1.cs new file mode 100644 index 000000000..96218793a --- /dev/null +++ b/core/Class1.cs @@ -0,0 +1,6 @@ +namespace core; + +public class Class1 +{ + +} diff --git a/core/core.csproj b/core/core.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/core/core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/solucao.sln b/solucao.sln new file mode 100644 index 000000000..bc510258a --- /dev/null +++ b/solucao.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api", "api\api.csproj", "{E4E78CE6-C619-4626-8F39-E4E8135F8293}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "common", "common\common.csproj", "{131B7BBF-94C4-4B7C-8927-A235F16D4E87}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "core", "core\core.csproj", "{B19FFD92-8123-4C0E-A81A-528A5F433E59}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E4E78CE6-C619-4626-8F39-E4E8135F8293}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4E78CE6-C619-4626-8F39-E4E8135F8293}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4E78CE6-C619-4626-8F39-E4E8135F8293}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4E78CE6-C619-4626-8F39-E4E8135F8293}.Release|Any CPU.Build.0 = Release|Any CPU + {131B7BBF-94C4-4B7C-8927-A235F16D4E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {131B7BBF-94C4-4B7C-8927-A235F16D4E87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {131B7BBF-94C4-4B7C-8927-A235F16D4E87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {131B7BBF-94C4-4B7C-8927-A235F16D4E87}.Release|Any CPU.Build.0 = Release|Any CPU + {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal From e8487209896d919101db393e61c291cfb2701cde Mon Sep 17 00:00:00 2001 From: inacio88 Date: Wed, 26 Nov 2025 22:55:59 -0300 Subject: [PATCH 03/40] feat: libs identity development --- api/api.csproj | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/api.csproj b/api/api.csproj index 8b2086b6a..98c6f7708 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -9,6 +9,16 @@ + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + From afe15a396a0697ad06d7f78693246d1ab8322b64 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Wed, 26 Nov 2025 22:56:31 -0300 Subject: [PATCH 04/40] feat: adicao do ApplicationDbContext development --- api/Data/ApplicationDbContext.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 api/Data/ApplicationDbContext.cs diff --git a/api/Data/ApplicationDbContext.cs b/api/Data/ApplicationDbContext.cs new file mode 100644 index 000000000..8f5e38fd5 --- /dev/null +++ b/api/Data/ApplicationDbContext.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace api.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) : + base(options) + { } + + } +} \ No newline at end of file From fe36003b7b465158ad4ea6cf3c9deeda2c52de6c Mon Sep 17 00:00:00 2001 From: inacio88 Date: Wed, 26 Nov 2025 22:59:27 -0300 Subject: [PATCH 05/40] feat: configuracoes basica da api e identity development --- api/Common/Api/BuilderExtension.cs | 76 ++++++++++++++++++++++++++++++ api/Common/Api/Configuration.cs | 8 ++++ api/Program.cs | 37 ++++----------- 3 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 api/Common/Api/BuilderExtension.cs create mode 100644 api/Common/Api/Configuration.cs diff --git a/api/Common/Api/BuilderExtension.cs b/api/Common/Api/BuilderExtension.cs new file mode 100644 index 000000000..e1821cf34 --- /dev/null +++ b/api/Common/Api/BuilderExtension.cs @@ -0,0 +1,76 @@ +using api.Data; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace api.Common.Api +{ + public static class BuilderExtension + { + public static void AddConfiguration(this WebApplicationBuilder builder) + { + Configuration.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? string.Empty; + + } + + public static void AddDocumentation(this WebApplicationBuilder builder) + { + + // builder.Services.AddEndpointsApiExplorer(); + // builder.Services.AddSwaggerGen(x => + // { + // x.CustomSchemaIds(x => x.FullName); + // }); + + + } + + public static void AddSecurity(this WebApplicationBuilder builder) + { + builder.Services.AddIdentityApiEndpoints(); + //.AddEntityFrameworkStores(); + + // builder.Services.Configure(options => + // { + // options.SignIn.RequireConfirmedEmail = true; + // options.Lockout.MaxFailedAccessAttempts = 20; + + // }); + + // builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) + // .AddIdentityCookies(); + // builder.Services.ConfigureApplicationCookie(options => + // { + // options.Cookie.Name = "Investment.AuthCookie"; + // options.Cookie.HttpOnly = true; + // options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + // options.Cookie.SameSite = SameSiteMode.None; + // options.Cookie.IsEssential = true; + // options.ExpireTimeSpan = TimeSpan.FromDays(7); + // options.SlidingExpiration = true; + // }); + + builder.Services.AddAuthorization(); + } + + public static void AddDataContexts(this WebApplicationBuilder builder) + { + + builder.Services.AddDbContext(options => + { + options.UseNpgsql(Configuration.ConnectionString); + }); + + builder.Services.AddIdentityCore() + .AddEntityFrameworkStores() + .AddApiEndpoints() + ; + } + + + public static void AddServices(this WebApplicationBuilder builder) + { + + } + } +} \ No newline at end of file diff --git a/api/Common/Api/Configuration.cs b/api/Common/Api/Configuration.cs new file mode 100644 index 000000000..b8ed82086 --- /dev/null +++ b/api/Common/Api/Configuration.cs @@ -0,0 +1,8 @@ +namespace api.Common.Api +{ + public static class Configuration + { + public static string ConnectionString { get; set; } = string.Empty; + + } +} \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 00ff53936..2da5aec7f 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,7 +1,12 @@ -var builder = WebApplication.CreateBuilder(args); +using api.Common.Api; +using Microsoft.AspNetCore.Identity; -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +var builder = WebApplication.CreateBuilder(args); +builder.AddConfiguration(); +builder.AddSecurity(); +builder.AddDataContexts(); +builder.AddDocumentation(); +builder.AddServices(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -15,30 +20,6 @@ } app.UseHttpsRedirection(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast") -.WithOpenApi(); +app.MapIdentityApi(); app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} From 21b354a26297eba31515c0da6d24359dabfd700e Mon Sep 17 00:00:00 2001 From: inacio88 Date: Wed, 26 Nov 2025 23:16:28 -0300 Subject: [PATCH 06/40] feat: secrets da conexao e migracoes development --- api/Common/Api/BuilderExtension.cs | 3 +- .../20251127021227_inicial.Designer.cs | 277 ++++++++++++++++++ api/Migrations/20251127021227_inicial.cs | 223 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 274 +++++++++++++++++ api/api.csproj | 3 +- 5 files changed, 777 insertions(+), 3 deletions(-) create mode 100644 api/Migrations/20251127021227_inicial.Designer.cs create mode 100644 api/Migrations/20251127021227_inicial.cs create mode 100644 api/Migrations/ApplicationDbContextModelSnapshot.cs diff --git a/api/Common/Api/BuilderExtension.cs b/api/Common/Api/BuilderExtension.cs index e1821cf34..795efdc5a 100644 --- a/api/Common/Api/BuilderExtension.cs +++ b/api/Common/Api/BuilderExtension.cs @@ -8,8 +8,7 @@ public static class BuilderExtension { public static void AddConfiguration(this WebApplicationBuilder builder) { - Configuration.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection") - ?? string.Empty; + Configuration.ConnectionString = builder.Configuration.GetValue("conexao") ?? string.Empty; } diff --git a/api/Migrations/20251127021227_inicial.Designer.cs b/api/Migrations/20251127021227_inicial.Designer.cs new file mode 100644 index 000000000..b36aa3f21 --- /dev/null +++ b/api/Migrations/20251127021227_inicial.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api.Data; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251127021227_inicial")] + partial class inicial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/Migrations/20251127021227_inicial.cs b/api/Migrations/20251127021227_inicial.cs new file mode 100644 index 000000000..05dee6be2 --- /dev/null +++ b/api/Migrations/20251127021227_inicial.cs @@ -0,0 +1,223 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api.Migrations +{ + /// + public partial class inicial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/api/Migrations/ApplicationDbContextModelSnapshot.cs b/api/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 000000000..4ae0e4a44 --- /dev/null +++ b/api/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,274 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api.Data; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/api.csproj b/api/api.csproj index 98c6f7708..ad640b72b 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -1,9 +1,10 @@ - + net8.0 enable enable + f8d8e878-4619-471e-b6ae-b93876ca851e From f3128b2e6ffebe0abad7f4a503d138f758d420c8 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Wed, 26 Nov 2025 23:16:57 -0300 Subject: [PATCH 07/40] feat: instrucoes setup banco development --- README.md | 91 +++---------------------------------------------------- 1 file changed, 5 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 664da6305..c1117af6b 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,7 @@ -# Back End Test Project +# Setup -You should see this challenge as an opportunity to create an application following modern development best practices (given the stack of your choice), but also feel free to use your own architecture preferences (coding standards, code organization, third-party libraries, etc). It’s perfectly fine to use vanilla code or any framework or libraries. +Banco: +docker run --name api-investments-postgres -e POSTGRES_PASSWORD=1q2w3e4r@@@ -d -p 5436:5432 postgres -## Scope - -In this challenge you should build an **API** for an application that stores and manages investments, it should have the following features: - -1. __Creation__ of an investment with an owner, a creation date and an amount. - 1. The creation date of an investment can be today or a date in the past. - 2. An investment should not be or become negative. -2. __View__ of an investment with its initial amount and expected balance. - 1. Expected balance should be the sum of the invested amount and the [gains][]. - 2. If an investment was already withdrawn then the balance must reflect the gains of that investment -3. __Withdrawal__ of a investment. - 1. The withdraw will always be the sum of the initial amount and its gains, - partial withdrawn is not supported. - 2. The withdrawal date must be informmed by the user, and it can be a date in the past or today, but can't happen before the investment creation or the future. - 3. [Taxes][taxes] need to be applied to the withdrawals before showing the final value. -4. __List__ of a person's investments - 1. This list should have pagination. - -__NOTE:__ the implementation of an interface will not be evaluated, and if implemented the endpoints still need to be usable/readable for a machine. - -### Gain Calculation - -The investment will pay 0.52% every month in the same day of the investment creation. - -Given that the gain is paid every month, it should be treated as [compound gain][], which means that every new period (month) the amount gained will become part of the investment balance for the next payment. - -### Taxation - -When money is withdrawn, tax is triggered. Taxes apply only to the gain portion of the money withdrawn. For example, if the initial investment was 1000.00, the current balance is 1200.00, then the taxes will be applied to the 200.00. - -The tax percentage changes according to the age of the investment: -* If it is less than one year old, the percentage will be 22.5% (tax = 45.00). -* If it is between one and two years old, the percentage will be 18.5% (tax = 37.00). -* If older than two years, the percentage will be 15% (tax = 30.00). - -## Requirements -1. Create project using any technology of your preference. It’s perfectly OK to use vanilla code or any framework or libraries; -2. Although you can use as many dependencies as you want, you should manage them wisely; -3. It is not necessary to send the notification emails, however, the code required for that would be welcome; -4. The API must be documented in some way. - -## Deliverables -The project source code and dependencies should be made available in GitHub. Here are the steps you should follow: -1. Fork this repository to your GitHub account (create an account if you don't have one, you will need it working with us). -2. Create a "development" branch and commit the code to it. Do not push the code to the main branch. -3. Include a README file that describes: - - Special build instructions, if any - - List of third-party libraries used and short description of why/how they were used - - A link to the API documentation. -4. Once the work is complete, create a pull request from "development" into "main" and send us the link. -5. Avoid using huge commits hiding your progress. Feel free to work on a branch and use `git rebase` to adjust your commits before submitting the final version. - -## Coding Standards -When working on the project be as clean and consistent as possible. - -## Project Deadline -Ideally you'd finish the test project in 5 days. It shouldn't take you longer than a entire week. - -## Quality Assurance -Use the following checklist to ensure high quality of the project. - -### General -- First of all, the application should run without errors. -- Are all requirements set above met? -- Is coding style consistent? -- The API is well documented? -- The API has unit tests? - -## Submission -1. A link to the Github repository. -2. Briefly describe how you decided on the tools that you used. - -## Have Fun Coding 🤘 -- This challenge description is intentionally vague in some aspects, but if you need assistance feel free to ask for help. -- If any of the seems out of your current level, you may skip it, but remember to tell us about it in the pull request. - -## Credits - -This coding challenge was inspired on [kinvoapp/kinvo-back-end-test](https://github.com/kinvoapp/kinvo-back-end-test/blob/2f17d713de739e309d17a1a74a82c3fd0e66d128/README.md) - -[gains]: #gain-calculation -[taxes]: #taxation -[interest]: #interest-calculation -[compound gain]: https://www.investopedia.com/terms/g/gain.asp +Stringconnection pelo user secrets ( mas também pode ser variável de ambiente normal) +dotnet user-secrets set conexao "User ID=postgres;Password=1q2w3e4r@@@;Host=localhost;Port=5436;Database=ClienteDB;Pooling=true;" \ No newline at end of file From 27a8ea58e4f760e2ff7a792b4c260171553bf265 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Thu, 27 Nov 2025 23:06:18 -0300 Subject: [PATCH 08/40] feat: mapeamento agrupado do identity development --- api/Program.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/Program.cs b/api/Program.cs index 2da5aec7f..b854f5350 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddControllers(); builder.AddConfiguration(); builder.AddSecurity(); builder.AddDataContexts(); @@ -20,6 +21,10 @@ } app.UseHttpsRedirection(); -app.MapIdentityApi(); + +app.MapGroup("/identity") + .MapIdentityApi(); + +app.MapControllers(); app.Run(); From 197465a63c3e7d0b5494df6b085a55aaa35ce59c Mon Sep 17 00:00:00 2001 From: inacio88 Date: Thu, 27 Nov 2025 23:10:53 -0300 Subject: [PATCH 09/40] feat: entidade principal e mapeamento development --- api/Data/ApplicationDbContext.cs | 7 +++ api/Data/Mapping/InvestmentMapping.cs | 36 +++++++++++++ core/Entities/Investment.cs | 74 +++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 api/Data/Mapping/InvestmentMapping.cs create mode 100644 core/Entities/Investment.cs diff --git a/api/Data/ApplicationDbContext.cs b/api/Data/ApplicationDbContext.cs index 8f5e38fd5..09b5a516e 100644 --- a/api/Data/ApplicationDbContext.cs +++ b/api/Data/ApplicationDbContext.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -10,5 +11,11 @@ public ApplicationDbContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder builder) + { + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + base.OnModelCreating(builder); + } + } } \ No newline at end of file diff --git a/api/Data/Mapping/InvestmentMapping.cs b/api/Data/Mapping/InvestmentMapping.cs new file mode 100644 index 000000000..8940f8672 --- /dev/null +++ b/api/Data/Mapping/InvestmentMapping.cs @@ -0,0 +1,36 @@ +using core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace api.Data.Mapping +{ + public class InvestmentMapping : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Investment"); + + builder.HasKey(x => x.Id); + + builder.Property(x => x.OwnerId) + .IsRequired() + .HasColumnType("varchar(128)"); + + builder.Property(x => x.CreationDate) + .IsRequired() + .HasColumnType("timestamptz"); + + builder.Property(x => x.InitialAmount) + .IsRequired() + .HasColumnType("decimal(18,2)"); + + + builder.Property(x => x.WithdrawalDate) + .HasColumnType("timestamptz"); + + + builder.HasIndex(x => x.OwnerId); + + } + } +} \ No newline at end of file diff --git a/core/Entities/Investment.cs b/core/Entities/Investment.cs new file mode 100644 index 000000000..9f3c0e86a --- /dev/null +++ b/core/Entities/Investment.cs @@ -0,0 +1,74 @@ +namespace core.Entities +{ + public class Investment + { + public Guid Id { get; set; } + public string OwnerId { get; set; } + public DateTime CreationDate { get; set; } + public decimal InitialAmount { get; set; } + public DateTime? WithdrawalDate { get; set; } + + + public decimal CurrentGains => CalculateGains(); + public decimal ExpectedBalance => InitialAmount + CurrentGains; + public bool IsWithdrawn => WithdrawalDate.HasValue; + + private decimal CalculateGains() + { + if (IsWithdrawn) + return CalculateGainsUpTo(WithdrawalDate.Value); + + return CalculateGainsUpTo(DateTime.Today); + } + + private decimal CalculateGainsUpTo(DateTime untilDate) + { + if (untilDate < CreationDate) + return 0; + + var months = GetFullMonthsBetween(CreationDate, untilDate); + var balance = InitialAmount; + + for (int i = 0; i < months; i++) + { + balance *= 1.0052m; + } + + return balance - InitialAmount; + } + + private int GetFullMonthsBetween(DateTime start, DateTime end) + { + int months = (end.Year - start.Year) * 12 + (end.Month - start.Month); + + + if (end.Day < start.Day) + months--; + + return Math.Max(0, months); + } + + public decimal GetTaxedWithdrawAmount() + { + if (!IsWithdrawn) + throw new InvalidOperationException("Investment has not been withdrawn."); + + var totalGain = CurrentGains; + var taxRate = GetTaxRate(WithdrawalDate.Value); + var tax = totalGain * taxRate; + return ExpectedBalance - tax; + } + + private decimal GetTaxRate(DateTime withdrawalDate) + { + var ageInYears = (withdrawalDate - CreationDate).TotalDays / 365.25; + + if (ageInYears < 1) + return 0.225m; + else if (ageInYears < 2) + return 0.185m; + else + return 0.15m; + } + } +} \ No newline at end of file From 743b4201a861bad761f93c57e1b6c76ec4d470e5 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Thu, 27 Nov 2025 23:22:53 -0300 Subject: [PATCH 10/40] feat: controller de teste development --- api/Controllers/WeatherForecastController.cs | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 api/Controllers/WeatherForecastController.cs diff --git a/api/Controllers/WeatherForecastController.cs b/api/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..dd24dcb75 --- /dev/null +++ b/api/Controllers/WeatherForecastController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace api.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [Authorize] + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } + + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} \ No newline at end of file From 67354f2f5e6ed5ac2afa0fb9458eb34818ee6997 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Thu, 27 Nov 2025 23:26:27 -0300 Subject: [PATCH 11/40] feat: migracoes da entidade development --- .../20251128022314_Investment.Designer.cs | 303 ++++++++++++++++++ api/Migrations/20251128022314_Investment.cs | 42 +++ .../ApplicationDbContextModelSnapshot.cs | 26 ++ 3 files changed, 371 insertions(+) create mode 100644 api/Migrations/20251128022314_Investment.Designer.cs create mode 100644 api/Migrations/20251128022314_Investment.cs diff --git a/api/Migrations/20251128022314_Investment.Designer.cs b/api/Migrations/20251128022314_Investment.Designer.cs new file mode 100644 index 000000000..1a86fa56b --- /dev/null +++ b/api/Migrations/20251128022314_Investment.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using api.Data; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251128022314_Investment")] + partial class Investment + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("core.Entities.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamptz"); + + b.Property("InitialAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(128)"); + + b.Property("WithdrawalDate") + .HasColumnType("timestamptz"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Investment", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/Migrations/20251128022314_Investment.cs b/api/Migrations/20251128022314_Investment.cs new file mode 100644 index 000000000..9109bcf57 --- /dev/null +++ b/api/Migrations/20251128022314_Investment.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api.Migrations +{ + /// + public partial class Investment : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Investment", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OwnerId = table.Column(type: "varchar(128)", nullable: false), + CreationDate = table.Column(type: "timestamptz", nullable: false), + InitialAmount = table.Column(type: "numeric(18,2)", nullable: false), + WithdrawalDate = table.Column(type: "timestamptz", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Investment", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Investment_OwnerId", + table: "Investment", + column: "OwnerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Investment"); + } + } +} diff --git a/api/Migrations/ApplicationDbContextModelSnapshot.cs b/api/Migrations/ApplicationDbContextModelSnapshot.cs index 4ae0e4a44..fab86da8c 100644 --- a/api/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/api/Migrations/ApplicationDbContextModelSnapshot.cs @@ -218,6 +218,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("core.Entities.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamptz"); + + b.Property("InitialAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(128)"); + + b.Property("WithdrawalDate") + .HasColumnType("timestamptz"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Investment", (string)null); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) From 1cd357363fb340601faaad6e4beff0ba2e87470f Mon Sep 17 00:00:00 2001 From: inacio88 Date: Fri, 28 Nov 2025 00:08:13 -0300 Subject: [PATCH 12/40] feat: irepo investment development --- core/Repositories/IInvestmentRepository.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 core/Repositories/IInvestmentRepository.cs diff --git a/core/Repositories/IInvestmentRepository.cs b/core/Repositories/IInvestmentRepository.cs new file mode 100644 index 000000000..36bc409c8 --- /dev/null +++ b/core/Repositories/IInvestmentRepository.cs @@ -0,0 +1,14 @@ +using core.Entities; + +namespace core.Repositories +{ + public interface IInvestmentRepository + { + Task CreateAsync(Investment investment); + Task UpdateAsync(Investment investment); + Task DeleteAsync(Guid id); + Task GetByIdAsync(Guid id); + Task> GetByFilterAsync(Guid id); + + } +} \ No newline at end of file From eec8f26da3cdb65c8513b01476864c12c40681c5 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Fri, 28 Nov 2025 00:09:22 -0300 Subject: [PATCH 13/40] feat: controller investment development --- api/Controllers/InvestmentController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 api/Controllers/InvestmentController.cs diff --git a/api/Controllers/InvestmentController.cs b/api/Controllers/InvestmentController.cs new file mode 100644 index 000000000..438f4bdd5 --- /dev/null +++ b/api/Controllers/InvestmentController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class InvestmentController : ControllerBase + { + + } +} \ No newline at end of file From deb352efca46ab670b8d2439bf4cabb297aeb09a Mon Sep 17 00:00:00 2001 From: inacio88 Date: Fri, 28 Nov 2025 00:09:40 -0300 Subject: [PATCH 14/40] feat: projeto infra development --- api/Data/ApplicationDbContext.cs | 3 +++ api/api.csproj | 1 + common/Class1.cs | 6 ------ core/Class1.cs | 6 ------ infra/infra.csproj | 9 +++++++++ solucao.sln | 6 ++++++ 6 files changed, 19 insertions(+), 12 deletions(-) delete mode 100644 common/Class1.cs delete mode 100644 core/Class1.cs create mode 100644 infra/infra.csproj diff --git a/api/Data/ApplicationDbContext.cs b/api/Data/ApplicationDbContext.cs index 09b5a516e..2ca19e8f0 100644 --- a/api/Data/ApplicationDbContext.cs +++ b/api/Data/ApplicationDbContext.cs @@ -1,4 +1,5 @@ using System.Reflection; +using core.Entities; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -7,6 +8,8 @@ namespace api.Data { public class ApplicationDbContext : IdentityDbContext { + public DbSet Investments { get; set; } + public ApplicationDbContext(DbContextOptions options) : base(options) { } diff --git a/api/api.csproj b/api/api.csproj index ad640b72b..bf0a1b337 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -20,6 +20,7 @@ + diff --git a/common/Class1.cs b/common/Class1.cs deleted file mode 100644 index 1dc009212..000000000 --- a/common/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace common; - -public class Class1 -{ - -} diff --git a/core/Class1.cs b/core/Class1.cs deleted file mode 100644 index 96218793a..000000000 --- a/core/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace core; - -public class Class1 -{ - -} diff --git a/infra/infra.csproj b/infra/infra.csproj new file mode 100644 index 000000000..125f4c93b --- /dev/null +++ b/infra/infra.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/solucao.sln b/solucao.sln index bc510258a..2d9c83c44 100644 --- a/solucao.sln +++ b/solucao.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "common", "common\common.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "core", "core\core.csproj", "{B19FFD92-8123-4C0E-A81A-528A5F433E59}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "infra", "infra\infra.csproj", "{AD8308B3-4A67-45BC-937F-259B4639B10A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Debug|Any CPU.Build.0 = Debug|Any CPU {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Release|Any CPU.ActiveCfg = Release|Any CPU {B19FFD92-8123-4C0E-A81A-528A5F433E59}.Release|Any CPU.Build.0 = Release|Any CPU + {AD8308B3-4A67-45BC-937F-259B4639B10A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD8308B3-4A67-45BC-937F-259B4639B10A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD8308B3-4A67-45BC-937F-259B4639B10A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD8308B3-4A67-45BC-937F-259B4639B10A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 1d74bb309499f234e80ff26a778eb3f0944caaf1 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Fri, 28 Nov 2025 00:26:53 -0300 Subject: [PATCH 15/40] feat: passando dbcontext para infra development --- api/Common/Api/BuilderExtension.cs | 2 +- .../20251127021227_inicial.Designer.cs | 277 ---------------- api/Migrations/20251127021227_inicial.cs | 223 ------------- .../20251128022314_Investment.Designer.cs | 303 ------------------ api/Migrations/20251128022314_Investment.cs | 42 --- .../ApplicationDbContextModelSnapshot.cs | 300 ----------------- {api => infra}/Data/ApplicationDbContext.cs | 2 +- .../Data/Mapping/InvestmentMapping.cs | 2 +- infra/infra.csproj | 14 +- 9 files changed, 16 insertions(+), 1149 deletions(-) delete mode 100644 api/Migrations/20251127021227_inicial.Designer.cs delete mode 100644 api/Migrations/20251127021227_inicial.cs delete mode 100644 api/Migrations/20251128022314_Investment.Designer.cs delete mode 100644 api/Migrations/20251128022314_Investment.cs delete mode 100644 api/Migrations/ApplicationDbContextModelSnapshot.cs rename {api => infra}/Data/ApplicationDbContext.cs (96%) rename {api => infra}/Data/Mapping/InvestmentMapping.cs (96%) diff --git a/api/Common/Api/BuilderExtension.cs b/api/Common/Api/BuilderExtension.cs index 795efdc5a..b0269c67f 100644 --- a/api/Common/Api/BuilderExtension.cs +++ b/api/Common/Api/BuilderExtension.cs @@ -1,4 +1,4 @@ -using api.Data; +using infra.Data; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; diff --git a/api/Migrations/20251127021227_inicial.Designer.cs b/api/Migrations/20251127021227_inicial.Designer.cs deleted file mode 100644 index b36aa3f21..000000000 --- a/api/Migrations/20251127021227_inicial.Designer.cs +++ /dev/null @@ -1,277 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; - -#nullable disable - -namespace api.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20251127021227_inicial")] - partial class inicial - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("ProviderKey") - .HasColumnType("text"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api/Migrations/20251127021227_inicial.cs b/api/Migrations/20251127021227_inicial.cs deleted file mode 100644 index 05dee6be2..000000000 --- a/api/Migrations/20251127021227_inicial.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace api.Migrations -{ - /// - public partial class inicial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AspNetRoles", - columns: table => new - { - Id = table.Column(type: "text", nullable: false), - Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetUsers", - columns: table => new - { - Id = table.Column(type: "text", nullable: false), - UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - EmailConfirmed = table.Column(type: "boolean", nullable: false), - PasswordHash = table.Column(type: "text", nullable: true), - SecurityStamp = table.Column(type: "text", nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true), - PhoneNumber = table.Column(type: "text", nullable: true), - PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), - TwoFactorEnabled = table.Column(type: "boolean", nullable: false), - LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), - LockoutEnabled = table.Column(type: "boolean", nullable: false), - AccessFailedCount = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUsers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetRoleClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RoleId = table.Column(type: "text", nullable: false), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserClaims", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "text", nullable: false), - ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetUserClaims_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserLogins", - columns: table => new - { - LoginProvider = table.Column(type: "text", nullable: false), - ProviderKey = table.Column(type: "text", nullable: false), - ProviderDisplayName = table.Column(type: "text", nullable: true), - UserId = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); - table.ForeignKey( - name: "FK_AspNetUserLogins_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserRoles", - columns: table => new - { - UserId = table.Column(type: "text", nullable: false), - RoleId = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserTokens", - columns: table => new - { - UserId = table.Column(type: "text", nullable: false), - LoginProvider = table.Column(type: "text", nullable: false), - Name = table.Column(type: "text", nullable: false), - Value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); - table.ForeignKey( - name: "FK_AspNetUserTokens_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AspNetRoleClaims_RoleId", - table: "AspNetRoleClaims", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "AspNetRoles", - column: "NormalizedName", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserClaims_UserId", - table: "AspNetUserClaims", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserLogins_UserId", - table: "AspNetUserLogins", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserRoles_RoleId", - table: "AspNetUserRoles", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "AspNetUsers", - column: "NormalizedEmail"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "AspNetUsers", - column: "NormalizedUserName", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AspNetRoleClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserLogins"); - - migrationBuilder.DropTable( - name: "AspNetUserRoles"); - - migrationBuilder.DropTable( - name: "AspNetUserTokens"); - - migrationBuilder.DropTable( - name: "AspNetRoles"); - - migrationBuilder.DropTable( - name: "AspNetUsers"); - } - } -} diff --git a/api/Migrations/20251128022314_Investment.Designer.cs b/api/Migrations/20251128022314_Investment.Designer.cs deleted file mode 100644 index 1a86fa56b..000000000 --- a/api/Migrations/20251128022314_Investment.Designer.cs +++ /dev/null @@ -1,303 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; - -#nullable disable - -namespace api.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20251128022314_Investment")] - partial class Investment - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("ProviderKey") - .HasColumnType("text"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("core.Entities.Investment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationDate") - .HasColumnType("timestamptz"); - - b.Property("InitialAmount") - .HasColumnType("decimal(18,2)"); - - b.Property("OwnerId") - .IsRequired() - .HasColumnType("varchar(128)"); - - b.Property("WithdrawalDate") - .HasColumnType("timestamptz"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("Investment", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api/Migrations/20251128022314_Investment.cs b/api/Migrations/20251128022314_Investment.cs deleted file mode 100644 index 9109bcf57..000000000 --- a/api/Migrations/20251128022314_Investment.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace api.Migrations -{ - /// - public partial class Investment : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Investment", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - OwnerId = table.Column(type: "varchar(128)", nullable: false), - CreationDate = table.Column(type: "timestamptz", nullable: false), - InitialAmount = table.Column(type: "numeric(18,2)", nullable: false), - WithdrawalDate = table.Column(type: "timestamptz", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Investment", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Investment_OwnerId", - table: "Investment", - column: "OwnerId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Investment"); - } - } -} diff --git a/api/Migrations/ApplicationDbContextModelSnapshot.cs b/api/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index fab86da8c..000000000 --- a/api/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,300 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using api.Data; - -#nullable disable - -namespace api.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - partial class ApplicationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("AccessFailedCount") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("text"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("EmailConfirmed") - .HasColumnType("boolean"); - - b.Property("LockoutEnabled") - .HasColumnType("boolean"); - - b.Property("LockoutEnd") - .HasColumnType("timestamp with time zone"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("PasswordHash") - .HasColumnType("text"); - - b.Property("PhoneNumber") - .HasColumnType("text"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("boolean"); - - b.Property("SecurityStamp") - .HasColumnType("text"); - - b.Property("TwoFactorEnabled") - .HasColumnType("boolean"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimType") - .HasColumnType("text"); - - b.Property("ClaimValue") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("ProviderKey") - .HasColumnType("text"); - - b.Property("ProviderDisplayName") - .HasColumnType("text"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("RoleId") - .HasColumnType("text"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("text"); - - b.Property("LoginProvider") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("core.Entities.Investment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationDate") - .HasColumnType("timestamptz"); - - b.Property("InitialAmount") - .HasColumnType("decimal(18,2)"); - - b.Property("OwnerId") - .IsRequired() - .HasColumnType("varchar(128)"); - - b.Property("WithdrawalDate") - .HasColumnType("timestamptz"); - - b.HasKey("Id"); - - b.HasIndex("OwnerId"); - - b.ToTable("Investment", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/api/Data/ApplicationDbContext.cs b/infra/Data/ApplicationDbContext.cs similarity index 96% rename from api/Data/ApplicationDbContext.cs rename to infra/Data/ApplicationDbContext.cs index 2ca19e8f0..607780cf4 100644 --- a/api/Data/ApplicationDbContext.cs +++ b/infra/Data/ApplicationDbContext.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace api.Data +namespace infra.Data { public class ApplicationDbContext : IdentityDbContext { diff --git a/api/Data/Mapping/InvestmentMapping.cs b/infra/Data/Mapping/InvestmentMapping.cs similarity index 96% rename from api/Data/Mapping/InvestmentMapping.cs rename to infra/Data/Mapping/InvestmentMapping.cs index 8940f8672..5c6632583 100644 --- a/api/Data/Mapping/InvestmentMapping.cs +++ b/infra/Data/Mapping/InvestmentMapping.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace api.Data.Mapping +namespace infra.Data.Mapping { public class InvestmentMapping : IEntityTypeConfiguration { diff --git a/infra/infra.csproj b/infra/infra.csproj index 125f4c93b..182fffcd4 100644 --- a/infra/infra.csproj +++ b/infra/infra.csproj @@ -1,9 +1,21 @@  - net9.0 + net8.0 enable enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + From f979da9d5185291987a644af78108f45f173a1b1 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Fri, 28 Nov 2025 00:34:29 -0300 Subject: [PATCH 16/40] feat: migracoes development --- ...0251128033107_InitialMigration.Designer.cs | 303 ++++++++++++++++++ .../20251128033107_InitialMigration.cs | 246 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 300 +++++++++++++++++ infra/infra.csproj | 7 +- 4 files changed, 853 insertions(+), 3 deletions(-) create mode 100644 infra/Migrations/20251128033107_InitialMigration.Designer.cs create mode 100644 infra/Migrations/20251128033107_InitialMigration.cs create mode 100644 infra/Migrations/ApplicationDbContextModelSnapshot.cs diff --git a/infra/Migrations/20251128033107_InitialMigration.Designer.cs b/infra/Migrations/20251128033107_InitialMigration.Designer.cs new file mode 100644 index 000000000..db2c912aa --- /dev/null +++ b/infra/Migrations/20251128033107_InitialMigration.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using infra.Data; + +#nullable disable + +namespace infra.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251128033107_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("core.Entities.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamptz"); + + b.Property("InitialAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(128)"); + + b.Property("WithdrawalDate") + .HasColumnType("timestamptz"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Investment", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/infra/Migrations/20251128033107_InitialMigration.cs b/infra/Migrations/20251128033107_InitialMigration.cs new file mode 100644 index 000000000..f7819588f --- /dev/null +++ b/infra/Migrations/20251128033107_InitialMigration.cs @@ -0,0 +1,246 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace infra.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Investment", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OwnerId = table.Column(type: "varchar(128)", nullable: false), + CreationDate = table.Column(type: "timestamptz", nullable: false), + InitialAmount = table.Column(type: "numeric(18,2)", nullable: false), + WithdrawalDate = table.Column(type: "timestamptz", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Investment", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + RoleId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Investment_OwnerId", + table: "Investment", + column: "OwnerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Investment"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/infra/Migrations/ApplicationDbContextModelSnapshot.cs b/infra/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 000000000..e3e9293e5 --- /dev/null +++ b/infra/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,300 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using infra.Data; + +#nullable disable + +namespace infra.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("core.Entities.Investment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamptz"); + + b.Property("InitialAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("varchar(128)"); + + b.Property("WithdrawalDate") + .HasColumnType("timestamptz"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Investment", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/infra/infra.csproj b/infra/infra.csproj index 182fffcd4..139d61b3d 100644 --- a/infra/infra.csproj +++ b/infra/infra.csproj @@ -11,11 +11,12 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + \ No newline at end of file From 7ad4962de1b9b42326f219c25bee9a08a8aa04e8 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 09:26:42 -0300 Subject: [PATCH 17/40] feat: projeto application com servico investimento development --- api/api.csproj | 3 +- application/Services/IInvestmentService.cs | 12 ++++ application/Services/InvestmentService.cs | 75 ++++++++++++++++++++++ application/application.csproj | 13 ++++ solucao.sln | 6 ++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 application/Services/IInvestmentService.cs create mode 100644 application/Services/InvestmentService.cs create mode 100644 application/application.csproj diff --git a/api/api.csproj b/api/api.csproj index bf0a1b337..f6bcf3099 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -19,8 +19,7 @@ - - + diff --git a/application/Services/IInvestmentService.cs b/application/Services/IInvestmentService.cs new file mode 100644 index 000000000..e4a4db6a6 --- /dev/null +++ b/application/Services/IInvestmentService.cs @@ -0,0 +1,12 @@ +using core.Entities; + +namespace application.Services +{ + public interface IInvestmentService + { + Task CreateInvestmentAsync(string ownerId, decimal amount, DateTime creationDate); + Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate); + Task GetInvestmentByIdAsync(Guid id); + Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize); + } +} \ No newline at end of file diff --git a/application/Services/InvestmentService.cs b/application/Services/InvestmentService.cs new file mode 100644 index 000000000..59adfeb36 --- /dev/null +++ b/application/Services/InvestmentService.cs @@ -0,0 +1,75 @@ +using core.Entities; +using core.Repositories; +using core.Services; + +namespace application.Services +{ + public class InvestmentService : IInvestmentService + { + private readonly IInvestmentRepository _repository; + private readonly IGainCalculationService _gainService; + private readonly ITaxCalculationService _taxService; + + public InvestmentService( + IInvestmentRepository repository, + IGainCalculationService gainService, + ITaxCalculationService taxService) + { + _repository = repository; + _gainService = gainService; + _taxService = taxService; + } + + public async Task CreateInvestmentAsync(string ownerId, decimal amount, DateTime creationDate) + { + if (string.IsNullOrWhiteSpace(ownerId)) + throw new ArgumentException("OwnerId é obrigatório."); + if (amount <= 0) + throw new ArgumentException("O valor do investimento deve ser maior que zero."); + if (creationDate > DateTime.Today) + throw new ArgumentException("A data de criação não pode ser futura."); + + var investment = new Investment + { + Id = Guid.NewGuid(), + OwnerId = ownerId, + InitialAmount = amount, + CreationDate = creationDate + }; + + await _repository.AddAsync(investment); + return investment; + } + + public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate) + { + var investment = await _repository.GetByIdAsync(investmentId); + if (investment == null) + throw new InvalidOperationException("Investimento não encontrado."); + + if (investment.IsWithdrawn) + throw new InvalidOperationException("Investimento já foi resgatado."); + + if (withdrawalDate < investment.CreationDate) + throw new ArgumentException("A data de resgate não pode ser anterior à data de criação."); + if (withdrawalDate > DateTime.Today) + throw new ArgumentException("A data de resgate não pode ser futura."); + + investment.WithdrawalDate = withdrawalDate; + + await _repository.UpdateAsync(investment); + } + + public async Task GetInvestmentByIdAsync(Guid id) + => await _repository.GetByIdAsync(id); + + public async Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize) + { + if (page < 1) page = 1; + if (pageSize < 1) pageSize = 10; + if (pageSize > 100) pageSize = 100; + + return (await _repository.GetByOwnerIdAsync(ownerId, page, pageSize)).ToList(); + } + } +} \ No newline at end of file diff --git a/application/application.csproj b/application/application.csproj new file mode 100644 index 000000000..10c1faff0 --- /dev/null +++ b/application/application.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/solucao.sln b/solucao.sln index 2d9c83c44..e44580c88 100644 --- a/solucao.sln +++ b/solucao.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "core", "core\core.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "infra", "infra\infra.csproj", "{AD8308B3-4A67-45BC-937F-259B4639B10A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "application", "application\application.csproj", "{D05E4524-B912-4FF2-B8B4-EBB14053123A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +38,9 @@ Global {AD8308B3-4A67-45BC-937F-259B4639B10A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD8308B3-4A67-45BC-937F-259B4639B10A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD8308B3-4A67-45BC-937F-259B4639B10A}.Release|Any CPU.Build.0 = Release|Any CPU + {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From a9e213afacc63ba94ad809ab12d3fae1e9962326 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 09:27:31 -0300 Subject: [PATCH 18/40] feat: separacao de logica com outras classes do core development --- core/Entities/Investment.cs | 60 -------------------- core/Repositories/IInvestmentRepository.cs | 8 +-- core/Services/GainCalculationService.cs | 30 ++++++++++ core/Services/IGainCalculationService.cs | 7 +++ core/Services/ITaxCalculationService.cs | 7 +++ core/Services/TaxCalculationService.cs | 27 +++++++++ infra/Repositories/InvestmentRepository.cs | 65 ++++++++++++++++++++++ 7 files changed, 140 insertions(+), 64 deletions(-) create mode 100644 core/Services/GainCalculationService.cs create mode 100644 core/Services/IGainCalculationService.cs create mode 100644 core/Services/ITaxCalculationService.cs create mode 100644 core/Services/TaxCalculationService.cs create mode 100644 infra/Repositories/InvestmentRepository.cs diff --git a/core/Entities/Investment.cs b/core/Entities/Investment.cs index 9f3c0e86a..27f00576a 100644 --- a/core/Entities/Investment.cs +++ b/core/Entities/Investment.cs @@ -8,67 +8,7 @@ public class Investment public decimal InitialAmount { get; set; } public DateTime? WithdrawalDate { get; set; } - - public decimal CurrentGains => CalculateGains(); - public decimal ExpectedBalance => InitialAmount + CurrentGains; public bool IsWithdrawn => WithdrawalDate.HasValue; - private decimal CalculateGains() - { - if (IsWithdrawn) - return CalculateGainsUpTo(WithdrawalDate.Value); - - return CalculateGainsUpTo(DateTime.Today); - } - - private decimal CalculateGainsUpTo(DateTime untilDate) - { - if (untilDate < CreationDate) - return 0; - - var months = GetFullMonthsBetween(CreationDate, untilDate); - var balance = InitialAmount; - - for (int i = 0; i < months; i++) - { - balance *= 1.0052m; - } - - return balance - InitialAmount; - } - - private int GetFullMonthsBetween(DateTime start, DateTime end) - { - int months = (end.Year - start.Year) * 12 + (end.Month - start.Month); - - - if (end.Day < start.Day) - months--; - - return Math.Max(0, months); - } - - public decimal GetTaxedWithdrawAmount() - { - if (!IsWithdrawn) - throw new InvalidOperationException("Investment has not been withdrawn."); - - var totalGain = CurrentGains; - var taxRate = GetTaxRate(WithdrawalDate.Value); - var tax = totalGain * taxRate; - return ExpectedBalance - tax; - } - - private decimal GetTaxRate(DateTime withdrawalDate) - { - var ageInYears = (withdrawalDate - CreationDate).TotalDays / 365.25; - - if (ageInYears < 1) - return 0.225m; - else if (ageInYears < 2) - return 0.185m; - else - return 0.15m; - } } } \ No newline at end of file diff --git a/core/Repositories/IInvestmentRepository.cs b/core/Repositories/IInvestmentRepository.cs index 36bc409c8..549740e82 100644 --- a/core/Repositories/IInvestmentRepository.cs +++ b/core/Repositories/IInvestmentRepository.cs @@ -4,11 +4,11 @@ namespace core.Repositories { public interface IInvestmentRepository { - Task CreateAsync(Investment investment); - Task UpdateAsync(Investment investment); - Task DeleteAsync(Guid id); Task GetByIdAsync(Guid id); - Task> GetByFilterAsync(Guid id); + Task> GetByOwnerIdAsync(string ownerId, int page, int pageSize); + Task AddAsync(Investment investment); + Task UpdateAsync(Investment investment); + Task ExistsAsync(Guid id); } } \ No newline at end of file diff --git a/core/Services/GainCalculationService.cs b/core/Services/GainCalculationService.cs new file mode 100644 index 000000000..7a0acb66d --- /dev/null +++ b/core/Services/GainCalculationService.cs @@ -0,0 +1,30 @@ +namespace core.Services +{ + public class GainCalculationService : IGainCalculationService + { + public decimal CalculateGains(decimal initialAmount, DateTime creationDate, DateTime untilDate) + { + if (untilDate < creationDate || initialAmount <= 0) + return 0; + + var months = GetFullMonthsBetween(creationDate, untilDate); + var balance = initialAmount; + + for (int i = 0; i < months; i++) + { + balance *= 1.0052m; // juros compostos de 0.52% ao mês + } + + var gains = balance - initialAmount; + return Math.Round(gains, 2); + } + + private int GetFullMonthsBetween(DateTime start, DateTime end) + { + int months = (end.Year - start.Year) * 12 + (end.Month - start.Month); + if (end.Day < start.Day) + months--; + return Math.Max(0, months); + } + } +} \ No newline at end of file diff --git a/core/Services/IGainCalculationService.cs b/core/Services/IGainCalculationService.cs new file mode 100644 index 000000000..7de841f75 --- /dev/null +++ b/core/Services/IGainCalculationService.cs @@ -0,0 +1,7 @@ +namespace core.Services +{ + public interface IGainCalculationService + { + decimal CalculateGains(decimal initialAmount, DateTime creationDate, DateTime untilDate); + } +} \ No newline at end of file diff --git a/core/Services/ITaxCalculationService.cs b/core/Services/ITaxCalculationService.cs new file mode 100644 index 000000000..5dc6d0da2 --- /dev/null +++ b/core/Services/ITaxCalculationService.cs @@ -0,0 +1,7 @@ +namespace core.Services +{ + public interface ITaxCalculationService + { + decimal CalculateTax(decimal gain, DateTime creationDate, DateTime withdrawalDate); + } +} \ No newline at end of file diff --git a/core/Services/TaxCalculationService.cs b/core/Services/TaxCalculationService.cs new file mode 100644 index 000000000..b2629dd1a --- /dev/null +++ b/core/Services/TaxCalculationService.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace core.Services +{ + public class TaxCalculationService : ITaxCalculationService + { + public decimal CalculateTax(decimal gain, DateTime creationDate, DateTime withdrawalDate) + { + if (gain <= 0) + return 0; + + var ageInYears = (withdrawalDate - creationDate).TotalDays / 365.25; + + decimal taxRate = ageInYears switch + { + < 1 => 0.225m, + < 2 => 0.185m, + _ => 0.15m + }; + + return Math.Round(gain * taxRate, 2); + } + } +} \ No newline at end of file diff --git a/infra/Repositories/InvestmentRepository.cs b/infra/Repositories/InvestmentRepository.cs new file mode 100644 index 000000000..b5a465c62 --- /dev/null +++ b/infra/Repositories/InvestmentRepository.cs @@ -0,0 +1,65 @@ +using core.Entities; +using core.Repositories; +using infra.Data; +using Microsoft.EntityFrameworkCore; + +namespace infra.Repositories +{ + public class InvestmentRepository : IInvestmentRepository + { + private readonly ApplicationDbContext _context; + + public InvestmentRepository(ApplicationDbContext context) + { + _context = context; + } + + public async Task AddAsync(Investment investment) + { + if (investment == null) + throw new ArgumentNullException(nameof(investment)); + + _context.Investments.Add(investment); + await _context.SaveChangesAsync(); + } + + public async Task ExistsAsync(Guid id) + { + return await _context.Investments.AnyAsync(e => e.Id == id); + } + + public async Task GetByIdAsync(Guid id) + { + return await _context.Investments + .AsNoTracking() + .FirstOrDefaultAsync(e => e.Id == id); + } + + public async Task> GetByOwnerIdAsync(string ownerId, int page, int pageSize) + { + if (string.IsNullOrWhiteSpace(ownerId)) + throw new ArgumentException("OwnerId não pode ser nulo ou vazio.", nameof(ownerId)); + + if (page < 1) page = 1; + if (pageSize < 1) pageSize = 10; + if (pageSize > 100) pageSize = 100; + + return await _context.Investments + .AsNoTracking() + .Where(e => e.OwnerId == ownerId) + .OrderBy(e => e.CreationDate) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + } + + public async Task UpdateAsync(Investment investment) + { + if (investment == null) + throw new ArgumentNullException(nameof(investment)); + + _context.Investments.Update(investment); + await _context.SaveChangesAsync(); + } + } +} \ No newline at end of file From 111a25df77242d3eb07ee22c4b3846a1d5ef00b5 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 09:58:19 -0300 Subject: [PATCH 19/40] feat: registrando servicos development --- api/Common/Api/BuilderExtension.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/Common/Api/BuilderExtension.cs b/api/Common/Api/BuilderExtension.cs index b0269c67f..052dab319 100644 --- a/api/Common/Api/BuilderExtension.cs +++ b/api/Common/Api/BuilderExtension.cs @@ -1,4 +1,8 @@ +using application.Services; +using core.Repositories; +using core.Services; using infra.Data; +using infra.Repositories; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -69,7 +73,11 @@ public static void AddDataContexts(this WebApplicationBuilder builder) public static void AddServices(this WebApplicationBuilder builder) { - + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); } } } \ No newline at end of file From 97ef329d5d1f80a24fc3084d2cffe8119d16cdac Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 09:58:53 -0300 Subject: [PATCH 20/40] feat: count async development --- core/Repositories/IInvestmentRepository.cs | 3 ++- infra/Repositories/InvestmentRepository.cs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/Repositories/IInvestmentRepository.cs b/core/Repositories/IInvestmentRepository.cs index 549740e82..5dac93631 100644 --- a/core/Repositories/IInvestmentRepository.cs +++ b/core/Repositories/IInvestmentRepository.cs @@ -4,8 +4,9 @@ namespace core.Repositories { public interface IInvestmentRepository { - Task GetByIdAsync(Guid id); + Task GetByIdAsync(Guid id); Task> GetByOwnerIdAsync(string ownerId, int page, int pageSize); + Task CountByOwnerIdAsync(string ownerId); Task AddAsync(Investment investment); Task UpdateAsync(Investment investment); Task ExistsAsync(Guid id); diff --git a/infra/Repositories/InvestmentRepository.cs b/infra/Repositories/InvestmentRepository.cs index b5a465c62..24bbe0e06 100644 --- a/infra/Repositories/InvestmentRepository.cs +++ b/infra/Repositories/InvestmentRepository.cs @@ -53,6 +53,16 @@ public async Task> GetByOwnerIdAsync(string ownerId, int .ToListAsync(); } + public async Task CountByOwnerIdAsync(string ownerId) + { + if (string.IsNullOrWhiteSpace(ownerId)) + throw new ArgumentException("OwnerId não pode ser nulo ou vazio.", nameof(ownerId)); + + return await _context.Investments + .AsNoTracking() + .LongCountAsync(e => e.OwnerId == ownerId); + } + public async Task UpdateAsync(Investment investment) { if (investment == null) From 0dd93c5b2a7ae8a8d78a0c08789a777e91a67cf1 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 10:06:11 -0300 Subject: [PATCH 21/40] feat: dtos development --- application/DTOs/CreateInvestmentInput.cs | 9 +++++++++ application/DTOs/InvestmentDto.cs | 14 ++++++++++++++ application/DTOs/PaginatedResult.cs | 11 +++++++++++ application/DTOs/WithdrawInvestmentInput.cs | 7 +++++++ 4 files changed, 41 insertions(+) create mode 100644 application/DTOs/CreateInvestmentInput.cs create mode 100644 application/DTOs/InvestmentDto.cs create mode 100644 application/DTOs/PaginatedResult.cs create mode 100644 application/DTOs/WithdrawInvestmentInput.cs diff --git a/application/DTOs/CreateInvestmentInput.cs b/application/DTOs/CreateInvestmentInput.cs new file mode 100644 index 000000000..46c0dc0c9 --- /dev/null +++ b/application/DTOs/CreateInvestmentInput.cs @@ -0,0 +1,9 @@ +namespace application.DTOs +{ + public class CreateInvestmentInput + { + public string OwnerId { get; set; } = string.Empty; + public decimal Amount { get; set; } + public DateTime CreationDate { get; set; } + } +} \ No newline at end of file diff --git a/application/DTOs/InvestmentDto.cs b/application/DTOs/InvestmentDto.cs new file mode 100644 index 000000000..79adc0560 --- /dev/null +++ b/application/DTOs/InvestmentDto.cs @@ -0,0 +1,14 @@ +namespace application.DTOs +{ + public class InvestmentDto + { + public Guid Id { get; set; } + public string OwnerId { get; set; } + public DateTime CreationDate { get; set; } + public decimal InitialAmount { get; set; } + public DateTime? WithdrawalDate { get; set; } + public decimal ExpectedBalance { get; set; } + public decimal? NetWithdrawAmount { get; set; } + public bool IsWithdrawn => WithdrawalDate.HasValue; + } +} \ No newline at end of file diff --git a/application/DTOs/PaginatedResult.cs b/application/DTOs/PaginatedResult.cs new file mode 100644 index 000000000..7e9b673c7 --- /dev/null +++ b/application/DTOs/PaginatedResult.cs @@ -0,0 +1,11 @@ +namespace application.DTOs +{ + public class PaginatedResult + { + public List Items { get; set; } = new(); + public int PageNumber { get; set; } + public int PageSize { get; set; } + public long TotalCount { get; set; } + public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); + } +} \ No newline at end of file diff --git a/application/DTOs/WithdrawInvestmentInput.cs b/application/DTOs/WithdrawInvestmentInput.cs new file mode 100644 index 000000000..20e1854fb --- /dev/null +++ b/application/DTOs/WithdrawInvestmentInput.cs @@ -0,0 +1,7 @@ +namespace application.DTOs +{ + public class WithdrawInvestmentInput + { + public DateTime WithdrawalDate { get; set; } + } +} \ No newline at end of file From 31e238d0363205d4e53eec6f5f9f8524eb32e264 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 10:06:30 -0300 Subject: [PATCH 22/40] feat: utilizando novas dtos development --- api/Controllers/InvestmentController.cs | 82 +++++++++- api/api.csproj | 2 + application/Services/IInvestmentService.cs | 7 +- application/Services/InvestmentService.cs | 170 ++++++++++++++------- 4 files changed, 203 insertions(+), 58 deletions(-) diff --git a/api/Controllers/InvestmentController.cs b/api/Controllers/InvestmentController.cs index 438f4bdd5..e378ac8e7 100644 --- a/api/Controllers/InvestmentController.cs +++ b/api/Controllers/InvestmentController.cs @@ -1,3 +1,5 @@ +using application.DTOs; +using application.Services; using Microsoft.AspNetCore.Mvc; namespace api.Controllers @@ -6,6 +8,84 @@ namespace api.Controllers [Route("api/[controller]")] public class InvestmentController : ControllerBase { - + private readonly IInvestmentService _investmentService; + + public InvestmentController(IInvestmentService investmentService) + { + _investmentService = investmentService; + } + + /// + /// Creates a new investment. + /// + [HttpPost] + public async Task> CreateInvestment([FromBody] CreateInvestmentInput input) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + try + { + var investment = await _investmentService.CreateInvestmentAsync(input); + return CreatedAtAction(nameof(GetInvestmentById), new { id = investment.Id }, investment); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Retrieves an investment by ID. + /// + [HttpGet("{id:guid}")] + public async Task> GetInvestmentById(Guid id) + { + var investment = await _investmentService.GetInvestmentByIdAsync(id); + if (investment == null) + return NotFound(); + + return Ok(investment); + } + + /// + /// Lists all investments for a given owner (with pagination). + /// + [HttpGet("owner/{ownerId}")] + public async Task>> GetInvestmentsByOwner( + string ownerId, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10) + { + if (string.IsNullOrWhiteSpace(ownerId)) + return BadRequest("OwnerId is required."); + + var result = await _investmentService.GetInvestmentsByOwnerAsync(ownerId, page, pageSize); + return Ok(result); + } + + /// + /// Withdraws an investment (full withdrawal only). + /// + [HttpPost("{id:guid}/withdraw")] + public async Task WithdrawInvestment(Guid id, [FromBody] WithdrawInvestmentInput input) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + try + { + await _investmentService.WithdrawInvestmentAsync(id, input.WithdrawalDate); + return NoContent(); + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + } } } \ No newline at end of file diff --git a/api/api.csproj b/api/api.csproj index f6bcf3099..b8faf87b6 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -20,6 +20,8 @@ + + diff --git a/application/Services/IInvestmentService.cs b/application/Services/IInvestmentService.cs index e4a4db6a6..9f19cccf0 100644 --- a/application/Services/IInvestmentService.cs +++ b/application/Services/IInvestmentService.cs @@ -1,12 +1,13 @@ +using application.DTOs; using core.Entities; namespace application.Services { public interface IInvestmentService { - Task CreateInvestmentAsync(string ownerId, decimal amount, DateTime creationDate); + Task CreateInvestmentAsync(CreateInvestmentInput input); Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate); - Task GetInvestmentByIdAsync(Guid id); - Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize); + Task GetInvestmentByIdAsync(Guid id); + Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize); } } \ No newline at end of file diff --git a/application/Services/InvestmentService.cs b/application/Services/InvestmentService.cs index 59adfeb36..1092efc2e 100644 --- a/application/Services/InvestmentService.cs +++ b/application/Services/InvestmentService.cs @@ -1,75 +1,137 @@ using core.Entities; using core.Repositories; using core.Services; +using application.DTOs; -namespace application.Services +namespace application.Services; + +public class InvestmentService : IInvestmentService { - public class InvestmentService : IInvestmentService + private readonly IInvestmentRepository _repository; + private readonly IGainCalculationService _gainService; + private readonly ITaxCalculationService _taxService; + + public InvestmentService( + IInvestmentRepository repository, + IGainCalculationService gainService, + ITaxCalculationService taxService) + { + _repository = repository; + _gainService = gainService; + _taxService = taxService; + } + + public async Task CreateInvestmentAsync(CreateInvestmentInput input) { - private readonly IInvestmentRepository _repository; - private readonly IGainCalculationService _gainService; - private readonly ITaxCalculationService _taxService; - - public InvestmentService( - IInvestmentRepository repository, - IGainCalculationService gainService, - ITaxCalculationService taxService) + ValidateCreateInput(input.OwnerId, input.Amount, input.CreationDate); + + var investment = new Investment { - _repository = repository; - _gainService = gainService; - _taxService = taxService; - } + Id = Guid.NewGuid(), + OwnerId = input.OwnerId, + InitialAmount = input.Amount, + CreationDate = input.CreationDate + }; + + await _repository.AddAsync(investment); + return await MapToDtoAsync(investment); + } + + public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate) + { + var investment = await _repository.GetByIdAsync(investmentId); + if (investment == null) + throw new InvalidOperationException("Investimento não encontrado."); + + if (investment.IsWithdrawn) + throw new InvalidOperationException("Investimento já foi resgatado."); - public async Task CreateInvestmentAsync(string ownerId, decimal amount, DateTime creationDate) + if (withdrawalDate < investment.CreationDate) + throw new ArgumentException("A data de resgate não pode ser anterior à data de criação."); + if (withdrawalDate > DateTime.Today) + throw new ArgumentException("A data de resgate não pode ser futura."); + + investment.WithdrawalDate = withdrawalDate; + await _repository.UpdateAsync(investment); + } + + public async Task GetInvestmentByIdAsync(Guid id) + { + var investment = await _repository.GetByIdAsync(id); + return investment == null ? null : await MapToDtoAsync(investment); + } + + public async Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize) + { + if (string.IsNullOrWhiteSpace(ownerId)) + throw new ArgumentException("OwnerId é obrigatório."); + + + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 1, 100); + + var investments = await _repository.GetByOwnerIdAsync(ownerId, page, pageSize); + var totalCount = await _repository.CountByOwnerIdAsync(ownerId); + + var items = new List(); + foreach (var investment in investments) { - if (string.IsNullOrWhiteSpace(ownerId)) - throw new ArgumentException("OwnerId é obrigatório."); - if (amount <= 0) - throw new ArgumentException("O valor do investimento deve ser maior que zero."); - if (creationDate > DateTime.Today) - throw new ArgumentException("A data de criação não pode ser futura."); - - var investment = new Investment - { - Id = Guid.NewGuid(), - OwnerId = ownerId, - InitialAmount = amount, - CreationDate = creationDate - }; - - await _repository.AddAsync(investment); - return investment; + items.Add(await MapToDtoAsync(investment)); } - public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate) + return new PaginatedResult { - var investment = await _repository.GetByIdAsync(investmentId); - if (investment == null) - throw new InvalidOperationException("Investimento não encontrado."); - - if (investment.IsWithdrawn) - throw new InvalidOperationException("Investimento já foi resgatado."); + Items = items, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount + }; + } - if (withdrawalDate < investment.CreationDate) - throw new ArgumentException("A data de resgate não pode ser anterior à data de criação."); - if (withdrawalDate > DateTime.Today) - throw new ArgumentException("A data de resgate não pode ser futura."); + private static void ValidateCreateInput(string ownerId, decimal amount, DateTime creationDate) + { + if (string.IsNullOrWhiteSpace(ownerId)) + throw new ArgumentException("OwnerId é obrigatório."); + if (amount <= 0) + throw new ArgumentException("O valor do investimento deve ser maior que zero."); + if (creationDate > DateTime.Today) + throw new ArgumentException("A data de criação não pode ser futura."); + } - investment.WithdrawalDate = withdrawalDate; + private async Task MapToDtoAsync(Investment investment) + { + DateTime calculationDate = investment.IsWithdrawn + ? investment.WithdrawalDate!.Value + : DateTime.Today; - await _repository.UpdateAsync(investment); - } + decimal gains = _gainService.CalculateGains( + investment.InitialAmount, + investment.CreationDate, + calculationDate + ); - public async Task GetInvestmentByIdAsync(Guid id) - => await _repository.GetByIdAsync(id); + decimal expectedBalance = investment.InitialAmount + gains; + decimal? netWithdrawAmount = null; - public async Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize) + if (investment.IsWithdrawn) { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 10; - if (pageSize > 100) pageSize = 100; - - return (await _repository.GetByOwnerIdAsync(ownerId, page, pageSize)).ToList(); + decimal tax = _taxService.CalculateTax( + gains, + investment.CreationDate, + investment.WithdrawalDate!.Value + ); + netWithdrawAmount = expectedBalance - tax; } + + return new InvestmentDto + { + Id = investment.Id, + OwnerId = investment.OwnerId, + CreationDate = investment.CreationDate, + InitialAmount = investment.InitialAmount, + WithdrawalDate = investment.WithdrawalDate, + ExpectedBalance = Math.Round(expectedBalance, 2), + NetWithdrawAmount = netWithdrawAmount.HasValue ? Math.Round(netWithdrawAmount.Value, 2) : null + }; } } \ No newline at end of file From c8a7e7c1a94f02275ec292cf7987d857891e4938 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 11:18:38 -0300 Subject: [PATCH 23/40] feat: utilizando ClaimTypes development --- api/Controllers/InvestmentController.cs | 30 ++++++++++++++++------- application/Services/InvestmentService.cs | 10 ++++---- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/api/Controllers/InvestmentController.cs b/api/Controllers/InvestmentController.cs index e378ac8e7..b670831ae 100644 --- a/api/Controllers/InvestmentController.cs +++ b/api/Controllers/InvestmentController.cs @@ -1,5 +1,7 @@ +using System.Security.Claims; using application.DTOs; using application.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace api.Controllers @@ -18,16 +20,23 @@ public InvestmentController(IInvestmentService investmentService) /// /// Creates a new investment. /// + [Authorize] [HttpPost] public async Task> CreateInvestment([FromBody] CreateInvestmentInput input) { if (!ModelState.IsValid) return BadRequest(ModelState); + var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) + return BadRequest("Usuário não autenticado."); + + input.OwnerId = userId; + try { var investment = await _investmentService.CreateInvestmentAsync(input); - return CreatedAtAction(nameof(GetInvestmentById), new { id = investment.Id }, investment); + return Ok(investment); } catch (ArgumentException ex) { @@ -35,9 +44,10 @@ public async Task> CreateInvestment([FromBody] Creat } } - /// - /// Retrieves an investment by ID. - /// + // /// + // /// Retrieves an investment by ID. + // /// + [Authorize] [HttpGet("{id:guid}")] public async Task> GetInvestmentById(Guid id) { @@ -51,22 +61,24 @@ public async Task> GetInvestmentById(Guid id) /// /// Lists all investments for a given owner (with pagination). /// - [HttpGet("owner/{ownerId}")] + [Authorize] + [HttpGet("owner")] public async Task>> GetInvestmentsByOwner( - string ownerId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) { - if (string.IsNullOrWhiteSpace(ownerId)) - return BadRequest("OwnerId is required."); + var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userId)) + return BadRequest("Usuário não autenticado."); - var result = await _investmentService.GetInvestmentsByOwnerAsync(ownerId, page, pageSize); + var result = await _investmentService.GetInvestmentsByOwnerAsync(userId, page, pageSize); return Ok(result); } /// /// Withdraws an investment (full withdrawal only). /// + [Authorize] [HttpPost("{id:guid}/withdraw")] public async Task WithdrawInvestment(Guid id, [FromBody] WithdrawInvestmentInput input) { diff --git a/application/Services/InvestmentService.cs b/application/Services/InvestmentService.cs index 1092efc2e..089b7849d 100644 --- a/application/Services/InvestmentService.cs +++ b/application/Services/InvestmentService.cs @@ -34,7 +34,7 @@ public async Task CreateInvestmentAsync(CreateInvestmentInput inp }; await _repository.AddAsync(investment); - return await MapToDtoAsync(investment); + return MapToDtoAsync(investment); } public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate) @@ -58,7 +58,7 @@ public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawal public async Task GetInvestmentByIdAsync(Guid id) { var investment = await _repository.GetByIdAsync(id); - return investment == null ? null : await MapToDtoAsync(investment); + return investment == null ? null : MapToDtoAsync(investment); } public async Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize) @@ -66,7 +66,7 @@ public async Task> GetInvestmentsByOwnerAsync(str if (string.IsNullOrWhiteSpace(ownerId)) throw new ArgumentException("OwnerId é obrigatório."); - + page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100); @@ -76,7 +76,7 @@ public async Task> GetInvestmentsByOwnerAsync(str var items = new List(); foreach (var investment in investments) { - items.Add(await MapToDtoAsync(investment)); + items.Add(MapToDtoAsync(investment)); } return new PaginatedResult @@ -98,7 +98,7 @@ private static void ValidateCreateInput(string ownerId, decimal amount, DateTime throw new ArgumentException("A data de criação não pode ser futura."); } - private async Task MapToDtoAsync(Investment investment) + private InvestmentDto MapToDtoAsync(Investment investment) { DateTime calculationDate = investment.IsWithdrawn ? investment.WithdrawalDate!.Value From d80fbf9ff8aeea8403760919c180eae694f61893 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 14:02:39 -0300 Subject: [PATCH 24/40] feat: extensoes datetime development --- common/TypeExtentions/DateTimeExtensions.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 common/TypeExtentions/DateTimeExtensions.cs diff --git a/common/TypeExtentions/DateTimeExtensions.cs b/common/TypeExtentions/DateTimeExtensions.cs new file mode 100644 index 000000000..c2407163c --- /dev/null +++ b/common/TypeExtentions/DateTimeExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace common.TypeExtentions +{ + public static class DateTimeExtensions + { + public static bool IsDateGreaterThan(this DateTime leftSide, DateTime rightSide) + { + return leftSide.Date > rightSide.Date; + } + + public static bool IsDateLessThan(this DateTime leftSide, DateTime rightSide) + { + return leftSide.Date > rightSide.Date; + } + } +} \ No newline at end of file From bb8b75f26f8915e42ddd89b5bfa9963b570987dc Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 14:03:26 -0300 Subject: [PATCH 25/40] feat: lista e contagem simultaneas development --- application/Services/InvestmentService.cs | 27 +++++++++++----------- application/application.csproj | 1 + core/Services/GainCalculationService.cs | 2 +- core/Services/TaxCalculationService.cs | 5 ---- infra/Repositories/InvestmentRepository.cs | 6 ----- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/application/Services/InvestmentService.cs b/application/Services/InvestmentService.cs index 089b7849d..940b18569 100644 --- a/application/Services/InvestmentService.cs +++ b/application/Services/InvestmentService.cs @@ -2,6 +2,7 @@ using core.Repositories; using core.Services; using application.DTOs; +using common.TypeExtentions; namespace application.Services; @@ -30,7 +31,7 @@ public async Task CreateInvestmentAsync(CreateInvestmentInput inp Id = Guid.NewGuid(), OwnerId = input.OwnerId, InitialAmount = input.Amount, - CreationDate = input.CreationDate + CreationDate = input.CreationDate.ToUniversalTime() }; await _repository.AddAsync(investment); @@ -39,16 +40,14 @@ public async Task CreateInvestmentAsync(CreateInvestmentInput inp public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate) { - var investment = await _repository.GetByIdAsync(investmentId); - if (investment == null) - throw new InvalidOperationException("Investimento não encontrado."); - + withdrawalDate = withdrawalDate.ToUniversalTime(); + var investment = await _repository.GetByIdAsync(investmentId) ?? throw new InvalidOperationException("Investimento não encontrado."); if (investment.IsWithdrawn) throw new InvalidOperationException("Investimento já foi resgatado."); - if (withdrawalDate < investment.CreationDate) + if (withdrawalDate.IsDateLessThan(investment.CreationDate)) throw new ArgumentException("A data de resgate não pode ser anterior à data de criação."); - if (withdrawalDate > DateTime.Today) + if (withdrawalDate.IsDateGreaterThan(DateTime.Today)) throw new ArgumentException("A data de resgate não pode ser futura."); investment.WithdrawalDate = withdrawalDate; @@ -70,9 +69,11 @@ public async Task> GetInvestmentsByOwnerAsync(str page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100); - var investments = await _repository.GetByOwnerIdAsync(ownerId, page, pageSize); - var totalCount = await _repository.CountByOwnerIdAsync(ownerId); - + var investmentsTask = _repository.GetByOwnerIdAsync(ownerId, page, pageSize); + var totalCountTask = _repository.CountByOwnerIdAsync(ownerId); + await Task.WhenAll(investmentsTask, totalCountTask); + var investments = investmentsTask.Result; + var totalCount = totalCountTask.Result; var items = new List(); foreach (var investment in investments) { @@ -94,15 +95,15 @@ private static void ValidateCreateInput(string ownerId, decimal amount, DateTime throw new ArgumentException("OwnerId é obrigatório."); if (amount <= 0) throw new ArgumentException("O valor do investimento deve ser maior que zero."); - if (creationDate > DateTime.Today) + if (creationDate.IsDateGreaterThan(DateTime.Today)) throw new ArgumentException("A data de criação não pode ser futura."); } private InvestmentDto MapToDtoAsync(Investment investment) { DateTime calculationDate = investment.IsWithdrawn - ? investment.WithdrawalDate!.Value - : DateTime.Today; + ? investment.WithdrawalDate!.Value.ToUniversalTime() + : DateTime.Today.ToUniversalTime(); decimal gains = _gainService.CalculateGains( investment.InitialAmount, diff --git a/application/application.csproj b/application/application.csproj index 10c1faff0..496120e58 100644 --- a/application/application.csproj +++ b/application/application.csproj @@ -8,6 +8,7 @@ + diff --git a/core/Services/GainCalculationService.cs b/core/Services/GainCalculationService.cs index 7a0acb66d..3b1cab040 100644 --- a/core/Services/GainCalculationService.cs +++ b/core/Services/GainCalculationService.cs @@ -19,7 +19,7 @@ public decimal CalculateGains(decimal initialAmount, DateTime creationDate, Date return Math.Round(gains, 2); } - private int GetFullMonthsBetween(DateTime start, DateTime end) + private static int GetFullMonthsBetween(DateTime start, DateTime end) { int months = (end.Year - start.Year) * 12 + (end.Month - start.Month); if (end.Day < start.Day) diff --git a/core/Services/TaxCalculationService.cs b/core/Services/TaxCalculationService.cs index b2629dd1a..c30e01802 100644 --- a/core/Services/TaxCalculationService.cs +++ b/core/Services/TaxCalculationService.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace core.Services { public class TaxCalculationService : ITaxCalculationService diff --git a/infra/Repositories/InvestmentRepository.cs b/infra/Repositories/InvestmentRepository.cs index 24bbe0e06..22b58cf96 100644 --- a/infra/Repositories/InvestmentRepository.cs +++ b/infra/Repositories/InvestmentRepository.cs @@ -16,9 +16,6 @@ public InvestmentRepository(ApplicationDbContext context) public async Task AddAsync(Investment investment) { - if (investment == null) - throw new ArgumentNullException(nameof(investment)); - _context.Investments.Add(investment); await _context.SaveChangesAsync(); } @@ -65,9 +62,6 @@ public async Task CountByOwnerIdAsync(string ownerId) public async Task UpdateAsync(Investment investment) { - if (investment == null) - throw new ArgumentNullException(nameof(investment)); - _context.Investments.Update(investment); await _context.SaveChangesAsync(); } From 918c31b22e3e13a3419dbecde0a754dadb8381d1 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 15:17:06 -0300 Subject: [PATCH 26/40] feat: extension claims development --- api/Controllers/InvestmentController.cs | 16 +++++++--------- .../TypeExtentions/ClaimsPrincipalExtension.cs | 13 +++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 common/TypeExtentions/ClaimsPrincipalExtension.cs diff --git a/api/Controllers/InvestmentController.cs b/api/Controllers/InvestmentController.cs index b670831ae..55b748eb0 100644 --- a/api/Controllers/InvestmentController.cs +++ b/api/Controllers/InvestmentController.cs @@ -1,8 +1,8 @@ -using System.Security.Claims; using application.DTOs; using application.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using common.TypeExtentions; namespace api.Controllers { @@ -27,11 +27,11 @@ public async Task> CreateInvestment([FromBody] Creat if (!ModelState.IsValid) return BadRequest(ModelState); - var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var userId = User.UserId(); if (string.IsNullOrEmpty(userId)) return BadRequest("Usuário não autenticado."); - input.OwnerId = userId; + input.SetOwner(userId); try { @@ -51,6 +51,7 @@ public async Task> CreateInvestment([FromBody] Creat [HttpGet("{id:guid}")] public async Task> GetInvestmentById(Guid id) { + var userId = User.UserId(); var investment = await _investmentService.GetInvestmentByIdAsync(id); if (investment == null) return NotFound(); @@ -63,11 +64,9 @@ public async Task> GetInvestmentById(Guid id) /// [Authorize] [HttpGet("owner")] - public async Task>> GetInvestmentsByOwner( - [FromQuery] int page = 1, - [FromQuery] int pageSize = 10) + public async Task>> GetInvestmentsByOwner([FromQuery] int page = 1, [FromQuery] int pageSize = 10) { - var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var userId = User.UserId(); if (string.IsNullOrEmpty(userId)) return BadRequest("Usuário não autenticado."); @@ -82,8 +81,7 @@ public async Task>> GetInvestmentsBy [HttpPost("{id:guid}/withdraw")] public async Task WithdrawInvestment(Guid id, [FromBody] WithdrawInvestmentInput input) { - if (!ModelState.IsValid) - return BadRequest(ModelState); + var userId = User.UserId(); try { diff --git a/common/TypeExtentions/ClaimsPrincipalExtension.cs b/common/TypeExtentions/ClaimsPrincipalExtension.cs new file mode 100644 index 000000000..cf377888c --- /dev/null +++ b/common/TypeExtentions/ClaimsPrincipalExtension.cs @@ -0,0 +1,13 @@ +using System.Security.Claims; + +namespace common.TypeExtentions +{ + public static class ClaimsPrincipalExtension + { + public static string? UserId(this ClaimsPrincipal principal) + { + return principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; + } + + } +} \ No newline at end of file From fceade0818f3be91629505e0d47e4f7846ad2211 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 15:17:34 -0300 Subject: [PATCH 27/40] feat: filtro development --- application/DTOs/CreateInvestmentInput.cs | 7 ++++++- application/DTOs/InvestmentDto.cs | 2 +- core/Entities/Investment.cs | 2 +- core/Filters/InvestmentFilter.cs | 20 ++++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 core/Filters/InvestmentFilter.cs diff --git a/application/DTOs/CreateInvestmentInput.cs b/application/DTOs/CreateInvestmentInput.cs index 46c0dc0c9..2c728d61e 100644 --- a/application/DTOs/CreateInvestmentInput.cs +++ b/application/DTOs/CreateInvestmentInput.cs @@ -2,8 +2,13 @@ namespace application.DTOs { public class CreateInvestmentInput { - public string OwnerId { get; set; } = string.Empty; + public string OwnerId { get; private set; } = string.Empty; public decimal Amount { get; set; } public DateTime CreationDate { get; set; } + + public void SetOwner(string ownerId) + { + OwnerId = ownerId; + } } } \ No newline at end of file diff --git a/application/DTOs/InvestmentDto.cs b/application/DTOs/InvestmentDto.cs index 79adc0560..da2291899 100644 --- a/application/DTOs/InvestmentDto.cs +++ b/application/DTOs/InvestmentDto.cs @@ -3,7 +3,7 @@ namespace application.DTOs public class InvestmentDto { public Guid Id { get; set; } - public string OwnerId { get; set; } + public string OwnerId { get; set; } = string.Empty; public DateTime CreationDate { get; set; } public decimal InitialAmount { get; set; } public DateTime? WithdrawalDate { get; set; } diff --git a/core/Entities/Investment.cs b/core/Entities/Investment.cs index 27f00576a..25530bd7e 100644 --- a/core/Entities/Investment.cs +++ b/core/Entities/Investment.cs @@ -3,7 +3,7 @@ namespace core.Entities public class Investment { public Guid Id { get; set; } - public string OwnerId { get; set; } + public string OwnerId { get; set; } = string.Empty; public DateTime CreationDate { get; set; } public decimal InitialAmount { get; set; } public DateTime? WithdrawalDate { get; set; } diff --git a/core/Filters/InvestmentFilter.cs b/core/Filters/InvestmentFilter.cs new file mode 100644 index 000000000..29a4070fd --- /dev/null +++ b/core/Filters/InvestmentFilter.cs @@ -0,0 +1,20 @@ +namespace core.Filters +{ + public class InvestmentFilter + { + public string OwnerId { get; private set; } = string.Empty; + public Guid? Id { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + + + public int Skip => (Math.Max(1, Page) - 1) * PageSizeClamped; + public int PageSizeClamped => Math.Clamp(PageSize, 1, 100); + + public void SetOwner(string ownerId) + { + OwnerId = ownerId; + } + + } +} \ No newline at end of file From e92852f42d419c070cd93bdbebc1ac765951816c Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 15:48:03 -0300 Subject: [PATCH 28/40] feat: usando o filtro development --- api/Controllers/InvestmentController.cs | 11 +++++-- application/Services/IInvestmentService.cs | 7 +++-- application/Services/InvestmentService.cs | 25 ++++++++-------- common/TypeExtentions/DateTimeExtensions.cs | 4 +-- core/Filters/InvestmentFilter.cs | 7 ++++- core/Repositories/IInvestmentRepository.cs | 9 +++--- infra/Repositories/InvestmentRepository.cs | 33 ++++++++++----------- 7 files changed, 51 insertions(+), 45 deletions(-) diff --git a/api/Controllers/InvestmentController.cs b/api/Controllers/InvestmentController.cs index 55b748eb0..35c1993b2 100644 --- a/api/Controllers/InvestmentController.cs +++ b/api/Controllers/InvestmentController.cs @@ -52,7 +52,10 @@ public async Task> CreateInvestment([FromBody] Creat public async Task> GetInvestmentById(Guid id) { var userId = User.UserId(); - var investment = await _investmentService.GetInvestmentByIdAsync(id); + if (string.IsNullOrEmpty(userId)) + return BadRequest("Usuário não autenticado."); + + var investment = await _investmentService.GetInvestmentByIdAsync(new core.Filters.InvestmentFilter(userId) { Id = id }); if (investment == null) return NotFound(); @@ -70,7 +73,7 @@ public async Task>> GetInvestmentsBy if (string.IsNullOrEmpty(userId)) return BadRequest("Usuário não autenticado."); - var result = await _investmentService.GetInvestmentsByOwnerAsync(userId, page, pageSize); + var result = await _investmentService.GetInvestmentsByOwnerAsync(new core.Filters.InvestmentFilter(userId) { Page = page, PageSize = pageSize }); return Ok(result); } @@ -82,10 +85,12 @@ public async Task>> GetInvestmentsBy public async Task WithdrawInvestment(Guid id, [FromBody] WithdrawInvestmentInput input) { var userId = User.UserId(); + if (string.IsNullOrEmpty(userId)) + return BadRequest("Usuário não autenticado."); try { - await _investmentService.WithdrawInvestmentAsync(id, input.WithdrawalDate); + await _investmentService.WithdrawInvestmentAsync(id, userId, input.WithdrawalDate); return NoContent(); } catch (InvalidOperationException ex) diff --git a/application/Services/IInvestmentService.cs b/application/Services/IInvestmentService.cs index 9f19cccf0..6bdd10c3f 100644 --- a/application/Services/IInvestmentService.cs +++ b/application/Services/IInvestmentService.cs @@ -1,13 +1,14 @@ using application.DTOs; using core.Entities; +using core.Filters; namespace application.Services { public interface IInvestmentService { Task CreateInvestmentAsync(CreateInvestmentInput input); - Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate); - Task GetInvestmentByIdAsync(Guid id); - Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize); + Task WithdrawInvestmentAsync(Guid investmentId, string ownerId, DateTime withdrawalDate); + Task GetInvestmentByIdAsync(InvestmentFilter filter); + Task> GetInvestmentsByOwnerAsync(InvestmentFilter filter); } } \ No newline at end of file diff --git a/application/Services/InvestmentService.cs b/application/Services/InvestmentService.cs index 940b18569..dcbccaec6 100644 --- a/application/Services/InvestmentService.cs +++ b/application/Services/InvestmentService.cs @@ -3,6 +3,7 @@ using core.Services; using application.DTOs; using common.TypeExtentions; +using core.Filters; namespace application.Services; @@ -38,10 +39,10 @@ public async Task CreateInvestmentAsync(CreateInvestmentInput inp return MapToDtoAsync(investment); } - public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawalDate) + public async Task WithdrawInvestmentAsync(Guid investmentId, string ownerId, DateTime withdrawalDate) { withdrawalDate = withdrawalDate.ToUniversalTime(); - var investment = await _repository.GetByIdAsync(investmentId) ?? throw new InvalidOperationException("Investimento não encontrado."); + var investment = await _repository.GetByIdAsync(new(ownerId) { Id = investmentId }) ?? throw new InvalidOperationException("Investimento não encontrado."); if (investment.IsWithdrawn) throw new InvalidOperationException("Investimento já foi resgatado."); @@ -54,24 +55,22 @@ public async Task WithdrawInvestmentAsync(Guid investmentId, DateTime withdrawal await _repository.UpdateAsync(investment); } - public async Task GetInvestmentByIdAsync(Guid id) + public async Task GetInvestmentByIdAsync(InvestmentFilter filter) { - var investment = await _repository.GetByIdAsync(id); + var investment = await _repository.GetByIdAsync(filter); return investment == null ? null : MapToDtoAsync(investment); } - public async Task> GetInvestmentsByOwnerAsync(string ownerId, int page, int pageSize) + public async Task> GetInvestmentsByOwnerAsync(InvestmentFilter filter) { - if (string.IsNullOrWhiteSpace(ownerId)) + if (string.IsNullOrWhiteSpace(filter.OwnerId)) throw new ArgumentException("OwnerId é obrigatório."); + var investmentsTask = _repository.GetByOwnerIdAsync(filter); + var totalCountTask = _repository.CountByOwnerIdAsync(filter); - page = Math.Max(1, page); - pageSize = Math.Clamp(pageSize, 1, 100); - - var investmentsTask = _repository.GetByOwnerIdAsync(ownerId, page, pageSize); - var totalCountTask = _repository.CountByOwnerIdAsync(ownerId); await Task.WhenAll(investmentsTask, totalCountTask); + var investments = investmentsTask.Result; var totalCount = totalCountTask.Result; var items = new List(); @@ -83,8 +82,8 @@ public async Task> GetInvestmentsByOwnerAsync(str return new PaginatedResult { Items = items, - PageNumber = page, - PageSize = pageSize, + PageNumber = filter.Page, + PageSize = filter.PageSizeClamped, TotalCount = totalCount }; } diff --git a/common/TypeExtentions/DateTimeExtensions.cs b/common/TypeExtentions/DateTimeExtensions.cs index c2407163c..b52a5992d 100644 --- a/common/TypeExtentions/DateTimeExtensions.cs +++ b/common/TypeExtentions/DateTimeExtensions.cs @@ -1,5 +1,3 @@ -using System; - namespace common.TypeExtentions { public static class DateTimeExtensions @@ -11,7 +9,7 @@ public static bool IsDateGreaterThan(this DateTime leftSide, DateTime rightSide) public static bool IsDateLessThan(this DateTime leftSide, DateTime rightSide) { - return leftSide.Date > rightSide.Date; + return leftSide.Date < rightSide.Date; } } } \ No newline at end of file diff --git a/core/Filters/InvestmentFilter.cs b/core/Filters/InvestmentFilter.cs index 29a4070fd..6451c8a71 100644 --- a/core/Filters/InvestmentFilter.cs +++ b/core/Filters/InvestmentFilter.cs @@ -7,10 +7,15 @@ public class InvestmentFilter public int Page { get; set; } = 1; public int PageSize { get; set; } = 10; - + public int Skip => (Math.Max(1, Page) - 1) * PageSizeClamped; public int PageSizeClamped => Math.Clamp(PageSize, 1, 100); + public InvestmentFilter(string ownerId) + { + OwnerId = ownerId; + } + public void SetOwner(string ownerId) { OwnerId = ownerId; diff --git a/core/Repositories/IInvestmentRepository.cs b/core/Repositories/IInvestmentRepository.cs index 5dac93631..f7fd4db86 100644 --- a/core/Repositories/IInvestmentRepository.cs +++ b/core/Repositories/IInvestmentRepository.cs @@ -1,15 +1,16 @@ using core.Entities; +using core.Filters; namespace core.Repositories { public interface IInvestmentRepository { - Task GetByIdAsync(Guid id); - Task> GetByOwnerIdAsync(string ownerId, int page, int pageSize); - Task CountByOwnerIdAsync(string ownerId); + Task GetByIdAsync(InvestmentFilter filter); + Task> GetByOwnerIdAsync(InvestmentFilter filter); + Task CountByOwnerIdAsync(InvestmentFilter filter); Task AddAsync(Investment investment); Task UpdateAsync(Investment investment); - Task ExistsAsync(Guid id); + Task ExistsAsync(InvestmentFilter filter); } } \ No newline at end of file diff --git a/infra/Repositories/InvestmentRepository.cs b/infra/Repositories/InvestmentRepository.cs index 22b58cf96..0d8e9399a 100644 --- a/infra/Repositories/InvestmentRepository.cs +++ b/infra/Repositories/InvestmentRepository.cs @@ -1,4 +1,5 @@ using core.Entities; +using core.Filters; using core.Repositories; using infra.Data; using Microsoft.EntityFrameworkCore; @@ -20,44 +21,40 @@ public async Task AddAsync(Investment investment) await _context.SaveChangesAsync(); } - public async Task ExistsAsync(Guid id) + public async Task ExistsAsync(InvestmentFilter filter) { - return await _context.Investments.AnyAsync(e => e.Id == id); + return await _context.Investments.AnyAsync(e => e.Id == filter.Id && e.OwnerId == filter.OwnerId); } - public async Task GetByIdAsync(Guid id) + public async Task GetByIdAsync(InvestmentFilter filter) { return await _context.Investments .AsNoTracking() - .FirstOrDefaultAsync(e => e.Id == id); + .FirstOrDefaultAsync(e => e.Id == filter.Id && e.OwnerId == filter.OwnerId); } - public async Task> GetByOwnerIdAsync(string ownerId, int page, int pageSize) + public async Task> GetByOwnerIdAsync(InvestmentFilter filter) { - if (string.IsNullOrWhiteSpace(ownerId)) - throw new ArgumentException("OwnerId não pode ser nulo ou vazio.", nameof(ownerId)); - - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 10; - if (pageSize > 100) pageSize = 100; + if (string.IsNullOrWhiteSpace(filter.OwnerId)) + throw new ArgumentException("OwnerId não pode ser nulo ou vazio."); return await _context.Investments .AsNoTracking() - .Where(e => e.OwnerId == ownerId) + .Where(e => e.OwnerId == filter.OwnerId) .OrderBy(e => e.CreationDate) - .Skip((page - 1) * pageSize) - .Take(pageSize) + .Skip(filter.Skip) + .Take(filter.PageSizeClamped) .ToListAsync(); } - public async Task CountByOwnerIdAsync(string ownerId) + public async Task CountByOwnerIdAsync(InvestmentFilter filter) { - if (string.IsNullOrWhiteSpace(ownerId)) - throw new ArgumentException("OwnerId não pode ser nulo ou vazio.", nameof(ownerId)); + if (string.IsNullOrWhiteSpace(filter.OwnerId)) + throw new ArgumentException("OwnerId não pode ser nulo ou vazio."); return await _context.Investments .AsNoTracking() - .LongCountAsync(e => e.OwnerId == ownerId); + .LongCountAsync(e => e.OwnerId == filter.OwnerId); } public async Task UpdateAsync(Investment investment) From d7df772214dc4166ea216ea8ae584b4cc0eaaaaf Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sat, 29 Nov 2025 19:05:31 -0300 Subject: [PATCH 29/40] feat: logout development --- api/Controllers/IdentityController.cs | 26 ++++++++++++++++++++++++++ api/Program.cs | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 api/Controllers/IdentityController.cs diff --git a/api/Controllers/IdentityController.cs b/api/Controllers/IdentityController.cs new file mode 100644 index 000000000..dba71a208 --- /dev/null +++ b/api/Controllers/IdentityController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class IdentityController : ControllerBase + { + private readonly SignInManager _signInManager; + + public IdentityController(SignInManager signInManager) + { + _signInManager = signInManager; + } + + [Authorize] + [HttpPost("logout")] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + return Ok(); + } + } +} \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index b854f5350..adf1a9519 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -22,7 +22,7 @@ app.UseHttpsRedirection(); -app.MapGroup("/identity") +app.MapGroup("/api/Identity") .MapIdentityApi(); app.MapControllers(); From 0e79adf8bd340d6b4afa55b4e6f81c89927e9a0b Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 13:06:37 -0300 Subject: [PATCH 30/40] feat: tags no identity e atualizacao swagger development --- api/Program.cs | 24 +++++++++++++++++------- api/api.csproj | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api/Program.cs b/api/Program.cs index adf1a9519..e45a581f7 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,5 +1,6 @@ using api.Common.Api; using Microsoft.AspNetCore.Identity; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); @@ -9,21 +10,30 @@ builder.AddDocumentation(); builder.AddServices(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Minha API", + Version = "v1" + }); +}); var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) + +app.UseSwagger(); +app.UseSwaggerUI(c => { - app.UseSwagger(); - app.UseSwaggerUI(); -} + c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"); +}); + app.UseHttpsRedirection(); app.MapGroup("/api/Identity") - .MapIdentityApi(); + .WithTags("Identity") + .MapIdentityApi(); app.MapControllers(); diff --git a/api/api.csproj b/api/api.csproj index b8faf87b6..6e2caeeac 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -9,7 +9,7 @@ - + From 1557a10f6a988a72b1e0f5822752073ed417e2ac Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 14:49:31 -0300 Subject: [PATCH 31/40] feat: configuracao e exemplo docs development --- api/Controllers/WeatherForecastController.cs | 34 +++++++++++++++++--- api/Program.cs | 24 +++++++++++--- api/api.csproj | 10 ++++-- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/api/Controllers/WeatherForecastController.cs b/api/Controllers/WeatherForecastController.cs index dd24dcb75..eaaf98b4f 100644 --- a/api/Controllers/WeatherForecastController.cs +++ b/api/Controllers/WeatherForecastController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; namespace api.Controllers { @@ -9,7 +10,8 @@ public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + "Freezing", "Bracing", "Chilly", "Cool", "Mild", + "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger _logger; @@ -19,11 +21,30 @@ public WeatherForecastController(ILogger logger) _logger = logger; } + /// + /// Exemplo de chamada: + /// + /// GET /WeatherForecast?days=5 + /// + /// [Authorize] [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() + [SwaggerOperation( + Summary = "Obtém previsões do tempo", + Description = "Retorna dados aleatórios de previsão dos próximos dias.", + OperationId = "GetWeatherForecast", + Tags = new[] { "Weather" } + )] + [SwaggerResponse(StatusCodes.Status200OK, "Previsão retornada com sucesso", typeof(IEnumerable))] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Requer autenticação")] + [SwaggerResponse(StatusCodes.Status500InternalServerError,"Erro interno no servidor")] + public IEnumerable Get( [SwaggerParameter("Número de dias de previsão entre 1 e 14")] int days = 5) { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast + if (days < 1 || days > 14) + throw new ArgumentOutOfRangeException(nameof(days), + "O número de dias deve ser entre 1 e 14."); + + return Enumerable.Range(1, days).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), @@ -31,16 +52,21 @@ public IEnumerable Get() }) .ToArray(); } + } public class WeatherForecast { + /// 2025-01-30 public DateOnly Date { get; set; } + /// 23 public int TemperatureC { get; set; } + /// 73 public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + /// Mild public string? Summary { get; set; } } -} \ No newline at end of file +} diff --git a/api/Program.cs b/api/Program.cs index e45a581f7..ebc4f4644 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,3 +1,4 @@ +using System.Reflection; using api.Common.Api; using Microsoft.AspNetCore.Identity; using Microsoft.OpenApi.Models; @@ -10,13 +11,28 @@ builder.AddDocumentation(); builder.AddServices(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => +builder.Services.AddSwaggerGen(options => { - c.SwaggerDoc("v1", new OpenApiInfo + options.EnableAnnotations(); + options.SwaggerDoc("v1", new OpenApiInfo { - Title = "Minha API", - Version = "v1" + Version = "v1", + Title = "Investment API", + Description = "An ASP.NET Core Web API for investments", + TermsOfService = new Uri("https://example.com/terms"), + Contact = new OpenApiContact + { + Name = "Investment API Contact", + Url = new Uri("https://example.com/contact") + }, + License = new OpenApiLicense + { + Name = "Investment API License", + Url = new Uri("https://example.com/license") + } }); + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); }); var app = builder.Build(); diff --git a/api/api.csproj b/api/api.csproj index 6e2caeeac..a8ff3d5bd 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -12,10 +12,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all + @@ -24,4 +25,9 @@ - + + true + $(NoWarn);1591 + + + \ No newline at end of file From 36942d8fad00a023d20ea4821cfaa9d9dddb4fdc Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 15:55:20 -0300 Subject: [PATCH 32/40] feat: documentacao dos endpoints development --- api/Controllers/InvestmentController.cs | 140 +++++++++++++++++++--- application/Services/InvestmentService.cs | 8 +- 2 files changed, 124 insertions(+), 24 deletions(-) diff --git a/api/Controllers/InvestmentController.cs b/api/Controllers/InvestmentController.cs index 35c1993b2..bf22e648e 100644 --- a/api/Controllers/InvestmentController.cs +++ b/api/Controllers/InvestmentController.cs @@ -2,7 +2,9 @@ using application.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; using common.TypeExtentions; +using core.Filters; namespace api.Controllers { @@ -17,12 +19,37 @@ public InvestmentController(IInvestmentService investmentService) _investmentService = investmentService; } - /// - /// Creates a new investment. - /// + /// + /// ### Exemplo de requisição + /// ```json + /// { + /// "amount": 1500.00, + /// "creationDate": "2023-04-10" + /// } + /// ``` + /// + /// ### Regras: + /// - O valor inicial **não pode ser negativo** + /// - A data de criação pode ser **hoje ou no passado** + /// - O dono é atribuído internamente a partir do contexto do usuário autenticado + /// + /// ### Ganhos: + /// - O investimento rende **0.52% ao mês** + /// - Regra de **juros compostos** + /// [Authorize] [HttpPost] - public async Task> CreateInvestment([FromBody] CreateInvestmentInput input) + [SwaggerOperation( + Summary = "Cria um novo investimento", + Description = "Cria um investimento definindo dono, data de criação e valor inicial.", + OperationId = "CreateInvestment", + Tags = new[] { "Investments" } + )] + [SwaggerResponse(StatusCodes.Status200OK, "Investimento criado com sucesso", typeof(InvestmentDto))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "Erro de validação ou dados inválidos")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Usuário não autenticado")] + public async Task> CreateInvestment( + [FromBody, SwaggerRequestBody("Dados para criação de um novo investimento")] CreateInvestmentInput input) { if (!ModelState.IsValid) return BadRequest(ModelState); @@ -44,45 +71,122 @@ public async Task> CreateInvestment([FromBody] Creat } } - // /// - // /// Retrieves an investment by ID. - // /// + + /// + /// ### Retorno inclui: + /// - Valor inicial + /// - Data de criação + /// - Saldo esperado (valor + ganhos) + /// - Data de retirada (quando existir) + /// - Valor líquido recebido após impostos (se retirado) + /// + /// ### Regras de ganhos: + /// - 0.52% ao mês + /// - Juros compostos + /// + /// Se o investimento já foi retirado: + /// - O saldo reflete somente os ganhos até a data do saque + /// - O campo **NetWithdrawAmount** no retorno representa o valor final após impostos, só é preenchido caso tenha sido retirado. + /// [Authorize] [HttpGet("{id:guid}")] + [SwaggerOperation( + Summary = "Obtém um investimento pelo ID", + Description = "Retorna os detalhes completos do investimento.", + OperationId = "GetInvestmentById", + Tags = new[] { "Investments" } + )] + [SwaggerResponse(StatusCodes.Status200OK, "Investimento encontrado", typeof(InvestmentDto))] + [SwaggerResponse(StatusCodes.Status404NotFound, "Investimento não encontrado")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Usuário não autenticado")] public async Task> GetInvestmentById(Guid id) { var userId = User.UserId(); if (string.IsNullOrEmpty(userId)) return BadRequest("Usuário não autenticado."); - var investment = await _investmentService.GetInvestmentByIdAsync(new core.Filters.InvestmentFilter(userId) { Id = id }); + var investment = await _investmentService.GetInvestmentByIdAsync( + new core.Filters.InvestmentFilter(userId) { Id = id }); + if (investment == null) return NotFound(); return Ok(investment); } - /// - /// Lists all investments for a given owner (with pagination). - /// + /// + /// ### Exemplo: + /// + /// GET /api/investment/owner?page=1&pageSize=10 + /// + /// ### Detalhes: + /// - Lista **todos os investimentos do usuário atual** + /// - Resposta é **paginada** + /// - Inclui total de itens e total de páginas + /// [Authorize] [HttpGet("owner")] - public async Task>> GetInvestmentsByOwner([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + [SwaggerOperation( + Summary = "Lista investimentos do usuário", + Description = "Retorna uma lista paginada de investimentos pertencentes ao usuário atual.", + OperationId = "GetInvestmentsByOwner", + Tags = new[] { "Investments" } + )] + [SwaggerResponse(StatusCodes.Status200OK, "Lista retornada com sucesso", typeof(PaginatedResult))] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Usuário não autenticado")] + public async Task>> GetInvestmentsByOwner( + [FromQuery, SwaggerParameter("Página atual (mínimo 1)")] int page = 1, + [FromQuery, SwaggerParameter("Quantidade de itens por página")] int pageSize = 10) { var userId = User.UserId(); if (string.IsNullOrEmpty(userId)) return BadRequest("Usuário não autenticado."); - var result = await _investmentService.GetInvestmentsByOwnerAsync(new core.Filters.InvestmentFilter(userId) { Page = page, PageSize = pageSize }); + var result = await _investmentService.GetInvestmentsByOwnerAsync( + new InvestmentFilter(userId) { Page = page, PageSize = pageSize }); + return Ok(result); } - /// - /// Withdraws an investment (full withdrawal only). - /// + // --------------------------------------------------------- + // POST /api/investment/{id}/withdraw + // Withdraw investment + // --------------------------------------------------------- + + /// + /// ### Exemplo de requisição + /// ```json + /// { + /// "withdrawalDate": "2025-11-30T18:11:06.825Z" + /// } + /// ``` + /// + /// ### Regras: + /// - O saque é sempre **total (100%)** + /// - A data de saque não pode ser: + /// - antes da criação + /// - no futuro + /// + /// ### Tributação aplicada ao ganho: + /// - Menos de 1 ano → **22.5%** + /// - Entre 1 e 2 anos → **18.5%** + /// - Mais de 2 anos → **15%** + /// + /// [Authorize] [HttpPost("{id:guid}/withdraw")] - public async Task WithdrawInvestment(Guid id, [FromBody] WithdrawInvestmentInput input) + [SwaggerOperation( + Summary = "Realiza o saque de um investimento", + Description = "Efetua o saque total e aplica impostos sobre os ganhos conforme regras de idade do investimento.", + OperationId = "WithdrawInvestment", + Tags = new[] { "Investments" } + )] + [SwaggerResponse(StatusCodes.Status204NoContent, "Saque realizado com sucesso")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "Regras de data inválidas ou investimento já retirado")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Usuário não autenticado")] + public async Task WithdrawInvestment( + Guid id, + [FromBody, SwaggerRequestBody("Data de retirada do investimento")] WithdrawInvestmentInput input) { var userId = User.UserId(); if (string.IsNullOrEmpty(userId)) @@ -103,4 +207,4 @@ public async Task WithdrawInvestment(Guid id, [FromBody] Withdraw } } } -} \ No newline at end of file +} diff --git a/application/Services/InvestmentService.cs b/application/Services/InvestmentService.cs index dcbccaec6..339e3a91d 100644 --- a/application/Services/InvestmentService.cs +++ b/application/Services/InvestmentService.cs @@ -66,13 +66,9 @@ public async Task> GetInvestmentsByOwnerAsync(Inv if (string.IsNullOrWhiteSpace(filter.OwnerId)) throw new ArgumentException("OwnerId é obrigatório."); - var investmentsTask = _repository.GetByOwnerIdAsync(filter); - var totalCountTask = _repository.CountByOwnerIdAsync(filter); + var investments = await _repository.GetByOwnerIdAsync(filter); + var totalCount = await _repository.CountByOwnerIdAsync(filter); - await Task.WhenAll(investmentsTask, totalCountTask); - - var investments = investmentsTask.Result; - var totalCount = totalCountTask.Result; var items = new List(); foreach (var investment in investments) { From 4a5a746a19508a76601c0dc340bd5cc7f05ffe72 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 15:56:38 -0300 Subject: [PATCH 33/40] chore: removendo codigo sem uso development --- api/Controllers/WeatherForecastController.cs | 72 -------------------- 1 file changed, 72 deletions(-) delete mode 100644 api/Controllers/WeatherForecastController.cs diff --git a/api/Controllers/WeatherForecastController.cs b/api/Controllers/WeatherForecastController.cs deleted file mode 100644 index eaaf98b4f..000000000 --- a/api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Swashbuckle.AspNetCore.Annotations; - -namespace api.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", - "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - /// - /// Exemplo de chamada: - /// - /// GET /WeatherForecast?days=5 - /// - /// - [Authorize] - [HttpGet(Name = "GetWeatherForecast")] - [SwaggerOperation( - Summary = "Obtém previsões do tempo", - Description = "Retorna dados aleatórios de previsão dos próximos dias.", - OperationId = "GetWeatherForecast", - Tags = new[] { "Weather" } - )] - [SwaggerResponse(StatusCodes.Status200OK, "Previsão retornada com sucesso", typeof(IEnumerable))] - [SwaggerResponse(StatusCodes.Status401Unauthorized, "Requer autenticação")] - [SwaggerResponse(StatusCodes.Status500InternalServerError,"Erro interno no servidor")] - public IEnumerable Get( [SwaggerParameter("Número de dias de previsão entre 1 e 14")] int days = 5) - { - if (days < 1 || days > 14) - throw new ArgumentOutOfRangeException(nameof(days), - "O número de dias deve ser entre 1 e 14."); - - return Enumerable.Range(1, days).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - - } - - public class WeatherForecast - { - /// 2025-01-30 - public DateOnly Date { get; set; } - - /// 23 - public int TemperatureC { get; set; } - - /// 73 - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - /// Mild - public string? Summary { get; set; } - } -} From 82ebcab15ffcb695f7a1cfbcba3f210974dfaac7 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 16:19:26 -0300 Subject: [PATCH 34/40] feat: cricao projeto de teste development --- solucao.sln | 6 ++++++ unitTest/GlobalUsings.cs | 1 + unitTest/unitTest.csproj | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 unitTest/GlobalUsings.cs create mode 100644 unitTest/unitTest.csproj diff --git a/solucao.sln b/solucao.sln index e44580c88..73b4b0087 100644 --- a/solucao.sln +++ b/solucao.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "infra", "infra\infra.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "application", "application\application.csproj", "{D05E4524-B912-4FF2-B8B4-EBB14053123A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unitTest", "unitTest\unitTest.csproj", "{F5C08DD3-6729-4123-A64A-1E3D32C76207}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,5 +44,9 @@ Global {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Debug|Any CPU.Build.0 = Debug|Any CPU {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Release|Any CPU.ActiveCfg = Release|Any CPU {D05E4524-B912-4FF2-B8B4-EBB14053123A}.Release|Any CPU.Build.0 = Release|Any CPU + {F5C08DD3-6729-4123-A64A-1E3D32C76207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5C08DD3-6729-4123-A64A-1E3D32C76207}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5C08DD3-6729-4123-A64A-1E3D32C76207}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5C08DD3-6729-4123-A64A-1E3D32C76207}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/unitTest/GlobalUsings.cs b/unitTest/GlobalUsings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/unitTest/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/unitTest/unitTest.csproj b/unitTest/unitTest.csproj new file mode 100644 index 000000000..d58b1d36b --- /dev/null +++ b/unitTest/unitTest.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + From f8bf7efccb8324cdde4453e347991e3dda31704f Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 16:20:12 -0300 Subject: [PATCH 35/40] feat: testando o gaincalculateservice development --- unitTest/Tests/TestGainCalculationService.cs | 94 ++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 unitTest/Tests/TestGainCalculationService.cs diff --git a/unitTest/Tests/TestGainCalculationService.cs b/unitTest/Tests/TestGainCalculationService.cs new file mode 100644 index 000000000..54c731604 --- /dev/null +++ b/unitTest/Tests/TestGainCalculationService.cs @@ -0,0 +1,94 @@ +using core.Services; + +namespace unitTest.Tests +{ + public class TestGainCalculationService + { + private GainCalculationService _service; + + [SetUp] + public void Setup() + { + _service = new GainCalculationService(); + } + + [Test] + public void CalculateGains_ReturnsZero_WhenInitialAmountIsZeroOrNegative() + { + var result1 = _service.CalculateGains(0, DateTime.Now, DateTime.Now.AddMonths(5)); + var result2 = _service.CalculateGains(-100, DateTime.Now, DateTime.Now.AddMonths(5)); + + Assert.That(result1, Is.EqualTo(0)); + Assert.That(result2, Is.EqualTo(0)); + } + + [Test] + public void CalculateGains_ReturnsZero_WhenUntilDateIsBeforeCreationDate() + { + var result = _service.CalculateGains(1000, new DateTime(2024, 5, 10), new DateTime(2024, 5, 5)); + + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void CalculateGains_ReturnsZero_WhenNoFullMonthsHavePassed() + { + var result = _service.CalculateGains( + 1000, + new DateTime(2024, 5, 10), + new DateTime(2024, 5, 30) + ); + + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void CalculateGains_ReturnsCorrectValue_ForOneFullMonth() + { + var result = _service.CalculateGains( + 1000, + new DateTime(2024, 1, 10), + new DateTime(2024, 2, 10) + ); + + Assert.That(result, Is.EqualTo(5.20m)); + } + + [Test] + public void CalculateGains_ReturnsCorrectValue_ForMultipleMonths() + { + var result = _service.CalculateGains( + 1000, + new DateTime(2024, 1, 10), + new DateTime(2024, 4, 10) + ); + + Assert.That(result, Is.EqualTo(15.68m)); + } + + [Test] + public void CalculateGains_HandlesEndDateEarlierInMonthCorrectly() + { + var result = _service.CalculateGains( + 1000, + new DateTime(2024, 1, 15), + new DateTime(2024, 2, 14) + ); + + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void CalculateGains_HandlesExactMonthBoundary() + { + var result = _service.CalculateGains( + 2000, + new DateTime(2024, 2, 20), + new DateTime(2024, 3, 20) + ); + + + Assert.That(result, Is.EqualTo(10.40m)); + } + } +} From 95179461fabc6468be5aef8189b66fe3d9846665 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 18:13:24 -0300 Subject: [PATCH 36/40] feat: teste do TaxCalculationService development --- core/Services/TaxCalculationService.cs | 2 +- unitTest/Tests/TestTaxCalculationService.cs | 88 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 unitTest/Tests/TestTaxCalculationService.cs diff --git a/core/Services/TaxCalculationService.cs b/core/Services/TaxCalculationService.cs index c30e01802..92c6f2094 100644 --- a/core/Services/TaxCalculationService.cs +++ b/core/Services/TaxCalculationService.cs @@ -7,7 +7,7 @@ public decimal CalculateTax(decimal gain, DateTime creationDate, DateTime withdr if (gain <= 0) return 0; - var ageInYears = (withdrawalDate - creationDate).TotalDays / 365.25; + var ageInYears = Math.Round((withdrawalDate - creationDate).TotalDays / 365.25); decimal taxRate = ageInYears switch { diff --git a/unitTest/Tests/TestTaxCalculationService.cs b/unitTest/Tests/TestTaxCalculationService.cs new file mode 100644 index 000000000..7f5d6105c --- /dev/null +++ b/unitTest/Tests/TestTaxCalculationService.cs @@ -0,0 +1,88 @@ +using core.Services; + +namespace unitTest.Tests +{ + public class TestTaxCalculationService + { + private TaxCalculationService _service; + + [SetUp] + public void Setup() + { + _service = new TaxCalculationService(); + } + + [Test] + public void CalculateTax_ReturnsZero_WhenGainIsZeroOrNegative() + { + Assert.That(_service.CalculateTax(0, DateTime.Now.AddYears(-1), DateTime.Now), Is.EqualTo(0)); + Assert.That(_service.CalculateTax(-50, DateTime.Now.AddYears(-1), DateTime.Now), Is.EqualTo(0)); + } + + [Test] + public void CalculateTax_Uses_22_5_Percent_WhenAgeIsLessThanOneYear() + { + var creation = new DateTime(2024, 1, 10); + var withdrawal = new DateTime(2024, 6, 10); + + var result = _service.CalculateTax(200m, creation, withdrawal); + + Assert.That(result, Is.EqualTo(45.00m)); + } + + [Test] + public void CalculateTax_Uses_18_5_Percent_WhenAgeIsBetweenOneAndTwoYears() + { + var creation = new DateTime(2023, 1, 10); + var withdrawal = new DateTime(2024, 6, 10); + + var result = _service.CalculateTax(200m, creation, withdrawal); + + Assert.That(result, Is.EqualTo(37.00m)); + } + + [Test] + public void CalculateTax_Uses_15_Percent_WhenAgeIsTwoYearsOrMore() + { + var creation = new DateTime(2022, 1, 10); + var withdrawal = new DateTime(2024, 6, 10); + + var result = _service.CalculateTax(200m, creation, withdrawal); + + Assert.That(result, Is.EqualTo(30.00m)); + } + + [Test] + public void CalculateTax_HandlesBoundaryAtExactlyOneYear() + { + var creation = new DateTime(2023, 1, 1); + var withdrawal = new DateTime(2024, 1, 1); + + var result = _service.CalculateTax(100m, creation, withdrawal); + + Assert.That(result, Is.EqualTo(18.50m)); + } + + [Test] + public void CalculateTax_HandlesBoundaryAtExactlyTwoYears() + { + var creation = new DateTime(2022, 1, 1); + var withdrawal = new DateTime(2024, 1, 1); + + var result = _service.CalculateTax(100m, creation, withdrawal); + + Assert.That(result, Is.EqualTo(15.00m)); + } + + [Test] + public void CalculateTax_RoundsToTwoDecimalPlaces() + { + var creation = new DateTime(2024, 1, 10); + var withdrawal = new DateTime(2024, 6, 10); + + var result = _service.CalculateTax(123.456m, creation, withdrawal); + + Assert.That(result, Is.EqualTo(Math.Round(123.456m * 0.225m, 2))); + } + } +} From d636a87a64ce077ca1e0221e2d9a0a672b7ae4a5 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 19:23:15 -0300 Subject: [PATCH 37/40] feat: testes do TestInvestmentService development --- unitTest/Tests/TestInvestmentService.cs | 226 ++++++++++++++++++++++++ unitTest/unitTest.csproj | 1 + 2 files changed, 227 insertions(+) create mode 100644 unitTest/Tests/TestInvestmentService.cs diff --git a/unitTest/Tests/TestInvestmentService.cs b/unitTest/Tests/TestInvestmentService.cs new file mode 100644 index 000000000..93b17ae19 --- /dev/null +++ b/unitTest/Tests/TestInvestmentService.cs @@ -0,0 +1,226 @@ +using application.DTOs; +using application.Services; +using core.Entities; +using core.Filters; +using core.Repositories; +using core.Services; +using Moq; + +namespace unitTest.Tests +{ + public class TestInvestmentService + { + private Mock _repoMock = null!; + private IGainCalculationService _gainMock = null!; + private ITaxCalculationService _taxMock = null!; + private InvestmentService _service = null!; + + [SetUp] + public void Setup() + { + _repoMock = new Mock(); + _gainMock = new GainCalculationService(); + _taxMock = new TaxCalculationService(); + + _service = new InvestmentService( + _repoMock.Object, + _gainMock, + _taxMock + ); + } + + + [Test] + public async Task CreateInvestmentAsync_ShouldCreateCorrectly() + { + var input = new CreateInvestmentInput + { + Amount = 1000, + CreationDate = DateTime.Today + }; + input.SetOwner("user-123"); + + Investment? savedInvestment = null!; + _repoMock.Setup(r => r.AddAsync(It.IsAny())) + .Callback(inv => savedInvestment = inv) + .Returns(Task.CompletedTask); + + var result = await _service.CreateInvestmentAsync(input); + + Assert.That(savedInvestment, Is.Not.Null); + Assert.That(savedInvestment.OwnerId, Is.EqualTo("user-123")); + Assert.That(savedInvestment.InitialAmount, Is.EqualTo(1000)); + Assert.That(result.InitialAmount, Is.EqualTo(1000)); + } + + [Test] + public void CreateInvestmentAsync_ShouldThrow_WhenOwnerIdMissing() + { + var input = new CreateInvestmentInput + { + Amount = 1000, + CreationDate = DateTime.Today + }; + + Assert.ThrowsAsync(() => _service.CreateInvestmentAsync(input)); + } + + + [Test] + public async Task WithdrawInvestmentAsync_ShouldUpdateWithdrawalDate() + { + var id = Guid.NewGuid(); + var investment = new Investment + { + Id = id, + OwnerId = "user-1", + CreationDate = DateTime.Today.AddDays(-10), + InitialAmount = 500 + }; + + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(investment); + + _repoMock.Setup(r => r.UpdateAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var withdrawalDate = DateTime.Today; + + await _service.WithdrawInvestmentAsync(id, "user-1", withdrawalDate); + + Assert.That(investment.WithdrawalDate, Is.Not.Null); + Assert.That(investment.WithdrawalDate!.Value.Date, Is.EqualTo(withdrawalDate.Date)); + } + + [Test] + public void WithdrawInvestmentAsync_ShouldThrow_WhenInvestmentNotFound() + { + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync((Investment?)null); + + Assert.ThrowsAsync(() => + _service.WithdrawInvestmentAsync(Guid.NewGuid(), "user-1", DateTime.Today)); + } + + [Test] + public void WithdrawInvestmentAsync_ShouldThrow_WhenAlreadyWithdrawn() + { + var investment = new Investment + { + Id = Guid.NewGuid(), + OwnerId = "user-1", + CreationDate = DateTime.Today.AddDays(-10), + WithdrawalDate = DateTime.Today + }; + + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(investment); + + Assert.ThrowsAsync(() => + _service.WithdrawInvestmentAsync(investment.Id, "user-1", DateTime.Today)); + } + + [Test] + public void WithdrawInvestmentAsync_ShouldThrow_WhenWithdrawalDateBeforeCreation() + { + var inv = new Investment + { + Id = Guid.NewGuid(), + OwnerId = "user-1", + CreationDate = DateTime.Today, + InitialAmount = 100 + }; + + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(inv); + + Assert.ThrowsAsync(() => + _service.WithdrawInvestmentAsync(inv.Id, "user-1", DateTime.Today.AddDays(-1))); + } + + [Test] + public void WithdrawInvestmentAsync_ShouldThrow_WhenWithdrawalDateInFuture() + { + var inv = new Investment + { + Id = Guid.NewGuid(), + OwnerId = "user-1", + CreationDate = DateTime.Today.AddDays(-5), + InitialAmount = 100 + }; + + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(inv); + + Assert.ThrowsAsync(() => + _service.WithdrawInvestmentAsync(inv.Id, "user-1", DateTime.Today.AddDays(1))); + } + + + [Test] + public async Task GetInvestmentByIdAsync_ShouldReturnDto() + { + var id = Guid.NewGuid(); + var inv = new Investment + { + Id = id, + OwnerId = "user-1", + CreationDate = DateTime.Today, + InitialAmount = 100 + }; + + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(inv); + + + var result = await _service.GetInvestmentByIdAsync(new InvestmentFilter("user-1"){ Id = id}); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.ExpectedBalance, Is.EqualTo(100)); + } + + + [Test] + public async Task GetInvestmentsByOwnerAsync_ShouldReturnPaginatedResult() + { + var invList = new List + { + new Investment { Id = Guid.NewGuid(), OwnerId = "user-1", InitialAmount = 100, CreationDate = DateTime.Today } + }; + + _repoMock.Setup(r => r.GetByOwnerIdAsync(It.IsAny())) + .ReturnsAsync(invList); + + _repoMock.Setup(r => r.CountByOwnerIdAsync(It.IsAny())) + .ReturnsAsync(1); + + + var result = await _service.GetInvestmentsByOwnerAsync(new InvestmentFilter("user-1")); + + Assert.That(result.Items.Count, Is.EqualTo(1)); + Assert.That(result.TotalCount, Is.EqualTo(1)); + Assert.That(result.Items[0].ExpectedBalance, Is.EqualTo(100)); + } + + [Test] + public async Task MapToDtoAsync_ShouldCalculateGainsAndTaxes() + { + var inv = new Investment + { + Id = Guid.NewGuid(), + OwnerId = "u1", + InitialAmount = 1000, + CreationDate = DateTime.Today.AddYears(-2), + WithdrawalDate = DateTime.Today + }; + + _repoMock.Setup(r => r.GetByIdAsync(It.IsAny())) + .ReturnsAsync(inv); + + var result = await _service.GetInvestmentByIdAsync(new InvestmentFilter("u1")); + + Assert.That(result!.ExpectedBalance, Is.EqualTo(1132.56m)); + Assert.That(result.NetWithdrawAmount, Is.EqualTo(1112.68m)); + } + } +} diff --git a/unitTest/unitTest.csproj b/unitTest/unitTest.csproj index d58b1d36b..43489598b 100644 --- a/unitTest/unitTest.csproj +++ b/unitTest/unitTest.csproj @@ -11,6 +11,7 @@ + From 92e01a7bee21cdd5e9fa8bb5688ccb157416df46 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 22:43:55 -0300 Subject: [PATCH 38/40] feat: aplicando migracoes no statup development --- api/Common/Api/BuilderExtension.cs | 37 +++++++++++++++++++++++------- api/Program.cs | 34 +++++++-------------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/api/Common/Api/BuilderExtension.cs b/api/Common/Api/BuilderExtension.cs index 052dab319..7765d5f96 100644 --- a/api/Common/Api/BuilderExtension.cs +++ b/api/Common/Api/BuilderExtension.cs @@ -1,3 +1,4 @@ +using System.Reflection; using application.Services; using core.Repositories; using core.Services; @@ -5,6 +6,7 @@ using infra.Repositories; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; namespace api.Common.Api { @@ -19,19 +21,38 @@ public static void AddConfiguration(this WebApplicationBuilder builder) public static void AddDocumentation(this WebApplicationBuilder builder) { - // builder.Services.AddEndpointsApiExplorer(); - // builder.Services.AddSwaggerGen(x => - // { - // x.CustomSchemaIds(x => x.FullName); - // }); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(options => + { + options.EnableAnnotations(); + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "Investment API", + Description = "Web API for investments", + TermsOfService = new Uri("https://example.com/terms"), + Contact = new OpenApiContact + { + Name = "Investment API Contact", + Url = new Uri("https://example.com/contact") + }, + License = new OpenApiLicense + { + Name = "Investment API License", + Url = new Uri("https://example.com/license") + } + }); + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + }); + - } public static void AddSecurity(this WebApplicationBuilder builder) { builder.Services.AddIdentityApiEndpoints(); - //.AddEntityFrameworkStores(); + //.AddEntityFrameworkStores(); // builder.Services.Configure(options => // { @@ -74,7 +95,7 @@ public static void AddDataContexts(this WebApplicationBuilder builder) public static void AddServices(this WebApplicationBuilder builder) { builder.Services.AddScoped(); - + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/api/Program.cs b/api/Program.cs index ebc4f4644..b26c9d611 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,7 +1,7 @@ -using System.Reflection; using api.Common.Api; +using infra.Data; using Microsoft.AspNetCore.Identity; -using Microsoft.OpenApi.Models; +using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); @@ -10,33 +10,15 @@ builder.AddDataContexts(); builder.AddDocumentation(); builder.AddServices(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - options.EnableAnnotations(); - options.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Title = "Investment API", - Description = "An ASP.NET Core Web API for investments", - TermsOfService = new Uri("https://example.com/terms"), - Contact = new OpenApiContact - { - Name = "Investment API Contact", - Url = new Uri("https://example.com/contact") - }, - License = new OpenApiLicense - { - Name = "Investment API License", - Url = new Uri("https://example.com/license") - } - }); - var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); -}); + var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); +} app.UseSwagger(); app.UseSwaggerUI(c => From 47f9cefd56983313288c37972b3413f779990b60 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Sun, 30 Nov 2025 23:05:18 -0300 Subject: [PATCH 39/40] feat: readme development --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1117af6b..c7f804513 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,24 @@ # Setup +- Necesário: .net 8 +- Necessário: docker ou postgres, coloquei aqui numa porta diferente o mapeamento caso já tenha outro rodando na padrão + Banco: docker run --name api-investments-postgres -e POSTGRES_PASSWORD=1q2w3e4r@@@ -d -p 5436:5432 postgres -Stringconnection pelo user secrets ( mas também pode ser variável de ambiente normal) -dotnet user-secrets set conexao "User ID=postgres;Password=1q2w3e4r@@@;Host=localhost;Port=5436;Database=ClienteDB;Pooling=true;" \ No newline at end of file + +- Stringconnection pelo user secrets ( mas também pode ser variável de ambiente normal) +dotnet user-secrets set conexao "User ID=postgres;Password=1q2w3e4r@@@;Host=localhost;Port=5436;Database=ClienteDB;Pooling=true;" + +# Libs +- Identity: Para autenticação e autorização, usei pois agiliza bastante a autenticação e autorização +- entityFrameWorkCore.Design: para gerar as migrações, ferramente muito boa de code em code first +- Swashbuckle.AspNetCore.Annotations: para fazer a documentação da api pelo swagger. +- EntityFrameworkCore.PostgreSQL: Oferece orm para lidar com banco postgres, também uma execelente lib com boa performance atualmente. Gostaria de ter inserido também o dapper para operações que exigissem um controle mais perfomátíco, mas pelo escopo acredito que o desempenho do entity é suficiente. +- Moq: Me auxiliou no mock do repositório para testes unitários e focar apenas nos serços e lógicas de negócio. + +# Run +- Todos os projetos estão .net8, então basta ter .net 8 sdk instalado e rodar "dotnet run" ele vai restaurar pacotes, compilar e rodar. + +# Obs +- Deixei as migrações sendo aplicadas no startup da aplicação, mas apenas facilitar o trabalho do avaliador; geralmente coloco isso no pipeline de publicação de uma realease e fica separado num passo de ci/cd. \ No newline at end of file From b3ef3969a233914ab4456eeb878369179223b074 Mon Sep 17 00:00:00 2001 From: inacio88 Date: Mon, 1 Dec 2025 07:46:41 -0300 Subject: [PATCH 40/40] chore: remocao codigo sem uso development --- api/Common/Api/BuilderExtension.cs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/api/Common/Api/BuilderExtension.cs b/api/Common/Api/BuilderExtension.cs index 7765d5f96..ea2ab2ec1 100644 --- a/api/Common/Api/BuilderExtension.cs +++ b/api/Common/Api/BuilderExtension.cs @@ -52,27 +52,7 @@ public static void AddDocumentation(this WebApplicationBuilder builder) public static void AddSecurity(this WebApplicationBuilder builder) { builder.Services.AddIdentityApiEndpoints(); - //.AddEntityFrameworkStores(); - - // builder.Services.Configure(options => - // { - // options.SignIn.RequireConfirmedEmail = true; - // options.Lockout.MaxFailedAccessAttempts = 20; - - // }); - - // builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme) - // .AddIdentityCookies(); - // builder.Services.ConfigureApplicationCookie(options => - // { - // options.Cookie.Name = "Investment.AuthCookie"; - // options.Cookie.HttpOnly = true; - // options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - // options.Cookie.SameSite = SameSiteMode.None; - // options.Cookie.IsEssential = true; - // options.ExpireTimeSpan = TimeSpan.FromDays(7); - // options.SlidingExpiration = true; - // }); + builder.Services.AddAuthorization(); }