feat(domain): ProductType entity + domain exceptions (PRD-001)

This commit is contained in:
2026-04-19 09:36:29 -03:00
parent de70152d3e
commit 132d17c99f
7 changed files with 616 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
using FluentAssertions;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Domain.ProductTypes;
public class ProductTypeExceptionsTests
{
// ── ProductTypeNotFoundException ──────────────────────────────────────────
[Fact]
public void ProductTypeNotFoundException_ContainsIdInMessage()
{
var ex = new ProductTypeNotFoundException(42);
ex.Message.Should().Contain("42");
}
[Fact]
public void ProductTypeNotFoundException_ExposesProductTypeId()
{
var ex = new ProductTypeNotFoundException(99);
ex.ProductTypeId.Should().Be(99);
}
[Fact]
public void ProductTypeNotFoundException_IsSubclassOfDomainException()
{
var ex = new ProductTypeNotFoundException(1);
ex.Should().BeAssignableTo<DomainException>();
}
// ── ProductTypeNombreDuplicadoException ───────────────────────────────────
[Fact]
public void ProductTypeNombreDuplicadoException_ContainsNombreInMessage()
{
var ex = new ProductTypeNombreDuplicadoException("Clasificados");
ex.Message.Should().Contain("Clasificados");
}
[Fact]
public void ProductTypeNombreDuplicadoException_ExposesNombre()
{
var ex = new ProductTypeNombreDuplicadoException("Notables");
ex.Nombre.Should().Be("Notables");
}
[Fact]
public void ProductTypeNombreDuplicadoException_IsSubclassOfDomainException()
{
var ex = new ProductTypeNombreDuplicadoException("x");
ex.Should().BeAssignableTo<DomainException>();
}
// ── ProductTypeEnUsoException ─────────────────────────────────────────────
[Fact]
public void ProductTypeEnUsoException_ContainsCountInMessage()
{
var ex = new ProductTypeEnUsoException(id: 5, productsActivos: 7);
ex.Message.Should().Contain("7");
}
[Fact]
public void ProductTypeEnUsoException_ExposesProductTypeIdAndCount()
{
var ex = new ProductTypeEnUsoException(id: 10, productsActivos: 3);
ex.ProductTypeId.Should().Be(10);
ex.ProductsActivos.Should().Be(3);
}
[Fact]
public void ProductTypeEnUsoException_IsSubclassOfDomainException()
{
var ex = new ProductTypeEnUsoException(1, 0);
ex.Should().BeAssignableTo<DomainException>();
}
// ── ProductTypeFlagsIncoherentesException ─────────────────────────────────
[Fact]
public void ProductTypeFlagsIncoherentesException_ContainsReasonInMessage()
{
var ex = new ProductTypeFlagsIncoherentesException("IsBundle sin hijos definidos");
ex.Message.Should().Contain("IsBundle sin hijos definidos");
}
[Fact]
public void ProductTypeFlagsIncoherentesException_ExposesReason()
{
var ex = new ProductTypeFlagsIncoherentesException("razón técnica");
ex.Reason.Should().Be("razón técnica");
}
[Fact]
public void ProductTypeFlagsIncoherentesException_IsSubclassOfDomainException()
{
var ex = new ProductTypeFlagsIncoherentesException("x");
ex.Should().BeAssignableTo<DomainException>();
}
}

View File

