From bc67204491020746c69c9bb6fdf24b8f7f9b9ea5 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Wed, 19 Nov 2025 13:49:54 -0300 Subject: [PATCH 1/6] refactor: rename Catalogs to ServiceCatalogs - Phase 1b (Application + API) - Renamed Application layer: commands, queries, handlers, DTOs, ModuleApi - Renamed API layer: endpoints, extensions - Updated all namespaces and class references - Renamed ServiceCatalogsModuleApi and related files Files: 68 (Application + API layers) Phase: 1b of 3 Next: Phase 1c will add Tests layer --- .../Service/ActivateServiceEndpoint.cs | 30 ++ .../Service/ChangeServiceCategoryEndpoint.cs | 33 ++ .../Service/CreateServiceEndpoint.cs | 38 +++ .../Service/DeactivateServiceEndpoint.cs | 30 ++ .../Service/DeleteServiceEndpoint.cs | 30 ++ .../Service/GetAllServicesEndpoint.cs | 31 ++ .../Service/GetServiceByIdEndpoint.cs | 37 +++ .../Service/GetServicesByCategoryEndpoint.cs | 32 ++ .../Service/UpdateServiceEndpoint.cs | 33 ++ .../Service/ValidateServicesEndpoint.cs | 42 +++ .../ServiceCatalogsModuleEndpoints.cs | 44 +++ .../ActivateServiceCategoryEndpoint.cs | 30 ++ .../CreateServiceCategoryEndpoint.cs | 40 +++ .../DeactivateServiceCategoryEndpoint.cs | 30 ++ .../DeleteServiceCategoryEndpoint.cs | 30 ++ .../GetAllServiceCategoriesEndpoint.cs | 35 ++ .../GetServiceCategoryByIdEndpoint.cs | 36 +++ .../UpdateServiceCategoryEndpoint.cs | 34 ++ src/Modules/ServiceCatalogs/API/Extensions.cs | 87 +++++ ...AjudaAi.Modules.ServiceCatalogs.API.csproj | 28 ++ .../Service/ActivateServiceCommand.cs | 9 + .../Service/ChangeServiceCategoryCommand.cs | 12 + .../Commands/Service/CreateServiceCommand.cs | 15 + .../Service/DeactivateServiceCommand.cs | 9 + .../Commands/Service/DeleteServiceCommand.cs | 10 + .../Commands/Service/UpdateServiceCommand.cs | 21 ++ .../ActivateServiceCategoryCommand.cs | 6 + .../CreateServiceCategoryCommand.cs | 11 + .../DeactivateServiceCategoryCommand.cs | 6 + .../DeleteServiceCategoryCommand.cs | 6 + .../UpdateServiceCategoryCommand.cs | 11 + .../Service/ChangeServiceCategoryRequest.cs | 8 + .../Requests/Service/CreateServiceRequest.cs | 11 + .../Requests/Service/UpdateServiceRequest.cs | 10 + .../Service/ValidateServicesRequest.cs | 8 + .../Service/ValidateServicesResponse.cs | 7 + .../UpdateServiceCategoryRequest.cs | 17 + .../Application/DTOs/ServiceCategoryDto.cs | 14 + .../DTOs/ServiceCategoryWithCountDto.cs | 14 + .../Application/DTOs/ServiceDto.cs | 16 + .../Application/DTOs/ServiceListDto.cs | 12 + .../ServiceCatalogs/Application/Extensions.cs | 20 ++ .../Service/ActivateServiceCommandHandler.cs | 30 ++ .../ChangeServiceCategoryCommandHandler.cs | 62 ++++ .../Service/CreateServiceCommandHandler.cs | 65 ++++ .../DeactivateServiceCommandHandler.cs | 30 ++ .../Service/DeleteServiceCommandHandler.cs | 35 ++ .../Service/UpdateServiceCommandHandler.cs | 47 +++ .../ActivateServiceCategoryCommandHandler.cs | 30 ++ .../CreateServiceCategoryCommandHandler.cs | 49 +++ ...DeactivateServiceCategoryCommandHandler.cs | 30 ++ .../DeleteServiceCategoryCommandHandler.cs | 34 ++ .../UpdateServiceCategoryCommandHandler.cs | 47 +++ .../Service/GetAllServicesQueryHandler.cs | 28 ++ .../Service/GetServiceByIdQueryHandler.cs | 44 +++ .../GetServicesByCategoryQueryHandler.cs | 33 ++ .../GetAllServiceCategoriesQueryHandler.cs | 30 ++ ...tServiceCategoriesWithCountQueryHandler.cs | 50 +++ .../GetServiceCategoryByIdQueryHandler.cs | 38 +++ ...Modules.ServiceCatalogs.Application.csproj | 20 ++ .../ModuleApi/ServiceCatalogsModuleApi.cs | 304 ++++++++++++++++++ .../Queries/Service/GetAllServicesQuery.cs | 8 + .../Queries/Service/GetServiceByIdQuery.cs | 8 + .../Service/GetServicesByCategoryQuery.cs | 8 + .../GetAllServiceCategoriesQuery.cs | 8 + .../GetServiceCategoriesWithCountQuery.cs | 8 + .../GetServiceCategoryByIdQuery.cs | 8 + 67 files changed, 2037 insertions(+) create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/CreateServiceEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCatalogsModuleEndpoints.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs create mode 100644 src/Modules/ServiceCatalogs/API/Extensions.cs create mode 100644 src/Modules/ServiceCatalogs/API/MeAjudaAi.Modules.ServiceCatalogs.API.csproj create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/Service/ActivateServiceCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/Service/ChangeServiceCategoryCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/Service/CreateServiceCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/Service/DeactivateServiceCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/Service/UpdateServiceCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/CreateServiceCategoryCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeactivateServiceCategoryCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeleteServiceCategoryCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryDto.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryWithCountDto.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/ServiceDto.cs create mode 100644 src/Modules/ServiceCatalogs/Application/DTOs/ServiceListDto.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Extensions.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs create mode 100644 src/Modules/ServiceCatalogs/Application/MeAjudaAi.Modules.ServiceCatalogs.Application.csproj create mode 100644 src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Queries/Service/GetAllServicesQuery.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Queries/Service/GetServiceByIdQuery.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetAllServiceCategoriesQuery.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs create mode 100644 src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs new file mode 100644 index 000000000..99d9c3fea --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ActivateServiceEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs new file mode 100644 index 000000000..63d926fb2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ChangeServiceCategoryEndpoint.cs @@ -0,0 +1,33 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/CreateServiceEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/CreateServiceEndpoint.cs new file mode 100644 index 000000000..4b24f9899 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/CreateServiceEndpoint.cs @@ -0,0 +1,38 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs new file mode 100644 index 000000000..fc856cc42 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/DeactivateServiceEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs new file mode 100644 index 000000000..cd85b0574 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/DeleteServiceEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs new file mode 100644 index 000000000..8ca4541e9 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetAllServicesEndpoint.cs @@ -0,0 +1,31 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs new file mode 100644 index 000000000..c968ea8c2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServiceByIdEndpoint.cs @@ -0,0 +1,37 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs new file mode 100644 index 000000000..a2aa91fb9 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs @@ -0,0 +1,32 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs new file mode 100644 index 000000000..f03034ca1 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/UpdateServiceEndpoint.cs @@ -0,0 +1,33 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs new file mode 100644 index 000000000..c8c8add14 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs @@ -0,0 +1,42 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; +using MeAjudaAi.Shared.Authorization; +using MeAjudaAi.Shared.Contracts; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +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.ServiceCatalogs.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] IServiceCatalogsModuleApi 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/ServiceCatalogs/API/Endpoints/ServiceCatalogsModuleEndpoints.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCatalogsModuleEndpoints.cs new file mode 100644 index 000000000..5995842df --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCatalogsModuleEndpoints.cs @@ -0,0 +1,44 @@ +using MeAjudaAi.Modules.ServiceCatalogs.API.Endpoints.Service; +using MeAjudaAi.Modules.ServiceCatalogs.API.Endpoints.ServiceCategory; +using MeAjudaAi.Shared.Endpoints; +using Microsoft.AspNetCore.Builder; + +namespace MeAjudaAi.Modules.ServiceCatalogs.API.Endpoints; + +/// +/// Classe responsável pelo mapeamento de todos os endpoints do módulo ServiceCatalogs. +/// +public static class ServiceCatalogsModuleEndpoints +{ + /// + /// Mapeia todos os endpoints do módulo ServiceCatalogs. + /// + /// Aplicação web para configuração das rotas + public static void MapServiceCatalogsEndpoints(this WebApplication app) + { + // Service Categories endpoints + var categoriesEndpoints = BaseEndpoint.CreateVersionedGroup(app, "catalogs/categories", "ServiceCategories"); + + categoriesEndpoints.MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint(); + + // Services endpoints + var servicesEndpoints = BaseEndpoint.CreateVersionedGroup(app, "catalogs/services", "Services"); + + servicesEndpoints.MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint(); + } +} diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..34b677509 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/ActivateServiceCategoryEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..6e6c9080b --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs @@ -0,0 +1,40 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..fa9076139 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/DeactivateServiceCategoryEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs new file mode 100644 index 000000000..d628d536a --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/DeleteServiceCategoryEndpoint.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs new file mode 100644 index 000000000..cab7e1514 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/GetAllServiceCategoriesEndpoint.cs @@ -0,0 +1,35 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs new file mode 100644 index 000000000..170099419 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/GetServiceCategoryByIdEndpoint.cs @@ -0,0 +1,36 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs new file mode 100644 index 000000000..da46150e0 --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/UpdateServiceCategoryEndpoint.cs @@ -0,0 +1,34 @@ +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.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/ServiceCatalogs/API/Extensions.cs b/src/Modules/ServiceCatalogs/API/Extensions.cs new file mode 100644 index 000000000..5313485ae --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/Extensions.cs @@ -0,0 +1,87 @@ +using MeAjudaAi.Modules.ServiceCatalogs.API.Endpoints; +using MeAjudaAi.Modules.ServiceCatalogs.Application; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.Modules.ServiceCatalogs.API; + +public static class Extensions +{ + /// + /// Adiciona os serviços do módulo ServiceCatalogs. + /// + public static IServiceCollection AddServiceCatalogsModule( + this IServiceCollection services, + IConfiguration configuration) + { + services.AddApplication(); + services.AddServiceCatalogsInfrastructure(configuration); + + return services; + } + + /// + /// Configura os endpoints do módulo ServiceCatalogs. + /// + public static WebApplication UseServiceCatalogsModule(this WebApplication app) + { + // Garantir que as migrações estão aplicadas + EnsureDatabaseMigrations(app); + + app.MapServiceCatalogsEndpoints(); + + return app; + } + + private static void EnsureDatabaseMigrations(WebApplication app) + { + if (app?.Services == null) return; + + try + { + using var scope = app.Services.CreateScope(); + var context = scope.ServiceProvider.GetService(); + if (context == null) return; + + // Em ambiente de teste, pular migrações automáticas + if (app.Environment.IsEnvironment("Test") || app.Environment.IsEnvironment("Testing")) + { + return; + } + + context.Database.Migrate(); + } + catch (Exception ex) + { + using var scope = app.Services.CreateScope(); + var logger = scope.ServiceProvider.GetService>(); + + // Only fallback to EnsureCreated in Development + if (app.Environment.IsDevelopment()) + { + logger?.LogWarning(ex, "Falha ao aplicar migrações do módulo ServiceCatalogs. Usando EnsureCreated como fallback em Development."); + try + { + var context = scope.ServiceProvider.GetService(); + context?.Database.EnsureCreated(); + } + catch (Exception fallbackEx) + { + logger?.LogError(fallbackEx, "Falha crítica ao inicializar o banco do módulo ServiceCatalogs."); + throw; // Fail fast even in Development if EnsureCreated fails + } + } + else + { + // Fail fast in non-development environments + logger?.LogError(ex, "Falha crítica ao aplicar migrações do módulo ServiceCatalogs em ambiente de produção."); + throw; + } + } + } +} diff --git a/src/Modules/ServiceCatalogs/API/MeAjudaAi.Modules.ServiceCatalogs.API.csproj b/src/Modules/ServiceCatalogs/API/MeAjudaAi.Modules.ServiceCatalogs.API.csproj new file mode 100644 index 000000000..4d8aa8ecc --- /dev/null +++ b/src/Modules/ServiceCatalogs/API/MeAjudaAi.Modules.ServiceCatalogs.API.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + true + + NU1701;NU1507 + true + true + latest + + + + + + + + + + + + + + + + diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/ActivateServiceCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/ActivateServiceCommand.cs new file mode 100644 index 000000000..387f7dcfc --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/ActivateServiceCommand.cs @@ -0,0 +1,9 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; + +/// +/// Command to activate a service, making it available for use. +/// +public sealed record ActivateServiceCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/ChangeServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/ChangeServiceCategoryCommand.cs new file mode 100644 index 000000000..829816ed5 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/ChangeServiceCategoryCommand.cs @@ -0,0 +1,12 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; + +/// +/// Command to move a service to a different category. +/// +public sealed record ChangeServiceCategoryCommand( + Guid ServiceId, + Guid NewCategoryId +) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/CreateServiceCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/CreateServiceCommand.cs new file mode 100644 index 000000000..c6c33878c --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/CreateServiceCommand.cs @@ -0,0 +1,15 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; + +/// +/// Command to create a new service in a specific category. +/// +public sealed record CreateServiceCommand( + Guid CategoryId, + string Name, + string? Description, + int DisplayOrder = 0 +) : Command>; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/DeactivateServiceCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/DeactivateServiceCommand.cs new file mode 100644 index 000000000..c0ba8501e --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/DeactivateServiceCommand.cs @@ -0,0 +1,9 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; + +/// +/// Command to deactivate a service, removing it from active use. +/// +public sealed record DeactivateServiceCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs new file mode 100644 index 000000000..75dcd25a6 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs @@ -0,0 +1,10 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; + +/// +/// Command to delete a service from the catalog. +/// Note: Currently does not check for provider references (see handler TODO). +/// +public sealed record DeleteServiceCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/UpdateServiceCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/UpdateServiceCommand.cs new file mode 100644 index 000000000..9b77ef7b0 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/UpdateServiceCommand.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; + +/// +/// Command to update an existing service's details. +/// Validation limits must match ValidationConstants.CatalogLimits. +/// Note: Guid.Empty validation is handled by the command handler to provide domain-specific error messages. +/// +public sealed record UpdateServiceCommand( + Guid Id, + [Required] + [MaxLength(150)] + string Name, + [MaxLength(1000)] + string? Description, + [Range(0, int.MaxValue)] + int DisplayOrder +) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs new file mode 100644 index 000000000..02db408f9 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs @@ -0,0 +1,6 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; + +public sealed record ActivateServiceCategoryCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/CreateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/CreateServiceCategoryCommand.cs new file mode 100644 index 000000000..ac7ad4db0 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/CreateServiceCategoryCommand.cs @@ -0,0 +1,11 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; + +public sealed record CreateServiceCategoryCommand( + string Name, + string? Description, + int DisplayOrder = 0 +) : Command>; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeactivateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeactivateServiceCategoryCommand.cs new file mode 100644 index 000000000..398adcada --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeactivateServiceCategoryCommand.cs @@ -0,0 +1,6 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; + +public sealed record DeactivateServiceCategoryCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeleteServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeleteServiceCategoryCommand.cs new file mode 100644 index 000000000..5c001f3e7 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/DeleteServiceCategoryCommand.cs @@ -0,0 +1,6 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; + +public sealed record DeleteServiceCategoryCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs new file mode 100644 index 000000000..9881ba17e --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs @@ -0,0 +1,11 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; + +public sealed record UpdateServiceCategoryCommand( + Guid Id, + string Name, + string? Description, + int DisplayOrder = 0 +) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs new file mode 100644 index 000000000..58815affe --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ChangeServiceCategoryRequest.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; + +public sealed record ChangeServiceCategoryRequest : Request +{ + public Guid NewCategoryId { get; init; } +} diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs new file mode 100644 index 000000000..a498b40b3 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/CreateServiceRequest.cs @@ -0,0 +1,11 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs new file mode 100644 index 000000000..e5a119bc4 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs @@ -0,0 +1,10 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs new file mode 100644 index 000000000..3e239c6ad --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesRequest.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; + +public sealed record ValidateServicesRequest : Request +{ + public IReadOnlyCollection ServiceIds { get; init; } = Array.Empty(); +} diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs new file mode 100644 index 000000000..df09e9723 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs @@ -0,0 +1,7 @@ +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; + +public sealed record ValidateServicesResponse( + bool AllValid, + IReadOnlyCollection InvalidServiceIds, + IReadOnlyCollection InactiveServiceIds +); diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs new file mode 100644 index 000000000..0e01f144e --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/ServiceCategory/UpdateServiceCategoryRequest.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using MeAjudaAi.Shared.Contracts; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/DTOs/ServiceCategoryDto.cs b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryDto.cs new file mode 100644 index 000000000..85ebb7b2f --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryDto.cs @@ -0,0 +1,14 @@ +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; + +/// +/// DTO para informações de categoria de serviço. +/// +public sealed record ServiceCategoryDto( + Guid Id, + string Name, + string? Description, + bool IsActive, + int DisplayOrder, + DateTime CreatedAt, + DateTime? UpdatedAt +); diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryWithCountDto.cs b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryWithCountDto.cs new file mode 100644 index 000000000..832d5e45c --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceCategoryWithCountDto.cs @@ -0,0 +1,14 @@ +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; + +/// +/// DTO para categoria com a contagem de seus serviços. +/// +public sealed record ServiceCategoryWithCountDto( + Guid Id, + string Name, + string? Description, + bool IsActive, + int DisplayOrder, + int ActiveServicesCount, + int TotalServicesCount +); diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/ServiceDto.cs b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceDto.cs new file mode 100644 index 000000000..fdd274eaf --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceDto.cs @@ -0,0 +1,16 @@ +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; + +/// +/// DTO para informações de serviço. +/// +public sealed record ServiceDto( + Guid Id, + Guid CategoryId, + string CategoryName, + string Name, + string? Description, + bool IsActive, + int DisplayOrder, + DateTime CreatedAt, + DateTime? UpdatedAt +); diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/ServiceListDto.cs b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceListDto.cs new file mode 100644 index 000000000..2daf727f2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/DTOs/ServiceListDto.cs @@ -0,0 +1,12 @@ +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; + +/// +/// DTO simplificado para serviço sem detalhes de categoria (para listas). +/// +public sealed record ServiceListDto( + Guid Id, + Guid CategoryId, + string Name, + string? Description, + bool IsActive +); diff --git a/src/Modules/ServiceCatalogs/Application/Extensions.cs b/src/Modules/ServiceCatalogs/Application/Extensions.cs new file mode 100644 index 000000000..0978dd19d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Extensions.cs @@ -0,0 +1,20 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +using Microsoft.Extensions.DependencyInjection; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application; + +public static class Extensions +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + // Note: Handlers are automatically registered through reflection in Infrastructure layer + // via AddApplicationHandlers() which scans the Application assembly + + // Module API - register both interface and concrete type for DI flexibility + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs new file mode 100644 index 000000000..2e9cdf9a6 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ActivateServiceCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..05da757da --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/ChangeServiceCategoryCommandHandler.cs @@ -0,0 +1,62 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs new file mode 100644 index 000000000..820e754f1 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs @@ -0,0 +1,65 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs new file mode 100644 index 000000000..387fb82cb --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/DeactivateServiceCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs new file mode 100644 index 000000000..fb7e91991 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/DeleteServiceCommandHandler.cs @@ -0,0 +1,35 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs new file mode 100644 index 000000000..8d04c4ec3 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/UpdateServiceCommandHandler.cs @@ -0,0 +1,47 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..93ee722d4 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/ActivateServiceCategoryCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..ec3306d5d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/CreateServiceCategoryCommandHandler.cs @@ -0,0 +1,49 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; +using ServiceCategoryEntity = MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.ServiceCategory; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..71fef834d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeactivateServiceCategoryCommandHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..7277308b9 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/DeleteServiceCategoryCommandHandler.cs @@ -0,0 +1,34 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs new file mode 100644 index 000000000..afd8901a2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/ServiceCategory/UpdateServiceCategoryCommandHandler.cs @@ -0,0 +1,47 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs new file mode 100644 index 000000000..39daf4daa --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs @@ -0,0 +1,28 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs new file mode 100644 index 000000000..0e2db5ef6 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs @@ -0,0 +1,44 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Constants; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs new file mode 100644 index 000000000..352f6c8af --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs @@ -0,0 +1,33 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs new file mode 100644 index 000000000..32562c302 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs @@ -0,0 +1,30 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs new file mode 100644 index 000000000..36e32012d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoriesWithCountQueryHandler.cs @@ -0,0 +1,50 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs new file mode 100644 index 000000000..d6045ecaa --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs @@ -0,0 +1,38 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.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/ServiceCatalogs/Application/MeAjudaAi.Modules.ServiceCatalogs.Application.csproj b/src/Modules/ServiceCatalogs/Application/MeAjudaAi.Modules.ServiceCatalogs.Application.csproj new file mode 100644 index 000000000..06f62376f --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/MeAjudaAi.Modules.ServiceCatalogs.Application.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + + + + + <_Parameter1>MeAjudaAi.Modules.ServiceCatalogs.Tests + + + + + + + + + diff --git a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs new file mode 100644 index 000000000..6e6dee3f0 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs @@ -0,0 +1,304 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Constants; +using MeAjudaAi.Shared.Contracts.Modules; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; + +/// +/// Implementação da API pública para o módulo ServiceCatalogs. +/// +[ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)] +public sealed class ServiceCatalogsModuleApi( + IServiceCategoryRepository categoryRepository, + IServiceRepository serviceRepository, + ILogger logger) : IServiceCatalogsModuleApi +{ + private static class ModuleMetadata + { + public const string Name = "ServiceCatalogs"; + public const string Version = "1.0"; + } + + public string ModuleName => ModuleMetadata.Name; + public string ApiVersion => ModuleMetadata.Version; + + public async Task IsAvailableAsync(CancellationToken cancellationToken = default) + { + try + { + logger.LogDebug("Checking ServiceCatalogs module availability"); + + // Simple database connectivity test + var categories = await categoryRepository.GetAllAsync(activeOnly: true, cancellationToken); + + logger.LogDebug("ServiceCatalogs module is available and healthy"); + return true; + } + catch (OperationCanceledException) + { + logger.LogDebug("ServiceCatalogs module availability check was cancelled"); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Error checking ServiceCatalogs module availability"); + return false; + } + } + + public async Task> GetServiceCategoryByIdAsync( + Guid categoryId, + CancellationToken cancellationToken = default) + { + try + { + if (categoryId == Guid.Empty) + return Result.Failure("Category id must be provided"); + + var id = ServiceCategoryId.From(categoryId); + var category = await categoryRepository.GetByIdAsync(id, cancellationToken); + + if (category is null) + return Result.Success(null); + + var dto = new ModuleServiceCategoryDto( + category.Id.Value, + category.Name, + category.Description, + category.IsActive, + category.DisplayOrder + ); + + return Result.Success(dto); + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving service category {CategoryId}", categoryId); + return Result.Failure($"Error retrieving service category: {ex.Message}"); + } + } + + public async Task>> GetAllServiceCategoriesAsync( + bool activeOnly = true, + CancellationToken cancellationToken = default) + { + try + { + var categories = await categoryRepository.GetAllAsync(activeOnly, cancellationToken); + + var dtos = categories.Select(c => new ModuleServiceCategoryDto( + c.Id.Value, + c.Name, + c.Description, + c.IsActive, + c.DisplayOrder + )).ToList(); + + return Result>.Success(dtos); + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving service categories"); + return Result>.Failure($"Error retrieving service categories: {ex.Message}"); + } + } + + public async Task> GetServiceByIdAsync( + Guid serviceId, + CancellationToken cancellationToken = default) + { + try + { + if (serviceId == Guid.Empty) + return Result.Failure("Service id must be provided"); + + var id = ServiceId.From(serviceId); + var service = await serviceRepository.GetByIdAsync(id, cancellationToken); + + if (service is null) + return Result.Success(null); + + var categoryName = service.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName; + + var dto = new ModuleServiceDto( + service.Id.Value, + service.CategoryId.Value, + categoryName, + service.Name, + service.Description, + service.IsActive + ); + + return Result.Success(dto); + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving service {ServiceId}", serviceId); + return Result.Failure($"Error retrieving service: {ex.Message}"); + } + } + + public async Task>> GetAllServicesAsync( + bool activeOnly = true, + CancellationToken cancellationToken = default) + { + try + { + var services = await serviceRepository.GetAllAsync(activeOnly, cancellationToken); + + var dtos = services.Select(s => new ModuleServiceListDto( + s.Id.Value, + s.CategoryId.Value, + s.Name, + s.IsActive + )).ToList(); + + return Result>.Success(dtos); + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving services"); + return Result>.Failure($"Error retrieving services: {ex.Message}"); + } + } + + public async Task>> GetServicesByCategoryAsync( + Guid categoryId, + bool activeOnly = true, + CancellationToken cancellationToken = default) + { + try + { + if (categoryId == Guid.Empty) + return Result>.Failure("Category id must be provided"); + + var id = ServiceCategoryId.From(categoryId); + var services = await serviceRepository.GetByCategoryAsync(id, activeOnly, cancellationToken); + + var dtos = services.Select(s => new ModuleServiceDto( + s.Id.Value, + s.CategoryId.Value, + s.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName, + s.Name, + s.Description, + s.IsActive + )).ToList(); + + return Result>.Success(dtos); + } + catch (Exception ex) + { + logger.LogError(ex, "Error retrieving services for category {CategoryId}", categoryId); + return Result>.Failure($"Error retrieving services: {ex.Message}"); + } + } + + public async Task> IsServiceActiveAsync( + Guid serviceId, + CancellationToken cancellationToken = default) + { + try + { + if (serviceId == Guid.Empty) + return Result.Failure("Service id must be provided"); + + var serviceIdValue = ServiceId.From(serviceId); + var service = await serviceRepository.GetByIdAsync(serviceIdValue, cancellationToken); + + // Return false for not-found to align with query semantics (vs Failure) + if (service is null) + return Result.Success(false); + + return Result.Success(service.IsActive); + } + catch (Exception ex) + { + logger.LogError(ex, "Error checking if service {ServiceId} is active", serviceId); + return Result.Failure($"Error checking service status: {ex.Message}"); + } + } + + public async Task> ValidateServicesAsync( + IReadOnlyCollection serviceIds, + CancellationToken cancellationToken = default) + { + try + { + if (serviceIds is null) + return Result.Failure("Service IDs collection cannot be null"); + + // Short-circuit for empty collection + if (serviceIds.Count == 0) + { + return Result.Success( + new ModuleServiceValidationResultDto(true, [], [])); + } + + var invalidIds = new List(); + var inactiveIds = new List(); + + // Deduplicate input IDs and separate empty GUIDs + var distinctIds = serviceIds.Distinct().ToList(); + var validGuids = new List(); + + foreach (var id in distinctIds) + { + if (id == Guid.Empty) + { + invalidIds.Add(id); + } + else + { + validGuids.Add(id); + } + } + + // Only convert non-empty GUIDs to ServiceId value objects + if (validGuids.Count > 0) + { + var serviceIdValues = validGuids.Select(ServiceId.From).ToList(); + + // Batch query to avoid N+1 problem + var services = await serviceRepository.GetByIdsAsync(serviceIdValues, cancellationToken); + var serviceLookup = services.ToDictionary(s => s.Id.Value); + + foreach (var serviceId in validGuids) + { + if (!serviceLookup.TryGetValue(serviceId, out var service)) + { + invalidIds.Add(serviceId); + } + else if (!service.IsActive) + { + inactiveIds.Add(serviceId); + } + } + } + + var allValid = invalidIds.Count == 0 && inactiveIds.Count == 0; + + var result = new ModuleServiceValidationResultDto( + allValid, + [.. invalidIds], + [.. inactiveIds] + ); + + return Result.Success(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Error validating services"); + return Result.Failure($"Error validating services: {ex.Message}"); + } + } +} diff --git a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetAllServicesQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetAllServicesQuery.cs new file mode 100644 index 000000000..ce43d0494 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetAllServicesQuery.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; + +public sealed record GetAllServicesQuery(bool ActiveOnly = false) + : Query>>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServiceByIdQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServiceByIdQuery.cs new file mode 100644 index 000000000..4906e923a --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServiceByIdQuery.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; + +public sealed record GetServiceByIdQuery(Guid Id) + : Query>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs new file mode 100644 index 000000000..7776a1d9d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; + +public sealed record GetServicesByCategoryQuery(Guid CategoryId, bool ActiveOnly = false) + : Query>>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetAllServiceCategoriesQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetAllServiceCategoriesQuery.cs new file mode 100644 index 000000000..d66fbf4ad --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetAllServiceCategoriesQuery.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; + +public sealed record GetAllServiceCategoriesQuery(bool ActiveOnly = false) + : Query>>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs new file mode 100644 index 000000000..d58686eb2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; + +public sealed record GetServiceCategoriesWithCountQuery(bool ActiveOnly = false) + : Query>>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs new file mode 100644 index 000000000..bd8590d4f --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs @@ -0,0 +1,8 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; + +public sealed record GetServiceCategoryByIdQuery(Guid Id) + : Query>; From 0d27e6aaed2120dcefa61284ec7ee5b23ab12d36 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Wed, 19 Nov 2025 15:14:14 -0300 Subject: [PATCH 2/6] feat: add ServiceCatalogs Application and API layers to solution (Phase 1b) --- MeAjudaAi.sln | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/MeAjudaAi.sln b/MeAjudaAi.sln index 77e73c1cc..fc5cc2d08 100644 --- a/MeAjudaAi.sln +++ b/MeAjudaAi.sln @@ -145,16 +145,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4726175B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.SearchProviders.Tests", "src\Modules\SearchProviders\Tests\MeAjudaAi.Modules.SearchProviders.Tests.csproj", "{C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalogs", "Catalogs", "{8B551008-B254-EBAF-1B6D-AB7C420234EA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceCatalogs", "ServiceCatalogs", "{8B551008-B254-EBAF-1B6D-AB7C420234EA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{B346CC0B-427A-E442-6F5D-8AAE1AB081D6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Domain", "src\Modules\ServiceCatalogs\Domain\MeAjudaAi.Modules.ServiceCatalogs.Domain.csproj", "{DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{44577491-2FC0-4F52-AF5C-2BC9B323CDB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Application", "src\Modules\ServiceCatalogs\Application\MeAjudaAi.Modules.ServiceCatalogs.Application.csproj", "{44577491-2FC0-4F52-AF5C-2BC9B323CDB7}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{8D23D6D3-2B2E-7F09-866F-FA51CC0FC081}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Infrastructure", "src\Modules\ServiceCatalogs\Infrastructure\MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj", "{3B6D6C13-1E04-47B9-B44E-36D25DF913C7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.API", "src\Modules\ServiceCatalogs\API\MeAjudaAi.Modules.ServiceCatalogs.API.csproj", "{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -597,6 +605,18 @@ Global {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x64.Build.0 = Release|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x86.ActiveCfg = Release|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x86.Build.0 = Release|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x64.Build.0 = Debug|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x86.Build.0 = Debug|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|Any CPU.Build.0 = Release|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x64.ActiveCfg = Release|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x64.Build.0 = Release|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x86.ActiveCfg = Release|Any CPU + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -671,8 +691,10 @@ Global {8B551008-B254-EBAF-1B6D-AB7C420234EA} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F} {B346CC0B-427A-E442-6F5D-8AAE1AB081D6} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A} = {B346CC0B-427A-E442-6F5D-8AAE1AB081D6} + {44577491-2FC0-4F52-AF5C-2BC9B323CDB7} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} {3B6D6C13-1E04-47B9-B44E-36D25DF913C7} = {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} + {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {391B5342-8EC5-4DF0-BCDA-6D73F87E8751} From 4f1b8dea208a57479699e79cd7fa2a91ab2b627d Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Wed, 19 Nov 2025 15:26:09 -0300 Subject: [PATCH 3/6] fix: resolve GUID collisions, add code review fixes, and create temporary DTOs for Phase 1b - Replace duplicate Application project GUID with unique GUID (A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D) - Replace duplicate API project GUID with unique GUID (B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E) - Update all GUID references in GlobalSection(ProjectConfigurationPlatforms) - Update all GUID references in GlobalSection(NestedProjects) - Add DisplayOrder validation in CreateServiceCommandHandler (cannot be negative) - Update DeleteServiceCommand documentation about soft-delete pattern (future enhancement) - Make ValidateServicesResponse collections non-nullable with required modifier and constructor validation - Comment shared contract imports (will be added in Phase 2) - Create temporary module DTOs to allow compilation without breaking module API pattern - Update Extensions.cs to register ServiceCatalogsModuleApi without interface (Phase 2) --- MeAjudaAi.sln | 54 ++++++++++--------- .../Commands/Service/DeleteServiceCommand.cs | 3 +- .../Service/ValidateServicesResponse.cs | 18 +++++-- .../ServiceCatalogs/Application/Extensions.cs | 8 +-- .../Service/CreateServiceCommandHandler.cs | 4 ++ .../ModuleApi/ServiceCatalogsModuleApi.cs | 8 +-- .../ModuleApi/TemporaryModuleDtos.cs | 47 ++++++++++++++++ 7 files changed, 104 insertions(+), 38 deletions(-) create mode 100644 src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs diff --git a/MeAjudaAi.sln b/MeAjudaAi.sln index fc5cc2d08..d855166a9 100644 --- a/MeAjudaAi.sln +++ b/MeAjudaAi.sln @@ -153,7 +153,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{44577491-2FC0-4F52-AF5C-2BC9B323CDB7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Application", "src\Modules\ServiceCatalogs\Application\MeAjudaAi.Modules.ServiceCatalogs.Application.csproj", "{44577491-2FC0-4F52-AF5C-2BC9B323CDB7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Application", "src\Modules\ServiceCatalogs\Application\MeAjudaAi.Modules.ServiceCatalogs.Application.csproj", "{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{8D23D6D3-2B2E-7F09-866F-FA51CC0FC081}" EndProject @@ -161,7 +161,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.API", "src\Modules\ServiceCatalogs\API\MeAjudaAi.Modules.ServiceCatalogs.API.csproj", "{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.API", "src\Modules\ServiceCatalogs\API\MeAjudaAi.Modules.ServiceCatalogs.API.csproj", "{B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -581,18 +581,18 @@ Global {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x64.Build.0 = Release|Any CPU {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x86.ActiveCfg = Release|Any CPU {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x86.Build.0 = Release|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Debug|x64.ActiveCfg = Debug|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Debug|x64.Build.0 = Debug|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Debug|x86.ActiveCfg = Debug|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Debug|x86.Build.0 = Debug|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Release|Any CPU.Build.0 = Release|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Release|x64.ActiveCfg = Release|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Release|x64.Build.0 = Release|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Release|x86.ActiveCfg = Release|Any CPU - {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}.Release|x86.Build.0 = Release|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.Build.0 = Debug|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.Build.0 = Debug|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.Build.0 = Release|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.ActiveCfg = Release|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.Build.0 = Release|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.ActiveCfg = Release|Any CPU + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.Build.0 = Release|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -605,18 +605,18 @@ Global {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x64.Build.0 = Release|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x86.ActiveCfg = Release|Any CPU {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x86.Build.0 = Release|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x64.ActiveCfg = Debug|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x64.Build.0 = Debug|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x86.ActiveCfg = Debug|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Debug|x86.Build.0 = Debug|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|Any CPU.Build.0 = Release|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x64.ActiveCfg = Release|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x64.Build.0 = Release|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x86.ActiveCfg = Release|Any CPU - {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}.Release|x86.Build.0 = Release|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x64.Build.0 = Debug|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x86.Build.0 = Debug|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.ActiveCfg = Release|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.Build.0 = Release|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.ActiveCfg = Release|Any CPU + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -692,9 +692,11 @@ Global {B346CC0B-427A-E442-6F5D-8AAE1AB081D6} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A} = {B346CC0B-427A-E442-6F5D-8AAE1AB081D6} {44577491-2FC0-4F52-AF5C-2BC9B323CDB7} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} + {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D} = {44577491-2FC0-4F52-AF5C-2BC9B323CDB7} {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} {3B6D6C13-1E04-47B9-B44E-36D25DF913C7} = {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} + {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E} = {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {391B5342-8EC5-4DF0-BCDA-6D73F87E8751} diff --git a/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs index 75dcd25a6..664991a50 100644 --- a/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs +++ b/src/Modules/ServiceCatalogs/Application/Commands/Service/DeleteServiceCommand.cs @@ -5,6 +5,7 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; /// /// Command to delete a service from the catalog. -/// Note: Currently does not check for provider references (see handler TODO). +/// Note: Future enhancement required - implement soft-delete pattern (IsActive = false) to preserve +/// audit history and prevent deletion when providers reference this service. See handler TODO. /// public sealed record DeleteServiceCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs index df09e9723..88796a90a 100644 --- a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs @@ -1,7 +1,15 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; -public sealed record ValidateServicesResponse( - bool AllValid, - IReadOnlyCollection InvalidServiceIds, - IReadOnlyCollection InactiveServiceIds -); +public sealed record ValidateServicesResponse +{ + public bool AllValid { get; init; } + public required IReadOnlyCollection InvalidServiceIds { get; init; } + public required IReadOnlyCollection InactiveServiceIds { get; init; } + + public ValidateServicesResponse(bool allValid, IReadOnlyCollection? invalidServiceIds, IReadOnlyCollection? inactiveServiceIds) + { + AllValid = allValid; + InvalidServiceIds = invalidServiceIds ?? Array.Empty(); + InactiveServiceIds = inactiveServiceIds ?? Array.Empty(); + } +} diff --git a/src/Modules/ServiceCatalogs/Application/Extensions.cs b/src/Modules/ServiceCatalogs/Application/Extensions.cs index 0978dd19d..5ea0abe68 100644 --- a/src/Modules/ServiceCatalogs/Application/Extensions.cs +++ b/src/Modules/ServiceCatalogs/Application/Extensions.cs @@ -1,5 +1,6 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; -using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +// TODO Phase 2: Uncomment when shared contracts are added +// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; using Microsoft.Extensions.DependencyInjection; namespace MeAjudaAi.Modules.ServiceCatalogs.Application; @@ -11,8 +12,9 @@ public static IServiceCollection AddApplication(this IServiceCollection services // Note: Handlers are automatically registered through reflection in Infrastructure layer // via AddApplicationHandlers() which scans the Application assembly - // Module API - register both interface and concrete type for DI flexibility - services.AddScoped(); + // Module API - register concrete type for DI (interface registration in Phase 2) + // TODO Phase 2: Uncomment interface registration when IServiceCatalogsModuleApi is added + // services.AddScoped(); services.AddScoped(); return services; diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs index 820e754f1..71a7ef09d 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Commands/Service/CreateServiceCommandHandler.cs @@ -40,6 +40,10 @@ public async Task> HandleAsync(CreateServiceCommand request, if (await serviceRepository.ExistsWithNameAsync(normalizedName, null, categoryId, cancellationToken)) return Result.Failure($"A service with name '{normalizedName}' already exists in this category."); + // Validar DisplayOrder + if (request.DisplayOrder < 0) + return Result.Failure("Display order cannot be negative."); + var service = Domain.Entities.Service.Create(categoryId, normalizedName, request.Description, request.DisplayOrder); await serviceRepository.AddAsync(service, cancellationToken); diff --git a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs index 6e6dee3f0..0c2554df3 100644 --- a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs +++ b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs @@ -4,8 +4,9 @@ using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; using MeAjudaAi.Shared.Constants; using MeAjudaAi.Shared.Contracts.Modules; -using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; -using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; +// TODO Phase 2: Uncomment when shared contracts are added +// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; using MeAjudaAi.Shared.Functional; using MeAjudaAi.Shared.Queries; using Microsoft.Extensions.DependencyInjection; @@ -16,12 +17,13 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; /// /// Implementação da API pública para o módulo ServiceCatalogs. +/// TODO Phase 2: Uncomment IServiceCatalogsModuleApi interface when shared contracts are added /// [ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)] public sealed class ServiceCatalogsModuleApi( IServiceCategoryRepository categoryRepository, IServiceRepository serviceRepository, - ILogger logger) : IServiceCatalogsModuleApi + ILogger logger) // : IServiceCatalogsModuleApi { private static class ModuleMetadata { diff --git a/src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs b/src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs new file mode 100644 index 000000000..952357d84 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs @@ -0,0 +1,47 @@ +// TODO Phase 2: Remove this file when proper shared contracts are added in MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs +// These are temporary placeholders to allow Phase 1b to compile without breaking the module API pattern + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; + +/// +/// Temporary DTO - will be replaced by shared contract in Phase 2 +/// +public sealed record ModuleServiceCategoryDto( + Guid Id, + string Name, + string? Description, + bool IsActive, + int DisplayOrder +); + +/// +/// Temporary DTO - will be replaced by shared contract in Phase 2 +/// +public sealed record ModuleServiceDto( + Guid Id, + Guid CategoryId, + string CategoryName, + string Name, + string? Description, + bool IsActive, + int DisplayOrder +); + +/// +/// Temporary DTO - will be replaced by shared contract in Phase 2 +/// +public sealed record ModuleServiceListDto( + Guid Id, + string Name, + string? Description, + bool IsActive +); + +/// +/// Temporary DTO - will be replaced by shared contract in Phase 2 +/// +public sealed record ModuleServiceValidationResultDto( + bool AllValid, + IReadOnlyCollection InvalidServiceIds, + IReadOnlyCollection InactiveServiceIds +); From dc29e7cd6cb28dc7195068931834bc3797dc95d5 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Wed, 19 Nov 2025 15:28:47 -0300 Subject: [PATCH 4/6] refactor: apply code review improvements to Phase 1b - Add XML documentation to all queries, commands, and requests for consistency - Extract duplicate DTO mapping logic to DtoMappingExtensions class - Use ToDto() and ToListDto() extension methods in all query handlers - Improve migration logging: warn when DbContext not found, log test environment skips - Treat Guid.Empty as validation error in GetServiceByIdQueryHandler (consistency with commands) - Add explicit null check in CreateServiceCategoryEndpoint for safer handling - Centralize mapping logic to avoid duplication and improve maintainability --- .../CreateServiceCategoryEndpoint.cs | 5 ++- src/Modules/ServiceCatalogs/API/Extensions.cs | 9 ++++- .../ActivateServiceCategoryCommand.cs | 3 ++ .../UpdateServiceCategoryCommand.cs | 3 ++ .../Requests/Service/UpdateServiceRequest.cs | 3 ++ .../Service/GetAllServicesQueryHandler.cs | 9 ++--- .../Service/GetServiceByIdQueryHandler.cs | 3 +- .../GetServicesByCategoryQueryHandler.cs | 9 ++--- .../GetAllServiceCategoriesQueryHandler.cs | 11 ++---- .../GetServiceCategoryByIdQueryHandler.cs | 11 ++---- .../Mappings/DtoMappingExtensions.cs | 35 +++++++++++++++++++ .../Service/GetServicesByCategoryQuery.cs | 3 ++ .../GetServiceCategoriesWithCountQuery.cs | 3 ++ .../GetServiceCategoryByIdQuery.cs | 3 ++ 14 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs index 6e6c9080b..0d2340b81 100644 --- a/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs +++ b/src/Modules/ServiceCatalogs/API/Endpoints/ServiceCategory/CreateServiceCategoryEndpoint.cs @@ -35,6 +35,9 @@ private static async Task CreateAsync( if (!result.IsSuccess) return Handle(result); - return Handle(result, "GetServiceCategoryById", new { id = result.Value!.Id }); + if (result.Value is null) + return Results.BadRequest("Unexpected null value in successful result."); + + return Handle(result, "GetServiceCategoryById", new { id = result.Value.Id }); } } diff --git a/src/Modules/ServiceCatalogs/API/Extensions.cs b/src/Modules/ServiceCatalogs/API/Extensions.cs index 5313485ae..c547be924 100644 --- a/src/Modules/ServiceCatalogs/API/Extensions.cs +++ b/src/Modules/ServiceCatalogs/API/Extensions.cs @@ -45,12 +45,19 @@ private static void EnsureDatabaseMigrations(WebApplication app) try { using var scope = app.Services.CreateScope(); + var logger = scope.ServiceProvider.GetService>(); var context = scope.ServiceProvider.GetService(); - if (context == null) return; + + if (context == null) + { + logger?.LogWarning("ServiceCatalogsDbContext not found in DI container. Skipping migrations."); + return; + } // Em ambiente de teste, pular migrações automáticas if (app.Environment.IsEnvironment("Test") || app.Environment.IsEnvironment("Testing")) { + logger?.LogInformation("Skipping ServiceCatalogs migrations in test environment: {Environment}", app.Environment.EnvironmentName); return; } diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs index 02db408f9..3775c01a9 100644 --- a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/ActivateServiceCategoryCommand.cs @@ -3,4 +3,7 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +/// +/// Command to activate a service category. +/// public sealed record ActivateServiceCategoryCommand(Guid Id) : Command; diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs index 9881ba17e..9856388c2 100644 --- a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs @@ -3,6 +3,9 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +/// +/// Command to update an existing service category's information. +/// public sealed record UpdateServiceCategoryCommand( Guid Id, string Name, diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs index e5a119bc4..acfbd7445 100644 --- a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/UpdateServiceRequest.cs @@ -2,6 +2,9 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; +/// +/// Request to update an existing service's information. +/// public sealed record UpdateServiceRequest : Request { public string Name { get; init; } = string.Empty; diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs index 39daf4daa..301b5af91 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetAllServicesQueryHandler.cs @@ -1,4 +1,5 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; using MeAjudaAi.Shared.Functional; @@ -15,13 +16,7 @@ public async Task>> HandleAsync( { 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(); + var dtos = services.Select(s => s.ToListDto()).ToList(); return Result>.Success(dtos); } diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs index 0e2db5ef6..65d6b4fa6 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs @@ -15,8 +15,9 @@ public sealed class GetServiceByIdQueryHandler(IServiceRepository repository) GetServiceByIdQuery request, CancellationToken cancellationToken = default) { + // Treat Guid.Empty as validation error for consistency with command handlers if (request.Id == Guid.Empty) - return Result.Success(null); + return Result.Failure("Service ID cannot be empty."); var serviceId = ServiceId.From(request.Id); var service = await repository.GetByIdAsync(serviceId, cancellationToken); diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs index 352f6c8af..8a49c40b4 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServicesByCategoryQueryHandler.cs @@ -1,4 +1,5 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; @@ -20,13 +21,7 @@ public async Task>> HandleAsync( 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(); + var dtos = services.Select(s => s.ToListDto()).ToList(); return Result>.Success(dtos); } diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs index 32562c302..0ae287589 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetAllServiceCategoriesQueryHandler.cs @@ -1,4 +1,5 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; using MeAjudaAi.Shared.Functional; @@ -15,15 +16,7 @@ public async Task>> HandleAsync( { 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(); + var dtos = categories.Select(c => c.ToDto()).ToList(); return Result>.Success(dtos); } diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs index d6045ecaa..370ea9fde 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs @@ -1,4 +1,5 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; @@ -23,15 +24,7 @@ public sealed class GetServiceCategoryByIdQueryHandler(IServiceCategoryRepositor 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 - ); + var dto = category.ToDto(); return Result.Success(dto); } diff --git a/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs b/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs new file mode 100644 index 000000000..c3853e6b4 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs @@ -0,0 +1,35 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; + +/// +/// Extension methods for mapping domain entities to DTOs. +/// Centralizes mapping logic to avoid duplication across handlers. +/// +public static class DtoMappingExtensions +{ + /// + /// Maps a Service entity to a ServiceListDto. + /// + public static ServiceListDto ToListDto(this Service service) + => new( + service.Id.Value, + service.CategoryId.Value, + service.Name, + service.Description, + service.IsActive); + + /// + /// Maps a ServiceCategory entity to a ServiceCategoryDto. + /// + public static ServiceCategoryDto ToDto(this ServiceCategory category) + => new( + category.Id.Value, + category.Name, + category.Description, + category.IsActive, + category.DisplayOrder, + category.CreatedAt, + category.UpdatedAt); +} diff --git a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs index 7776a1d9d..ffe18d3b9 100644 --- a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs +++ b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs @@ -4,5 +4,8 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +/// +/// Query to retrieve all services within a specific category. +/// public sealed record GetServicesByCategoryQuery(Guid CategoryId, bool ActiveOnly = false) : Query>>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs index d58686eb2..1207fca32 100644 --- a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs +++ b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoriesWithCountQuery.cs @@ -4,5 +4,8 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +/// +/// Query to retrieve service categories with their service counts. +/// public sealed record GetServiceCategoriesWithCountQuery(bool ActiveOnly = false) : Query>>; diff --git a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs index bd8590d4f..8fdb89504 100644 --- a/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs +++ b/src/Modules/ServiceCatalogs/Application/Queries/ServiceCategory/GetServiceCategoryByIdQuery.cs @@ -4,5 +4,8 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +/// +/// Query to retrieve a service category by its identifier. +/// public sealed record GetServiceCategoryByIdQuery(Guid Id) : Query>; From 9d56f6d6a124f05319a8bbd5f3e0861a2a73b91e Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Wed, 19 Nov 2025 15:39:11 -0300 Subject: [PATCH 5/6] fix: resolve compilation errors in ServiceCatalogs Phase 1b - Add missing DisplayOrder parameter to ModuleServiceDto constructors - Fix ModuleServiceListDto constructor parameter order - Remove 'required' modifier from ValidateServicesResponse properties (conflicts with constructor) - Update ValidateServicesEndpoint to use concrete ServiceCatalogsModuleApi type --- .../API/Endpoints/Service/ValidateServicesEndpoint.cs | 6 ++++-- .../DTOs/Requests/Service/ValidateServicesResponse.cs | 4 ++-- .../Application/ModuleApi/ServiceCatalogsModuleApi.cs | 8 +++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs index c8c8add14..e04f20b81 100644 --- a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs @@ -1,7 +1,9 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; using MeAjudaAi.Shared.Authorization; using MeAjudaAi.Shared.Contracts; -using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +// TODO Phase 2: Uncomment when shared contracts are added +// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; using MeAjudaAi.Shared.Endpoints; using MeAjudaAi.Shared.Functional; using Microsoft.AspNetCore.Builder; @@ -22,7 +24,7 @@ public static void Map(IEndpointRouteBuilder app) private static async Task ValidateAsync( [FromBody] ValidateServicesRequest request, - [FromServices] IServiceCatalogsModuleApi moduleApi, + [FromServices] ServiceCatalogsModuleApi moduleApi, CancellationToken cancellationToken) { var result = await moduleApi.ValidateServicesAsync(request.ServiceIds, cancellationToken); diff --git a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs index 88796a90a..1433c17d6 100644 --- a/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs +++ b/src/Modules/ServiceCatalogs/Application/DTOs/Requests/Service/ValidateServicesResponse.cs @@ -3,8 +3,8 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; public sealed record ValidateServicesResponse { public bool AllValid { get; init; } - public required IReadOnlyCollection InvalidServiceIds { get; init; } - public required IReadOnlyCollection InactiveServiceIds { get; init; } + public IReadOnlyCollection InvalidServiceIds { get; init; } + public IReadOnlyCollection InactiveServiceIds { get; init; } public ValidateServicesResponse(bool allValid, IReadOnlyCollection? invalidServiceIds, IReadOnlyCollection? inactiveServiceIds) { diff --git a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs index 0c2554df3..162ab05c2 100644 --- a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs +++ b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs @@ -138,7 +138,8 @@ public async Task>> GetAllService categoryName, service.Name, service.Description, - service.IsActive + service.IsActive, + service.DisplayOrder ); return Result.Success(dto); @@ -160,8 +161,8 @@ public async Task>> GetAllServicesAsy var dtos = services.Select(s => new ModuleServiceListDto( s.Id.Value, - s.CategoryId.Value, s.Name, + s.Description, s.IsActive )).ToList(); @@ -193,7 +194,8 @@ public async Task>> GetServicesByCategory s.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName, s.Name, s.Description, - s.IsActive + s.IsActive, + s.DisplayOrder )).ToList(); return Result>.Success(dtos); From 293fd27c42d4c2c1af42fd79b053bca2b8ce3b30 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Wed, 19 Nov 2025 15:47:36 -0300 Subject: [PATCH 6/6] refactor: apply additional code review improvements to Phase 1b - Fix ValidateServicesEndpoint to use relative route 'validate' instead of absolute '/validate' so it registers under versioned services group (/api/v{version}/catalogs/services/validate) - Change GetServiceCategoryByIdQueryHandler to return Failure for Guid.Empty instead of Success(null) for consistency with GetServiceByIdQueryHandler - Add Service.ToDto() extension method to DtoMappingExtensions for centralized mapping - Refactor GetServiceByIdQueryHandler to use Service.ToDto() extension method instead of manual construction - Add XML documentation to UpdateServiceCategoryCommand noting full-update pattern and future partial update consideration --- .../Service/ValidateServicesEndpoint.cs | 2 +- .../UpdateServiceCategoryCommand.cs | 3 +++ .../Service/GetServiceByIdQueryHandler.cs | 16 ++------------- .../GetServiceCategoryByIdQueryHandler.cs | 2 +- .../Mappings/DtoMappingExtensions.cs | 20 +++++++++++++++++++ 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs index e04f20b81..671419d99 100644 --- a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs @@ -16,7 +16,7 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.API.Endpoints.Service; public class ValidateServicesEndpoint : BaseEndpoint, IEndpoint { public static void Map(IEndpointRouteBuilder app) - => app.MapPost("/validate", ValidateAsync) + => app.MapPost("validate", ValidateAsync) .WithName("ValidateServices") .WithSummary("Validar múltiplos serviços") .Produces>(StatusCodes.Status200OK) diff --git a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs index 9856388c2..660c01c0d 100644 --- a/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs +++ b/src/Modules/ServiceCatalogs/Application/Commands/ServiceCategory/UpdateServiceCategoryCommand.cs @@ -5,6 +5,9 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory /// /// Command to update an existing service category's information. +/// Note: This command requires all fields for updates (full-update pattern). +/// Future enhancement: Consider supporting partial updates where clients only send changed fields +/// using nullable fields or optional wrapper types if API requirements evolve. /// public sealed record UpdateServiceCategoryCommand( Guid Id, diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs index 65d6b4fa6..5e28c87be 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/Service/GetServiceByIdQueryHandler.cs @@ -1,8 +1,8 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; -using MeAjudaAi.Shared.Constants; using MeAjudaAi.Shared.Functional; using MeAjudaAi.Shared.Queries; @@ -26,19 +26,7 @@ public sealed class GetServiceByIdQueryHandler(IServiceRepository repository) 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 - ); + var dto = service.ToDto(); return Result.Success(dto); } diff --git a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs index 370ea9fde..0b15b4ffd 100644 --- a/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs +++ b/src/Modules/ServiceCatalogs/Application/Handlers/Queries/ServiceCategory/GetServiceCategoryByIdQueryHandler.cs @@ -16,7 +16,7 @@ public sealed class GetServiceCategoryByIdQueryHandler(IServiceCategoryRepositor CancellationToken cancellationToken = default) { if (request.Id == Guid.Empty) - return Result.Success(null); + return Result.Failure("Service Category ID cannot be empty."); var categoryId = ServiceCategoryId.From(request.Id); var category = await repository.GetByIdAsync(categoryId, cancellationToken); diff --git a/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs b/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs index c3853e6b4..3f87c9781 100644 --- a/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs +++ b/src/Modules/ServiceCatalogs/Application/Mappings/DtoMappingExtensions.cs @@ -1,5 +1,6 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Shared.Constants; namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings; @@ -20,6 +21,25 @@ public static ServiceListDto ToListDto(this Service service) service.Description, service.IsActive); + /// + /// Maps a Service entity to a ServiceDto. + /// + public static ServiceDto ToDto(this Service service) + { + var categoryName = service.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName; + + return new ServiceDto( + service.Id.Value, + service.CategoryId.Value, + categoryName, + service.Name, + service.Description, + service.IsActive, + service.DisplayOrder, + service.CreatedAt, + service.UpdatedAt); + } + /// /// Maps a ServiceCategory entity to a ServiceCategoryDto. ///