Pular para conteúdo

📋 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_services linking
  • 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


📅 Implementado: Novembro 2025
✅ Status: Produção Ready
🧪 Testes: 102 unit + 29 integration (100% passing)