Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a6df0f2
refactor: rename Catalogs to ServiceCatalogs - Phase 1a (Domain + Inf…
Nov 19, 2025
811e957
refactor: rename Catalogs to ServiceCatalogs - Phase 1b (Application …
Nov 19, 2025
b07d182
refactor: rename Catalogs to ServiceCatalogs - Phase 1c (Module Tests)
Nov 19, 2025
280a587
refactor: rename Catalogs to ServiceCatalogs - Phase 2 (Shared + Test…
Nov 19, 2025
df566da
merge: integrate Locations rename from master into Catalogs branch
Nov 19, 2025
0f6ad4f
merge: integrate latest changes from master (SearchProviders rename) …
Nov 19, 2025
32d206f
fix: update solution file to reflect Search → SearchProviders rename
Nov 19, 2025
8aed422
Merge branch 'master' into rename-domain-catalogs
Nov 19, 2025
d48f6a9
feat: add ServiceCatalogs module tests to solution (Phase 1c)
Nov 19, 2025
37e7204
feat: complete ServiceCatalogs module rename with Phase 2 integration
Nov 19, 2025
7a67606
fix: apply documentation and code quality improvements
Nov 19, 2025
ac7f9a5
fix: complete ServiceCatalogs module renaming consistency
Nov 19, 2025
2e74c66
refactor: update test class names and comments for ServiceCatalogs co…
Nov 19, 2025
7c64028
refactor: improve test code quality and maintainability
Nov 19, 2025
f3e6d4d
fix: complete naming consistency and strengthen test assertions
Nov 19, 2025
53c05c8
feat: register ServiceCatalogs module in ApiService
Nov 19, 2025
4f2cd6f
refactor: complete old Catalogs module removal and fix ROADMAP class …
Nov 19, 2025
aea3282
fix: correct ServiceCatalogs schema to snake_case and update ROADMAP …
Nov 19, 2025
72c518d
fix: standardize ServiceCatalogs schema to snake_case across all migr…
Nov 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
refactor: rename Catalogs to ServiceCatalogs - Phase 1a (Domain + Inf…
…rastructure)

- Renamed Domain layer: entities, value objects, repositories, events
- Renamed Infrastructure layer: DbContext, migrations, repositories
- Updated namespaces to MeAjudaAi.Modules.ServiceCatalogs
- Updated solution file to reference new paths
- Updated Shared validation constants

Files: 32 (Domain + Infrastructure layers)
Phase: 1a of 3
Next: Phase 1b will add Application + API layers
  • Loading branch information
Filipe Frigini committed Nov 19, 2025
commit a6df0f2175d1dbe0a697e994938c14a149247db7
12 changes: 6 additions & 6 deletions MeAjudaAi.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157
Expand Down Expand Up @@ -149,23 +149,23 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Catalogs", "Catalogs", "{8B
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{B346CC0B-427A-E442-6F5D-8AAE1AB081D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Catalogs.Domain", "src\Modules\Catalogs\Domain\MeAjudaAi.Modules.Catalogs.Domain.csproj", "{DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}"
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", "{1510B873-F5F8-8A20-05CA-B70BA1F93C8F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Catalogs.Application", "src\Modules\Catalogs\Application\MeAjudaAi.Modules.Catalogs.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", "{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.Catalogs.Infrastructure", "src\Modules\Catalogs\Infrastructure\MeAjudaAi.Modules.Catalogs.Infrastructure.csproj", "{3B6D6C13-1E04-47B9-B44E-36D25DF913C7}"
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", "{A63FE417-CEAA-2A64-637A-6EABC61CE16D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Catalogs.API", "src\Modules\Catalogs\API\MeAjudaAi.Modules.Catalogs.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", "{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BDD25844-1435-F5BA-1F9B-EFB3B12C916F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Catalogs.Tests", "src\Modules\Catalogs\Tests\MeAjudaAi.Modules.Catalogs.Tests.csproj", "{2C85E336-66A2-4B4F-845A-DBA2A6520162}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Tests", "src\Modules\ServiceCatalogs\Tests\MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj", "{2C85E336-66A2-4B4F-845A-DBA2A6520162}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
159 changes: 159 additions & 0 deletions src/Modules/ServiceCatalogs/Domain/Entities/Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.Service;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Constants;
using MeAjudaAi.Shared.Domain;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities;

/// <summary>
/// Representa um serviço específico que provedores podem oferecer (ex: "Limpeza de Apartamento", "Conserto de Torneira").
/// Serviços pertencem a uma categoria e podem ser ativados/desativados por administradores.
/// </summary>
public sealed class Service : AggregateRoot<ServiceId>
{
/// <summary>
/// ID da categoria à qual este serviço pertence.
/// </summary>
public ServiceCategoryId CategoryId { get; private set; } = null!;

/// <summary>
/// Nome do serviço.
/// </summary>
public string Name { get; private set; } = string.Empty;

/// <summary>
/// Descrição opcional explicando o que este serviço inclui.
/// </summary>
public string? Description { get; private set; }

/// <summary>
/// Indica se este serviço está atualmente ativo e disponível para provedores oferecerem.
/// Serviços desativados são ocultados do catálogo.
/// </summary>
public bool IsActive { get; private set; }

/// <summary>
/// Ordem de exibição opcional dentro da categoria para ordenação na UI.
/// </summary>
public int DisplayOrder { get; private set; }

// Navigation property (loaded explicitly when needed)
public ServiceCategory? Category { get; private set; }

// EF Core constructor
private Service() { }

/// <summary>
/// Cria um novo serviço dentro de uma categoria.
/// </summary>
/// <param name="categoryId">ID da categoria pai</param>
/// <param name="name">Nome do serviço (obrigatório, 1-150 caracteres)</param>
/// <param name="description">Descrição opcional do serviço (máx 1000 caracteres)</param>
/// <param name="displayOrder">Ordem de exibição para ordenação (padrão: 0)</param>
/// <exception cref="CatalogDomainException">Lançada quando a validação falha</exception>
public static Service Create(ServiceCategoryId categoryId, string name, string? description = null, int displayOrder = 0)
{
if (categoryId is null)
throw new CatalogDomainException("Category ID is required.");

ValidateName(name);
ValidateDescription(description);
ValidateDisplayOrder(displayOrder);

var service = new Service
{
Id = ServiceId.New(),
CategoryId = categoryId,
Name = name.Trim(),
Description = description?.Trim(),
IsActive = true,
DisplayOrder = displayOrder
};

service.AddDomainEvent(new ServiceCreatedDomainEvent(service.Id, categoryId));
return service;
}

/// <summary>
/// Atualiza as informações do serviço.
/// </summary>
public void Update(string name, string? description = null, int displayOrder = 0)
{
ValidateName(name);
ValidateDescription(description);
ValidateDisplayOrder(displayOrder);

Name = name.Trim();
Description = description?.Trim();
DisplayOrder = displayOrder;
MarkAsUpdated();

AddDomainEvent(new ServiceUpdatedDomainEvent(Id));
}

/// <summary>
/// Altera a categoria deste serviço.
/// </summary>
public void ChangeCategory(ServiceCategoryId newCategoryId)
{
if (newCategoryId is null)
throw new CatalogDomainException("Category ID is required.");

if (CategoryId.Value == newCategoryId.Value)
return;

var oldCategoryId = CategoryId;
CategoryId = newCategoryId;
Category = null; // Invalidar navegação para forçar recarga quando necessário
MarkAsUpdated();

AddDomainEvent(new ServiceCategoryChangedDomainEvent(Id, oldCategoryId, newCategoryId));
}

/// <summary>
/// Ativa o serviço, tornando-o disponível no catálogo.
/// </summary>
public void Activate()
{
if (IsActive) return;

IsActive = true;
MarkAsUpdated();
AddDomainEvent(new ServiceActivatedDomainEvent(Id));
}

/// <summary>
/// Desativa o serviço, removendo-o do catálogo.
/// Provedores que atualmente oferecem este serviço o mantêm, mas novas atribuições são impedidas.
/// </summary>
public void Deactivate()
{
if (!IsActive) return;

IsActive = false;
MarkAsUpdated();
AddDomainEvent(new ServiceDeactivatedDomainEvent(Id));
}

private static void ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new CatalogDomainException("Service name is required.");

if (name.Trim().Length > ValidationConstants.CatalogLimits.ServiceNameMaxLength)
throw new CatalogDomainException($"Service name cannot exceed {ValidationConstants.CatalogLimits.ServiceNameMaxLength} characters.");
}

private static void ValidateDescription(string? description)
{
if (description is not null && description.Trim().Length > ValidationConstants.CatalogLimits.ServiceDescriptionMaxLength)
throw new CatalogDomainException($"Service description cannot exceed {ValidationConstants.CatalogLimits.ServiceDescriptionMaxLength} characters.");
}

private static void ValidateDisplayOrder(int displayOrder)
{
if (displayOrder < 0)
throw new CatalogDomainException("Display order cannot be negative.");
}
}
127 changes: 127 additions & 0 deletions src/Modules/ServiceCatalogs/Domain/Entities/ServiceCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.ServiceCategory;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Constants;
using MeAjudaAi.Shared.Domain;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities;

/// <summary>
/// Representa uma categoria de serviço no catálogo (ex: "Limpeza", "Reparos").
/// Categorias organizam serviços em grupos lógicos para facilitar a descoberta.
/// </summary>
public sealed class ServiceCategory : AggregateRoot<ServiceCategoryId>
{
/// <summary>
/// Nome da categoria.
/// </summary>
public string Name { get; private set; } = string.Empty;

/// <summary>
/// Descrição opcional explicando quais serviços pertencem a esta categoria.
/// </summary>
public string? Description { get; private set; }

/// <summary>
/// Indica se esta categoria está atualmente ativa e disponível para uso.
/// Categorias desativadas não podem ser atribuídas a novos serviços.
/// </summary>
public bool IsActive { get; private set; }

/// <summary>
/// Ordem de exibição opcional para ordenação na UI.
/// </summary>
public int DisplayOrder { get; private set; }

// EF Core constructor
private ServiceCategory() { }

/// <summary>
/// Cria uma nova categoria de serviço.
/// </summary>
/// <param name="name">Nome da categoria (obrigatório, 1-100 caracteres)</param>
/// <param name="description">Descrição opcional da categoria (máx 500 caracteres)</param>
/// <param name="displayOrder">Ordem de exibição para ordenação (padrão: 0)</param>
/// <exception cref="CatalogDomainException">Lançada quando a validação falha</exception>
public static ServiceCategory Create(string name, string? description = null, int displayOrder = 0)
{
ValidateName(name);
ValidateDescription(description);
ValidateDisplayOrder(displayOrder);

var category = new ServiceCategory
{
Id = ServiceCategoryId.New(),
Name = name.Trim(),
Description = description?.Trim(),
IsActive = true,
DisplayOrder = displayOrder
};

category.AddDomainEvent(new ServiceCategoryCreatedDomainEvent(category.Id));
return category;
}

/// <summary>
/// Atualiza as informações da categoria.
/// </summary>
public void Update(string name, string? description = null, int displayOrder = 0)
{
ValidateName(name);
ValidateDescription(description);
ValidateDisplayOrder(displayOrder);

Name = name.Trim();
Description = description?.Trim();
DisplayOrder = displayOrder;
MarkAsUpdated();

AddDomainEvent(new ServiceCategoryUpdatedDomainEvent(Id));
}

/// <summary>
/// Ativa a categoria, tornando-a disponível para uso.
/// </summary>
public void Activate()
{
if (IsActive) return;

IsActive = true;
MarkAsUpdated();
AddDomainEvent(new ServiceCategoryActivatedDomainEvent(Id));
}

/// <summary>
/// Desativa a categoria, impedindo que seja atribuída a novos serviços.
/// Serviços existentes mantêm sua atribuição de categoria.
/// </summary>
public void Deactivate()
{
if (!IsActive) return;

IsActive = false;
MarkAsUpdated();
AddDomainEvent(new ServiceCategoryDeactivatedDomainEvent(Id));
}

private static void ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new CatalogDomainException("Category name is required.");

if (name.Trim().Length > ValidationConstants.CatalogLimits.ServiceCategoryNameMaxLength)
throw new CatalogDomainException($"Category name cannot exceed {ValidationConstants.CatalogLimits.ServiceCategoryNameMaxLength} characters.");
}

private static void ValidateDescription(string? description)
{
if (description is not null && description.Trim().Length > ValidationConstants.CatalogLimits.ServiceCategoryDescriptionMaxLength)
throw new CatalogDomainException($"Category description cannot exceed {ValidationConstants.CatalogLimits.ServiceCategoryDescriptionMaxLength} characters.");
}

private static void ValidateDisplayOrder(int displayOrder)
{
if (displayOrder < 0)
throw new CatalogDomainException("Display order cannot be negative.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.Service;

public sealed record ServiceActivatedDomainEvent(ServiceId ServiceId)
: DomainEvent(ServiceId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.Service;

public sealed record ServiceCategoryChangedDomainEvent(
ServiceId ServiceId,
ServiceCategoryId OldCategoryId,
ServiceCategoryId NewCategoryId)
: DomainEvent(ServiceId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.Service;

public sealed record ServiceCreatedDomainEvent(ServiceId ServiceId, ServiceCategoryId CategoryId)
: DomainEvent(ServiceId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.Service;

public sealed record ServiceDeactivatedDomainEvent(ServiceId ServiceId)
: DomainEvent(ServiceId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.Service;

public sealed record ServiceUpdatedDomainEvent(ServiceId ServiceId)
: DomainEvent(ServiceId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.ServiceCategory;

public sealed record ServiceCategoryActivatedDomainEvent(ServiceCategoryId CategoryId)
: DomainEvent(CategoryId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.ServiceCategory;

public sealed record ServiceCategoryCreatedDomainEvent(ServiceCategoryId CategoryId)
: DomainEvent(CategoryId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.ServiceCategory;

public sealed record ServiceCategoryDeactivatedDomainEvent(ServiceCategoryId CategoryId)
: DomainEvent(CategoryId.Value, Version: 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
using MeAjudaAi.Shared.Events;

namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Events.ServiceCategory;

public sealed record ServiceCategoryUpdatedDomainEvent(ServiceCategoryId CategoryId)
: DomainEvent(CategoryId.Value, Version: 1);
Loading