Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 2 additions & 44 deletions MeAjudaAi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -149,23 +149,11 @@ 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}"
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.Domain", "src\Modules\ServiceCatalogs\Domain\MeAjudaAi.Modules.ServiceCatalogs.Domain.csproj", "{DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}"
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}"
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}"
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.Infrastructure", "src\Modules\ServiceCatalogs\Infrastructure\MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj", "{3B6D6C13-1E04-47B9-B44E-36D25DF913C7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -609,30 +597,6 @@ 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
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x64.ActiveCfg = Debug|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x64.Build.0 = Debug|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x86.ActiveCfg = Debug|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x86.Build.0 = Debug|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|Any CPU.Build.0 = Release|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x64.ActiveCfg = Release|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x64.Build.0 = Release|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x86.ActiveCfg = Release|Any CPU
{2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -707,14 +671,8 @@ 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}
{1510B873-F5F8-8A20-05CA-B70BA1F93C8F} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
{44577491-2FC0-4F52-AF5C-2BC9B323CDB7} = {1510B873-F5F8-8A20-05CA-B70BA1F93C8F}
{8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
{3B6D6C13-1E04-47B9-B44E-36D25DF913C7} = {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081}
{A63FE417-CEAA-2A64-637A-6EABC61CE16D} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} = {A63FE417-CEAA-2A64-637A-6EABC61CE16D}
{BDD25844-1435-F5BA-1F9B-EFB3B12C916F} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
{2C85E336-66A2-4B4F-845A-DBA2A6520162} = {BDD25844-1435-F5BA-1F9B-EFB3B12C916F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {391B5342-8EC5-4DF0-BCDA-6D73F87E8751}
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);
Loading