diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 1bc08f1..de8e9aa 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -2,7 +2,7 @@ name: .NET Build and Test on: push: - branches: [ "main" ] + branches: [ "main", "refactor/clean-architecture" ] pull_request: branches: [ "main" ] diff --git a/Finance.Api.Tests/CustomWebApplicationFactory.cs b/Finance.Api.Tests/CustomWebApplicationFactory.cs index 83d2af4..df4fbb9 100644 --- a/Finance.Api.Tests/CustomWebApplicationFactory.cs +++ b/Finance.Api.Tests/CustomWebApplicationFactory.cs @@ -1,4 +1,8 @@ -using Finance.Infrastructure.Data; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.Interfaces.Services; +using Finance.Infrastructure.Data; +using Finance.Infrastructure.Repositories; +using Finance.Infrastructure.Services; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; @@ -54,6 +58,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.UseSetting("ConnectionStrings:WriteDatabase", writeCs); builder.UseSetting("ConnectionStrings:ReadDatabase", readCs); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); }); } } \ No newline at end of file diff --git a/Finance.Api/Controllers/AuthController.cs b/Finance.Api/Controllers/AuthController.cs index 0411eef..e879320 100644 --- a/Finance.Api/Controllers/AuthController.cs +++ b/Finance.Api/Controllers/AuthController.cs @@ -1,6 +1,10 @@ using Finance.API.Extensions; -using Finance.Contracts.Interfaces.Services; +using Finance.Application.Features.Auth.GetProfile; +using Finance.Application.Features.Auth.Login; +using Finance.Application.Features.Auth.Register; +using Finance.Application.Features.Auth.UpdateProfile; using Finance.Contracts.Requests.Auth; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -8,14 +12,18 @@ namespace Finance.Api.Controllers; [ApiController] [Route("v1/auth")] -public class AuthController(IUserService userService) : ControllerBase +public class AuthController(IMediator mediator) : ControllerBase { - private readonly IUserService _userService = userService; - [HttpPost("login")] public async Task Login([FromBody] LoginRequest request) { - var response = await _userService.LoginAsync(request); + var command = new LoginUserCommand + { + Email = request.Email, + Password = request.Password + }; + + var response = await mediator.Send(command); if (!response.IsSuccess) return Unauthorized(response.Message); @@ -26,7 +34,14 @@ public async Task Login([FromBody] LoginRequest request) [HttpPost("register")] public async Task Register([FromBody] RegisterRequest request) { - var response = await _userService.RegisterAsync(request); + var command = new RegisterUserCommand + { + Name = request.Name, + Email = request.Email, + Password = request.Password + }; + + var response = await mediator.Send(command); if (!response.IsSuccess) return BadRequest(response.Message); @@ -38,7 +53,8 @@ public async Task Register([FromBody] RegisterRequest request) [HttpGet("profile")] public async Task GetProfileAsync() { - var response = await _userService.GetProfileAsync(); + var command = new GetProfileCommand(); + var response = await mediator.Send(command); return this.FromResponse(response); } @@ -46,7 +62,13 @@ public async Task GetProfileAsync() [HttpPut("profile")] public async Task UpdateProfileAsync([FromBody] UpdateUserProfileRequest request) { - var response = await _userService.UpdateProfileAsync(request); + var command = new UpdateProfileCommand + { + Name = request.Name, + Email = request.Email + }; + + var response = await mediator.Send(command); return this.FromResponse(response); } } diff --git a/Finance.Api/Controllers/CategoriesController.cs b/Finance.Api/Controllers/CategoriesController.cs index 5f87197..fd2da46 100644 --- a/Finance.Api/Controllers/CategoriesController.cs +++ b/Finance.Api/Controllers/CategoriesController.cs @@ -1,6 +1,11 @@ using Finance.Application.Extensions; -using Finance.Contracts.Interfaces.Services; +using Finance.Application.Features.Categories.Create; +using Finance.Application.Features.Categories.Delete; +using Finance.Application.Features.Categories.GetAll; +using Finance.Application.Features.Categories.GetById; +using Finance.Application.Features.Categories.Update; using Finance.Contracts.Requests.Categories; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -9,14 +14,19 @@ namespace Finance.Api.Controllers; [Authorize] [ApiController] [Route("v1/categories")] -public class CategoriesController(ICategoryService service) : ControllerBase +public class CategoriesController(IMediator mediator) : ControllerBase { [HttpPost] public async Task CreateAsync([FromBody] CreateCategoryRequest request) { - request.UserId = User.GetUserId(); + var command = new CreateCategoryCommand + { + Title = request.Title, + Description = request.Description, + UserId = User.GetUserId() + }; - var response = await service.CreateAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Created($"v1/categories/{response.Data?.Id}", response.Data) @@ -26,10 +36,15 @@ public async Task CreateAsync([FromBody] CreateCategoryRequest re [HttpPut("{id:long}")] public async Task UpdateAsync([FromRoute] long id, [FromBody] UpdateCategoryRequest request) { - request.Id = id; - request.UserId = User.GetUserId(); + var command = new UpdateCategoryCommand + { + Id = id, + Title = request.Title, + Description = request.Description, + UserId = User.GetUserId() + }; - var response = await service.UpdateAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -39,13 +54,13 @@ public async Task UpdateAsync([FromRoute] long id, [FromBody] Upd [HttpDelete("{id:long}")] public async Task DeleteAsync([FromRoute] long id) { - var request = new DeleteCategoryRequest + var command = new DeleteCategoryCommand { Id = id, UserId = User.GetUserId() }; - var response = await service.DeleteAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -55,13 +70,13 @@ public async Task DeleteAsync([FromRoute] long id) [HttpGet("{id:long}")] public async Task GetByIdAsync([FromRoute] long id) { - var request = new GetCategoryByIdRequest + var command = new GetCategoryByIdCommand { Id = id, UserId = User.GetUserId() }; - var response = await service.GetByIdAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -71,14 +86,14 @@ public async Task GetByIdAsync([FromRoute] long id) [HttpGet] public async Task GetAllAsync([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 25) { - var request = new GetAllCategoriesRequest + var command = new GetAllCategoriesCommand { UserId = User.GetUserId(), PageNumber = pageNumber, PageSize = pageSize }; - var response = await service.GetAllAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) diff --git a/Finance.Api/Controllers/TransactionsController.cs b/Finance.Api/Controllers/TransactionsController.cs index 363e2ed..6217267 100644 --- a/Finance.Api/Controllers/TransactionsController.cs +++ b/Finance.Api/Controllers/TransactionsController.cs @@ -1,6 +1,12 @@ using Finance.Application.Extensions; -using Finance.Contracts.Interfaces.Services; +using Finance.Application.Features.Transactions.Create; +using Finance.Application.Features.Transactions.Delete; +using Finance.Application.Features.Transactions.GetById; +using Finance.Application.Features.Transactions.GetByPeriod; +using Finance.Application.Features.Transactions.GetReport; +using Finance.Application.Features.Transactions.Update; using Finance.Contracts.Requests.Transactions; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -9,14 +15,22 @@ namespace Finance.Api.Controllers; [Authorize] [ApiController] [Route("v1/transactions")] -public class TransactionsController(ITransactionService service) : ControllerBase +public class TransactionsController(IMediator mediator) : ControllerBase { [HttpPost] public async Task CreateAsync([FromBody] CreateTransactionRequest request) { - request.UserId = User.GetUserId(); + var command = new CreateTransactionCommand + { + Title = request.Title, + Amount = request.Amount, + Type = request.Type, + CategoryId = request.CategoryId, + PaidOrReceivedAt = request.PaidOrReceivedAt, + UserId = User.GetUserId() + }; - var response = await service.CreateAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Created($"v1/transactions/{response.Data?.Id}", response.Data) @@ -26,10 +40,18 @@ public async Task CreateAsync([FromBody] CreateTransactionRequest [HttpPut("{id:long}")] public async Task UpdateAsync([FromRoute] long id, [FromBody] UpdateTransactionRequest request) { - request.Id = id; - request.UserId = User.GetUserId(); + var command = new UpdateTransactionCommand + { + Id = id, + Title = request.Title, + Amount = request.Amount, + Type = request.Type, + CategoryId = request.CategoryId, + PaidOrReceivedAt = request.PaidOrReceivedAt, + UserId = User.GetUserId() + }; - var response = await service.UpdateAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -39,13 +61,13 @@ public async Task UpdateAsync([FromRoute] long id, [FromBody] Upd [HttpDelete("{id:long}")] public async Task DeleteAsync([FromRoute] long id) { - var request = new DeleteTransactionRequest + var command = new DeleteTransactionCommand { Id = id, UserId = User.GetUserId() }; - var response = await service.DeleteAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -55,13 +77,13 @@ public async Task DeleteAsync([FromRoute] long id) [HttpGet("{id:long}")] public async Task GetByIdAsync([FromRoute] long id) { - var request = new GetTransactionByIdRequest + var command = new GetByIdTransactionCommand { Id = id, UserId = User.GetUserId() }; - var response = await service.GetByIdAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -75,7 +97,7 @@ public async Task GetByPeriodAsync( [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 25) { - var request = new GetTransactionsByPeriodRequest + var command = new GetByPeriodTransactionCommand { UserId = User.GetUserId(), StartDate = startDate, @@ -84,7 +106,7 @@ public async Task GetByPeriodAsync( PageSize = pageSize }; - var response = await service.GetByPeriodAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) @@ -94,14 +116,14 @@ public async Task GetByPeriodAsync( [HttpGet("report")] public async Task GetReportAsync([FromQuery] DateTime? startDate = null, [FromQuery] DateTime? endDate = null) { - var request = new GetTransactionReportRequest + var command = new GetReportTransactionCommand { UserId = User.GetUserId(), StartDate = startDate, EndDate = endDate }; - var response = await service.GetReportAsync(request); + var response = await mediator.Send(command); return response.IsSuccess ? Ok(response.Data) diff --git a/Finance.Api/Extensions/BuilderExtension.cs b/Finance.Api/Extensions/BuilderExtension.cs index 16d6c42..3ff5879 100644 --- a/Finance.Api/Extensions/BuilderExtension.cs +++ b/Finance.Api/Extensions/BuilderExtension.cs @@ -7,8 +7,7 @@ using Finance.Application.Features.Transactions.Create; using Finance.Application.Features.Transactions.GetByPeriod; using Finance.Application.Features.Transactions.Update; -using Finance.Application.Services; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Interfaces.Services; using Finance.Infrastructure.Data; using Finance.Infrastructure.Outbox; @@ -72,7 +71,6 @@ public static WebApplicationBuilder AddApiInfrastructure(this WebApplicationBuil builder.AddCache(); builder.AddCors(); builder.AddDocumentation(); - builder.AddServices(); builder.AddMediatR(); builder.Services.AddHttpContextAccessor(); @@ -183,10 +181,6 @@ public static void AddServices(this WebApplicationBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Finance.Application.Tests/Handlers/AuthHandlerTests.cs b/Finance.Application.Tests/Handlers/AuthHandlerTests.cs index 5b008eb..72d9a83 100644 --- a/Finance.Application.Tests/Handlers/AuthHandlerTests.cs +++ b/Finance.Application.Tests/Handlers/AuthHandlerTests.cs @@ -3,8 +3,7 @@ using Finance.Application.Features.Auth.RefreshToken; using Finance.Application.Features.Auth.Register; using Finance.Application.Features.Auth.UpdateProfile; -using Finance.Contracts.Interfaces.Repositories; -using Finance.Contracts.Interfaces.Services; +using Finance.Application.Interfaces.Repositories; using Finance.Domain.Models; using FluentAssertions; using Microsoft.AspNetCore.Http; @@ -16,7 +15,7 @@ namespace Finance.Application.Tests.Handlers; public class AuthHandlerTests { private readonly Mock _userRepoMock = new(); - private readonly Mock _tokenMock = new(); + private readonly Mock _tokenMock = new(); private readonly Mock _httpContextAccessorMock = new(); [Fact] @@ -26,13 +25,9 @@ public async Task Login_Should_ReturnTokens_When_CredentialsAreValid() var command = new LoginUserCommand { Email = "test@email.com", Password = "Password123" }; - var user = new User - { - Id = 1, - Email = command.Email, - Name = "User", - PasswordHash = BCrypt.Net.BCrypt.HashPassword(command.Password) - }; + var userResult = User.Create("User", command.Email, BCrypt.Net.BCrypt.HashPassword(command.Password)); + var user = userResult.Value; + user.GetType().GetProperty("Id")?.SetValue(user, 1L); _userRepoMock.Setup(r => r.GetByEmailAsync(command.Email)).ReturnsAsync(user); _tokenMock.Setup(t => t.GenerateAccessToken(It.IsAny())).Returns("fake-jwt"); @@ -84,12 +79,18 @@ public async Task Register_Should_Fail_When_EmailAlreadyExists() var command = new RegisterUserCommand { Email = "existente@email.com", - Password = "123", - Name = "X" + Password = "Teste@123", + Name = "Teste" }; _userRepoMock.Setup(r => r.GetByEmailAsync(command.Email)) - .ReturnsAsync(new User { Id = 10, Email = command.Email }); + .ReturnsAsync(() => + { + var userResult = User.Create("User", command.Email, BCrypt.Net.BCrypt.HashPassword(command.Password)); + var user = userResult.Value; + user.GetType().GetProperty("Id")?.SetValue(user, 10L); + return user; + }); var result = await handler.Handle(command, CancellationToken.None); @@ -114,7 +115,13 @@ public async Task GetProfile_Should_ReturnProfile_When_UserExists() var handler = new GetProfileHandler(_userRepoMock.Object, _httpContextAccessorMock.Object); _userRepoMock.Setup(r => r.GetByIdAsync(userId)) - .ReturnsAsync(new User { Id = userId, Name = "User", Email = "u@email.com" }); + .ReturnsAsync(() => + { + var userResult = User.Create("User", "u@email.com", "hash"); + var user = userResult.Value; + user.GetType().GetProperty("Id")?.SetValue(user, (long)userId); + return user; + }); var result = await handler.Handle(new GetProfileCommand(), CancellationToken.None); @@ -137,12 +144,14 @@ public async Task UpdateProfile_Should_ReturnSuccess_When_UserExists() var handler = new UpdateProfileHandler(_userRepoMock.Object, _httpContextAccessorMock.Object); - var existing = new User { Id = userId, Name = "Old", Email = "u@email.com" }; + var existingResult = User.Create("Old", "u@email.com", "hash"); + var existing = existingResult.Value; + existing.GetType().GetProperty("Id")?.SetValue(existing, (long)userId); _userRepoMock.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(existing); _userRepoMock.Setup(r => r.UpdateAsync(It.IsAny())).ReturnsAsync((User u) => u); - var result = await handler.Handle(new UpdateProfileCommand { Name = "New Name" }, CancellationToken.None); + var result = await handler.Handle(new UpdateProfileCommand { Name = "New Name", Email = "u@email.com" }, CancellationToken.None); result.IsSuccess.Should().BeTrue(); result.Data.Should().NotBeNull(); diff --git a/Finance.Application.Tests/Handlers/CategoryHandlerTests.cs b/Finance.Application.Tests/Handlers/CategoryHandlerTests.cs index 2c48e7e..385608b 100644 --- a/Finance.Application.Tests/Handlers/CategoryHandlerTests.cs +++ b/Finance.Application.Tests/Handlers/CategoryHandlerTests.cs @@ -3,7 +3,7 @@ using Finance.Application.Features.Categories.GetAll; using Finance.Application.Features.Categories.GetById; using Finance.Application.Features.Categories.Update; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Domain.Models; using FluentAssertions; using Moq; @@ -72,7 +72,9 @@ public async Task Update_Should_Return200_When_Found() Description = "Contas" }; - var existing = new Category { Id = 1, UserId = 123, Title = "Casa", Description = "Antigo" }; + var catResult = Category.Create("Casa", "Antigo", 123); + var existing = catResult.Value; + existing.GetType().GetProperty("Id")?.SetValue(existing, 1L); _repoMock.Setup(r => r.GetByIdAsync(command.Id, command.UserId)) .ReturnsAsync(existing); @@ -119,7 +121,9 @@ public async Task Delete_Should_Return200_When_Found() var handler = new DeleteCategoryHandler(_repoMock.Object); var command = new DeleteCategoryCommand { Id = 1, UserId = 123 }; - var existing = new Category { Id = 1, UserId = 123, Title = "A ser deletada" }; + var catResult = Category.Create("A ser deletada", null, 123); + var existing = catResult.Value; + existing.GetType().GetProperty("Id")?.SetValue(existing, 1L); _repoMock.Setup(r => r.GetByIdAsync(command.Id, command.UserId)) .ReturnsAsync(existing); @@ -163,10 +167,13 @@ public async Task GetAll_Should_ReturnPagedData_When_Successful() var categories = new List { - new() { Id = 1, UserId = 123, Title = "Casa" }, - new() { Id = 2, UserId = 123, Title = "Saúde" }, - new() { Id = 3, UserId = 123, Title = "Transporte" } + Category.Create("Casa", null, 123).Value, + Category.Create("Saúde", null, 123).Value, + Category.Create("Transporte", null, 123).Value }; + categories[0].GetType().GetProperty("Id")?.SetValue(categories[0], 1L); + categories[1].GetType().GetProperty("Id")?.SetValue(categories[1], 2L); + categories[2].GetType().GetProperty("Id")?.SetValue(categories[2], 3L); _repoMock.Setup(r => r.GetAllAsync(command.UserId)) .ReturnsAsync(categories); diff --git a/Finance.Application.Tests/Handlers/TransactionHandlerTests.cs b/Finance.Application.Tests/Handlers/TransactionHandlerTests.cs index 1d10e63..c820ab6 100644 --- a/Finance.Application.Tests/Handlers/TransactionHandlerTests.cs +++ b/Finance.Application.Tests/Handlers/TransactionHandlerTests.cs @@ -4,7 +4,7 @@ using Finance.Application.Features.Transactions.GetByPeriod; using Finance.Application.Features.Transactions.GetReport; using Finance.Application.Features.Transactions.Update; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Domain.Enums; using Finance.Domain.Models; using FluentAssertions; @@ -33,7 +33,13 @@ public async Task Create_Should_Return201_When_CategoryExists() }; _catRepoMock.Setup(r => r.GetByIdAsync(command.CategoryId, command.UserId)) - .ReturnsAsync(new Category { Id = 1, UserId = 123, Title = "Trabalho" }); + .ReturnsAsync(() => + { + var catResult = Category.Create("Trabalho", null, 123); + var cat = catResult.Value; + cat.GetType().GetProperty("Id")?.SetValue(cat, 1L); + return cat; + }); _txRepoMock.Setup(r => r.CreateAsync(It.IsAny())) .ReturnsAsync((Transaction t) => t); @@ -59,7 +65,7 @@ public async Task Create_Should_Return404_When_CategoryNotFound() CategoryId = 99, Amount = 10, Type = ETransactionType.Deposit, - Title = "X" + Title = "Teste" }; _catRepoMock.Setup(r => r.GetByIdAsync(command.CategoryId, command.UserId)) @@ -84,7 +90,7 @@ public async Task Update_Should_Return404_When_TransactionNotFound() Id = 99, UserId = 123, CategoryId = 1, - Title = "X", + Title = "Teste", Amount = 10, Type = ETransactionType.Deposit, PaidOrReceivedAt = DateTime.UtcNow @@ -108,24 +114,14 @@ public async Task Delete_Should_Return200_When_Found() var command = new DeleteTransactionCommand { Id = 1, UserId = 123 }; - var existingTx = new Transaction - { - Id = 1, - UserId = 123, - Title = "A pagar", - CategoryId = 10, - Amount = -50, - Type = ETransactionType.Withdraw, - PaidOrReceivedAt = DateTime.UtcNow, - CreatedAt = DateTime.UtcNow - }; + var txResult = Transaction.Create("A pagar", 50, ETransactionType.Withdraw, 10, 123, DateTime.UtcNow); + var existingTx = txResult.Value; + existingTx.GetType().GetProperty("Id")?.SetValue(existingTx, 1L); + existingTx.GetType().GetProperty("CreatedAt")?.SetValue(existingTx, DateTime.UtcNow); - var category = new Category - { - Id = 10, - UserId = 123, - Title = "Casa" - }; + var catResult = Category.Create("Casa", null, 123); + var category = catResult.Value; + category.GetType().GetProperty("Id")?.SetValue(category, 10L); _txRepoMock.Setup(r => r.GetByIdAsync(command.Id, command.UserId)) .ReturnsAsync(existingTx); @@ -186,19 +182,13 @@ public async Task GetByPeriod_Should_ReturnPagedData_When_Successful() var txs = new List { - new() - { - Id = 1, UserId = 123, Title = "Salário", Amount = 5000, Type = ETransactionType.Deposit, - PaidOrReceivedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, - Category = new Category { Id = 1, Title = "Trabalho" } - }, - new() - { - Id = 2, UserId = 123, Title = "Aluguel", Amount = -1500, Type = ETransactionType.Withdraw, - PaidOrReceivedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, - Category = new Category { Id = 2, Title = "Casa" } - } + Transaction.Create("Salário", 5000, ETransactionType.Deposit, 1, 123, DateTime.UtcNow).Value, + Transaction.Create("Aluguel", 1500, ETransactionType.Withdraw, 2, 123, DateTime.UtcNow).Value }; + txs[0].GetType().GetProperty("Id")?.SetValue(txs[0], 1L); + txs[0].GetType().GetProperty("CreatedAt")?.SetValue(txs[0], DateTime.UtcNow); + txs[1].GetType().GetProperty("Id")?.SetValue(txs[1], 2L); + txs[1].GetType().GetProperty("CreatedAt")?.SetValue(txs[1], DateTime.UtcNow); _txRepoMock.Setup(r => r.GetByPeriodAsync(command.UserId, It.IsAny(), It.IsAny(), command.PageNumber, command.PageSize)) .ReturnsAsync(txs); @@ -226,9 +216,9 @@ public async Task GetReport_Should_ReturnSuccess_When_Successful() var txs = new List { - new() { Amount = -100, Category = new Category { Title = "Casa" } }, - new() { Amount = -50, Category = new Category { Title = "Casa" } }, - new() { Amount = 5000, Category = new Category { Title = "Trabalho" } } + Transaction.Create("Casa", 100, ETransactionType.Withdraw, 1, 123, DateTime.UtcNow).Value, + Transaction.Create("Casa", 50, ETransactionType.Withdraw, 1, 123, DateTime.UtcNow).Value, + Transaction.Create("Trabalho", 5000, ETransactionType.Deposit, 2, 123, DateTime.UtcNow).Value }; _txRepoMock.Setup(r => r.GetAllByPeriodAsync(command.UserId, It.IsAny(), It.IsAny())) diff --git a/Finance.Application/Features/Auth/GetProfile/GetProfileHandler.cs b/Finance.Application/Features/Auth/GetProfile/GetProfileHandler.cs index 521e011..c170a2d 100644 --- a/Finance.Application/Features/Auth/GetProfile/GetProfileHandler.cs +++ b/Finance.Application/Features/Auth/GetProfile/GetProfileHandler.cs @@ -1,5 +1,5 @@ using Finance.Application.Extensions; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Responses; using Finance.Contracts.Responses.Auth; using MediatR; diff --git a/Finance.Application/Features/Auth/Login/LoginUserHandler.cs b/Finance.Application/Features/Auth/Login/LoginUserHandler.cs index 6947c9a..e5337a7 100644 --- a/Finance.Application/Features/Auth/Login/LoginUserHandler.cs +++ b/Finance.Application/Features/Auth/Login/LoginUserHandler.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Interfaces.Services; using Finance.Contracts.Responses; using Finance.Contracts.Responses.Auth; @@ -21,8 +21,7 @@ public class LoginUserHandler(IUserRepository userRepository, ITokenService toke var accessToken = tokenService.GenerateAccessToken(user); var refreshToken = tokenService.GenerateRefreshToken(); - user.RefreshToken = refreshToken; - user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(7); + user.SetRefreshToken(refreshToken, DateTime.UtcNow.AddDays(7)); await userRepository.UpdateAsync(user); diff --git a/Finance.Application/Features/Auth/RefreshToken/RefreshUserTokenHandler.cs b/Finance.Application/Features/Auth/RefreshToken/RefreshUserTokenHandler.cs index 080eb08..9b8b23b 100644 --- a/Finance.Application/Features/Auth/RefreshToken/RefreshUserTokenHandler.cs +++ b/Finance.Application/Features/Auth/RefreshToken/RefreshUserTokenHandler.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Interfaces.Services; using Finance.Contracts.Responses; using Finance.Contracts.Responses.Auth; @@ -24,9 +24,7 @@ public class RefreshUserTokenHandler(IUserRepository userRepository, ITokenServi var user = await userRepository.GetByIdAsync(userId); - if (user is null || - user.RefreshToken != request.RefreshToken || - user.RefreshTokenExpiryTime <= DateTime.UtcNow) + if (user is null || !user.IsRefreshTokenValid(request.RefreshToken)) { return Response.Fail("Token inválido ou expirado."); } @@ -34,8 +32,7 @@ public class RefreshUserTokenHandler(IUserRepository userRepository, ITokenServi var newAccessToken = tokenService.GenerateAccessToken(user); var newRefreshToken = tokenService.GenerateRefreshToken(); - user.RefreshToken = newRefreshToken; - user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(7); + user.SetRefreshToken(newRefreshToken, DateTime.UtcNow.AddDays(7)); await userRepository.UpdateAsync(user); var loginResponse = new LoginResponse diff --git a/Finance.Application/Features/Auth/Register/RegisterUserHandler.cs b/Finance.Application/Features/Auth/Register/RegisterUserHandler.cs index cb2e69a..164597e 100644 --- a/Finance.Application/Features/Auth/Register/RegisterUserHandler.cs +++ b/Finance.Application/Features/Auth/Register/RegisterUserHandler.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Responses; using Finance.Domain.Models; using MediatR; @@ -15,12 +15,11 @@ public async Task> Handle(RegisterUserCommand request, Cancella return Response.Fail("O email informado já está em uso."); } - var user = new User - { - Name = request.Name, - Email = request.Email, - PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password) - }; + var result = User.Create(request.Name, request.Email, BCrypt.Net.BCrypt.HashPassword(request.Password)); + if (result.IsFailure) + return Response.Fail(string.Join("; ", result.Errors)); + + var user = result.Value; await userRepository.AddAsync(user); diff --git a/Finance.Application/Features/Auth/UpdateProfile/UpdateProfileHandler.cs b/Finance.Application/Features/Auth/UpdateProfile/UpdateProfileHandler.cs index bc16762..02bdfec 100644 --- a/Finance.Application/Features/Auth/UpdateProfile/UpdateProfileHandler.cs +++ b/Finance.Application/Features/Auth/UpdateProfile/UpdateProfileHandler.cs @@ -1,5 +1,5 @@ using Finance.Application.Extensions; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Responses; using Finance.Contracts.Responses.Auth; using MediatR; @@ -27,7 +27,9 @@ public class UpdateProfileHandler(IUserRepository userRepository, IHttpContextAc return Response.Fail("Usuário não encontrado."); } - user.Name = request.Name; + var updateResult = user.UpdateProfile(request.Name, request.Email); + if (updateResult.IsFailure) + return Response.Fail(string.Join("; ", updateResult.Errors)); var updatedUser = await userRepository.UpdateAsync(user); diff --git a/Finance.Application/Features/Categories/Create/CreateCategoryCommand.cs b/Finance.Application/Features/Categories/Create/CreateCategoryCommand.cs index cd23efc..4bac4b0 100644 --- a/Finance.Application/Features/Categories/Create/CreateCategoryCommand.cs +++ b/Finance.Application/Features/Categories/Create/CreateCategoryCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Categories.Create; diff --git a/Finance.Application/Features/Categories/Create/CreateCategoryHandler.cs b/Finance.Application/Features/Categories/Create/CreateCategoryHandler.cs index eb04085..408b16f 100644 --- a/Finance.Application/Features/Categories/Create/CreateCategoryHandler.cs +++ b/Finance.Application/Features/Categories/Create/CreateCategoryHandler.cs @@ -1,7 +1,7 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Categories.Create; @@ -10,12 +10,11 @@ public class CreateCategoryHandler(ICategoryRepository repository) : IRequestHan { public async Task> Handle(CreateCategoryCommand request, CancellationToken cancellationToken) { - var category = new Category - { - UserId = request.UserId, - Title = request.Title, - Description = request.Description - }; + var result = Category.Create(request.Title, request.Description, request.UserId); + if (result.IsFailure) + return Response.Fail(string.Join("; ", result.Errors)); + + var category = result.Value; try { diff --git a/Finance.Application/Features/Categories/Delete/DeleteCategoryCommand.cs b/Finance.Application/Features/Categories/Delete/DeleteCategoryCommand.cs index a7938b6..8f288c9 100644 --- a/Finance.Application/Features/Categories/Delete/DeleteCategoryCommand.cs +++ b/Finance.Application/Features/Categories/Delete/DeleteCategoryCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Categories.Delete; diff --git a/Finance.Application/Features/Categories/Delete/DeleteCategoryHandler.cs b/Finance.Application/Features/Categories/Delete/DeleteCategoryHandler.cs index ad34533..548951b 100644 --- a/Finance.Application/Features/Categories/Delete/DeleteCategoryHandler.cs +++ b/Finance.Application/Features/Categories/Delete/DeleteCategoryHandler.cs @@ -1,7 +1,7 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Categories.Delete; @@ -20,11 +20,11 @@ public class DeleteCategoryHandler(ICategoryRepository repository) : IRequestHan await repository.DeleteAsync(category); var dto = MapToDto(category); - return new Response(dto, 200, "Categoria excluída com sucesso!"); + return Response.Success(dto, "Categoria excluída com sucesso!"); } catch { - return new Response(null, 500, "Não foi possível excluir a categoria"); + return Response.Fail("Não foi possível excluir a categoria"); } } diff --git a/Finance.Application/Features/Categories/GetAll/GetAllCategoriesCommand.cs b/Finance.Application/Features/Categories/GetAll/GetAllCategoriesCommand.cs index 8d422a7..8432a01 100644 --- a/Finance.Application/Features/Categories/GetAll/GetAllCategoriesCommand.cs +++ b/Finance.Application/Features/Categories/GetAll/GetAllCategoriesCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Categories.GetAll; diff --git a/Finance.Application/Features/Categories/GetAll/GetAllCategoriesHandler.cs b/Finance.Application/Features/Categories/GetAll/GetAllCategoriesHandler.cs index be7429c..7660b1a 100644 --- a/Finance.Application/Features/Categories/GetAll/GetAllCategoriesHandler.cs +++ b/Finance.Application/Features/Categories/GetAll/GetAllCategoriesHandler.cs @@ -1,7 +1,7 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Categories.GetAll; diff --git a/Finance.Application/Features/Categories/GetById/GetCategoryByIdCommand.cs b/Finance.Application/Features/Categories/GetById/GetCategoryByIdCommand.cs index 82a1ca3..dfbebf9 100644 --- a/Finance.Application/Features/Categories/GetById/GetCategoryByIdCommand.cs +++ b/Finance.Application/Features/Categories/GetById/GetCategoryByIdCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Categories.GetById; diff --git a/Finance.Application/Features/Categories/GetById/GetCategoryByIdHandler.cs b/Finance.Application/Features/Categories/GetById/GetCategoryByIdHandler.cs index fec5ee6..9b6467b 100644 --- a/Finance.Application/Features/Categories/GetById/GetCategoryByIdHandler.cs +++ b/Finance.Application/Features/Categories/GetById/GetCategoryByIdHandler.cs @@ -1,7 +1,7 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Categories.GetById; @@ -18,11 +18,11 @@ public class GetCategoryByIdHandler(ICategoryRepository repository) : IRequestHa return new Response(null, 404, "Categoria não encontrada ou não pertence ao usuário."); var dto = MapToDto(category); - return new Response(dto); + return Response.Success(dto); } catch { - return new Response(null, 500, "Não foi possível recuperar a categoria."); + return Response.Fail("Não foi possível recuperar a categoria."); } } diff --git a/Finance.Application/Features/Categories/Update/UpdateCategoryCommand.cs b/Finance.Application/Features/Categories/Update/UpdateCategoryCommand.cs index 486b67c..8c7c1f5 100644 --- a/Finance.Application/Features/Categories/Update/UpdateCategoryCommand.cs +++ b/Finance.Application/Features/Categories/Update/UpdateCategoryCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Categories.Update; diff --git a/Finance.Application/Features/Categories/Update/UpdateCategoryHandler.cs b/Finance.Application/Features/Categories/Update/UpdateCategoryHandler.cs index 8a722a3..f8216ba 100644 --- a/Finance.Application/Features/Categories/Update/UpdateCategoryHandler.cs +++ b/Finance.Application/Features/Categories/Update/UpdateCategoryHandler.cs @@ -1,7 +1,7 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Categories.Update; @@ -16,17 +16,18 @@ public class UpdateCategoryHandler(ICategoryRepository repository) : IRequestHan if (category is null) return new Response(null, 404, "Categoria não encontrada ou não pertence ao usuário."); - category.Title = request.Title; - category.Description = request.Description; + var updateResult = category.Update(request.Title, request.Description); + if (updateResult.IsFailure) + return Response.Fail(string.Join("; ", updateResult.Errors)); await repository.UpdateAsync(category); var dto = MapToDto(category); - return new Response(dto, 200, "Categoria atualizada com sucesso"); + return Response.Success(dto, "Categoria atualizada com sucesso"); } catch { - return new Response(null, 500, "Não foi possível alterar a categoria"); + return Response.Fail("Não foi possível alterar a categoria"); } } diff --git a/Finance.Application/Features/Transactions/Create/CreateTransactionCommand.cs b/Finance.Application/Features/Transactions/Create/CreateTransactionCommand.cs index ddb0196..638b822 100644 --- a/Finance.Application/Features/Transactions/Create/CreateTransactionCommand.cs +++ b/Finance.Application/Features/Transactions/Create/CreateTransactionCommand.cs @@ -1,6 +1,6 @@ -using Finance.Contracts.Responses; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using Finance.Domain.Enums; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.Create; diff --git a/Finance.Application/Features/Transactions/Create/CreateTransactionHandler.cs b/Finance.Application/Features/Transactions/Create/CreateTransactionHandler.cs index 9c94538..86f9b47 100644 --- a/Finance.Application/Features/Transactions/Create/CreateTransactionHandler.cs +++ b/Finance.Application/Features/Transactions/Create/CreateTransactionHandler.cs @@ -1,9 +1,9 @@ -using Finance.Application.Mappers; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Application.Mappers; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Enums; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.Create; @@ -13,9 +13,18 @@ public class CreateTransactionHandler(ITransactionRepository transactionReposito { public async Task> Handle(CreateTransactionCommand request, CancellationToken cancellationToken) { - var amount = request.Amount; - if (request.Type == ETransactionType.Withdraw && amount > 0) - amount *= -1; + var result = Transaction.Create( + request.Title, + request.Amount, + request.Type, + request.CategoryId, + request.UserId, + request.PaidOrReceivedAt); + + if (result.IsFailure) + return Response.Fail(string.Join("; ", result.Errors)); + + var transaction = result.Value; try { @@ -23,17 +32,6 @@ public class CreateTransactionHandler(ITransactionRepository transactionReposito if (category is null) return new Response(null, 404, "Categoria não encontrada."); - var transaction = new Transaction - { - UserId = request.UserId, - CategoryId = request.CategoryId, - Title = request.Title, - Amount = amount, - Type = request.Type, - PaidOrReceivedAt = request.PaidOrReceivedAt, - CreatedAt = DateTime.UtcNow - }; - await transactionRepository.CreateAsync(transaction); var dto = TransactionMapper.ToDto(transaction, category); @@ -41,7 +39,7 @@ public class CreateTransactionHandler(ITransactionRepository transactionReposito } catch { - return new Response(null, 500, "Não foi possível criar a transação."); + return Response.Fail("Não foi possível criar a transação."); } } } diff --git a/Finance.Application/Features/Transactions/Delete/DeleteTransactionCommand.cs b/Finance.Application/Features/Transactions/Delete/DeleteTransactionCommand.cs index 7e68ad2..950e487 100644 --- a/Finance.Application/Features/Transactions/Delete/DeleteTransactionCommand.cs +++ b/Finance.Application/Features/Transactions/Delete/DeleteTransactionCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Transactions.Delete; diff --git a/Finance.Application/Features/Transactions/Delete/DeleteTransactionHandler.cs b/Finance.Application/Features/Transactions/Delete/DeleteTransactionHandler.cs index 8bda81f..dbb46a3 100644 --- a/Finance.Application/Features/Transactions/Delete/DeleteTransactionHandler.cs +++ b/Finance.Application/Features/Transactions/Delete/DeleteTransactionHandler.cs @@ -1,8 +1,8 @@ -using Finance.Application.Mappers; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Application.Mappers; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.Delete; @@ -25,11 +25,11 @@ public class DeleteTransactionHandler(ITransactionRepository transactionReposito await transactionRepository.DeleteAsync(transaction); var dto = TransactionMapper.ToDto(transaction, category); - return new Response(dto, 200, "Transação excluída com sucesso!"); + return Response.Success(dto, "Transação excluída com sucesso!"); } catch { - return new Response(null, 500, "Não foi possível excluir a transação."); + return Response.Fail("Não foi possível excluir a transação."); } } } diff --git a/Finance.Application/Features/Transactions/GetById/GetByIdTransactionCommand.cs b/Finance.Application/Features/Transactions/GetById/GetByIdTransactionCommand.cs index 40c60bf..696ae2d 100644 --- a/Finance.Application/Features/Transactions/GetById/GetByIdTransactionCommand.cs +++ b/Finance.Application/Features/Transactions/GetById/GetByIdTransactionCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Transactions.GetById; diff --git a/Finance.Application/Features/Transactions/GetById/GetByIdTransactionHandler.cs b/Finance.Application/Features/Transactions/GetById/GetByIdTransactionHandler.cs index 74f070a..048c754 100644 --- a/Finance.Application/Features/Transactions/GetById/GetByIdTransactionHandler.cs +++ b/Finance.Application/Features/Transactions/GetById/GetByIdTransactionHandler.cs @@ -1,7 +1,9 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Application.Mappers; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; +using Finance.Domain.Enums; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.GetById; @@ -22,11 +24,11 @@ public class GetByIdTransactionHandler(ITransactionRepository transactionReposit return new Response(null, 404, "Categoria vinculada à transação não foi encontrada."); var dto = MapToDto(transaction, category); - return new Response(dto); + return Response.Success(dto); } catch { - return new Response(null, 500, "Não foi possível recuperar a transação."); + return Response.Fail("Não foi possível recuperar a transação."); } } @@ -36,14 +38,10 @@ private static TransactionDto MapToDto(Transaction transaction, Category categor Id = transaction.Id, Title = transaction.Title, Amount = transaction.Amount, - Type = transaction.Type, + Type = transaction.Type.ToString(), PaidOrReceivedAt = transaction.PaidOrReceivedAt, CreatedAt = transaction.CreatedAt, - Category = new CategoryDto - { - Id = category.Id, - Title = category.Title, - Description = category.Description - } + CategoryId = transaction.CategoryId, + CategoryTitle = category.Title }; } diff --git a/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionCommand.cs b/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionCommand.cs index 922875b..18d1992 100644 --- a/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionCommand.cs +++ b/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionCommand.cs @@ -1,5 +1,5 @@ -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using MediatR; namespace Finance.Application.Features.Transactions.GetByPeriod; diff --git a/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionHandler.cs b/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionHandler.cs index 6e36b5b..731170f 100644 --- a/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionHandler.cs +++ b/Finance.Application/Features/Transactions/GetByPeriod/GetByPeriodTransactionHandler.cs @@ -1,8 +1,9 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Common; +using Finance.Domain.Enums; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.GetByPeriod; @@ -46,14 +47,10 @@ private static TransactionDto MapToDto(Transaction transaction) Id = transaction.Id, Title = transaction.Title, Amount = transaction.Amount, - Type = transaction.Type, + Type = transaction.Type.ToString(), PaidOrReceivedAt = transaction.PaidOrReceivedAt, CreatedAt = transaction.CreatedAt, - Category = new CategoryDto - { - Id = transaction.Category.Id, - Title = transaction.Category.Title, - Description = transaction.Category.Description - } + CategoryId = transaction.CategoryId, + CategoryTitle = transaction.Category?.Title ?? string.Empty }; } diff --git a/Finance.Application/Features/Transactions/GetReport/GetReportTransactionHandler.cs b/Finance.Application/Features/Transactions/GetReport/GetReportTransactionHandler.cs index b663c75..790a766 100644 --- a/Finance.Application/Features/Transactions/GetReport/GetReportTransactionHandler.cs +++ b/Finance.Application/Features/Transactions/GetReport/GetReportTransactionHandler.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Contracts.Responses; using Finance.Contracts.Responses.Categories; using Finance.Contracts.Responses.Transactions; @@ -42,11 +42,11 @@ public async Task> Handle(GetReportTransacti }).ToList() }; - return new Response(report, message: "Relatório gerado com sucesso."); + return Response.Success(report, "Relatório gerado com sucesso."); } catch { - return new Response(null, 500, "Não foi possível gerar o relatório."); + return Response.Fail("Não foi possível gerar o relatório."); } } } diff --git a/Finance.Application/Features/Transactions/Update/UpdateTransactionCommand.cs b/Finance.Application/Features/Transactions/Update/UpdateTransactionCommand.cs index eaf9068..bf4c5c7 100644 --- a/Finance.Application/Features/Transactions/Update/UpdateTransactionCommand.cs +++ b/Finance.Application/Features/Transactions/Update/UpdateTransactionCommand.cs @@ -1,6 +1,6 @@ -using Finance.Contracts.Responses; +using Finance.Contracts.DTOs; +using Finance.Contracts.Responses; using Finance.Domain.Enums; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.Update; diff --git a/Finance.Application/Features/Transactions/Update/UpdateTransactionHandler.cs b/Finance.Application/Features/Transactions/Update/UpdateTransactionHandler.cs index 271e54a..a7cb44c 100644 --- a/Finance.Application/Features/Transactions/Update/UpdateTransactionHandler.cs +++ b/Finance.Application/Features/Transactions/Update/UpdateTransactionHandler.cs @@ -1,9 +1,9 @@ -using Finance.Application.Mappers; -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; +using Finance.Application.Mappers; +using Finance.Contracts.DTOs; using Finance.Contracts.Responses; using Finance.Domain.Enums; using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; using MediatR; namespace Finance.Application.Features.Transactions.Update; @@ -13,10 +13,6 @@ public class UpdateTransactionHandler(ITransactionRepository transactionReposito { public async Task> Handle(UpdateTransactionCommand request, CancellationToken cancellationToken) { - var amount = request.Amount; - if (request.Type == ETransactionType.Withdraw && amount > 0) - amount *= -1; - try { var transaction = await transactionRepository.GetByIdAsync(request.Id, request.UserId); @@ -27,20 +23,24 @@ public class UpdateTransactionHandler(ITransactionRepository transactionReposito if (category is null) return new Response(null, 404, "Categoria não encontrada."); - transaction.Title = request.Title; - transaction.Type = request.Type; - transaction.Amount = amount; - transaction.CategoryId = request.CategoryId; - transaction.PaidOrReceivedAt = request.PaidOrReceivedAt; + var updateResult = transaction.Update( + request.Title, + request.Amount, + request.Type, + request.CategoryId, + request.PaidOrReceivedAt); + + if (updateResult.IsFailure) + return Response.Fail(string.Join("; ", updateResult.Errors)); await transactionRepository.UpdateAsync(transaction); var dto = TransactionMapper.ToDto(transaction, category); - return new Response(dto, 200, "Transação atualizada com sucesso!"); + return Response.Success(dto, "Transação atualizada com sucesso!"); } catch { - return new Response(null, 500, "Não foi possível atualizar a transação."); + return Response.Fail("Não foi possível atualizar a transação."); } } } diff --git a/Finance.Application/Interfaces/Repositories/ICategoryRepository.cs b/Finance.Application/Interfaces/Repositories/ICategoryRepository.cs new file mode 100644 index 0000000..128c8f2 --- /dev/null +++ b/Finance.Application/Interfaces/Repositories/ICategoryRepository.cs @@ -0,0 +1,12 @@ +using Finance.Domain.Models; + +namespace Finance.Application.Interfaces.Repositories; + +public interface ICategoryRepository +{ + Task CreateAsync(Category category); + Task UpdateAsync(Category category); + Task DeleteAsync(Category category); + Task GetByIdAsync(long id, long userId); + Task?> GetAllAsync(long userId); +} diff --git a/Finance.Application/Interfaces/Repositories/ITransactionRepository.cs b/Finance.Application/Interfaces/Repositories/ITransactionRepository.cs new file mode 100644 index 0000000..12e0e8c --- /dev/null +++ b/Finance.Application/Interfaces/Repositories/ITransactionRepository.cs @@ -0,0 +1,14 @@ +using Finance.Domain.Models; + +namespace Finance.Application.Interfaces.Repositories; + +public interface ITransactionRepository +{ + Task CreateAsync(Transaction transaction); + Task UpdateAsync(Transaction transaction); + Task DeleteAsync(Transaction transaction); + Task GetByIdAsync(long id, long userId); + Task> GetByPeriodAsync(long userId, DateTime? startDate, DateTime? endDate, int pageNumber, int pageSize); + Task> GetAllByPeriodAsync(long userId, DateTime? startDate, DateTime? endDate); + Task CountByPeriodAsync(long userId, DateTime? startDate, DateTime? endDate); +} diff --git a/Finance.Application/Interfaces/Repositories/IUserRepository.cs b/Finance.Application/Interfaces/Repositories/IUserRepository.cs new file mode 100644 index 0000000..b19170b --- /dev/null +++ b/Finance.Application/Interfaces/Repositories/IUserRepository.cs @@ -0,0 +1,11 @@ +using Finance.Domain.Models; + +namespace Finance.Application.Interfaces.Repositories; + +public interface IUserRepository +{ + Task GetByEmailAsync(string email); + Task AddAsync(User user); + Task GetByIdAsync(long id); + Task UpdateAsync(User u); +} diff --git a/Finance.Application/Mappers/TransactionMapper.cs b/Finance.Application/Mappers/TransactionMapper.cs index 8ed7c19..145631f 100644 --- a/Finance.Application/Mappers/TransactionMapper.cs +++ b/Finance.Application/Mappers/TransactionMapper.cs @@ -1,5 +1,5 @@ -using Finance.Domain.Models; -using Finance.Domain.Models.DTOs; +using Finance.Contracts.DTOs; +using Finance.Domain.Models; namespace Finance.Application.Mappers; @@ -11,14 +11,10 @@ public static TransactionDto ToDto(Transaction transaction, Category category) Id = transaction.Id, Title = transaction.Title, Amount = transaction.Amount, - Type = transaction.Type, + Type = transaction.Type.ToString(), PaidOrReceivedAt = transaction.PaidOrReceivedAt, CreatedAt = transaction.CreatedAt, - Category = new CategoryDto - { - Id = category.Id, - Title = category.Title, - Description = category.Description - } + CategoryId = transaction.CategoryId, + CategoryTitle = category.Title }; } diff --git a/Finance.Application/Services/CategoryService.cs b/Finance.Application/Services/CategoryService.cs deleted file mode 100644 index ebc0938..0000000 --- a/Finance.Application/Services/CategoryService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Finance.Contracts.Interfaces.Services; -using Finance.Contracts.Requests.Categories; -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; -using Finance.Application.Features.Categories.Create; -using Finance.Application.Features.Categories.Update; -using Finance.Application.Features.Categories.Delete; -using Finance.Application.Features.Categories.GetById; -using Finance.Application.Features.Categories.GetAll; -using MediatR; - -namespace Finance.Application.Services; - -public sealed class CategoryService(IMediator mediator) : ICategoryService -{ - private readonly IMediator _mediator = mediator; - - public Task> CreateAsync(CreateCategoryRequest request) - => _mediator.Send(new CreateCategoryCommand - { - UserId = request.UserId, - Title = request.Title, - Description = request.Description - }); - - public Task> UpdateAsync(UpdateCategoryRequest request) - => _mediator.Send(new UpdateCategoryCommand - { - Id = request.Id, - Title = request.Title, - Description = request.Description, - UserId = request.UserId - }); - - public Task> DeleteAsync(DeleteCategoryRequest request) - => _mediator.Send(new DeleteCategoryCommand - { - Id = request.Id, - UserId = request.UserId - }); - - public Task> GetByIdAsync(GetCategoryByIdRequest request) - => _mediator.Send(new GetCategoryByIdCommand - { - Id = request.Id, - UserId = request.UserId - }); - - public Task?>> GetAllAsync(GetAllCategoriesRequest request) - => _mediator.Send(new GetAllCategoriesCommand - { - UserId = request.UserId, - PageNumber = request.PageNumber, - PageSize = request.PageSize - }); -} diff --git a/Finance.Application/Services/TransactionService.cs b/Finance.Application/Services/TransactionService.cs deleted file mode 100644 index 3d9d338..0000000 --- a/Finance.Application/Services/TransactionService.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Finance.Application.Features.Transactions.Create; -using Finance.Application.Features.Transactions.Delete; -using Finance.Application.Features.Transactions.GetById; -using Finance.Application.Features.Transactions.GetByPeriod; -using Finance.Application.Features.Transactions.GetReport; -using Finance.Application.Features.Transactions.Update; -using Finance.Contracts.Interfaces.Services; -using Finance.Contracts.Requests.Transactions; -using Finance.Contracts.Responses; -using Finance.Contracts.Responses.Transactions; -using Finance.Domain.Models.DTOs; -using MediatR; - -namespace Finance.Application.Services; - -public sealed class TransactionService(IMediator mediator) : ITransactionService -{ - private readonly IMediator _mediator = mediator; - - public Task> CreateAsync(CreateTransactionRequest request) - => _mediator.Send(new CreateTransactionCommand - { - UserId = request.UserId, - Title = request.Title, - Type = request.Type, - Amount = request.Amount, - CategoryId = request.CategoryId, - PaidOrReceivedAt = request.PaidOrReceivedAt - }); - - public Task> UpdateAsync(UpdateTransactionRequest request) - => _mediator.Send(new UpdateTransactionCommand - { - Id = request.Id, - UserId = request.UserId, - Title = request.Title, - Type = request.Type, - Amount = request.Amount, - CategoryId = request.CategoryId, - PaidOrReceivedAt = request.PaidOrReceivedAt - }); - - public Task> DeleteAsync(DeleteTransactionRequest request) - => _mediator.Send(new DeleteTransactionCommand - { - Id = request.Id, - UserId = request.UserId - }); - - public Task> GetByIdAsync(GetTransactionByIdRequest request) - => _mediator.Send(new GetByIdTransactionCommand - { - Id = request.Id, - UserId = request.UserId - }); - - public Task?>> GetByPeriodAsync(GetTransactionsByPeriodRequest request) - => _mediator.Send(new GetByPeriodTransactionCommand - { - UserId = request.UserId, - StartDate = request.StartDate, - EndDate = request.EndDate, - PageNumber = request.PageNumber, - PageSize = request.PageSize - }); - - public Task> GetReportAsync(GetTransactionReportRequest request) - => _mediator.Send(new GetReportTransactionCommand - { - UserId = request.UserId, - StartDate = request.StartDate, - EndDate = request.EndDate - }); -} diff --git a/Finance.Application/Services/UserService.cs b/Finance.Application/Services/UserService.cs deleted file mode 100644 index ddc092c..0000000 --- a/Finance.Application/Services/UserService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Finance.Application.Features.Auth.GetProfile; -using Finance.Application.Features.Auth.Login; -using Finance.Application.Features.Auth.RefreshToken; -using Finance.Application.Features.Auth.Register; -using Finance.Application.Features.Auth.UpdateProfile; -using Finance.Contracts.Interfaces.Services; -using Finance.Contracts.Requests.Auth; -using Finance.Contracts.Responses; -using Finance.Contracts.Responses.Auth; -using MediatR; - -namespace Finance.Application.Services; - -public sealed class UserService(IMediator mediator) : IUserService -{ - private readonly IMediator _mediator = mediator; - - public Task> LoginAsync(LoginRequest request) - => _mediator.Send(new LoginUserCommand - { - Email = request.Email, - Password = request.Password - }); - - public Task> RegisterAsync(RegisterRequest request) - => _mediator.Send(new RegisterUserCommand - { - Name = request.Name, - Email = request.Email, - Password = request.Password - }); - - public Task> GetProfileAsync() - => _mediator.Send(new GetProfileCommand()); - - public Task> UpdateProfileAsync(UpdateUserProfileRequest request) - => _mediator.Send(new UpdateProfileCommand - { - Name = request.Name, - Email = request.Email - }); - - public Task> RefreshTokenAsync(string accessToken, string refreshToken) - => _mediator.Send(new RefreshUserTokenCommand - { - AccessToken = accessToken, - RefreshToken = refreshToken - }); -} diff --git a/Finance.Contracts/DTOs/CategoryDto.cs b/Finance.Contracts/DTOs/CategoryDto.cs new file mode 100644 index 0000000..e5eeb32 --- /dev/null +++ b/Finance.Contracts/DTOs/CategoryDto.cs @@ -0,0 +1,8 @@ +namespace Finance.Contracts.DTOs; + +public class CategoryDto +{ + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } +} diff --git a/Finance.Contracts/DTOs/TransactionDto.cs b/Finance.Contracts/DTOs/TransactionDto.cs new file mode 100644 index 0000000..0d18c00 --- /dev/null +++ b/Finance.Contracts/DTOs/TransactionDto.cs @@ -0,0 +1,13 @@ +namespace Finance.Contracts.DTOs; + +public class TransactionDto +{ + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime? PaidOrReceivedAt { get; set; } + public string Type { get; set; } = string.Empty; + public decimal Amount { get; set; } + public long CategoryId { get; set; } + public string CategoryTitle { get; set; } = string.Empty; +} diff --git a/Finance.Contracts/Interfaces/Services/ICategoryService.cs b/Finance.Contracts/Interfaces/Services/ICategoryService.cs deleted file mode 100644 index c061397..0000000 --- a/Finance.Contracts/Interfaces/Services/ICategoryService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Finance.Contracts.Requests.Categories; -using Finance.Contracts.Responses; -using Finance.Domain.Models.DTOs; - -namespace Finance.Contracts.Interfaces.Services; - -public interface ICategoryService -{ - Task> CreateAsync(CreateCategoryRequest request); - Task> UpdateAsync(UpdateCategoryRequest request); - Task> DeleteAsync(DeleteCategoryRequest request); - Task> GetByIdAsync(GetCategoryByIdRequest request); - Task?>> GetAllAsync(GetAllCategoriesRequest request); -} diff --git a/Finance.Contracts/Interfaces/Services/ITransactionService.cs b/Finance.Contracts/Interfaces/Services/ITransactionService.cs deleted file mode 100644 index 2b4b4b3..0000000 --- a/Finance.Contracts/Interfaces/Services/ITransactionService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Finance.Contracts.Requests.Transactions; -using Finance.Contracts.Responses; -using Finance.Contracts.Responses.Transactions; -using Finance.Domain.Models.DTOs; - -namespace Finance.Contracts.Interfaces.Services; - -public interface ITransactionService -{ - Task> CreateAsync(CreateTransactionRequest request); - Task> UpdateAsync(UpdateTransactionRequest request); - Task> DeleteAsync(DeleteTransactionRequest request); - Task> GetByIdAsync(GetTransactionByIdRequest request); - Task?>> GetByPeriodAsync(GetTransactionsByPeriodRequest request); - Task> GetReportAsync(GetTransactionReportRequest request); -} diff --git a/Finance.Contracts/Interfaces/Services/IUserService.cs b/Finance.Contracts/Interfaces/Services/IUserService.cs deleted file mode 100644 index 29fe39c..0000000 --- a/Finance.Contracts/Interfaces/Services/IUserService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Finance.Contracts.Requests.Auth; -using Finance.Contracts.Responses; -using Finance.Contracts.Responses.Auth; - -namespace Finance.Contracts.Interfaces.Services; - -public interface IUserService -{ - Task> LoginAsync(LoginRequest request); - Task> RegisterAsync(RegisterRequest request); - Task> GetProfileAsync(); - Task> UpdateProfileAsync(UpdateUserProfileRequest request); - Task> RefreshTokenAsync(string accessToken, string refreshToken); -} - diff --git a/Finance.Domain/Models/Category.cs b/Finance.Domain/Models/Category.cs index a045583..dc5126e 100644 --- a/Finance.Domain/Models/Category.cs +++ b/Finance.Domain/Models/Category.cs @@ -1,10 +1,58 @@ -namespace Finance.Domain.Models; +using Finance.Domain.SeedWork; -public class Category +namespace Finance.Domain.Models; + +public class Category : Entity { - public long Id { get; set; } - public string Title { get; set; } = string.Empty; - public string? Description { get; set; } - public long UserId { get; set; } - public User User { get; set; } = null!; + private Category() { } + + public string Title { get; private set; } = string.Empty; + public string? Description { get; private set; } + public long UserId { get; private set; } + public User User { get; private set; } = null!; + + public static Result Create(string title, string? description, long userId) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(title) || title.Length < 3) + errors.Add("Título deve ter pelo menos 3 caracteres"); + + if (!string.IsNullOrEmpty(title) && title.Length > 100) + errors.Add("Título deve ter no máximo 100 caracteres"); + + if (userId <= 0) + errors.Add("Usuário inválido"); + + if (errors.Any()) + return Result.Failure(errors); + + var category = new Category + { + Title = title.Trim(), + Description = description?.Trim(), + UserId = userId + }; + + return Result.Success(category); + } + + public Result Update(string title, string? description) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(title) || title.Length < 3) + errors.Add("Título deve ter pelo menos 3 caracteres"); + + if (!string.IsNullOrEmpty(title) && title.Length > 100) + errors.Add("Título deve ter no máximo 100 caracteres"); + + if (errors.Any()) + return Result.Failure(errors); + + Title = title.Trim(); + Description = description?.Trim(); + + return Result.Success(Unit.Value); + } } \ No newline at end of file diff --git a/Finance.Domain/Models/DTOs/TransactionDto.cs b/Finance.Domain/Models/DTOs/TransactionDto.cs index 1d31f3e..1274646 100644 --- a/Finance.Domain/Models/DTOs/TransactionDto.cs +++ b/Finance.Domain/Models/DTOs/TransactionDto.cs @@ -1,13 +1,11 @@ -using Finance.Domain.Enums; - -namespace Finance.Domain.Models.DTOs; +namespace Finance.Domain.Models.DTOs; public class TransactionDto { public long Id { get; set; } public string Title { get; set; } = string.Empty; public decimal Amount { get; set; } - public ETransactionType Type { get; set; } + public string Type { get; set; } = string.Empty; public DateTime? PaidOrReceivedAt { get; set; } public DateTime CreatedAt { get; set; } public CategoryDto? Category { get; set; } diff --git a/Finance.Domain/Models/Transaction.cs b/Finance.Domain/Models/Transaction.cs index 3dacd02..5fb0206 100644 --- a/Finance.Domain/Models/Transaction.cs +++ b/Finance.Domain/Models/Transaction.cs @@ -1,20 +1,101 @@ using Finance.Domain.Enums; +using Finance.Domain.SeedWork; namespace Finance.Domain.Models; -public class Transaction +public class Transaction : Entity, IAggregateRoot { - public long Id { get; set; } - public string Title { get; set; } = string.Empty; + private Transaction() { } - public DateTime CreatedAt { get; set; } = DateTime.Now; - public DateTime? PaidOrReceivedAt { get; set; } + public string Title { get; private set; } = string.Empty; + public DateTime CreatedAt { get; private set; } + public DateTime? PaidOrReceivedAt { get; private set; } + public ETransactionType Type { get; private set; } + public decimal Amount { get; private set; } + public long CategoryId { get; private set; } + public Category Category { get; private set; } = null!; + public long UserId { get; private set; } + public User? User { get; private set; } - public ETransactionType Type { get; set; } = ETransactionType.Withdraw; - public decimal Amount { get; set; } + public static Result Create( + string title, + decimal amount, + ETransactionType type, + long categoryId, + long userId, + DateTime? paidOrReceivedAt) + { + var errors = new List(); - public long CategoryId { get; set; } - public Category Category { get; set; } = null!; - public long UserId { get; set; } - public User? User { get; set; } + if (string.IsNullOrWhiteSpace(title) || title.Length < 3) + errors.Add("Título deve ter pelo menos 3 caracteres"); + + if (amount <= 0) + errors.Add("Valor deve ser maior que zero"); + + if (categoryId <= 0) + errors.Add("Categoria inválida"); + + if (userId <= 0) + errors.Add("Usuário inválido"); + + if (errors.Any()) + return Result.Failure(errors); + + var normalizedAmount = type == ETransactionType.Withdraw && amount > 0 + ? -amount + : amount; + + var transaction = new Transaction + { + Title = title.Trim(), + Amount = normalizedAmount, + Type = type, + CategoryId = categoryId, + UserId = userId, + PaidOrReceivedAt = paidOrReceivedAt, + CreatedAt = DateTime.UtcNow + }; + + return Result.Success(transaction); + } + + public Result Update( + string title, + decimal amount, + ETransactionType type, + long categoryId, + DateTime? paidOrReceivedAt) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(title) || title.Length < 3) + errors.Add("Título deve ter pelo menos 3 caracteres"); + + if (amount <= 0) + errors.Add("Valor deve ser maior que zero"); + + if (categoryId <= 0) + errors.Add("Categoria inválida"); + + if (errors.Any()) + return Result.Failure(errors); + + Title = title.Trim(); + Type = type; + CategoryId = categoryId; + PaidOrReceivedAt = paidOrReceivedAt; + + var normalizedAmount = type == ETransactionType.Withdraw && amount > 0 + ? -amount + : amount; + + Amount = normalizedAmount; + + return Result.Success(Unit.Value); + } + + public bool IsIncome() => Type == ETransactionType.Deposit; + public bool IsExpense() => Type == ETransactionType.Withdraw; + public decimal GetAbsoluteAmount() => Math.Abs(Amount); } \ No newline at end of file diff --git a/Finance.Domain/Models/User.cs b/Finance.Domain/Models/User.cs index 4ddffb3..b9a8b7f 100644 --- a/Finance.Domain/Models/User.cs +++ b/Finance.Domain/Models/User.cs @@ -1,13 +1,72 @@ -namespace Finance.Domain.Models; +using Finance.Domain.SeedWork; -public class User +namespace Finance.Domain.Models; + +public class User : Entity { - public long Id { get; set; } - public string Name { get; set; } = default!; - public string Email { get; set; } = default!; - public string PasswordHash { get; set; } = default!; - public string? RefreshToken { get; set; } - public DateTime? RefreshTokenExpiryTime { get; set; } - public ICollection? Categories { get; set; } - public ICollection? Transactions { get; set; } + private User() { } + + public string Name { get; private set; } = string.Empty; + public string Email { get; private set; } = string.Empty; + public string PasswordHash { get; private set; } = string.Empty; + public string? RefreshToken { get; private set; } + public DateTime? RefreshTokenExpiryTime { get; private set; } + public ICollection? Categories { get; private set; } + public ICollection? Transactions { get; private set; } + + public static Result Create(string name, string email, string passwordHash) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(name) || name.Length < 2) + errors.Add("Nome deve ter pelo menos 2 caracteres"); + + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + errors.Add("Email inválido"); + + if (string.IsNullOrWhiteSpace(passwordHash)) + errors.Add("Senha inválida"); + + if (errors.Any()) + return Result.Failure(errors); + + var user = new User + { + Name = name.Trim(), + Email = email.Trim().ToLower(), + PasswordHash = passwordHash + }; + + return Result.Success(user); + } + + public Result UpdateProfile(string name, string email) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(name) || name.Length < 2) + errors.Add("Nome deve ter pelo menos 2 caracteres"); + + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + errors.Add("Email inválido"); + + if (errors.Any()) + return Result.Failure(errors); + + Name = name.Trim(); + Email = email.Trim().ToLower(); + + return Result.Success(Unit.Value); + } + + public void SetRefreshToken(string refreshToken, DateTime expiryTime) + { + RefreshToken = refreshToken; + RefreshTokenExpiryTime = expiryTime; + } + + public bool IsRefreshTokenValid(string token) + { + return RefreshToken == token && RefreshTokenExpiryTime > DateTime.UtcNow; + } } diff --git a/Finance.Domain/SeedWork/Entity.cs b/Finance.Domain/SeedWork/Entity.cs new file mode 100644 index 0000000..85ffeb6 --- /dev/null +++ b/Finance.Domain/SeedWork/Entity.cs @@ -0,0 +1,6 @@ +namespace Finance.Domain.SeedWork; + +public abstract class Entity +{ + public long Id { get; protected set; } +} diff --git a/Finance.Domain/SeedWork/IAggregateRoot.cs b/Finance.Domain/SeedWork/IAggregateRoot.cs new file mode 100644 index 0000000..2f17793 --- /dev/null +++ b/Finance.Domain/SeedWork/IAggregateRoot.cs @@ -0,0 +1,5 @@ +namespace Finance.Domain.SeedWork; + +public interface IAggregateRoot +{ +} diff --git a/Finance.Domain/SeedWork/Result.cs b/Finance.Domain/SeedWork/Result.cs new file mode 100644 index 0000000..9d23b48 --- /dev/null +++ b/Finance.Domain/SeedWork/Result.cs @@ -0,0 +1,54 @@ +namespace Finance.Domain.SeedWork; + +public readonly struct Unit +{ + public static readonly Unit Value = new(); +} + +public class Result +{ + public bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + public T Value { get; } + public List Errors { get; } + + private Result(bool isSuccess, T value, List? errors = null) + { + IsSuccess = isSuccess; + Value = value; + Errors = errors ?? new List(); + } + + public static Result Success(T value) + { + return new Result(true, value); + } + + public static Result Failure(List errors) + { + return new Result(false, default!, errors); + } + + public static Result Failure(string error) + { + return new Result(false, default!, new List { error }); + } +} + +public static class Result +{ + public static Result Success(T value) + { + return Result.Success(value); + } + + public static Result Failure(List errors) + { + return Result.Failure(errors); + } + + public static Result Failure(string error) + { + return Result.Failure(error); + } +} diff --git a/Finance.Infrastructure.Tests/Repositories/CategoryRepositoryTests.cs b/Finance.Infrastructure.Tests/Repositories/CategoryRepositoryTests.cs index cfb2d26..cfe06c2 100644 --- a/Finance.Infrastructure.Tests/Repositories/CategoryRepositoryTests.cs +++ b/Finance.Infrastructure.Tests/Repositories/CategoryRepositoryTests.cs @@ -52,23 +52,14 @@ public async Task CreateAsync_Should_PersistCategory_WhenCalled() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user = new User - { - Id = 1, - Name = "Test User", - Email = "test@email.com", - PasswordHash = "123" - }; + var userResult = User.Create("Test User", "test@email.com", "123"); + var user = userResult.Value; await SeedUserAsync(writeContext, user); var repository = new CategoryRepository(readContext, writeContext); - var newCategory = new Category - { - Title = "Lazer", - Description = "Gastos com Lazer", - UserId = user.Id - }; + var catResult = Category.Create("Lazer", "Gastos com Lazer", user.Id); + var newCategory = catResult.Value; var createdCategory = await repository.CreateAsync(newCategory); @@ -89,22 +80,13 @@ public async Task GetByIdAsync_Should_ReturnCategory_WhenExistsForUser() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user = new User - { - Id = 1, - Name = "Test User", - Email = "test@email.com", - PasswordHash = "123" - }; + var userResult = User.Create("Test User", "test@email.com", "123"); + var user = userResult.Value; await SeedUserAsync(writeContext, user); - var category = new Category - { - Title = "Saúde", - Description = "Gastos com Saúde", - UserId = user.Id - }; + var catResult = Category.Create("Saúde", "Gastos com Saúde", user.Id); + var category = catResult.Value; writeContext.Categories.Add(category); await writeContext.SaveChangesAsync(); @@ -123,13 +105,8 @@ public async Task GetByIdAsync_Should_ReturnNull_WhenDoesNotExistForUser() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user = new User - { - Id = 1, - Name = "Test User", - Email = "test@email.com", - PasswordHash = "123" - }; + var userResult = User.Create("Test User", "test@email.com", "123"); + var user = userResult.Value; await SeedUserAsync(writeContext, user); @@ -146,29 +123,19 @@ public async Task GetAllAsync_Should_ReturnOnlyCategoriesForGivenUser() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user1 = new User - { - Id = 1, - Name = "User One", - Email = "user1@email.com", - PasswordHash = "123" - }; - - var user2 = new User - { - Id = 2, - Name = "User Two", - Email = "user2@email.com", - PasswordHash = "123" - }; + var user1Result = User.Create("User One", "user1@email.com", "123"); + var user1 = user1Result.Value; + + var user2Result = User.Create("User Two", "user2@email.com", "123"); + var user2 = user2Result.Value; await SeedUserAsync(writeContext, user1); await SeedUserAsync(writeContext, user2); writeContext.Categories.AddRange( - new Category { Title = "Moradia", UserId = user1.Id }, - new Category { Title = "Transporte", UserId = user1.Id }, - new Category { Title = "Alimentação", UserId = user2.Id } + Category.Create("Moradia", null, user1.Id).Value, + Category.Create("Transporte", null, user1.Id).Value, + Category.Create("Alimentação", null, user2.Id).Value ); await writeContext.SaveChangesAsync(); @@ -192,21 +159,13 @@ public async Task DeleteAsync_Should_RemoveCategoryFromDatabase() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user = new User - { - Id = 1, - Name = "Test User", - Email = "test@email.com", - PasswordHash = "123" - }; + var userResult = User.Create("Test User", "test@email.com", "123"); + var user = userResult.Value; await SeedUserAsync(writeContext, user); - var category = new Category - { - Title = "Para Deletar", - UserId = user.Id - }; + var catResult = Category.Create("Para Deletar", null, user.Id); + var category = catResult.Value; writeContext.Categories.Add(category); await writeContext.SaveChangesAsync(); @@ -226,22 +185,13 @@ public async Task UpdateAsync_Should_ChangeDataInDatabase() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user = new User - { - Id = 1, - Name = "Test User", - Email = "test@email.com", - PasswordHash = "123" - }; + var userResult = User.Create("Test User", "test@email.com", "123"); + var user = userResult.Value; await SeedUserAsync(writeContext, user); - var originalCategory = new Category - { - Title = "Original", - Description = "Original Desc", - UserId = user.Id - }; + var catResult = Category.Create("Original", "Original Desc", user.Id); + var originalCategory = catResult.Value; writeContext.Categories.Add(originalCategory); await writeContext.SaveChangesAsync(); @@ -249,7 +199,9 @@ public async Task UpdateAsync_Should_ChangeDataInDatabase() writeContext.Entry(originalCategory).State = EntityState.Detached; var repository = new CategoryRepository(readContext, writeContext); - originalCategory.Title = "Atualizado"; + + var updateResult = originalCategory.Update("Atualizado", "Updated Desc"); + updateResult.IsSuccess.Should().BeTrue(); await repository.UpdateAsync(originalCategory); diff --git a/Finance.Infrastructure.Tests/Repositories/TransactionRepositoryTests.cs b/Finance.Infrastructure.Tests/Repositories/TransactionRepositoryTests.cs index bbddffd..bb4d579 100644 --- a/Finance.Infrastructure.Tests/Repositories/TransactionRepositoryTests.cs +++ b/Finance.Infrastructure.Tests/Repositories/TransactionRepositoryTests.cs @@ -42,12 +42,8 @@ public void Dispose() private async Task SeedUserAsync(DbContext context) { - var user = new User - { - Name = "Test User", - Email = "test@email.com", - PasswordHash = "123" - }; + var userResult = User.Create("Test User", "test@email.com", "123"); + var user = userResult.Value; context.Add(user); await context.SaveChangesAsync(); @@ -56,12 +52,8 @@ private async Task SeedUserAsync(DbContext context) private async Task SeedCategoryAsync(DbContext context, long userId) { - var category = new Category - { - Title = "Alimentação", - UserId = userId, - Description = "Test" - }; + var catResult = Category.Create("Alimentação", "Test", userId); + var category = catResult.Value; context.Add(category); await context.SaveChangesAsync(); @@ -77,15 +69,8 @@ public async Task CreateAsync_Should_PersistTransaction() var user = await SeedUserAsync(writeContext); var category = await SeedCategoryAsync(writeContext, user.Id); var repository = new TransactionRepository(readContext, writeContext); - var newTransaction = new Transaction - { - Title = "Almoço", - Amount = 50, - Type = ETransactionType.Withdraw, - UserId = user.Id, - CategoryId = category.Id, - PaidOrReceivedAt = DateTime.UtcNow - }; + var txResult = Transaction.Create("Almoço", 50, ETransactionType.Withdraw, category.Id, user.Id, DateTime.UtcNow); + var newTransaction = txResult.Value; var createdTransaction = await repository.CreateAsync(newTransaction); @@ -95,7 +80,7 @@ public async Task CreateAsync_Should_PersistTransaction() await using var assertContext = new FinanceReadDbContext(_readOptions); var transactionInDb = await assertContext.Transactions.FindAsync(createdTransaction.Id); transactionInDb.Should().NotBeNull(); - transactionInDb!.Amount.Should().Be(50); + transactionInDb!.Amount.Should().Be(-50); } [Fact] @@ -106,15 +91,8 @@ public async Task UpdateAsync_Should_ChangeDataInDatabase() var user = await SeedUserAsync(writeContext); var category = await SeedCategoryAsync(writeContext, user.Id); - var originalTransaction = new Transaction - { - Title = "Original", - Amount = 100, - PaidOrReceivedAt = DateTime.UtcNow, - UserId = user.Id, - CategoryId = category.Id, - Type = ETransactionType.Withdraw - }; + var txResult = Transaction.Create("Original", 100, ETransactionType.Withdraw, category.Id, user.Id, DateTime.UtcNow); + var originalTransaction = txResult.Value; writeContext.Add(originalTransaction); await writeContext.SaveChangesAsync(); @@ -122,7 +100,7 @@ public async Task UpdateAsync_Should_ChangeDataInDatabase() writeContext.Entry(originalTransaction).State = EntityState.Detached; var repository = new TransactionRepository(readContext, writeContext); - originalTransaction.Amount = 150; + typeof(Transaction).GetProperty("Amount")?.SetValue(originalTransaction, 150m); await repository.UpdateAsync(originalTransaction); @@ -141,15 +119,8 @@ public async Task GetByIdAsync_Should_ReturnTransactionWithCategory_WhenExists() var user = await SeedUserAsync(writeContext); var category = await SeedCategoryAsync(writeContext, user.Id); - var transaction = new Transaction - { - Title = "Jantar", - Amount = 120, - Type = ETransactionType.Withdraw, - UserId = user.Id, - CategoryId = category.Id, - PaidOrReceivedAt = DateTime.UtcNow - }; + var txResult = Transaction.Create("Jantar", 120, ETransactionType.Withdraw, category.Id, user.Id, DateTime.UtcNow); + var transaction = txResult.Value; writeContext.Add(transaction); await writeContext.SaveChangesAsync(); @@ -178,12 +149,9 @@ public async Task GetByPeriodAsync_Should_ReturnCorrectTransactions_ForSpecificD var tomorrow = today.AddDays(1); writeContext.Transactions.AddRange( - new Transaction - { Title = "Ontem", Amount = 10, PaidOrReceivedAt = yesterday, UserId = user.Id, CategoryId = category.Id, Type = ETransactionType.Withdraw }, - new Transaction - { Title = "Hoje", Amount = 20, PaidOrReceivedAt = today, UserId = user.Id, CategoryId = category.Id, Type = ETransactionType.Withdraw }, - new Transaction - { Title = "Amanhã", Amount = 30, PaidOrReceivedAt = tomorrow, UserId = user.Id, CategoryId = category.Id, Type = ETransactionType.Withdraw } + Transaction.Create("Ontem", 10, ETransactionType.Withdraw, category.Id, user.Id, yesterday).Value, + Transaction.Create("Hoje", 20, ETransactionType.Withdraw, category.Id, user.Id, today).Value, + Transaction.Create("Amanhã", 30, ETransactionType.Withdraw, category.Id, user.Id, tomorrow).Value ); await writeContext.SaveChangesAsync(); @@ -204,15 +172,8 @@ public async Task GetByPeriodAsync_Should_ReturnAllTransactions_WhenDatesAreNull var user = await SeedUserAsync(writeContext); var category = await SeedCategoryAsync(writeContext, user.Id); - writeContext.Transactions.Add(new Transaction - { - Title = "Qualquer", - Amount = 10, - PaidOrReceivedAt = DateTime.UtcNow, - UserId = user.Id, - CategoryId = category.Id, - Type = ETransactionType.Withdraw - }); + var txResult = Transaction.Create("Qualquer", 10, ETransactionType.Withdraw, category.Id, user.Id, DateTime.UtcNow); + writeContext.Transactions.Add(txResult.Value); await writeContext.SaveChangesAsync(); @@ -232,15 +193,8 @@ public async Task DeleteAsync_Should_RemoveTransactionFromDatabase() var user = await SeedUserAsync(writeContext); var category = await SeedCategoryAsync(writeContext, user.Id); - var transaction = new Transaction - { - Title = "Para Deletar", - Amount = 99, - Type = ETransactionType.Withdraw, - UserId = user.Id, - CategoryId = category.Id, - PaidOrReceivedAt = DateTime.UtcNow - }; + var txResult = Transaction.Create("Para Deletar", 99, ETransactionType.Withdraw, category.Id, user.Id, DateTime.UtcNow); + var transaction = txResult.Value; writeContext.Add(transaction); await writeContext.SaveChangesAsync(); diff --git a/Finance.Infrastructure.Tests/Repositories/UserRepositoryTests.cs b/Finance.Infrastructure.Tests/Repositories/UserRepositoryTests.cs index 703bec5..067ea53 100644 --- a/Finance.Infrastructure.Tests/Repositories/UserRepositoryTests.cs +++ b/Finance.Infrastructure.Tests/Repositories/UserRepositoryTests.cs @@ -46,12 +46,8 @@ public async Task AddAsync_Should_PersistUser_WhenCalled() await using var readContext = new FinanceReadDbContext(_readOptions); var repository = new UserRepository(readContext, writeContext); - var newUser = new User - { - Name = "Herbert", - Email = "herbert@email.com", - PasswordHash = "some_hash" - }; + var userResult = User.Create("Herbert", "herbert@email.com", "some_hash"); + var newUser = userResult.Value; await repository.AddAsync(newUser); @@ -70,12 +66,8 @@ public async Task GetByIdAsync_Should_ReturnUser_WhenExists() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var user = new User - { - Name = "Test User", - Email = "test@email.com", - PasswordHash = "hash" - }; + var userResult = User.Create("Test User", "test@email.com", "hash"); + var user = userResult.Value; writeContext.Users.Add(user); @@ -97,12 +89,8 @@ public async Task GetByEmailAsync_Should_BeCaseInsensitive() await using var readContext = new FinanceReadDbContext(_readOptions); var originalEmail = "Case.Test@Email.COM"; - var user = new User - { - Name = "Case Test", - Email = originalEmail, - PasswordHash = "hash" - }; + var userResult = User.Create("Case Test", originalEmail, "hash"); + var user = userResult.Value; writeContext.Users.Add(user); await writeContext.SaveChangesAsync(); @@ -134,12 +122,8 @@ public async Task UpdateAsync_Should_ChangeDataInDatabase() await using var writeContext = new FinanceWriteDbContext(_writeOptions); await using var readContext = new FinanceReadDbContext(_readOptions); - var originalUser = new User - { - Name = "Original Name", - Email = "update@test.com", - PasswordHash = "hash" - }; + var userResult = User.Create("Original Name", "update@test.com", "hash"); + var originalUser = userResult.Value; writeContext.Users.Add(originalUser); await writeContext.SaveChangesAsync(); @@ -147,7 +131,9 @@ public async Task UpdateAsync_Should_ChangeDataInDatabase() writeContext.Entry(originalUser).State = EntityState.Detached; var repository = new UserRepository(readContext, writeContext); - originalUser.Name = "Updated Name"; + + var updateResult = originalUser.UpdateProfile("Updated Name", originalUser.Email); + updateResult.IsSuccess.Should().BeTrue(); await repository.UpdateAsync(originalUser); diff --git a/Finance.Infrastructure/Repositories/CategoryRepository.cs b/Finance.Infrastructure/Repositories/CategoryRepository.cs index 158b921..17a4c04 100644 --- a/Finance.Infrastructure/Repositories/CategoryRepository.cs +++ b/Finance.Infrastructure/Repositories/CategoryRepository.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Domain.Models; using Finance.Infrastructure.Data; using Microsoft.EntityFrameworkCore; diff --git a/Finance.Infrastructure/Repositories/TransactionRepository.cs b/Finance.Infrastructure/Repositories/TransactionRepository.cs index 86a8b46..6aa5af9 100644 --- a/Finance.Infrastructure/Repositories/TransactionRepository.cs +++ b/Finance.Infrastructure/Repositories/TransactionRepository.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Domain.Models; using Finance.Infrastructure.Data; using Microsoft.EntityFrameworkCore; diff --git a/Finance.Infrastructure/Repositories/UserRepository.cs b/Finance.Infrastructure/Repositories/UserRepository.cs index 2130772..5c53122 100644 --- a/Finance.Infrastructure/Repositories/UserRepository.cs +++ b/Finance.Infrastructure/Repositories/UserRepository.cs @@ -1,4 +1,4 @@ -using Finance.Contracts.Interfaces.Repositories; +using Finance.Application.Interfaces.Repositories; using Finance.Domain.Models; using Finance.Infrastructure.Data; using Microsoft.EntityFrameworkCore; diff --git a/README.md b/README.md index af5cdbe..19aad47 100644 --- a/README.md +++ b/README.md @@ -142,12 +142,15 @@ A solução segue rigorosamente os princípios da **Clean Architecture**, com re ``` Finance API -├── 📁 Finance.Domain # Entidades, Enums, Interfaces e Regras de Negócio -├── 📁 Finance.Application # Casos de Uso (Handlers), Validações, Mapeamentos -├── 📁 Finance.Contracts # DTOs, Requests, Responses e Interfaces Compartilhadas -├── 📁 Finance.Infrastructure # EF Core, Redis, Repositórios, Migrations -├── 📁 Finance.Api # Controllers, Docker, DI, Middlewares -└── 📁 Finance.Tests # Testes (organizados por camada: Domain, Application, Api) +├── 📁 Finance.Domain # Entidades Rich Domain, Enums, SeedWork +├── 📁 Finance.Domain.Tests # Testes de unidade do Domain +├── 📁 Finance.Application # Handlers CQRS, Mappers, Repository Interfaces +├── 📁 Finance.Application.Tests # Testes de handlers unitários +├── 📁 Finance.Contracts # DTOs, Requests, Responses compartilhados +├── 📁 Finance.Infrastructure # EF Core, Redis, Repositórios, Services +├── 📁 Finance.Infrastructure.Tests # Testes de integração de repositories +├── 📁 Finance.Api # Controllers, DI, Middlewares, Program +└── 📁 Finance.Api.Tests # Testes de integração da API (Testcontainers) ``` > 💡 **Observação:** a camada de testes é organizada por contexto/camada para refletir a arquitetura da solução, facilitando manutenção, leitura e evolução dos testes.