@@ -0,0 +1,254 @@
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using SIGCM2.Domain.Entities;
namespace SIGCM2.Application.Tests.Domain.ProductTypes;
public class ProductTypeTests
{
private static readonly FakeTimeProvider FakeTime =
new(new DateTimeOffset(2026, 4, 19, 10, 0, 0, TimeSpan.Zero));
// ── ForCreation: happy path ──────────────────────────────────────────────
[Fact]
public void ForCreation_ValidInputs_ReturnsNewInstance_WithActiveTrue()
{
var pt = ProductType.ForCreation(
"Clasificados",
hasDuration: true, requiresText: true, requiresCategory: false, isBundle: false,
allowImages: false,
maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
FakeTime);
pt.Id.Should().Be(0);
pt.Nombre.Should().Be("Clasificados");
pt.HasDuration.Should().BeTrue();
pt.RequiresText.Should().BeTrue();
pt.RequiresCategory.Should().BeFalse();
pt.IsBundle.Should().BeFalse();
pt.AllowImages.Should().BeFalse();
pt.IsActive.Should().BeTrue();
pt.FechaCreacion.Should().Be(FakeTime.GetUtcNow().UtcDateTime);
pt.FechaModificacion.Should().BeNull();
}
// ── ForCreation: Nombre validations ──────────────────────────────────────
[Fact]
public void ForCreation_NombreNull_ThrowsArgumentException()
{
var act = () => ProductType.ForCreation(
null!, false, false, false, false, false,
null, null, null, null, FakeTime);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void ForCreation_NombreWhitespace_ThrowsArgumentException()
{
var act = () => ProductType.ForCreation(
" ", false, false, false, false, false,
null, null, null, null, FakeTime);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void ForCreation_NombreOver200Chars_ThrowsArgumentException()
{
var nombre = new string('X', 201);
var act = () => ProductType.ForCreation(
nombre, false, false, false, false, false,
null, null, null, null, FakeTime);
act.Should().Throw<ArgumentException>();
}
// ── ForCreation: multimedia normalization ────────────────────────────────
[Fact]
public void ForCreation_AllowImagesFalse_NormalizesAll4MultimediaFieldsToNull()
{
var pt = ProductType.ForCreation(
"Clasificados",
false, false, false, false,
allowImages: false,
maxImages: 5, maxImageSizeMB: 2.5m, maxImageWidth: 1920, maxImageHeight: 1080,
FakeTime);
pt.MaxImages.Should().BeNull();
pt.MaxImageSizeMB.Should().BeNull();
pt.MaxImageWidth.Should().BeNull();
pt.MaxImageHeight.Should().BeNull();
}
[Fact]
public void ForCreation_AllowImagesTrue_PreservesMultimediaValues()
{
var pt = ProductType.ForCreation(
"Fotos",
false, false, false, false,
allowImages: true,
maxImages: 10, maxImageSizeMB: 5.0m, maxImageWidth: 800, maxImageHeight: 600,
FakeTime);
pt.AllowImages.Should().BeTrue();
pt.MaxImages.Should().Be(10);
pt.MaxImageSizeMB.Should().Be(5.0m);
pt.MaxImageWidth.Should().Be(800);
pt.MaxImageHeight.Should().Be(600);
}
[Fact]
public void ForCreation_AllowImagesTrue_AllMaxNull_IsValid()
{
var pt = ProductType.ForCreation(
"Fotos sin límite",
false, false, false, false,
allowImages: true,
maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
FakeTime);
pt.AllowImages.Should().BeTrue();
pt.MaxImages.Should().BeNull();
pt.MaxImageSizeMB.Should().BeNull();
pt.MaxImageWidth.Should().BeNull();
pt.MaxImageHeight.Should().BeNull();
}
// ── WithRenamed ──────────────────────────────────────────────────────────
[Fact]
public void WithRenamed_ValidNombre_ReturnsNewInstance_WithFechaModificacionUpdated()
{
var original = ProductType.ForCreation("Original", false, false, false, false, false, null, null, null, null, FakeTime);
var updated = FakeTimeProvider2();
var renamed = original.WithRenamed("Nuevo", updated);
renamed.Nombre.Should().Be("Nuevo");
renamed.FechaModificacion.Should().Be(updated.GetUtcNow().UtcDateTime);
}
[Fact]
public void WithRenamed_DoesNotMutateOriginal()
{
var original = ProductType.ForCreation("Original", false, false, false, false, false, null, null, null, null, FakeTime);
_ = original.WithRenamed("Nuevo", FakeTime);
original.Nombre.Should().Be("Original");
original.FechaModificacion.Should().BeNull();
}
// ── WithUpdatedFlags ─────────────────────────────────────────────────────
[Fact]
public void WithUpdatedFlags_SetsFlags_PreservesMultimedia()
{
var original = ProductType.ForCreation(
"Tipo", false, false, false, false,
allowImages: true, maxImages: 5, maxImageSizeMB: 2.0m, maxImageWidth: null, maxImageHeight: null,
FakeTime);
var updated = original.WithUpdatedFlags(true, true, true, true, FakeTime);
updated.HasDuration.Should().BeTrue();
updated.RequiresText.Should().BeTrue();
updated.RequiresCategory.Should().BeTrue();
updated.IsBundle.Should().BeTrue();
updated.AllowImages.Should().BeTrue();
updated.MaxImages.Should().Be(5);
updated.MaxImageSizeMB.Should().Be(2.0m);
}
// ── WithUpdatedMultimedia ─────────────────────────────────────────────────
[Fact]
public void WithUpdatedMultimedia_AllowImagesFalse_NullifiesAllLimits()
{
var original = ProductType.ForCreation(
"Tipo", false, false, false, false,
allowImages: true, maxImages: 5, maxImageSizeMB: 2.0m, maxImageWidth: 1024, maxImageHeight: 768,
FakeTime);
var updated = original.WithUpdatedMultimedia(false, 3, 1.0m, 800, 600, FakeTime);
updated.AllowImages.Should().BeFalse();
updated.MaxImages.Should().BeNull();
updated.MaxImageSizeMB.Should().BeNull();
updated.MaxImageWidth.Should().BeNull();
updated.MaxImageHeight.Should().BeNull();
}
[Fact]
public void WithUpdatedMultimedia_AllowImagesTrue_PreservesLimits()
{
var original = ProductType.ForCreation("Tipo", false, false, false, false, false, null, null, null, null, FakeTime);
var updated = original.WithUpdatedMultimedia(true, 8, 3.5m, 1920, 1080, FakeTime);
updated.AllowImages.Should().BeTrue();
updated.MaxImages.Should().Be(8);
updated.MaxImageSizeMB.Should().Be(3.5m);
updated.MaxImageWidth.Should().Be(1920);
updated.MaxImageHeight.Should().Be(1080);
}
// ── WithDeactivated ──────────────────────────────────────────────────────
[Fact]
public void WithDeactivated_SetsIsActiveFalse_AndFechaModificacion()
{
var original = ProductType.ForCreation("Tipo", false, false, false, false, false, null, null, null, null, FakeTime);
var tp2 = FakeTimeProvider2();
var deactivated = original.WithDeactivated(tp2);
deactivated.IsActive.Should().BeFalse();
deactivated.FechaModificacion.Should().Be(tp2.GetUtcNow().UtcDateTime);
original.IsActive.Should().BeTrue(); // original not mutated
}
// ── Hydration ────────────────────────────────────────────────────────────
[Fact]
public void Constructor_HydrationRoundtrip_PreservesAllFields()
{
var fechaCreacion = new DateTime(2026, 1, 15, 8, 0, 0, DateTimeKind.Utc);
var fechaModificacion = new DateTime(2026, 3, 10, 9, 30, 0, DateTimeKind.Utc);
var pt = new ProductType(
id: 42,
nombre: "Bundle Aniversario",
hasDuration: true,
requiresText: false,
requiresCategory: true,
isBundle: true,
allowImages: true,
maxImages: 12,
maxImageSizeMB: 2.75m,
maxImageWidth: 1920,
maxImageHeight: 1080,
isActive: false,
fechaCreacion: fechaCreacion,
fechaModificacion: fechaModificacion);
pt.Id.Should().Be(42);
pt.Nombre.Should().Be("Bundle Aniversario");
pt.HasDuration.Should().BeTrue();
pt.RequiresText.Should().BeFalse();
pt.RequiresCategory.Should().BeTrue();
pt.IsBundle.Should().BeTrue();
pt.AllowImages.Should().BeTrue();
pt.MaxImages.Should().Be(12);
pt.MaxImageSizeMB.Should().Be(2.75m);
pt.MaxImageWidth.Should().Be(1920);
pt.MaxImageHeight.Should().Be(1080);
pt.IsActive.Should().BeFalse();
pt.FechaCreacion.Should().Be(fechaCreacion);
pt.FechaModificacion.Should().Be(fechaModificacion);
}
// ── Helper ───────────────────────────────────────────────────────────────
private static FakeTimeProvider FakeTimeProvider2() =>
new(new DateTimeOffset(2026, 4, 20, 15, 0, 0, TimeSpan.Zero));
}