📋 Módulo service_catalogs - Catálogo de Serviços¶
✅ Status: Módulo implementado e funcional (Novembro 2025)
🎯 Visão Geral¶
O módulo service_catalogs é responsável pelo catálogo administrativo de serviços oferecidos na plataforma MeAjudaAi, implementando um Bounded Context dedicado para gestão hierárquica de categorias e serviços.
Responsabilidades¶
- ✅ Catálogo hierárquico de categorias de serviços
- ✅ Gestão de serviços por categoria
- ✅ CRUD administrativo de categorias e serviços
- ✅ Ativação/desativação de serviços
- ✅ API pública para consulta por outros módulos
- ✅ Validação de serviços em batch
🏗️ Arquitetura Implementada¶
Bounded Context: service_catalogs¶
- Schema:
service_catalogs(isolado no PostgreSQL) - Padrão: DDD + CQRS
- Naming: snake_case no banco, PascalCase no código
Agregados de Domínio¶
ServiceCategory (Aggregate Root)¶
public sealed class ServiceCategory : AggregateRoot<ServiceCategoryId>
{
public string Name { get; private set; } // Nome da categoria
public string? Description { get; private set; } // Descrição opcional
public bool IsActive { get; private set; } // Status ativo/inativo
public int DisplayOrder { get; private set; } // Ordem de exibição
// Factory method
public static ServiceCategory Create(string name, string? description, int displayOrder);
// Behavior
public void Update(string name, string? description, int displayOrder);
public void Activate();
public void Deactivate();
}
Regras de Negócio: - Nome deve ser único - DisplayOrder deve ser >= 0 - Descrição é opcional (max 500 caracteres) - Não pode ser deletada se tiver serviços vinculados
Service (Aggregate Root)¶
public sealed class Service : AggregateRoot<ServiceId>
{
public ServiceCategoryId CategoryId { get; private set; } // Categoria pai
public string Name { get; private set; } // Nome do serviço
public string? Description { get; private set; } // Descrição opcional
public bool IsActive { get; private set; } // Status ativo/inativo
public int DisplayOrder { get; private set; } // Ordem de exibição
public ServiceCategory? Category { get; private set; } // Navegação
// Factory method
public static Service Create(ServiceCategoryId categoryId, string name, string? description, int displayOrder);
// Behavior
public void Update(string name, string? description, int displayOrder);
public void ChangeCategory(ServiceCategoryId newCategoryId);
public void Activate();
public void Deactivate();
}
Regras de Negócio: - Nome deve ser único - DisplayOrder deve ser >= 0 - Categoria deve estar ativa - Descrição é opcional (max 1000 caracteres)
Value Objects¶
// Strongly-typed IDs
public sealed record ServiceCategoryId(Guid Value) : EntityId(Value);
public sealed record ServiceId(Guid Value) : EntityId(Value);
Constantes de Validação¶
// Shared/Constants/ValidationConstants.cs
public static class CatalogLimits
{
public const int ServiceCategoryNameMaxLength = 100;
public const int ServiceCategoryDescriptionMaxLength = 500;
public const int ServiceNameMaxLength = 150;
public const int ServiceDescriptionMaxLength = 1000;
}
🔄 Domain Events¶
// ServiceCategory Events
public sealed record ServiceCategoryCreatedDomainEvent(ServiceCategoryId CategoryId);
public sealed record ServiceCategoryUpdatedDomainEvent(ServiceCategoryId CategoryId);
public sealed record ServiceCategoryActivatedDomainEvent(ServiceCategoryId CategoryId);
public sealed record ServiceCategoryDeactivatedDomainEvent(ServiceCategoryId CategoryId);
// Service Events
public sealed record ServiceCreatedDomainEvent(ServiceId ServiceId, ServiceCategoryId CategoryId);
public sealed record ServiceUpdatedDomainEvent(ServiceId ServiceId);
public sealed record ServiceActivatedDomainEvent(ServiceId ServiceId);
public sealed record ServiceDeactivatedDomainEvent(ServiceId ServiceId);
public sealed record ServiceCategoryChangedDomainEvent(ServiceId ServiceId, ServiceCategoryId OldCategoryId, ServiceCategoryId NewCategoryId);
⚡ CQRS Implementado¶
Commands¶
ServiceCategory Commands¶
// Commands/ServiceCategory/
CreateServiceCategoryCommand(string Name, string? Description, int DisplayOrder)
UpdateServiceCategoryCommand(Guid Id, string Name, string? Description, int DisplayOrder)
DeleteServiceCategoryCommand(Guid Id)
ActivateServiceCategoryCommand(Guid Id)
DeactivateServiceCategoryCommand(Guid Id)
Service Commands¶
// Commands/Service/
CreateServiceCommand(Guid CategoryId, string Name, string? Description, int DisplayOrder)
UpdateServiceCommand(Guid Id, string Name, string? Description, int DisplayOrder)
DeleteServiceCommand(Guid Id)
ActivateServiceCommand(Guid Id)
DeactivateServiceCommand(Guid Id)
ChangeServiceCategoryCommand(Guid ServiceId, Guid NewCategoryId)
Queries¶
ServiceCategory Queries¶
// Queries/ServiceCategory/
GetServiceCategoryByIdQuery(Guid Id)
GetAllServiceCategoriesQuery(bool ActiveOnly = false)
GetServiceCategoriesWithCountQuery(bool ActiveOnly = false)
Service Queries¶
// Queries/Service/
GetServiceByIdQuery(Guid Id)
GetAllServicesQuery(bool ActiveOnly = false)
GetServicesByCategoryQuery(Guid CategoryId, bool ActiveOnly = false)
Command & Query Handlers¶
Handlers consolidados em:
- Application/Handlers/Commands/CommandHandlers.cs (11 handlers)
- Application/Handlers/Queries/QueryHandlers.cs (6 handlers)
🌐 API REST Implementada¶
ServiceCategory Endpoints¶
GET /api/v1/catalogs/categories # Listar categorias
GET /api/v1/catalogs/categories/{id} # Buscar categoria
GET /api/v1/catalogs/categories/with-counts # Categorias com contagem de serviços
POST /api/v1/catalogs/categories # Criar categoria [Admin]
PUT /api/v1/catalogs/categories/{id} # Atualizar categoria [Admin]
DELETE /api/v1/catalogs/categories/{id} # Deletar categoria [Admin]
POST /api/v1/catalogs/categories/{id}/activate # Ativar [Admin]
POST /api/v1/catalogs/categories/{id}/deactivate # Desativar [Admin]
Service Endpoints¶
GET /api/v1/catalogs/services # Listar serviços
GET /api/v1/catalogs/services/{id} # Buscar serviço
GET /api/v1/catalogs/services/category/{categoryId} # Por categoria
POST /api/v1/catalogs/services # Criar serviço [Admin]
PUT /api/v1/catalogs/services/{id} # Atualizar serviço [Admin]
DELETE /api/v1/catalogs/services/{id} # Deletar serviço [Admin]
POST /api/v1/catalogs/services/{id}/activate # Ativar [Admin]
POST /api/v1/catalogs/services/{id}/deactivate # Desativar [Admin]
POST /api/v1/catalogs/services/{id}/change-category # Mudar categoria [Admin]
POST /api/v1/catalogs/services/validate # Validar batch de serviços
Autorização: Todos os endpoints requerem role Admin, exceto GET e validate.
🔌 Module API - Comunicação Inter-Módulos¶
Interface Iservice_catalogsModuleApi¶
public interface Iservice_catalogsModuleApi : IModuleApi
{
// Service Categories
Task<Result<ModuleServiceCategoryDto?>> GetServiceCategoryByIdAsync(
Guid categoryId, CancellationToken ct = default);
Task<Result<IReadOnlyList<ModuleServiceCategoryDto>>> GetAllServiceCategoriesAsync(
bool activeOnly = true, CancellationToken ct = default);
// Services
Task<Result<ModuleServiceDto?>> GetServiceByIdAsync(
Guid serviceId, CancellationToken ct = default);
Task<Result<IReadOnlyList<ModuleServiceListDto>>> GetAllServicesAsync(
bool activeOnly = true, CancellationToken ct = default);
Task<Result<IReadOnlyList<ModuleServiceDto>>> GetServicesByCategoryAsync(
Guid categoryId, bool activeOnly = true, CancellationToken ct = default);
Task<Result<bool>> IsServiceActiveAsync(
Guid serviceId, CancellationToken ct = default);
// Batch Validation
Task<Result<ModuleServiceValidationResultDto>> ValidateServicesAsync(
IReadOnlyCollection<Guid> serviceIds, CancellationToken ct = default);
}
DTOs Públicos¶
public sealed record ModuleServiceCategoryDto(
Guid Id,
string Name,
string? Description,
bool IsActive,
int DisplayOrder
);
public sealed record ModuleServiceDto(
Guid Id,
Guid CategoryId,
string CategoryName,
string Name,
string? Description,
bool IsActive
);
public sealed record ModuleServiceListDto(
Guid Id,
Guid CategoryId,
string Name,
bool IsActive
);
public sealed record ModuleServiceValidationResultDto(
bool AllValid,
IReadOnlyList<Guid> InvalidServiceIds,
IReadOnlyList<Guid> InactiveServiceIds
);
Implementação¶
[ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)]
public sealed class service_catalogsModuleApi : Iservice_catalogsModuleApi
{
private static class ModuleMetadata
{
public const string Name = "service_catalogs";
public const string Version = "1.0";
}
// Health check via query materialização
public async Task<bool> IsAvailableAsync(CancellationToken ct = default)
{
var categories = await categoryRepository.GetAllAsync(activeOnly: true, ct);
return true; // Se query executou, módulo está disponível
}
}
Recursos: - ✅ Guid.Empty guards em todos os métodos - ✅ Batch query otimizada em ValidateServicesAsync (evita N+1) - ✅ GetByIdsAsync no repository para queries em lote - ✅ Health check via database connectivity
🗄️ Schema de Banco de Dados¶
-- Schema: service_catalogs
CREATE SCHEMA IF NOT EXISTS service_catalogs;
-- Tabela: service_categories
CREATE TABLE service_catalogs.service_categories (
id UUID PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(500),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
display_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CONSTRAINT ck_service_categories_display_order CHECK (display_order >= 0)
);
-- Tabela: services
CREATE TABLE service_catalogs.services (
id UUID PRIMARY KEY,
category_id UUID NOT NULL REFERENCES service_catalogs.service_categories(id),
name VARCHAR(150) NOT NULL UNIQUE,
description VARCHAR(1000),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
display_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CONSTRAINT ck_services_display_order CHECK (display_order >= 0)
);
-- Índices
CREATE INDEX idx_services_category_id ON service_catalogs.services(category_id);
CREATE INDEX idx_services_is_active ON service_catalogs.services(is_active);
CREATE INDEX idx_service_categories_is_active ON service_catalogs.service_categories(is_active);
CREATE INDEX idx_service_categories_display_order ON service_catalogs.service_categories(display_order);
CREATE INDEX idx_services_display_order ON service_catalogs.services(display_order);
🔗 Integração com Outros Módulos¶
Providers Module (Futuro)¶
// Providers poderá vincular serviços aos prestadores
public class Provider
{
public IReadOnlyCollection<ProviderService> Services { get; }
}
public class ProviderService
{
public Guid ServiceId { get; set; } // FK para service_catalogs.Service
public decimal Price { get; set; }
public bool IsOffered { get; set; }
}
Search Module (Futuro)¶
// Search denormalizará serviços no SearchableProvider
public class SearchableProvider
{
public Guid[] ServiceIds { get; set; } // Array de IDs de serviços
}
📊 Estrutura de Pastas¶
src/Modules/ServiceCatalogs/
├── API/
│ ├── Endpoints/
│ │ ├── ServiceCategoryEndpoints.cs
│ │ ├── ServiceEndpoints.cs
│ │ └── service_catalogsModuleEndpoints.cs
│ └── MeAjudaAi.Modules.service_catalogs.API.csproj
├── Application/
│ ├── Commands/
│ │ ├── Service/ # 6 commands
│ │ └── ServiceCategory/ # 5 commands
│ ├── Queries/
│ │ ├── Service/ # 3 queries
│ │ └── ServiceCategory/ # 3 queries
│ ├── Handlers/
│ │ ├── Commands/
│ │ │ └── CommandHandlers.cs # 11 handlers consolidados
│ │ └── Queries/
│ │ └── QueryHandlers.cs # 6 handlers consolidados
│ ├── DTOs/ # 5 DTOs
│ ├── ModuleApi/
│ │ └── service_catalogsModuleApi.cs
│ └── MeAjudaAi.Modules.service_catalogs.Application.csproj
├── Domain/
│ ├── Entities/
│ │ ├── Service.cs
│ │ └── ServiceCategory.cs
│ ├── Events/
│ │ ├── ServiceDomainEvents.cs
│ │ └── ServiceCategoryDomainEvents.cs
│ ├── Exceptions/
│ │ └── CatalogDomainException.cs
│ ├── Repositories/
│ │ ├── IServiceRepository.cs
│ │ └── IServiceCategoryRepository.cs
│ ├── ValueObjects/
│ │ ├── ServiceId.cs
│ │ └── ServiceCategoryId.cs
│ └── MeAjudaAi.Modules.service_catalogs.Domain.csproj
├── Infrastructure/
│ ├── Persistence/
│ │ ├── service_catalogsDbContext.cs
│ │ ├── Configurations/
│ │ │ ├── ServiceConfiguration.cs
│ │ │ └── ServiceCategoryConfiguration.cs
│ │ └── Repositories/
│ │ ├── ServiceRepository.cs
│ │ └── ServiceCategoryRepository.cs
│ ├── Extensions.cs
│ └── MeAjudaAi.Modules.service_catalogs.Infrastructure.csproj
└── Tests/
├── Builders/
│ ├── ServiceBuilder.cs
│ └── ServiceCategoryBuilder.cs
└── Unit/
├── Application/
│ └── Handlers/ # Testes de handlers
└── Domain/
└── Entities/
├── ServiceTests.cs # 15+ testes
└── ServiceCategoryTests.cs # 102 testes
🧪 Testes Implementados¶
Testes Unitários de Domínio¶
- ✅ ServiceCategoryTests: 102 testes passando
- Criação, atualização, ativação/desativação
- Boundary testing (MaxLength, MaxLength+1)
- Trimming de name/description
- Timestamp verification
- Idempotent operations
- ✅ ServiceTests: 15+ testes
- CRUD completo
- ChangeCategory
- Domain events
Testes de Integração¶
- ✅ service_catalogsIntegrationTests: 29 testes passando
- Endpoints REST completos
- Module API
- Repository operations
Cobertura de Código¶
- Domain: >95%
- Application: >85%
- Infrastructure: >70%
📈 Métricas e Desempenho¶
Otimizações Implementadas¶
- ✅ Batch query em ValidateServicesAsync (Contains predicate)
- ✅ GetByIdsAsync para evitar N+1
- ✅ AsNoTracking() em queries read-only
- ✅ Índices em is_active, category_id, display_order
- ✅ Health check via query materialização (não Count extra)
SLAs Esperados¶
- GetById: <50ms
- GetAll: <200ms
- Create/Update: <100ms
- ValidateServices (batch): <300ms
🚀 Próximos Passos¶
Fase 2 - Integração com Providers¶
- Criar tabela
provider_serviceslinking - Permitir prestadores vincularem serviços do catálogo
- Adicionar pricing customizado por prestador
Fase 3 - Search Integration¶
- Denormalizar services em SearchableProvider
- Worker para sincronizar alterações via Integration Events
- Filtros de busca por serviço
Melhorias Futuras¶
- Hierarquia de subcategorias (atualmente flat)
- Ícones para categorias
- Localização (i18n) de nomes/descrições
- Versionamento de catálogo
- Audit log de mudanças administrativas
📚 Referências¶
- Roadmap - Planejamento estratégico
- Architecture - Padrões arquiteturais
- Providers Module - Integração futura
- SearchProviders Module - Integração de busca
📅 Implementado: Novembro 2025
✅ Status: Produção Ready
🧪 Testes: 102 unit + 29 integration (100% passing)