diff --git a/src/Modules/Catalogs/API/Endpoints/CatalogsModuleEndpoints.cs b/src/Modules/Catalogs/API/Endpoints/CatalogsModuleEndpoints.cs index ab387f946..c4926f451 100644 --- a/src/Modules/Catalogs/API/Endpoints/CatalogsModuleEndpoints.cs +++ b/src/Modules/Catalogs/API/Endpoints/CatalogsModuleEndpoints.cs @@ -1,3 +1,5 @@ +using MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; +using MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; using MeAjudaAi.Shared.Endpoints; using Microsoft.AspNetCore.Builder; diff --git a/src/Modules/Catalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs new file mode 100644 index 000000000..51e09c9a1 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class ActivateServiceEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/{id:guid}/activate", ActivateAsync) + .WithName("ActivateService") + .WithSummary("Ativar serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task ActivateAsync( + Guid id, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new ActivateServiceCommand(id); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs new file mode 100644 index 000000000..c71e420eb --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs @@ -0,0 +1,33 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class ChangeServiceCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/{id:guid}/change-category", ChangeAsync) + .WithName("ChangeServiceCategory") + .WithSummary("Alterar categoria do serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task ChangeAsync( + Guid id, + [FromBody] ChangeServiceCategoryRequest request, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new ChangeServiceCategoryCommand(id, request.NewCategoryId); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/CreateServiceEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/CreateServiceEndpoint.cs new file mode 100644 index 000000000..47d31d65b --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/CreateServiceEndpoint.cs @@ -0,0 +1,38 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class CreateServiceEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/", CreateAsync) + .WithName("CreateService") + .WithSummary("Criar serviço") + .Produces>(StatusCodes.Status201Created) + .RequireAdmin(); + + private static async Task CreateAsync( + [FromBody] CreateServiceRequest request, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new CreateServiceCommand(request.CategoryId, request.Name, request.Description, request.DisplayOrder); + var result = await commandDispatcher.SendAsync>(command, cancellationToken); + + if (!result.IsSuccess) + return Handle(result); + + return Handle(result, "GetServiceById", new { id = result.Value!.Id }); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs new file mode 100644 index 000000000..e2856f744 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class DeactivateServiceEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/{id:guid}/deactivate", DeactivateAsync) + .WithName("DeactivateService") + .WithSummary("Desativar serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task DeactivateAsync( + Guid id, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new DeactivateServiceCommand(id); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs new file mode 100644 index 000000000..a8616ccaf --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class DeleteServiceEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapDelete("/{id:guid}", DeleteAsync) + .WithName("DeleteService") + .WithSummary("Deletar serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task DeleteAsync( + Guid id, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new DeleteServiceCommand(id); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs new file mode 100644 index 000000000..530354cf2 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs @@ -0,0 +1,31 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class GetAllServicesEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapGet("/", GetAllAsync) + .WithName("GetAllServices") + .WithSummary("Listar todos os serviços") + .Produces>>(StatusCodes.Status200OK); + + private static async Task GetAllAsync( + [AsParameters] GetAllServicesQuery query, + IQueryDispatcher queryDispatcher, + CancellationToken cancellationToken) + { + var result = await queryDispatcher.QueryAsync>>( + query, cancellationToken); + return Handle(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs new file mode 100644 index 000000000..a661b4e2c --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs @@ -0,0 +1,37 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class GetServiceByIdEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapGet("/{id:guid}", GetByIdAsync) + .WithName("GetServiceById") + .WithSummary("Buscar serviço por ID") + .Produces>(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); + + private static async Task GetByIdAsync( + Guid id, + IQueryDispatcher queryDispatcher, + CancellationToken cancellationToken) + { + var query = new GetServiceByIdQuery(id); + var result = await queryDispatcher.QueryAsync>(query, cancellationToken); + + if (result.IsSuccess && result.Value is null) + { + return Results.NotFound(); + } + + return Handle(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs new file mode 100644 index 000000000..eded83ca1 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs @@ -0,0 +1,32 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class GetServicesByCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapGet("/category/{categoryId:guid}", GetByCategoryAsync) + .WithName("GetServicesByCategory") + .WithSummary("Listar serviços por categoria") + .Produces>>(StatusCodes.Status200OK); + + private static async Task GetByCategoryAsync( + Guid categoryId, + [AsParameters] GetServicesByCategoryQuery query, + IQueryDispatcher queryDispatcher, + CancellationToken cancellationToken) + { + var queryWithCategory = query with { CategoryId = categoryId }; + var result = await queryDispatcher.QueryAsync>>(queryWithCategory, cancellationToken); + return Handle(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs new file mode 100644 index 000000000..007299d6b --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs @@ -0,0 +1,33 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class UpdateServiceEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPut("/{id:guid}", UpdateAsync) + .WithName("UpdateService") + .WithSummary("Atualizar serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task UpdateAsync( + Guid id, + [FromBody] UpdateServiceRequest request, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new UpdateServiceCommand(id, request.Name, request.Description, request.DisplayOrder); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs new file mode 100644 index 000000000..64b3203b6 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs @@ -0,0 +1,42 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Contracts.Modules.Catalogs; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.Service; + +public class ValidateServicesEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/validate", ValidateAsync) + .WithName("ValidateServices") + .WithSummary("Validar múltiplos serviços") + .Produces>(StatusCodes.Status200OK) + .RequireAdmin(); + + private static async Task ValidateAsync( + [FromBody] ValidateServicesRequest request, + [FromServices] ICatalogsModuleApi moduleApi, + CancellationToken cancellationToken) + { + var result = await moduleApi.ValidateServicesAsync(request.ServiceIds, cancellationToken); + + if (!result.IsSuccess) + return Handle(result); + + // Mapear do DTO do módulo para o DTO da API + var response = new ValidateServicesResponse( + result.Value!.AllValid, + result.Value.InvalidServiceIds, + result.Value.InactiveServiceIds + ); + + return Handle(Result.Success(response)); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..77704ba66 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public class ActivateServiceCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/{id:guid}/activate", ActivateAsync) + .WithName("ActivateServiceCategory") + .WithSummary("Ativar categoria de serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task ActivateAsync( + Guid id, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new ActivateServiceCategoryCommand(id); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..3750097ae --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs @@ -0,0 +1,40 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public record CreateServiceCategoryRequest(string Name, string? Description, int DisplayOrder); + +public class CreateServiceCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/", CreateAsync) + .WithName("CreateServiceCategory") + .WithSummary("Criar categoria de serviço") + .Produces>(StatusCodes.Status201Created) + .RequireAdmin(); + + private static async Task CreateAsync( + [FromBody] CreateServiceCategoryRequest request, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new CreateServiceCategoryCommand(request.Name, request.Description, request.DisplayOrder); + var result = await commandDispatcher.SendAsync>( + command, cancellationToken); + + if (!result.IsSuccess) + return Handle(result); + + return Handle(result, "GetServiceCategoryById", new { id = result.Value!.Id }); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..5175711e4 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public class DeactivateServiceCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPost("/{id:guid}/deactivate", DeactivateAsync) + .WithName("DeactivateServiceCategory") + .WithSummary("Desativar categoria de serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task DeactivateAsync( + Guid id, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new DeactivateServiceCategoryCommand(id); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs new file mode 100644 index 000000000..cef204cd7 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public class DeleteServiceCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapDelete("/{id:guid}", DeleteAsync) + .WithName("DeleteServiceCategory") + .WithSummary("Deletar categoria de serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task DeleteAsync( + Guid id, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new DeleteServiceCategoryCommand(id); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs new file mode 100644 index 000000000..e3b8427e3 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs @@ -0,0 +1,35 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public record GetAllCategoriesQuery(bool ActiveOnly = false); + +public class GetAllServiceCategoriesEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapGet("/", GetAllAsync) + .WithName("GetAllServiceCategories") + .WithSummary("Listar todas as categorias") + .Produces>>(StatusCodes.Status200OK); + + private static async Task GetAllAsync( + [AsParameters] GetAllCategoriesQuery query, + IQueryDispatcher queryDispatcher, + CancellationToken cancellationToken) + { + var qry = new GetAllServiceCategoriesQuery(query.ActiveOnly); + var result = await queryDispatcher.QueryAsync>>( + qry, cancellationToken); + + return Handle(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs new file mode 100644 index 000000000..28ce306d4 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs @@ -0,0 +1,36 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public class GetServiceCategoryByIdEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapGet("/{id:guid}", GetByIdAsync) + .WithName("GetServiceCategoryById") + .WithSummary("Buscar categoria por ID") + .Produces>(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); + + private static async Task GetByIdAsync( + Guid id, + IQueryDispatcher queryDispatcher, + CancellationToken cancellationToken) + { + var query = new GetServiceCategoryByIdQuery(id); + var result = await queryDispatcher.QueryAsync>( + query, cancellationToken); + + if (result.IsSuccess && result.Value == null) + return Results.NotFound(); + + return Handle(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..6dfa06221 --- /dev/null +++ b/src/Modules/Catalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs @@ -0,0 +1,34 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Endpoints; +using MeAjudaAi.Shared.Functional; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Catalogs.API.Endpoints.ServiceCategory; + +public record UpdateServiceCategoryRequest(string Name, string? Description, int DisplayOrder); + +public class UpdateServiceCategoryEndpoint : BaseEndpoint, IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + => app.MapPut("/{id:guid}", UpdateAsync) + .WithName("UpdateServiceCategory") + .WithSummary("Atualizar categoria de serviço") + .Produces(StatusCodes.Status204NoContent) + .RequireAdmin(); + + private static async Task UpdateAsync( + Guid id, + [FromBody] UpdateServiceCategoryRequest request, + ICommandDispatcher commandDispatcher, + CancellationToken cancellationToken) + { + var command = new UpdateServiceCategoryCommand(id, request.Name, request.Description, request.DisplayOrder); + var result = await commandDispatcher.SendAsync(command, cancellationToken); + return HandleNoContent(result); + } +} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceCategoryEndpoints.cs b/src/Modules/Catalogs/API/Endpoints/ServiceCategoryEndpoints.cs deleted file mode 100644 index 421b7f2a5..000000000 --- a/src/Modules/Catalogs/API/Endpoints/ServiceCategoryEndpoints.cs +++ /dev/null @@ -1,196 +0,0 @@ -using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Application.DTOs; -using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; -using MeAjudaAi.Shared.Authorization; -using MeAjudaAi.Shared.Commands; -using MeAjudaAi.Shared.Contracts; -using MeAjudaAi.Shared.Endpoints; -using MeAjudaAi.Shared.Functional; -using MeAjudaAi.Shared.Queries; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; - -namespace MeAjudaAi.Modules.Catalogs.API.Endpoints; - -// ============================================================ -// Request DTOs -// ============================================================ - -public record CreateServiceCategoryRequest(string Name, string? Description, int DisplayOrder); -public record UpdateServiceCategoryRequest(string Name, string? Description, int DisplayOrder); - -// ============================================================ -// CREATE -// ============================================================ - -public class CreateServiceCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/", CreateAsync) - .WithName("CreateServiceCategory") - .WithSummary("Criar categoria de serviço") - .Produces>(StatusCodes.Status201Created) - .RequireAdmin(); - - private static async Task CreateAsync( - [FromBody] CreateServiceCategoryRequest request, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new CreateServiceCategoryCommand(request.Name, request.Description, request.DisplayOrder); - var result = await commandDispatcher.SendAsync>( - command, cancellationToken); - - if (!result.IsSuccess) - return Handle(result); - - return Handle(result, "GetServiceCategoryById", new { id = result.Value!.Id }); - } -} - -// ============================================================ -// READ -// ============================================================ - -public class GetAllServiceCategoriesEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapGet("/", GetAllAsync) - .WithName("GetAllServiceCategories") - .WithSummary("Listar todas as categorias") - .Produces>>(StatusCodes.Status200OK); - - private static async Task GetAllAsync( - [AsParameters] GetAllCategoriesQuery query, - IQueryDispatcher queryDispatcher, - CancellationToken cancellationToken) - { - var qry = new GetAllServiceCategoriesQuery(query.ActiveOnly); - var result = await queryDispatcher.QueryAsync>>( - qry, cancellationToken); - - return Handle(result); - } -} - -public record GetAllCategoriesQuery(bool ActiveOnly = false); - -public class GetServiceCategoryByIdEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapGet("/{id:guid}", GetByIdAsync) - .WithName("GetServiceCategoryById") - .WithSummary("Buscar categoria por ID") - .Produces>(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound); - - private static async Task GetByIdAsync( - Guid id, - IQueryDispatcher queryDispatcher, - CancellationToken cancellationToken) - { - var query = new GetServiceCategoryByIdQuery(id); - var result = await queryDispatcher.QueryAsync>( - query, cancellationToken); - - if (result.IsSuccess && result.Value == null) - return Results.NotFound(); - - return Handle(result); - } -} - -// ============================================================ -// UPDATE -// ============================================================ - -public class UpdateServiceCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPut("/{id:guid}", UpdateAsync) - .WithName("UpdateServiceCategory") - .WithSummary("Atualizar categoria de serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task UpdateAsync( - Guid id, - [FromBody] UpdateServiceCategoryRequest request, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new UpdateServiceCategoryCommand(id, request.Name, request.Description, request.DisplayOrder); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -// ============================================================ -// DELETE -// ============================================================ - -public class DeleteServiceCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapDelete("/{id:guid}", DeleteAsync) - .WithName("DeleteServiceCategory") - .WithSummary("Deletar categoria de serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task DeleteAsync( - Guid id, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new DeleteServiceCategoryCommand(id); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -// ============================================================ -// ACTIVATE / DEACTIVATE -// ============================================================ - -public class ActivateServiceCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/{id:guid}/activate", ActivateAsync) - .WithName("ActivateServiceCategory") - .WithSummary("Ativar categoria de serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task ActivateAsync( - Guid id, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new ActivateServiceCategoryCommand(id); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -public class DeactivateServiceCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/{id:guid}/deactivate", DeactivateAsync) - .WithName("DeactivateServiceCategory") - .WithSummary("Desativar categoria de serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task DeactivateAsync( - Guid id, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new DeactivateServiceCategoryCommand(id); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} diff --git a/src/Modules/Catalogs/API/Endpoints/ServiceEndpoints.cs b/src/Modules/Catalogs/API/Endpoints/ServiceEndpoints.cs deleted file mode 100644 index e3e7ebcbf..000000000 --- a/src/Modules/Catalogs/API/Endpoints/ServiceEndpoints.cs +++ /dev/null @@ -1,261 +0,0 @@ -using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; -using MeAjudaAi.Modules.Catalogs.Application.DTOs; -using MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests; -using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; -using MeAjudaAi.Shared.Authorization; -using MeAjudaAi.Shared.Commands; -using MeAjudaAi.Shared.Contracts; -using MeAjudaAi.Shared.Contracts.Modules.Catalogs; -using MeAjudaAi.Shared.Endpoints; -using MeAjudaAi.Shared.Functional; -using MeAjudaAi.Shared.Queries; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; - -namespace MeAjudaAi.Modules.Catalogs.API.Endpoints; - -// ============================================================ -// CREATE -// ============================================================ - -public class CreateServiceEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/", CreateAsync) - .WithName("CreateService") - .WithSummary("Criar serviço") - .Produces>(StatusCodes.Status201Created) - .RequireAdmin(); - - private static async Task CreateAsync( - [FromBody] CreateServiceRequest request, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new CreateServiceCommand(request.CategoryId, request.Name, request.Description, request.DisplayOrder); - var result = await commandDispatcher.SendAsync>(command, cancellationToken); - - if (!result.IsSuccess) - return Handle(result); - - return Handle(result, "GetServiceById", new { id = result.Value!.Id }); - } -} - -// ============================================================ -// READ -// ============================================================ - -public class GetAllServicesEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapGet("/", GetAllAsync) - .WithName("GetAllServices") - .WithSummary("Listar todos os serviços") - .Produces>>(StatusCodes.Status200OK); - - private static async Task GetAllAsync( - [AsParameters] GetAllServicesQuery query, - IQueryDispatcher queryDispatcher, - CancellationToken cancellationToken) - { - var result = await queryDispatcher.QueryAsync>>( - query, cancellationToken); - return Handle(result); - } -} - -public class GetServiceByIdEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapGet("/{id:guid}", GetByIdAsync) - .WithName("GetServiceById") - .WithSummary("Buscar serviço por ID") - .Produces>(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound); - - private static async Task GetByIdAsync( - Guid id, - IQueryDispatcher queryDispatcher, - CancellationToken cancellationToken) - { - var query = new GetServiceByIdQuery(id); - var result = await queryDispatcher.QueryAsync>(query, cancellationToken); - - if (result.IsSuccess && result.Value is null) - { - return Results.NotFound(); - } - - return Handle(result); - } -} - -public class GetServicesByCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapGet("/category/{categoryId:guid}", GetByCategoryAsync) - .WithName("GetServicesByCategory") - .WithSummary("Listar serviços por categoria") - .Produces>>(StatusCodes.Status200OK); - - private static async Task GetByCategoryAsync( - Guid categoryId, - [AsParameters] GetServicesByCategoryQuery query, - IQueryDispatcher queryDispatcher, - CancellationToken cancellationToken) - { - var queryWithCategory = query with { CategoryId = categoryId }; - var result = await queryDispatcher.QueryAsync>>(queryWithCategory, cancellationToken); - return Handle(result); - } -} - -// ============================================================ -// UPDATE -// ============================================================ - -public class UpdateServiceEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPut("/{id:guid}", UpdateAsync) - .WithName("UpdateService") - .WithSummary("Atualizar serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task UpdateAsync( - Guid id, - [FromBody] UpdateServiceRequest request, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new UpdateServiceCommand(id, request.Name, request.Description, request.DisplayOrder); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -public class ChangeServiceCategoryEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/{id:guid}/change-category", ChangeAsync) - .WithName("ChangeServiceCategory") - .WithSummary("Alterar categoria do serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task ChangeAsync( - Guid id, - [FromBody] ChangeServiceCategoryRequest request, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new ChangeServiceCategoryCommand(id, request.NewCategoryId); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -// ============================================================ -// DELETE -// ============================================================ - -public class DeleteServiceEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapDelete("/{id:guid}", DeleteAsync) - .WithName("DeleteService") - .WithSummary("Deletar serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task DeleteAsync( - Guid id, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new DeleteServiceCommand(id); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -// ============================================================ -// ACTIVATE / DEACTIVATE -// ============================================================ - -public class ActivateServiceEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/{id:guid}/activate", ActivateAsync) - .WithName("ActivateService") - .WithSummary("Ativar serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task ActivateAsync( - Guid id, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new ActivateServiceCommand(id); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -public class DeactivateServiceEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/{id:guid}/deactivate", DeactivateAsync) - .WithName("DeactivateService") - .WithSummary("Desativar serviço") - .Produces(StatusCodes.Status204NoContent) - .RequireAdmin(); - - private static async Task DeactivateAsync( - Guid id, - ICommandDispatcher commandDispatcher, - CancellationToken cancellationToken) - { - var command = new DeactivateServiceCommand(id); - var result = await commandDispatcher.SendAsync(command, cancellationToken); - return HandleNoContent(result); - } -} - -// ============================================================ -// VALIDATE -// ============================================================ - -public class ValidateServicesEndpoint : BaseEndpoint, IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/validate", ValidateAsync) - .WithName("ValidateServices") - .WithSummary("Validar múltiplos serviços") - .Produces>(StatusCodes.Status200OK) - .AllowAnonymous(); - - private static async Task ValidateAsync( - [FromBody] ValidateServicesRequest request, - [FromServices] ICatalogsModuleApi moduleApi, - CancellationToken cancellationToken) - { - var result = await moduleApi.ValidateServicesAsync(request.ServiceIds, cancellationToken); - - if (!result.IsSuccess) - return Handle(result); - - var response = new ValidateServicesResponse( - result.Value!.AllValid, - result.Value.InvalidServiceIds, - result.Value.InactiveServiceIds - ); - - return Handle(Result.Success(response)); - } -} diff --git a/src/Modules/Catalogs/Application/DTOs/Requests.cs b/src/Modules/Catalogs/Application/DTOs/Requests.cs deleted file mode 100644 index 9dcffdaaf..000000000 --- a/src/Modules/Catalogs/Application/DTOs/Requests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using MeAjudaAi.Shared.Contracts; - -namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests; - -// ============================================================================ -// SERVICE CATEGORY REQUESTS -// ============================================================================ - -public sealed record UpdateServiceCategoryRequest : Request -{ - public string Name { get; init; } = string.Empty; - public string? Description { get; init; } - public int DisplayOrder { get; init; } -} - -// ============================================================================ -// SERVICE REQUESTS -// ============================================================================ - -public sealed record CreateServiceRequest : Request -{ - public Guid CategoryId { get; init; } - public string Name { get; init; } = string.Empty; - public string? Description { get; init; } - public int DisplayOrder { get; init; } = 0; -} - -public sealed record UpdateServiceRequest : Request -{ - public string Name { get; init; } = string.Empty; - public string? Description { get; init; } - public int DisplayOrder { get; init; } -} - -public sealed record ChangeServiceCategoryRequest : Request -{ - public Guid NewCategoryId { get; init; } -} - -public sealed record ValidateServicesRequest : Request -{ - public IReadOnlyCollection ServiceIds { get; init; } = Array.Empty(); -} - -// ============================================================================ -// RESPONSES -// ============================================================================ - -public sealed record ValidateServicesResponse( - bool AllValid, - IReadOnlyCollection InvalidServiceIds, - IReadOnlyCollection InactiveServiceIds -); diff --git a/src/Modules/Catalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs b/src/Modules/Catalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs new file mode 100644 index 000000000..d8cf2bb58 --- /dev/null +++ b/src/Modules/Catalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; + +public sealed record ChangeServiceCategoryRequest : Request +{ + public Guid NewCategoryId { get; init; } +} diff --git a/src/Modules/Catalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs b/src/Modules/Catalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs new file mode 100644 index 000000000..e4970c636 --- /dev/null +++ b/src/Modules/Catalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs @@ -0,0 +1,11 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; + +public sealed record CreateServiceRequest : Request +{ + public Guid CategoryId { get; init; } + public string Name { get; init; } = string.Empty; + public string? Description { get; init; } + public int DisplayOrder { get; init; } = 0; +} diff --git a/src/Modules/Catalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs b/src/Modules/Catalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs new file mode 100644 index 000000000..99f8f3c2b --- /dev/null +++ b/src/Modules/Catalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs @@ -0,0 +1,10 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; + +public sealed record UpdateServiceRequest : Request +{ + public string Name { get; init; } = string.Empty; + public string? Description { get; init; } + public int DisplayOrder { get; init; } +} diff --git a/src/Modules/Catalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs b/src/Modules/Catalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs new file mode 100644 index 000000000..56e2db827 --- /dev/null +++ b/src/Modules/Catalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; + +public sealed record ValidateServicesRequest : Request +{ + public IReadOnlyCollection ServiceIds { get; init; } = Array.Empty(); +} diff --git a/src/Modules/Catalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs b/src/Modules/Catalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs new file mode 100644 index 000000000..48170e5fe --- /dev/null +++ b/src/Modules/Catalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs @@ -0,0 +1,7 @@ +namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.Service; + +public sealed record ValidateServicesResponse( + bool AllValid, + IReadOnlyCollection InvalidServiceIds, + IReadOnlyCollection InactiveServiceIds +); diff --git a/src/Modules/Catalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs b/src/Modules/Catalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs new file mode 100644 index 000000000..b5218c937 --- /dev/null +++ b/src/Modules/Catalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.Catalogs.Application.DTOs.Requests.ServiceCategory; + +public sealed record UpdateServiceCategoryRequest : Request +{ + [Required] + [MaxLength(100)] + public string Name { get; init; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; init; } + + [Range(0, int.MaxValue)] + public int DisplayOrder { get; init; } +} diff --git a/src/Modules/Catalogs/Application/DTOs/ServiceCategoryDto.cs b/src/Modules/Catalogs/Application/DTOs/ServiceCategoryDto.cs index c97ff411e..f3518507d 100644 --- a/src/Modules/Catalogs/Application/DTOs/ServiceCategoryDto.cs +++ b/src/Modules/Catalogs/Application/DTOs/ServiceCategoryDto.cs @@ -1,7 +1,7 @@ namespace MeAjudaAi.Modules.Catalogs.Application.DTOs; /// -/// DTO for service category information. +/// DTO para informações de categoria de serviço. /// public sealed record ServiceCategoryDto( Guid Id, diff --git a/src/Modules/Catalogs/Application/DTOs/ServiceCategoryWithCountDto.cs b/src/Modules/Catalogs/Application/DTOs/ServiceCategoryWithCountDto.cs index d870ec5ca..15467e5e4 100644 --- a/src/Modules/Catalogs/Application/DTOs/ServiceCategoryWithCountDto.cs +++ b/src/Modules/Catalogs/Application/DTOs/ServiceCategoryWithCountDto.cs @@ -1,7 +1,7 @@ namespace MeAjudaAi.Modules.Catalogs.Application.DTOs; /// -/// DTO for category with its services count. +/// DTO para categoria com a contagem de seus serviços. /// public sealed record ServiceCategoryWithCountDto( Guid Id, diff --git a/src/Modules/Catalogs/Application/DTOs/ServiceDto.cs b/src/Modules/Catalogs/Application/DTOs/ServiceDto.cs index 2be2450ba..9e288e2c4 100644 --- a/src/Modules/Catalogs/Application/DTOs/ServiceDto.cs +++ b/src/Modules/Catalogs/Application/DTOs/ServiceDto.cs @@ -1,7 +1,7 @@ namespace MeAjudaAi.Modules.Catalogs.Application.DTOs; /// -/// DTO for service information. +/// DTO para informações de serviço. /// public sealed record ServiceDto( Guid Id, diff --git a/src/Modules/Catalogs/Application/DTOs/ServiceListDto.cs b/src/Modules/Catalogs/Application/DTOs/ServiceListDto.cs index 82598b4eb..527aa0fd5 100644 --- a/src/Modules/Catalogs/Application/DTOs/ServiceListDto.cs +++ b/src/Modules/Catalogs/Application/DTOs/ServiceListDto.cs @@ -1,7 +1,7 @@ namespace MeAjudaAi.Modules.Catalogs.Application.DTOs; /// -/// Simplified DTO for service without category details (for lists). +/// DTO simplificado para serviço sem detalhes de categoria (para listas). /// public sealed record ServiceListDto( Guid Id, diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/CommandHandlers.cs b/src/Modules/Catalogs/Application/Handlers/Commands/CommandHandlers.cs deleted file mode 100644 index 1092fc40f..000000000 --- a/src/Modules/Catalogs/Application/Handlers/Commands/CommandHandlers.cs +++ /dev/null @@ -1,372 +0,0 @@ -using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; -using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Application.DTOs; -using MeAjudaAi.Modules.Catalogs.Domain.Entities; -using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; -using MeAjudaAi.Modules.Catalogs.Domain.Repositories; -using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; -using MeAjudaAi.Shared.Commands; -using MeAjudaAi.Shared.Functional; - -namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands; - -// ============================================================================ -// SERVICE CATEGORY COMMAND HANDLERS -// ============================================================================ - -public sealed class CreateServiceCategoryCommandHandler( - IServiceCategoryRepository categoryRepository) - : ICommandHandler> -{ - public async Task> HandleAsync(CreateServiceCategoryCommand request, CancellationToken cancellationToken = default) - { - try - { - // Check for duplicate name - if (await categoryRepository.ExistsWithNameAsync(request.Name, null, cancellationToken)) - return Result.Failure($"A category with name '{request.Name}' already exists."); - - var category = ServiceCategory.Create(request.Name, request.Description, request.DisplayOrder); - - await categoryRepository.AddAsync(category, cancellationToken); - - var dto = new ServiceCategoryDto( - category.Id.Value, - category.Name, - category.Description, - category.IsActive, - category.DisplayOrder, - category.CreatedAt, - category.UpdatedAt - ); - - return Result.Success(dto); - } - catch (CatalogDomainException ex) - { - return Result.Failure(ex.Message); - } - } -} -public sealed class UpdateServiceCategoryCommandHandler( - IServiceCategoryRepository categoryRepository) - : ICommandHandler -{ - public async Task HandleAsync(UpdateServiceCategoryCommand request, CancellationToken cancellationToken = default) - { - try - { - if (request.Id == Guid.Empty) - return Result.Failure("Category ID cannot be empty."); - - var categoryId = ServiceCategoryId.From(request.Id); - var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); - - if (category is null) - return Result.Failure($"Category with ID '{request.Id}' not found."); - - // Check for duplicate name (excluding current category) - if (await categoryRepository.ExistsWithNameAsync(request.Name, categoryId, cancellationToken)) - return Result.Failure($"A category with name '{request.Name}' already exists."); - - category.Update(request.Name, request.Description, request.DisplayOrder); - - await categoryRepository.UpdateAsync(category, cancellationToken); - - return Result.Success(); - } - catch (CatalogDomainException ex) - { - return Result.Failure(ex.Message); - } - } -} - -public sealed class DeleteServiceCategoryCommandHandler( - IServiceCategoryRepository categoryRepository, - IServiceRepository serviceRepository) - : ICommandHandler -{ - public async Task HandleAsync(DeleteServiceCategoryCommand request, CancellationToken cancellationToken = default) - { - if (request.Id == Guid.Empty) - return Result.Failure("Category ID cannot be empty."); - - var categoryId = ServiceCategoryId.From(request.Id); - var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); - - if (category is null) - return Result.Failure($"Category with ID '{request.Id}' not found."); - - // Check if category has services - var serviceCount = await serviceRepository.CountByCategoryAsync(categoryId, activeOnly: false, cancellationToken); - if (serviceCount > 0) - return Result.Failure($"Cannot delete category with {serviceCount} service(s). Remove or reassign services first."); - - await categoryRepository.DeleteAsync(categoryId, cancellationToken); - - return Result.Success(); - } -} - -public sealed class ActivateServiceCategoryCommandHandler( - IServiceCategoryRepository categoryRepository) - : ICommandHandler -{ - public async Task HandleAsync(ActivateServiceCategoryCommand request, CancellationToken cancellationToken = default) - { - if (request.Id == Guid.Empty) - return Result.Failure("Category ID cannot be empty."); - - var categoryId = ServiceCategoryId.From(request.Id); - var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); - - if (category is null) - return Result.Failure($"Category with ID '{request.Id}' not found."); - - category.Activate(); - - await categoryRepository.UpdateAsync(category, cancellationToken); - - return Result.Success(); - } -} - -public sealed class DeactivateServiceCategoryCommandHandler( - IServiceCategoryRepository categoryRepository) - : ICommandHandler -{ - public async Task HandleAsync(DeactivateServiceCategoryCommand request, CancellationToken cancellationToken = default) - { - if (request.Id == Guid.Empty) - return Result.Failure("Category ID cannot be empty."); - - var categoryId = ServiceCategoryId.From(request.Id); - var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); - - if (category is null) - return Result.Failure($"Category with ID '{request.Id}' not found."); - - category.Deactivate(); - - await categoryRepository.UpdateAsync(category, cancellationToken); - - return Result.Success(); - } -} - -// ============================================================================ -// SERVICE COMMAND HANDLERS -// ============================================================================ - -public sealed class CreateServiceCommandHandler( - IServiceRepository serviceRepository, - IServiceCategoryRepository categoryRepository) - : ICommandHandler> -{ - public async Task> HandleAsync(CreateServiceCommand request, CancellationToken cancellationToken = default) - { - try - { - if (request.CategoryId == Guid.Empty) - return Result.Failure("Category ID cannot be empty."); - - var categoryId = ServiceCategoryId.From(request.CategoryId); - - // Verify category exists and is active - var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); - if (category is null) - return Result.Failure($"Category with ID '{request.CategoryId}' not found."); - - if (!category.IsActive) - return Result.Failure("Cannot create service in inactive category."); - - // Check for duplicate name within the same category - if (await serviceRepository.ExistsWithNameAsync(request.Name, null, categoryId, cancellationToken)) - return Result.Failure($"A service with name '{request.Name}' already exists in this category."); - - var service = Service.Create(categoryId, request.Name, request.Description, request.DisplayOrder); - - await serviceRepository.AddAsync(service, cancellationToken); - - var dto = new ServiceDto( - service.Id.Value, - service.CategoryId.Value, - category.Name, - service.Name, - service.Description, - service.IsActive, - service.DisplayOrder, - service.CreatedAt, - service.UpdatedAt - ); - return Result.Success(dto); - } - catch (CatalogDomainException ex) - { - return Result.Failure(ex.Message); - } - } -} - -public sealed class UpdateServiceCommandHandler( - IServiceRepository serviceRepository) - : ICommandHandler -{ - public async Task HandleAsync(UpdateServiceCommand request, CancellationToken cancellationToken = default) - { - try - { - if (request.Id == Guid.Empty) - return Result.Failure("Service ID cannot be empty."); - - var serviceId = ServiceId.From(request.Id); - var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); - - if (service is null) - return Result.Failure($"Service with ID '{request.Id}' not found."); - - // Check for duplicate name within the same category (excluding current service) - if (await serviceRepository.ExistsWithNameAsync(request.Name, serviceId, service.CategoryId, cancellationToken)) - return Result.Failure($"A service with name '{request.Name}' already exists in this category."); - - service.Update(request.Name, request.Description, request.DisplayOrder); - - await serviceRepository.UpdateAsync(service, cancellationToken); - - return Result.Success(); - } - catch (CatalogDomainException ex) - { - return Result.Failure(ex.Message); - } - } -} - -public sealed class DeleteServiceCommandHandler( - IServiceRepository serviceRepository) - : ICommandHandler -{ - public async Task HandleAsync(DeleteServiceCommand request, CancellationToken cancellationToken = default) - { - if (request.Id == Guid.Empty) - return Result.Failure("Service ID cannot be empty."); - - var serviceId = ServiceId.From(request.Id); - var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); - - if (service is null) - return Result.Failure($"Service with ID '{request.Id}' not found."); - - // TODO: Check if any provider offers this service before deleting - // This requires integration with Providers module via IProvidersModuleApi - // Consider implementing: - // 1. Call IProvidersModuleApi.HasProvidersOfferingServiceAsync(serviceId) - // 2. Return failure if providers exist: "Cannot delete service: X providers offer this service" - // 3. Or implement soft-delete pattern to preserve historical data - - await serviceRepository.DeleteAsync(serviceId, cancellationToken); - - return Result.Success(); - } -} - -public sealed class ActivateServiceCommandHandler( - IServiceRepository serviceRepository) - : ICommandHandler -{ - public async Task HandleAsync(ActivateServiceCommand request, CancellationToken cancellationToken = default) - { - if (request.Id == Guid.Empty) - return Result.Failure("Service ID cannot be empty."); - - var serviceId = ServiceId.From(request.Id); - var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); - - if (service is null) - return Result.Failure($"Service with ID '{request.Id}' not found."); - - service.Activate(); - - await serviceRepository.UpdateAsync(service, cancellationToken); - - return Result.Success(); - } -} - -public sealed class DeactivateServiceCommandHandler( - IServiceRepository serviceRepository) - : ICommandHandler -{ - public async Task HandleAsync(DeactivateServiceCommand request, CancellationToken cancellationToken = default) - { - if (request.Id == Guid.Empty) - return Result.Failure("Service ID cannot be empty."); - - var serviceId = ServiceId.From(request.Id); - var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); - - if (service is null) - return Result.Failure($"Service with ID '{request.Id}' not found."); - - service.Deactivate(); - - await serviceRepository.UpdateAsync(service, cancellationToken); - - return Result.Success(); - } -} - -public sealed class ChangeServiceCategoryCommandHandler( - IServiceRepository serviceRepository, - IServiceCategoryRepository categoryRepository) - : ICommandHandler -{ - public async Task HandleAsync(ChangeServiceCategoryCommand request, CancellationToken cancellationToken = default) - { - try - { - if (request.ServiceId == Guid.Empty) - return Result.Failure("Service ID cannot be empty."); - - if (request.NewCategoryId == Guid.Empty) - return Result.Failure("New category ID cannot be empty."); - - var serviceId = ServiceId.From(request.ServiceId); - var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); - - if (service is null) - return Result.Failure($"Service with ID '{request.ServiceId}' not found."); - - var newCategoryId = ServiceCategoryId.From(request.NewCategoryId); - var newCategory = await categoryRepository.GetByIdAsync(newCategoryId, cancellationToken); - - if (newCategory is null) - return Result.Failure($"Category with ID '{request.NewCategoryId}' not found."); - - if (!newCategory.IsActive) - return Result.Failure("Cannot move service to inactive category."); - - // Ensure the name is still unique in the target category - if (await serviceRepository.ExistsWithNameAsync( - service.Name, - service.Id, - newCategoryId, - cancellationToken)) - { - return Result.Failure( - $"A service with name '{service.Name}' already exists in the target category."); - } - - service.ChangeCategory(newCategoryId); - - await serviceRepository.UpdateAsync(service, cancellationToken); - - return Result.Success(); - } - catch (CatalogDomainException ex) - { - return Result.Failure(ex.Message); - } - } -} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs new file mode 100644 index 000000000..9717cbd9f --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; + +public sealed class ActivateServiceCommandHandler( + IServiceRepository serviceRepository) + : ICommandHandler +{ + public async Task HandleAsync(ActivateServiceCommand request, CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Failure("Service ID cannot be empty."); + + var serviceId = ServiceId.From(request.Id); + var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); + + if (service is null) + return Result.Failure($"Service with ID '{request.Id}' not found."); + + service.Activate(); + + await serviceRepository.UpdateAsync(service, cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..ef82e424e --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs @@ -0,0 +1,62 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; + +public sealed class ChangeServiceCategoryCommandHandler( + IServiceRepository serviceRepository, + IServiceCategoryRepository categoryRepository) + : ICommandHandler +{ + public async Task HandleAsync(ChangeServiceCategoryCommand request, CancellationToken cancellationToken = default) + { + try + { + if (request.ServiceId == Guid.Empty) + return Result.Failure("Service ID cannot be empty."); + + if (request.NewCategoryId == Guid.Empty) + return Result.Failure("New category ID cannot be empty."); + + var serviceId = ServiceId.From(request.ServiceId); + var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); + + if (service is null) + return Result.Failure($"Service with ID '{request.ServiceId}' not found."); + + var newCategoryId = ServiceCategoryId.From(request.NewCategoryId); + var newCategory = await categoryRepository.GetByIdAsync(newCategoryId, cancellationToken); + + if (newCategory is null) + return Result.Failure($"Category with ID '{request.NewCategoryId}' not found."); + + if (!newCategory.IsActive) + return Result.Failure("Cannot move service to inactive category."); + + // Garantir que o nome ainda é único na categoria de destino + if (await serviceRepository.ExistsWithNameAsync( + service.Name, + service.Id, + newCategoryId, + cancellationToken)) + { + return Result.Failure( + $"A service with name '{service.Name}' already exists in the target category."); + } + + service.ChangeCategory(newCategoryId); + + await serviceRepository.UpdateAsync(service, cancellationToken); + + return Result.Success(); + } + catch (CatalogDomainException ex) + { + return Result.Failure(ex.Message); + } + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs new file mode 100644 index 000000000..f36da5d73 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs @@ -0,0 +1,65 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; + +public sealed class CreateServiceCommandHandler( + IServiceRepository serviceRepository, + IServiceCategoryRepository categoryRepository) + : ICommandHandler> +{ + public async Task> HandleAsync(CreateServiceCommand request, CancellationToken cancellationToken = default) + { + try + { + if (request.CategoryId == Guid.Empty) + return Result.Failure("Category ID cannot be empty."); + + var categoryId = ServiceCategoryId.From(request.CategoryId); + + // Verificar se a categoria existe e está ativa + var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); + if (category is null) + return Result.Failure($"Category with ID '{request.CategoryId}' not found."); + + if (!category.IsActive) + return Result.Failure("Cannot create service in inactive category."); + + var normalizedName = request.Name?.Trim(); + + if (string.IsNullOrWhiteSpace(normalizedName)) + return Result.Failure("Service name is required."); + + // Verificar se já existe serviço com o mesmo nome na categoria + if (await serviceRepository.ExistsWithNameAsync(normalizedName, null, categoryId, cancellationToken)) + return Result.Failure($"A service with name '{normalizedName}' already exists in this category."); + + var service = Domain.Entities.Service.Create(categoryId, normalizedName, request.Description, request.DisplayOrder); + + await serviceRepository.AddAsync(service, cancellationToken); + + var dto = new ServiceDto( + service.Id.Value, + service.CategoryId.Value, + category.Name, + service.Name, + service.Description, + service.IsActive, + service.DisplayOrder, + service.CreatedAt, + service.UpdatedAt + ); + return Result.Success(dto); + } + catch (CatalogDomainException ex) + { + return Result.Failure(ex.Message); + } + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs new file mode 100644 index 000000000..de78ac31e --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; + +public sealed class DeactivateServiceCommandHandler( + IServiceRepository serviceRepository) + : ICommandHandler +{ + public async Task HandleAsync(DeactivateServiceCommand request, CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Failure("Service ID cannot be empty."); + + var serviceId = ServiceId.From(request.Id); + var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); + + if (service is null) + return Result.Failure($"Service with ID '{request.Id}' not found."); + + service.Deactivate(); + + await serviceRepository.UpdateAsync(service, cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs new file mode 100644 index 000000000..3a0dfcecd --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs @@ -0,0 +1,35 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; + +public sealed class DeleteServiceCommandHandler( + IServiceRepository serviceRepository) + : ICommandHandler +{ + public async Task HandleAsync(DeleteServiceCommand request, CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Failure("Service ID cannot be empty."); + + var serviceId = ServiceId.From(request.Id); + var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); + + if (service is null) + return Result.Failure($"Service with ID '{request.Id}' not found."); + + // TODO: Verificar se algum provedor oferece este serviço antes de deletar + // Isso requer integração com o módulo Providers via IProvidersModuleApi + // Considerar implementar: + // 1. Chamar IProvidersModuleApi.HasProvidersOfferingServiceAsync(serviceId) + // 2. Retornar falha se existirem provedores: "Cannot delete service: X providers offer this service" + // 3. Ou implementar padrão de soft-delete para preservar dados históricos + + await serviceRepository.DeleteAsync(serviceId, cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs new file mode 100644 index 000000000..ddf858f53 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs @@ -0,0 +1,47 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; + +public sealed class UpdateServiceCommandHandler( + IServiceRepository serviceRepository) + : ICommandHandler +{ + public async Task HandleAsync(UpdateServiceCommand request, CancellationToken cancellationToken = default) + { + try + { + if (request.Id == Guid.Empty) + return Result.Failure("Service ID cannot be empty."); + + var serviceId = ServiceId.From(request.Id); + var service = await serviceRepository.GetByIdAsync(serviceId, cancellationToken); + + if (service is null) + return Result.Failure($"Service with ID '{request.Id}' not found."); + + var normalizedName = request.Name?.Trim(); + + if (string.IsNullOrWhiteSpace(normalizedName)) + return Result.Failure("Service name cannot be empty."); + + // Verificar se já existe serviço com o mesmo nome na categoria (excluindo o serviço atual) + if (await serviceRepository.ExistsWithNameAsync(normalizedName, serviceId, service.CategoryId, cancellationToken)) + return Result.Failure($"A service with name '{normalizedName}' already exists in this category."); + + service.Update(normalizedName, request.Description, request.DisplayOrder); + + await serviceRepository.UpdateAsync(service, cancellationToken); + + return Result.Success(); + } + catch (CatalogDomainException ex) + { + return Result.Failure(ex.Message); + } + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..3f4080bc2 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; + +public sealed class ActivateServiceCategoryCommandHandler( + IServiceCategoryRepository categoryRepository) + : ICommandHandler +{ + public async Task HandleAsync(ActivateServiceCategoryCommand request, CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Failure("Category ID cannot be empty."); + + var categoryId = ServiceCategoryId.From(request.Id); + var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); + + if (category is null) + return Result.Failure($"Category with ID '{request.Id}' not found."); + + category.Activate(); + + await categoryRepository.UpdateAsync(category, cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..cb1601272 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs @@ -0,0 +1,49 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; +using ServiceCategoryEntity = MeAjudaAi.Modules.Catalogs.Domain.Entities.ServiceCategory; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; + +public sealed class CreateServiceCategoryCommandHandler( + IServiceCategoryRepository categoryRepository) + : ICommandHandler> +{ + public async Task> HandleAsync(CreateServiceCategoryCommand request, CancellationToken cancellationToken = default) + { + try + { + var normalizedName = request.Name?.Trim(); + + if (string.IsNullOrWhiteSpace(normalizedName)) + return Result.Failure("Category name is required."); + + // Verificar se já existe categoria com o mesmo nome + if (await categoryRepository.ExistsWithNameAsync(normalizedName, null, cancellationToken)) + return Result.Failure($"A category with name '{normalizedName}' already exists."); + + var category = ServiceCategoryEntity.Create(normalizedName, request.Description, request.DisplayOrder); + + await categoryRepository.AddAsync(category, cancellationToken); + + var dto = new ServiceCategoryDto( + category.Id.Value, + category.Name, + category.Description, + category.IsActive, + category.DisplayOrder, + category.CreatedAt, + category.UpdatedAt + ); + + return Result.Success(dto); + } + catch (CatalogDomainException ex) + { + return Result.Failure(ex.Message); + } + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..c04fca252 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; + +public sealed class DeactivateServiceCategoryCommandHandler( + IServiceCategoryRepository categoryRepository) + : ICommandHandler +{ + public async Task HandleAsync(DeactivateServiceCategoryCommand request, CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Failure("Category ID cannot be empty."); + + var categoryId = ServiceCategoryId.From(request.Id); + var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); + + if (category is null) + return Result.Failure($"Category with ID '{request.Id}' not found."); + + category.Deactivate(); + + await categoryRepository.UpdateAsync(category, cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..277def7f1 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs @@ -0,0 +1,34 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; + +public sealed class DeleteServiceCategoryCommandHandler( + IServiceCategoryRepository categoryRepository, + IServiceRepository serviceRepository) + : ICommandHandler +{ + public async Task HandleAsync(DeleteServiceCategoryCommand request, CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Failure("Category ID cannot be empty."); + + var categoryId = ServiceCategoryId.From(request.Id); + var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); + + if (category is null) + return Result.Failure($"Category with ID '{request.Id}' not found."); + + // Verificar se a categoria possui serviços + var serviceCount = await serviceRepository.CountByCategoryAsync(categoryId, activeOnly: false, cancellationToken); + if (serviceCount > 0) + return Result.Failure($"Cannot delete category with {serviceCount} service(s). Remove or reassign services first."); + + await categoryRepository.DeleteAsync(categoryId, cancellationToken); + + return Result.Success(); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..2f3bf7df0 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs @@ -0,0 +1,47 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; + +public sealed class UpdateServiceCategoryCommandHandler( + IServiceCategoryRepository categoryRepository) + : ICommandHandler +{ + public async Task HandleAsync(UpdateServiceCategoryCommand request, CancellationToken cancellationToken = default) + { + try + { + if (request.Id == Guid.Empty) + return Result.Failure("Category ID cannot be empty."); + + var categoryId = ServiceCategoryId.From(request.Id); + var category = await categoryRepository.GetByIdAsync(categoryId, cancellationToken); + + if (category is null) + return Result.Failure($"Category with ID '{request.Id}' not found."); + + var normalizedName = request.Name?.Trim(); + + if (string.IsNullOrWhiteSpace(normalizedName)) + return Result.Failure("Category name cannot be empty."); + + // Verificar se já existe categoria com o mesmo nome (excluindo a categoria atual) + if (await categoryRepository.ExistsWithNameAsync(normalizedName, categoryId, cancellationToken)) + return Result.Failure($"A category with name '{normalizedName}' already exists."); + + category.Update(normalizedName, request.Description, request.DisplayOrder); + + await categoryRepository.UpdateAsync(category, cancellationToken); + + return Result.Success(); + } + catch (CatalogDomainException ex) + { + return Result.Failure(ex.Message); + } + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/QueryHandlers.cs b/src/Modules/Catalogs/Application/Handlers/Queries/QueryHandlers.cs deleted file mode 100644 index 50fa1b34e..000000000 --- a/src/Modules/Catalogs/Application/Handlers/Queries/QueryHandlers.cs +++ /dev/null @@ -1,185 +0,0 @@ -using MeAjudaAi.Modules.Catalogs.Application.DTOs; -using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; -using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Domain.Repositories; -using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; -using MeAjudaAi.Shared.Functional; -using MeAjudaAi.Shared.Queries; - -namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; - -// ============================================================================ -// SERVICE QUERY HANDLERS -// ============================================================================ - -public sealed class GetServiceByIdQueryHandler(IServiceRepository repository) - : IQueryHandler> -{ - public async Task> HandleAsync( - GetServiceByIdQuery request, - CancellationToken cancellationToken = default) - { - var serviceId = ServiceId.From(request.Id); - var service = await repository.GetByIdAsync(serviceId, cancellationToken); - - if (service is null) - return Result.Success(null); - - // Note: Category navigation property should be loaded by repository - var categoryName = service.Category?.Name ?? "Unknown"; - - var dto = new ServiceDto( - service.Id.Value, - service.CategoryId.Value, - categoryName, - service.Name, - service.Description, - service.IsActive, - service.DisplayOrder, - service.CreatedAt, - service.UpdatedAt - ); - - return Result.Success(dto); - } -} - -public sealed class GetAllServicesQueryHandler(IServiceRepository repository) - : IQueryHandler>> -{ - public async Task>> HandleAsync( - GetAllServicesQuery request, - CancellationToken cancellationToken = default) - { - var services = await repository.GetAllAsync(request.ActiveOnly, cancellationToken); - - var dtos = services.Select(s => new ServiceListDto( - s.Id.Value, - s.CategoryId.Value, - s.Name, - s.Description, - s.IsActive - )).ToList(); - - return Result>.Success(dtos); - } -} - -public sealed class GetServicesByCategoryQueryHandler(IServiceRepository repository) - : IQueryHandler>> -{ - public async Task>> HandleAsync( - GetServicesByCategoryQuery request, - CancellationToken cancellationToken = default) - { - var categoryId = ServiceCategoryId.From(request.CategoryId); - var services = await repository.GetByCategoryAsync(categoryId, request.ActiveOnly, cancellationToken); - - var dtos = services.Select(s => new ServiceListDto( - s.Id.Value, - s.CategoryId.Value, - s.Name, - s.Description, - s.IsActive - )).ToList(); - - return Result>.Success(dtos); - } -} - -// ============================================================================ -// SERVICE CATEGORY QUERY HANDLERS -// ============================================================================ - -public sealed class GetServiceCategoryByIdQueryHandler(IServiceCategoryRepository repository) - : IQueryHandler> -{ - public async Task> HandleAsync( - GetServiceCategoryByIdQuery request, - CancellationToken cancellationToken = default) - { - var categoryId = ServiceCategoryId.From(request.Id); - var category = await repository.GetByIdAsync(categoryId, cancellationToken); - - if (category is null) - return Result.Success(null); - - var dto = new ServiceCategoryDto( - category.Id.Value, - category.Name, - category.Description, - category.IsActive, - category.DisplayOrder, - category.CreatedAt, - category.UpdatedAt - ); - - return Result.Success(dto); - } -} - -public sealed class GetAllServiceCategoriesQueryHandler(IServiceCategoryRepository repository) - : IQueryHandler>> -{ - public async Task>> HandleAsync( - GetAllServiceCategoriesQuery request, - CancellationToken cancellationToken = default) - { - var categories = await repository.GetAllAsync(request.ActiveOnly, cancellationToken); - - var dtos = categories.Select(c => new ServiceCategoryDto( - c.Id.Value, - c.Name, - c.Description, - c.IsActive, - c.DisplayOrder, - c.CreatedAt, - c.UpdatedAt - )).ToList(); - - return Result>.Success(dtos); - } -} - -public sealed class GetServiceCategoriesWithCountQueryHandler( - IServiceCategoryRepository categoryRepository, - IServiceRepository serviceRepository) - : IQueryHandler>> -{ - public async Task>> HandleAsync( - GetServiceCategoriesWithCountQuery request, - CancellationToken cancellationToken = default) - { - var categories = await categoryRepository.GetAllAsync(request.ActiveOnly, cancellationToken); - - var dtos = new List(); - - // NOTE: This performs 2 * N count queries (one for total, one for active per category). - // For small-to-medium catalogs this is acceptable. If this becomes a performance bottleneck - // with many categories, consider optimizing with a batched query or grouping in the repository. - foreach (var category in categories) - { - var totalCount = await serviceRepository.CountByCategoryAsync( - category.Id, - activeOnly: false, - cancellationToken); - - var activeCount = await serviceRepository.CountByCategoryAsync( - category.Id, - activeOnly: true, - cancellationToken); - - dtos.Add(new ServiceCategoryWithCountDto( - category.Id.Value, - category.Name, - category.Description, - category.IsActive, - category.DisplayOrder, - activeCount, - totalCount - )); - } - - return Result>.Success(dtos); - } -} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs b/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs new file mode 100644 index 000000000..3248b9c66 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs @@ -0,0 +1,28 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; + +public sealed class GetAllServicesQueryHandler(IServiceRepository repository) + : IQueryHandler>> +{ + public async Task>> HandleAsync( + GetAllServicesQuery request, + CancellationToken cancellationToken = default) + { + var services = await repository.GetAllAsync(request.ActiveOnly, cancellationToken); + + var dtos = services.Select(s => new ServiceListDto( + s.Id.Value, + s.CategoryId.Value, + s.Name, + s.Description, + s.IsActive + )).ToList(); + + return Result>.Success(dtos); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs b/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs new file mode 100644 index 000000000..344464a76 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs @@ -0,0 +1,44 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Constants; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; + +public sealed class GetServiceByIdQueryHandler(IServiceRepository repository) + : IQueryHandler> +{ + public async Task> HandleAsync( + GetServiceByIdQuery request, + CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Success(null); + + var serviceId = ServiceId.From(request.Id); + var service = await repository.GetByIdAsync(serviceId, cancellationToken); + + if (service is null) + return Result.Success(null); + + // Nota: A propriedade de navegação Category deve ser carregada pelo repositório + var categoryName = service.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName; + + var dto = new ServiceDto( + service.Id.Value, + service.CategoryId.Value, + categoryName, + service.Name, + service.Description, + service.IsActive, + service.DisplayOrder, + service.CreatedAt, + service.UpdatedAt + ); + + return Result.Success(dto); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs b/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs new file mode 100644 index 000000000..4a44b6384 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs @@ -0,0 +1,33 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; + +public sealed class GetServicesByCategoryQueryHandler(IServiceRepository repository) + : IQueryHandler>> +{ + public async Task>> HandleAsync( + GetServicesByCategoryQuery request, + CancellationToken cancellationToken = default) + { + if (request.CategoryId == Guid.Empty) + return Result>.Success(Array.Empty()); + + var categoryId = ServiceCategoryId.From(request.CategoryId); + var services = await repository.GetByCategoryAsync(categoryId, request.ActiveOnly, cancellationToken); + + var dtos = services.Select(s => new ServiceListDto( + s.Id.Value, + s.CategoryId.Value, + s.Name, + s.Description, + s.IsActive + )).ToList(); + + return Result>.Success(dtos); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs b/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs new file mode 100644 index 000000000..b42ff2e66 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; + +public sealed class GetAllServiceCategoriesQueryHandler(IServiceCategoryRepository repository) + : IQueryHandler>> +{ + public async Task>> HandleAsync( + GetAllServiceCategoriesQuery request, + CancellationToken cancellationToken = default) + { + var categories = await repository.GetAllAsync(request.ActiveOnly, cancellationToken); + + var dtos = categories.Select(c => new ServiceCategoryDto( + c.Id.Value, + c.Name, + c.Description, + c.IsActive, + c.DisplayOrder, + c.CreatedAt, + c.UpdatedAt + )).ToList(); + + return Result>.Success(dtos); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs b/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs new file mode 100644 index 000000000..f4955012b --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs @@ -0,0 +1,50 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; + +public sealed class GetServiceCategoriesWithCountQueryHandler( + IServiceCategoryRepository categoryRepository, + IServiceRepository serviceRepository) + : IQueryHandler>> +{ + public async Task>> HandleAsync( + GetServiceCategoriesWithCountQuery request, + CancellationToken cancellationToken = default) + { + var categories = await categoryRepository.GetAllAsync(request.ActiveOnly, cancellationToken); + + var dtos = new List(); + + // NOTA: Isso executa 2 * N consultas de contagem (uma para total, uma para ativo por categoria). + // Para catálogos pequenos a médios isso é aceitável. Se isso se tornar um gargalo de performance + // com muitas categorias, considere otimizar com uma consulta em lote ou agrupamento no repositório. + foreach (var category in categories) + { + var totalCount = await serviceRepository.CountByCategoryAsync( + category.Id, + activeOnly: false, + cancellationToken); + + var activeCount = await serviceRepository.CountByCategoryAsync( + category.Id, + activeOnly: true, + cancellationToken); + + dtos.Add(new ServiceCategoryWithCountDto( + category.Id.Value, + category.Name, + category.Description, + category.IsActive, + category.DisplayOrder, + activeCount, + totalCount + )); + } + + return Result>.Success(dtos); + } +} diff --git a/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs b/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs new file mode 100644 index 000000000..67f4ad931 --- /dev/null +++ b/src/Modules/Catalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs @@ -0,0 +1,38 @@ +using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; + +public sealed class GetServiceCategoryByIdQueryHandler(IServiceCategoryRepository repository) + : IQueryHandler> +{ + public async Task> HandleAsync( + GetServiceCategoryByIdQuery request, + CancellationToken cancellationToken = default) + { + if (request.Id == Guid.Empty) + return Result.Success(null); + + var categoryId = ServiceCategoryId.From(request.Id); + var category = await repository.GetByIdAsync(categoryId, cancellationToken); + + if (category is null) + return Result.Success(null); + + var dto = new ServiceCategoryDto( + category.Id.Value, + category.Name, + category.Description, + category.IsActive, + category.DisplayOrder, + category.CreatedAt, + category.UpdatedAt + ); + + return Result.Success(dto); + } +} diff --git a/src/Modules/Catalogs/Application/ModuleApi/CatalogsModuleApi.cs b/src/Modules/Catalogs/Application/ModuleApi/CatalogsModuleApi.cs index f6d51dc17..22d6a9cd3 100644 --- a/src/Modules/Catalogs/Application/ModuleApi/CatalogsModuleApi.cs +++ b/src/Modules/Catalogs/Application/ModuleApi/CatalogsModuleApi.cs @@ -2,6 +2,7 @@ using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Constants; using MeAjudaAi.Shared.Contracts.Modules; using MeAjudaAi.Shared.Contracts.Modules.Catalogs; using MeAjudaAi.Shared.Contracts.Modules.Catalogs.DTOs; @@ -14,7 +15,7 @@ namespace MeAjudaAi.Modules.Catalogs.Application.ModuleApi; /// -/// Implementation of the public API for the Catalogs module. +/// Implementação da API pública para o módulo Catalogs. /// [ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)] public sealed class CatalogsModuleApi( @@ -28,8 +29,6 @@ private static class ModuleMetadata public const string Version = "1.0"; } - private const string UnknownCategoryName = "Unknown"; - public string ModuleName => ModuleMetadata.Name; public string ApiVersion => ModuleMetadata.Version; @@ -129,7 +128,7 @@ public async Task>> GetAllService if (service is null) return Result.Success(null); - var categoryName = service.Category?.Name ?? UnknownCategoryName; + var categoryName = service.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName; var dto = new ModuleServiceDto( service.Id.Value, @@ -189,7 +188,7 @@ public async Task>> GetServicesByCategory var dtos = services.Select(s => new ModuleServiceDto( s.Id.Value, s.CategoryId.Value, - s.Category?.Name ?? UnknownCategoryName, + s.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName, s.Name, s.Description, s.IsActive diff --git a/src/Modules/Catalogs/Domain/Entities/Service.cs b/src/Modules/Catalogs/Domain/Entities/Service.cs index 0e3e272c1..522b3d200 100644 --- a/src/Modules/Catalogs/Domain/Entities/Service.cs +++ b/src/Modules/Catalogs/Domain/Entities/Service.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Domain.Events; +using MeAjudaAi.Modules.Catalogs.Domain.Events.Service; using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; using MeAjudaAi.Shared.Constants; @@ -7,34 +7,34 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.Entities; /// -/// Represents a specific service that providers can offer (e.g., "Limpeza de Apartamento", "Conserto de Torneira"). -/// Services belong to a category and can be activated/deactivated by administrators. +/// Representa um serviço específico que provedores podem oferecer (ex: "Limpeza de Apartamento", "Conserto de Torneira"). +/// Serviços pertencem a uma categoria e podem ser ativados/desativados por administradores. /// public sealed class Service : AggregateRoot { /// - /// ID of the category this service belongs to. + /// ID da categoria à qual este serviço pertence. /// public ServiceCategoryId CategoryId { get; private set; } = null!; /// - /// Name of the service. + /// Nome do serviço. /// public string Name { get; private set; } = string.Empty; /// - /// Optional description explaining what this service includes. + /// Descrição opcional explicando o que este serviço inclui. /// public string? Description { get; private set; } /// - /// Indicates if this service is currently active and available for providers to offer. - /// Deactivated services are hidden from the catalog. + /// Indica se este serviço está atualmente ativo e disponível para provedores oferecerem. + /// Serviços desativados são ocultados do catálogo. /// public bool IsActive { get; private set; } /// - /// Optional display order within the category for UI sorting. + /// Ordem de exibição opcional dentro da categoria para ordenação na UI. /// public int DisplayOrder { get; private set; } @@ -45,13 +45,13 @@ public sealed class Service : AggregateRoot private Service() { } /// - /// Creates a new service within a category. + /// Cria um novo serviço dentro de uma categoria. /// - /// ID of the parent category - /// Service name (required, 1-150 characters) - /// Optional service description (max 1000 characters) - /// Display order for sorting (default: 0) - /// Thrown when validation fails + /// ID da categoria pai + /// Nome do serviço (obrigatório, 1-150 caracteres) + /// Descrição opcional do serviço (máx 1000 caracteres) + /// Ordem de exibição para ordenação (padrão: 0) + /// Lançada quando a validação falha public static Service Create(ServiceCategoryId categoryId, string name, string? description = null, int displayOrder = 0) { if (categoryId is null) @@ -76,7 +76,7 @@ public static Service Create(ServiceCategoryId categoryId, string name, string? } /// - /// Updates the service information. + /// Atualiza as informações do serviço. /// public void Update(string name, string? description = null, int displayOrder = 0) { @@ -93,7 +93,7 @@ public void Update(string name, string? description = null, int displayOrder = 0 } /// - /// Changes the category of this service. + /// Altera a categoria deste serviço. /// public void ChangeCategory(ServiceCategoryId newCategoryId) { @@ -105,13 +105,14 @@ public void ChangeCategory(ServiceCategoryId newCategoryId) var oldCategoryId = CategoryId; CategoryId = newCategoryId; + Category = null; // Invalidar navegação para forçar recarga quando necessário MarkAsUpdated(); AddDomainEvent(new ServiceCategoryChangedDomainEvent(Id, oldCategoryId, newCategoryId)); } /// - /// Activates the service, making it available in the catalog. + /// Ativa o serviço, tornando-o disponível no catálogo. /// public void Activate() { @@ -123,8 +124,8 @@ public void Activate() } /// - /// Deactivates the service, removing it from the catalog. - /// Providers who currently offer this service retain it, but new assignments are prevented. + /// Desativa o serviço, removendo-o do catálogo. + /// Provedores que atualmente oferecem este serviço o mantêm, mas novas atribuições são impedidas. /// public void Deactivate() { diff --git a/src/Modules/Catalogs/Domain/Entities/ServiceCategory.cs b/src/Modules/Catalogs/Domain/Entities/ServiceCategory.cs index 98f660047..3ba8b1dcc 100644 --- a/src/Modules/Catalogs/Domain/Entities/ServiceCategory.cs +++ b/src/Modules/Catalogs/Domain/Entities/ServiceCategory.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Domain.Events; +using MeAjudaAi.Modules.Catalogs.Domain.Events.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Exceptions; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; using MeAjudaAi.Shared.Constants; @@ -7,29 +7,29 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.Entities; /// -/// Represents a service category in the catalog (e.g., "Limpeza", "Reparos"). -/// Categories organize services into logical groups for easier discovery. +/// Representa uma categoria de serviço no catálogo (ex: "Limpeza", "Reparos"). +/// Categorias organizam serviços em grupos lógicos para facilitar a descoberta. /// public sealed class ServiceCategory : AggregateRoot { /// - /// Name of the category. + /// Nome da categoria. /// public string Name { get; private set; } = string.Empty; /// - /// Optional description explaining what services belong to this category. + /// Descrição opcional explicando quais serviços pertencem a esta categoria. /// public string? Description { get; private set; } /// - /// Indicates if this category is currently active and available for use. - /// Deactivated categories cannot be assigned to new services. + /// Indica se esta categoria está atualmente ativa e disponível para uso. + /// Categorias desativadas não podem ser atribuídas a novos serviços. /// public bool IsActive { get; private set; } /// - /// Optional display order for UI sorting. + /// Ordem de exibição opcional para ordenação na UI. /// public int DisplayOrder { get; private set; } @@ -37,12 +37,12 @@ public sealed class ServiceCategory : AggregateRoot private ServiceCategory() { } /// - /// Creates a new service category. + /// Cria uma nova categoria de serviço. /// - /// Category name (required, 1-100 characters) - /// Optional category description (max 500 characters) - /// Display order for sorting (default: 0) - /// Thrown when validation fails + /// Nome da categoria (obrigatório, 1-100 caracteres) + /// Descrição opcional da categoria (máx 500 caracteres) + /// Ordem de exibição para ordenação (padrão: 0) + /// Lançada quando a validação falha public static ServiceCategory Create(string name, string? description = null, int displayOrder = 0) { ValidateName(name); @@ -63,7 +63,7 @@ public static ServiceCategory Create(string name, string? description = null, in } /// - /// Updates the category information. + /// Atualiza as informações da categoria. /// public void Update(string name, string? description = null, int displayOrder = 0) { @@ -80,7 +80,7 @@ public void Update(string name, string? description = null, int displayOrder = 0 } /// - /// Activates the category, making it available for use. + /// Ativa a categoria, tornando-a disponível para uso. /// public void Activate() { @@ -92,8 +92,8 @@ public void Activate() } /// - /// Deactivates the category, preventing it from being assigned to new services. - /// Existing services retain their category assignment. + /// Desativa a categoria, impedindo que seja atribuída a novos serviços. + /// Serviços existentes mantêm sua atribuição de categoria. /// public void Deactivate() { diff --git a/src/Modules/Catalogs/Domain/Events/Service/ServiceActivatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/Service/ServiceActivatedDomainEvent.cs new file mode 100644 index 000000000..17366188c --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/Service/ServiceActivatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.Service; + +public sealed record ServiceActivatedDomainEvent(ServiceId ServiceId) + : DomainEvent(ServiceId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/Service/ServiceCategoryChangedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/Service/ServiceCategoryChangedDomainEvent.cs new file mode 100644 index 000000000..e892f445d --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/Service/ServiceCategoryChangedDomainEvent.cs @@ -0,0 +1,10 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.Service; + +public sealed record ServiceCategoryChangedDomainEvent( + ServiceId ServiceId, + ServiceCategoryId OldCategoryId, + ServiceCategoryId NewCategoryId) + : DomainEvent(ServiceId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/Service/ServiceCreatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/Service/ServiceCreatedDomainEvent.cs new file mode 100644 index 000000000..12a975a9e --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/Service/ServiceCreatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.Service; + +public sealed record ServiceCreatedDomainEvent(ServiceId ServiceId, ServiceCategoryId CategoryId) + : DomainEvent(ServiceId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/Service/ServiceDeactivatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/Service/ServiceDeactivatedDomainEvent.cs new file mode 100644 index 000000000..8478b712c --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/Service/ServiceDeactivatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.Service; + +public sealed record ServiceDeactivatedDomainEvent(ServiceId ServiceId) + : DomainEvent(ServiceId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/Service/ServiceUpdatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/Service/ServiceUpdatedDomainEvent.cs new file mode 100644 index 000000000..5e88dde98 --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/Service/ServiceUpdatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.Service; + +public sealed record ServiceUpdatedDomainEvent(ServiceId ServiceId) + : DomainEvent(ServiceId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryActivatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryActivatedDomainEvent.cs new file mode 100644 index 000000000..b3b6f4f4e --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryActivatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.ServiceCategory; + +public sealed record ServiceCategoryActivatedDomainEvent(ServiceCategoryId CategoryId) + : DomainEvent(CategoryId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryCreatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryCreatedDomainEvent.cs new file mode 100644 index 000000000..fe35ecd91 --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryCreatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.ServiceCategory; + +public sealed record ServiceCategoryCreatedDomainEvent(ServiceCategoryId CategoryId) + : DomainEvent(CategoryId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryDeactivatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryDeactivatedDomainEvent.cs new file mode 100644 index 000000000..562486e0d --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryDeactivatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.ServiceCategory; + +public sealed record ServiceCategoryDeactivatedDomainEvent(ServiceCategoryId CategoryId) + : DomainEvent(CategoryId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryUpdatedDomainEvent.cs b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryUpdatedDomainEvent.cs new file mode 100644 index 000000000..8d06ff3c6 --- /dev/null +++ b/src/Modules/Catalogs/Domain/Events/ServiceCategory/ServiceCategoryUpdatedDomainEvent.cs @@ -0,0 +1,7 @@ +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Catalogs.Domain.Events.ServiceCategory; + +public sealed record ServiceCategoryUpdatedDomainEvent(ServiceCategoryId CategoryId) + : DomainEvent(CategoryId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/ServiceCategoryDomainEvents.cs b/src/Modules/Catalogs/Domain/Events/ServiceCategoryDomainEvents.cs deleted file mode 100644 index b3b0b01af..000000000 --- a/src/Modules/Catalogs/Domain/Events/ServiceCategoryDomainEvents.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; -using MeAjudaAi.Shared.Events; - -namespace MeAjudaAi.Modules.Catalogs.Domain.Events; - -public sealed record ServiceCategoryCreatedDomainEvent(ServiceCategoryId CategoryId) - : DomainEvent(CategoryId.Value, Version: 1); - -public sealed record ServiceCategoryUpdatedDomainEvent(ServiceCategoryId CategoryId) - : DomainEvent(CategoryId.Value, Version: 1); - -public sealed record ServiceCategoryActivatedDomainEvent(ServiceCategoryId CategoryId) - : DomainEvent(CategoryId.Value, Version: 1); - -public sealed record ServiceCategoryDeactivatedDomainEvent(ServiceCategoryId CategoryId) - : DomainEvent(CategoryId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Events/ServiceDomainEvents.cs b/src/Modules/Catalogs/Domain/Events/ServiceDomainEvents.cs deleted file mode 100644 index cd6fe5a7f..000000000 --- a/src/Modules/Catalogs/Domain/Events/ServiceDomainEvents.cs +++ /dev/null @@ -1,22 +0,0 @@ -using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; -using MeAjudaAi.Shared.Events; - -namespace MeAjudaAi.Modules.Catalogs.Domain.Events; - -public sealed record ServiceCreatedDomainEvent(ServiceId ServiceId, ServiceCategoryId CategoryId) - : DomainEvent(ServiceId.Value, Version: 1); - -public sealed record ServiceUpdatedDomainEvent(ServiceId ServiceId) - : DomainEvent(ServiceId.Value, Version: 1); - -public sealed record ServiceActivatedDomainEvent(ServiceId ServiceId) - : DomainEvent(ServiceId.Value, Version: 1); - -public sealed record ServiceDeactivatedDomainEvent(ServiceId ServiceId) - : DomainEvent(ServiceId.Value, Version: 1); - -public sealed record ServiceCategoryChangedDomainEvent( - ServiceId ServiceId, - ServiceCategoryId OldCategoryId, - ServiceCategoryId NewCategoryId) - : DomainEvent(ServiceId.Value, Version: 1); diff --git a/src/Modules/Catalogs/Domain/Exceptions/CatalogDomainException.cs b/src/Modules/Catalogs/Domain/Exceptions/CatalogDomainException.cs index 98891096a..33b59c364 100644 --- a/src/Modules/Catalogs/Domain/Exceptions/CatalogDomainException.cs +++ b/src/Modules/Catalogs/Domain/Exceptions/CatalogDomainException.cs @@ -1,7 +1,7 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.Exceptions; /// -/// Exception thrown when a domain rule is violated in the Catalogs module. +/// Exceção lançada quando uma regra de domínio é violada no módulo Catalogs. /// public sealed class CatalogDomainException : Exception { diff --git a/src/Modules/Catalogs/Domain/Repositories/IServiceCategoryRepository.cs b/src/Modules/Catalogs/Domain/Repositories/IServiceCategoryRepository.cs index 84c1a7122..8c9518787 100644 --- a/src/Modules/Catalogs/Domain/Repositories/IServiceCategoryRepository.cs +++ b/src/Modules/Catalogs/Domain/Repositories/IServiceCategoryRepository.cs @@ -4,44 +4,57 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.Repositories; /// -/// Repository contract for ServiceCategory aggregate. +/// Contrato de repositório para o agregado ServiceCategory. /// public interface IServiceCategoryRepository { /// - /// Retrieves a service category by its ID. + /// Recupera uma categoria de serviço por seu ID. /// + /// ID da categoria de serviço + /// Token de cancelamento para operações assíncronas Task GetByIdAsync(ServiceCategoryId id, CancellationToken cancellationToken = default); /// - /// Retrieves a service category by its name. + /// Recupera uma categoria de serviço por seu nome. /// + /// Nome da categoria de serviço + /// Token de cancelamento para operações assíncronas Task GetByNameAsync(string name, CancellationToken cancellationToken = default); /// - /// Retrieves all service categories. + /// Recupera todas as categorias de serviço. /// - /// If true, returns only active categories - /// + /// Se verdadeiro, retorna apenas categorias ativas + /// Token de cancelamento para operações assíncronas Task> GetAllAsync(bool activeOnly = false, CancellationToken cancellationToken = default); /// - /// Checks if a category with the given name already exists. + /// Verifica se já existe uma categoria com o nome fornecido. /// + /// Nome da categoria a verificar + /// ID opcional da categoria a excluir da verificação + /// Token de cancelamento para operações assíncronas Task ExistsWithNameAsync(string name, ServiceCategoryId? excludeId = null, CancellationToken cancellationToken = default); /// - /// Adds a new service category. + /// Adiciona uma nova categoria de serviço. /// + /// Categoria de serviço a ser adicionada + /// Token de cancelamento para operações assíncronas Task AddAsync(ServiceCategory category, CancellationToken cancellationToken = default); /// - /// Updates an existing service category. + /// Atualiza uma categoria de serviço existente. /// + /// Categoria de serviço a ser atualizada + /// Token de cancelamento para operações assíncronas Task UpdateAsync(ServiceCategory category, CancellationToken cancellationToken = default); /// - /// Deletes a service category by its ID (hard delete - use with caution). + /// Deleta uma categoria de serviço por seu ID (exclusão física - usar com cautela). /// + /// ID da categoria de serviço a ser deletada + /// Token de cancelamento para operações assíncronas Task DeleteAsync(ServiceCategoryId id, CancellationToken cancellationToken = default); } diff --git a/src/Modules/Catalogs/Domain/Repositories/IServiceRepository.cs b/src/Modules/Catalogs/Domain/Repositories/IServiceRepository.cs index 3ee8e34e0..47aed884b 100644 --- a/src/Modules/Catalogs/Domain/Repositories/IServiceRepository.cs +++ b/src/Modules/Catalogs/Domain/Repositories/IServiceRepository.cs @@ -4,66 +4,81 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.Repositories; /// -/// Repository contract for Service aggregate. +/// Contrato de repositório para o agregado Service. /// public interface IServiceRepository { /// - /// Retrieves a service by its ID. + /// Recupera um serviço por seu ID. /// + /// ID do serviço + /// Token de cancelamento para operações assíncronas Task GetByIdAsync(ServiceId id, CancellationToken cancellationToken = default); /// - /// Retrieves multiple services by their IDs (batch query). + /// Recupera múltiplos serviços por seus IDs (consulta em lote). /// + /// Coleção de IDs de serviços + /// Token de cancelamento para operações assíncronas Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default); /// - /// Retrieves a service by its name. + /// Recupera um serviço por seu nome. /// + /// Nome do serviço + /// Token de cancelamento para operações assíncronas Task GetByNameAsync(string name, CancellationToken cancellationToken = default); /// - /// Retrieves all services. + /// Recupera todos os serviços. /// - /// If true, returns only active services - /// + /// Se verdadeiro, retorna apenas serviços ativos + /// Token de cancelamento para operações assíncronas Task> GetAllAsync(bool activeOnly = false, CancellationToken cancellationToken = default); /// - /// Retrieves all services in a specific category. + /// Recupera todos os serviços de uma categoria específica. /// - /// ID of the category - /// If true, returns only active services - /// + /// ID da categoria + /// Se verdadeiro, retorna apenas serviços ativos + /// Token de cancelamento para operações assíncronas Task> GetByCategoryAsync(ServiceCategoryId categoryId, bool activeOnly = false, CancellationToken cancellationToken = default); /// - /// Checks if a service with the given name already exists. + /// Verifica se já existe um serviço com o nome fornecido. /// - /// The service name to check - /// Optional service ID to exclude from the check - /// Optional category ID to scope the check to a specific category - /// + /// O nome do serviço a verificar + /// ID opcional do serviço a excluir da verificação + /// ID opcional da categoria para restringir a verificação a uma categoria específica + /// Token de cancelamento para operações assíncronas Task ExistsWithNameAsync(string name, ServiceId? excludeId = null, ServiceCategoryId? categoryId = null, CancellationToken cancellationToken = default); /// - /// Counts how many services exist in a category. + /// Conta quantos serviços existem em uma categoria. /// + /// ID da categoria + /// Se verdadeiro, conta apenas serviços ativos + /// Token de cancelamento para operações assíncronas Task CountByCategoryAsync(ServiceCategoryId categoryId, bool activeOnly = false, CancellationToken cancellationToken = default); /// - /// Adds a new service. + /// Adiciona um novo serviço. /// + /// Serviço a ser adicionado + /// Token de cancelamento para operações assíncronas Task AddAsync(Service service, CancellationToken cancellationToken = default); /// - /// Updates an existing service. + /// Atualiza um serviço existente. /// + /// Serviço a ser atualizado + /// Token de cancelamento para operações assíncronas Task UpdateAsync(Service service, CancellationToken cancellationToken = default); /// - /// Deletes a service by its ID (hard delete - use with caution). + /// Deleta um serviço por seu ID (exclusão física - usar com cautela). /// + /// ID do serviço a ser deletado + /// Token de cancelamento para operações assíncronas Task DeleteAsync(ServiceId id, CancellationToken cancellationToken = default); } diff --git a/src/Modules/Catalogs/Domain/ValueObjects/ServiceCategoryId.cs b/src/Modules/Catalogs/Domain/ValueObjects/ServiceCategoryId.cs index 4204e8cd2..a4bf29c3d 100644 --- a/src/Modules/Catalogs/Domain/ValueObjects/ServiceCategoryId.cs +++ b/src/Modules/Catalogs/Domain/ValueObjects/ServiceCategoryId.cs @@ -4,7 +4,7 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; /// -/// Strongly-typed identifier for ServiceCategory aggregate. +/// Identificador fortemente tipado para o agregado ServiceCategory. /// public class ServiceCategoryId : ValueObject { diff --git a/src/Modules/Catalogs/Domain/ValueObjects/ServiceId.cs b/src/Modules/Catalogs/Domain/ValueObjects/ServiceId.cs index fc1ed028d..9de565f3c 100644 --- a/src/Modules/Catalogs/Domain/ValueObjects/ServiceId.cs +++ b/src/Modules/Catalogs/Domain/ValueObjects/ServiceId.cs @@ -4,7 +4,7 @@ namespace MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; /// -/// Strongly-typed identifier for Service aggregate. +/// Identificador fortemente tipado para o agregado Service. /// public class ServiceId : ValueObject { diff --git a/src/Modules/Catalogs/Infrastructure/Extensions.cs b/src/Modules/Catalogs/Infrastructure/Extensions.cs index 343405c74..495791c4c 100644 --- a/src/Modules/Catalogs/Infrastructure/Extensions.cs +++ b/src/Modules/Catalogs/Infrastructure/Extensions.cs @@ -1,8 +1,10 @@ using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Application.DTOs; -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands; -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; @@ -22,7 +24,7 @@ namespace MeAjudaAi.Modules.Catalogs.Infrastructure; public static class Extensions { /// - /// Adds Catalogs module infrastructure services. + /// Adiciona os serviços de infraestrutura do módulo Catalogs. /// public static IServiceCollection AddCatalogsInfrastructure( this IServiceCollection services, diff --git a/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContext.cs b/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContext.cs index d3395049d..eaa3d2add 100644 --- a/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContext.cs +++ b/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContext.cs @@ -5,7 +5,7 @@ namespace MeAjudaAi.Modules.Catalogs.Infrastructure.Persistence; /// -/// Entity Framework context for the Catalogs module. +/// Contexto Entity Framework para o módulo Catalogs. /// public class CatalogsDbContext(DbContextOptions options) : DbContext(options) { diff --git a/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContextFactory.cs b/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContextFactory.cs index 23ae908d6..fe74c9185 100644 --- a/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContextFactory.cs +++ b/src/Modules/Catalogs/Infrastructure/Persistence/CatalogsDbContextFactory.cs @@ -4,19 +4,46 @@ namespace MeAjudaAi.Modules.Catalogs.Infrastructure.Persistence; /// -/// Design-time factory for creating CatalogsDbContext during EF Core migrations. -/// This allows migrations to be created without running the full application. +/// Fábrica em tempo de design para criar CatalogsDbContext durante as migrações do EF Core. +/// Isso permite que as migrações sejam criadas sem executar a aplicação completa. /// +/// +/// Configuração Requerida: +/// +/// Esta fábrica requer a variável de ambiente CATALOGS_DB_CONNECTION contendo +/// a string de conexão Npgsql para operações de design-time do EF Core. +/// +/// Como Configurar: +/// +/// PowerShell: $env:CATALOGS_DB_CONNECTION="Host=localhost;Port=5432;Database=meajudaai;Username=postgres;Password=yourpass" +/// Bash/Linux: export CATALOGS_DB_CONNECTION="Host=localhost;Port=5432;Database=meajudaai;Username=postgres;Password=yourpass" +/// IDE: Configurar nas variáveis de ambiente do projeto/run configuration +/// Arquivo .env na raiz do projeto (se suportado pela sua ferramenta de build) +/// +/// +/// Nota: Esta configuração é usada apenas para geração de migrações (dotnet ef migrations add), +/// não para execução da aplicação em runtime. +/// +/// public sealed class CatalogsDbContextFactory : IDesignTimeDbContextFactory { public CatalogsDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder(); - // Use default development connection string for design-time operations - // This is only used for migrations generation, not runtime + var connectionString = Environment.GetEnvironmentVariable("CATALOGS_DB_CONNECTION"); + + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException( + "CATALOGS_DB_CONNECTION environment variable is not set. " + + "This is required for EF Core design-time operations (migrations). " + + "Set it in your shell (e.g., $env:CATALOGS_DB_CONNECTION='Host=localhost;...'), " + + "IDE run configuration, or .env file."); + } + optionsBuilder.UseNpgsql( - "Host=localhost;Port=5432;Database=meajudaai;Username=postgres;Password=development123", + connectionString, npgsqlOptions => { npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "catalogs"); diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCategoryCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..478c804b5 --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,114 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class ActivateServiceCategoryCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly ActivateServiceCategoryCommandHandler _handler; + + public ActivateServiceCategoryCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new ActivateServiceCategoryCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsInactive() + .Build(); + var command = new ActivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new ActivateServiceCategoryCommand(categoryId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new ActivateServiceCategoryCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyActiveCategory_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsActive() + .Build(); + var command = new ActivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..089d4f438 --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCommandHandlerTests.cs @@ -0,0 +1,118 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class ActivateServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly ActivateServiceCommandHandler _handler; + + public ActivateServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new ActivateServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsInactive() + .Build(); + var command = new ActivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new ActivateServiceCommand(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new ActivateServiceCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyActiveService_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsActive() + .Build(); + var command = new ActivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ChangeServiceCategoryCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ChangeServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..c95cfc05d --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/ChangeServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,206 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class ChangeServiceCategoryCommandHandlerTests +{ + private readonly Mock _serviceRepositoryMock; + private readonly Mock _categoryRepositoryMock; + private readonly ChangeServiceCategoryCommandHandler _handler; + + public ChangeServiceCategoryCommandHandlerTests() + { + _serviceRepositoryMock = new Mock(); + _categoryRepositoryMock = new Mock(); + _handler = new ChangeServiceCategoryCommandHandler(_serviceRepositoryMock.Object, _categoryRepositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var oldCategory = new ServiceCategoryBuilder().AsActive().Build(); + var newCategory = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(oldCategory.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategory.Id.Value); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(newCategory); + + _serviceRepositoryMock + .Setup(x => x.ExistsWithNameAsync(service.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _serviceRepositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.CategoryId.Should().Be(newCategory.Id); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var categoryId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(serviceId, categoryId); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Service").And.Contain("not found"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .Build(); + var newCategoryId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategoryId); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Category").And.Contain("not found"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithInactiveCategory_ShouldReturnFailure() + { + // Arrange + var oldCategory = new ServiceCategoryBuilder().AsActive().Build(); + var newCategory = new ServiceCategoryBuilder().AsInactive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(oldCategory.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategory.Id.Value); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(newCategory); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("inactive"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithDuplicateNameInTargetCategory_ShouldReturnFailure() + { + // Arrange + var oldCategory = new ServiceCategoryBuilder().AsActive().Build(); + var newCategory = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(oldCategory.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategory.Id.Value); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(newCategory); + + _serviceRepositoryMock + .Setup(x => x.ExistsWithNameAsync(service.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("already exists"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyServiceId_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(Guid.Empty, categoryId); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Service ID").And.Contain("cannot be empty"); + _serviceRepositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyCategoryId_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(serviceId, Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("category ID").And.Contain("cannot be empty"); + _serviceRepositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs index d776c9513..e7db3c6bb 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs @@ -1,5 +1,5 @@ using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Entities; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using Microsoft.Extensions.Logging; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs index ca2c617d7..980252988 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs @@ -1,5 +1,5 @@ using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; using MeAjudaAi.Modules.Catalogs.Domain.Entities; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCategoryCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..a36cc4bd3 --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,114 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class DeactivateServiceCategoryCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly DeactivateServiceCategoryCommandHandler _handler; + + public DeactivateServiceCategoryCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new DeactivateServiceCategoryCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsActive() + .Build(); + var command = new DeactivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new DeactivateServiceCategoryCommand(categoryId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new DeactivateServiceCategoryCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyInactiveCategory_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsInactive() + .Build(); + var command = new DeactivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..75480a052 --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCommandHandlerTests.cs @@ -0,0 +1,118 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class DeactivateServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly DeactivateServiceCommandHandler _handler; + + public DeactivateServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new DeactivateServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsActive() + .Build(); + var command = new DeactivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new DeactivateServiceCommand(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new DeactivateServiceCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyInactiveService_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsInactive() + .Build(); + var command = new DeactivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs index 9c5a9408b..08389c6dc 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs @@ -1,5 +1,5 @@ using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Entities; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCommandHandlerTests.cs new file mode 100644 index 000000000..387256be5 --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCommandHandlerTests.cs @@ -0,0 +1,87 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class DeleteServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly DeleteServiceCommandHandler _handler; + + public DeleteServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new DeleteServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new DeleteServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new DeleteServiceCommand(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new DeleteServiceCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs index d741e24bc..e4771a163 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs @@ -1,5 +1,5 @@ using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Entities; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCommandHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..6b84286db --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCommandHandlerTests.cs @@ -0,0 +1,148 @@ +using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.Catalogs.Domain.Entities; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class UpdateServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly UpdateServiceCommandHandler _handler; + + public UpdateServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new UpdateServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Original Name") + .Build(); + var command = new UpdateServiceCommand(service.Id.Value, "Updated Name", "Updated Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.Name.Should().Be(command.Name); + service.Description.Should().Be(command.Description); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new UpdateServiceCommand(serviceId, "Updated Name", "Updated Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new UpdateServiceCommand(Guid.Empty, "Name", "Description", 1); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithDuplicateName_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Original Name") + .Build(); + var command = new UpdateServiceCommand(service.Id.Value, "Duplicate Name", "Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("already exists"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public async Task Handle_WithInvalidName_ShouldReturnFailure(string? invalidName) + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Valid Name") + .Build(); + var command = new UpdateServiceCommand(service.Id.Value, invalidName!, "Description", 1); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs index ea27aa590..47662b408 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Tests.Builders; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs index 4b38745a6..e79f2b9ae 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Tests.Builders; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs index bb5671e63..a9d485893 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; using MeAjudaAi.Modules.Catalogs.Domain.Entities; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoriesWithCountQueryHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoriesWithCountQueryHandlerTests.cs new file mode 100644 index 000000000..8ff578487 --- /dev/null +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoriesWithCountQueryHandlerTests.cs @@ -0,0 +1,163 @@ +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.Catalogs.Domain.Repositories; +using MeAjudaAi.Modules.Catalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.Catalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "Catalogs")] +[Trait("Layer", "Application")] +public class GetServiceCategoriesWithCountQueryHandlerTests +{ + private readonly Mock _categoryRepositoryMock; + private readonly Mock _serviceRepositoryMock; + private readonly GetServiceCategoriesWithCountQueryHandler _handler; + + public GetServiceCategoriesWithCountQueryHandlerTests() + { + _categoryRepositoryMock = new Mock(); + _serviceRepositoryMock = new Mock(); + _handler = new GetServiceCategoriesWithCountQueryHandler(_categoryRepositoryMock.Object, _serviceRepositoryMock.Object); + } + + [Fact] + public async Task Handle_ShouldReturnCategoriesWithCounts() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: false); + var category1 = new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(); + var category2 = new ServiceCategoryBuilder().WithName("Reparos").AsActive().Build(); + var categories = new List + { + category1, + category2 + }; + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(categories); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category1.Id, false, It.IsAny())) + .ReturnsAsync(5); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category1.Id, true, It.IsAny())) + .ReturnsAsync(3); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category2.Id, false, It.IsAny())) + .ReturnsAsync(8); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category2.Id, true, It.IsAny())) + .ReturnsAsync(6); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + + var limpeza = result.Value.First(c => c.Name == "Limpeza"); + limpeza.TotalServicesCount.Should().Be(5); + limpeza.ActiveServicesCount.Should().Be(3); + + var reparos = result.Value.First(c => c.Name == "Reparos"); + reparos.TotalServicesCount.Should().Be(8); + reparos.ActiveServicesCount.Should().Be(6); + + _categoryRepositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + _serviceRepositoryMock.Verify(x => x.CountByCategoryAsync(It.IsAny(), false, It.IsAny()), Times.Exactly(2)); + _serviceRepositoryMock.Verify(x => x.CountByCategoryAsync(It.IsAny(), true, It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task Handle_WithActiveOnlyTrue_ShouldReturnOnlyActiveCategories() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: true); + var category1 = new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(); + var category2 = new ServiceCategoryBuilder().WithName("Reparos").AsActive().Build(); + var categories = new List + { + category1, + category2 + }; + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(true, It.IsAny())) + .ReturnsAsync(categories); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(It.IsAny(), false, It.IsAny())) + .ReturnsAsync(5); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(It.IsAny(), true, It.IsAny())) + .ReturnsAsync(3); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + result.Value.Should().OnlyContain(c => c.IsActive); + + _categoryRepositoryMock.Verify(x => x.GetAllAsync(true, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNoCategories_ShouldReturnEmptyList() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: false); + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEmpty(); + + _categoryRepositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + _serviceRepositoryMock.Verify(x => x.CountByCategoryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithCategoriesWithNoServices_ShouldReturnZeroCounts() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: false); + var category = new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(); + var categories = new List { category }; + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(categories); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category.Id, false, It.IsAny())) + .ReturnsAsync(0); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category.Id, true, It.IsAny())) + .ReturnsAsync(0); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(1); + result.Value.First().TotalServicesCount.Should().Be(0); + result.Value.First().ActiveServicesCount.Should().Be(0); + } +} diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs index f5f1dd0a1..21549b246 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; diff --git a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs index ce2793d44..8053aa27b 100644 --- a/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs +++ b/src/Modules/Catalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs @@ -1,4 +1,4 @@ -using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries; +using MeAjudaAi.Modules.Catalogs.Application.Handlers.Queries.Service; using MeAjudaAi.Modules.Catalogs.Application.Queries.Service; using MeAjudaAi.Modules.Catalogs.Domain.Repositories; using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; diff --git a/src/Shared/Constants/ValidationMessages.cs b/src/Shared/Constants/ValidationMessages.cs index 644020af8..d4f666150 100644 --- a/src/Shared/Constants/ValidationMessages.cs +++ b/src/Shared/Constants/ValidationMessages.cs @@ -76,4 +76,15 @@ public static class Generic public const string Forbidden = "Acesso negado. Permissões insuficientes."; public const string RateLimitExceeded = "Muitas tentativas. Tente novamente em alguns minutos."; } + + /// + /// Mensagens e valores padrão para o módulo Catalogs + /// + public static class Catalogs + { + /// + /// Valor exibido quando o nome da categoria não está disponível (navegação não carregada) + /// + public const string UnknownCategoryName = "Desconhecida"; + } }