feat: PRD-001 ProductType (flags + multimedia) #38
186
src/api/SIGCM2.Domain/Entities/ProductType.cs
Normal file
186
src/api/SIGCM2.Domain/Entities/ProductType.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
namespace SIGCM2.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable product-type descriptor for the commercial catalog.
|
||||
/// Flags drive form behavior (HasDuration, RequiresText, RequiresCategory, IsBundle).
|
||||
/// Multimedia limits (Max*) are null = no limit.
|
||||
/// Invariant: if AllowImages == false, the 4 Max* fields must be null
|
||||
/// (enforced by ForCreation and WithUpdatedMultimedia).
|
||||
/// </summary>
|
||||
public sealed class ProductType
|
||||
{
|
||||
private const int NombreMaxLength = 200;
|
||||
|
||||
public int Id { get; }
|
||||
public string Nombre { get; }
|
||||
public bool HasDuration { get; }
|
||||
public bool RequiresText { get; }
|
||||
public bool RequiresCategory { get; }
|
||||
public bool IsBundle { get; }
|
||||
public bool AllowImages { get; }
|
||||
public int? MaxImages { get; }
|
||||
public decimal? MaxImageSizeMB { get; }
|
||||
public int? MaxImageWidth { get; }
|
||||
public int? MaxImageHeight { get; }
|
||||
public bool IsActive { get; }
|
||||
public DateTime FechaCreacion { get; }
|
||||
public DateTime? FechaModificacion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Full hydration constructor — used by the repository to reconstruct from DB rows.
|
||||
/// </summary>
|
||||
public ProductType(
|
||||
int id,
|
||||
string nombre,
|
||||
bool hasDuration,
|
||||
bool requiresText,
|
||||
bool requiresCategory,
|
||||
bool isBundle,
|
||||
bool allowImages,
|
||||
int? maxImages,
|
||||
decimal? maxImageSizeMB,
|
||||
int? maxImageWidth,
|
||||
int? maxImageHeight,
|
||||
bool isActive,
|
||||
DateTime fechaCreacion,
|
||||
DateTime? fechaModificacion)
|
||||
{
|
||||
Id = id;
|
||||
Nombre = nombre;
|
||||
HasDuration = hasDuration;
|
||||
RequiresText = requiresText;
|
||||
RequiresCategory = requiresCategory;
|
||||
IsBundle = isBundle;
|
||||
AllowImages = allowImages;
|
||||
MaxImages = maxImages;
|
||||
MaxImageSizeMB = maxImageSizeMB;
|
||||
MaxImageWidth = maxImageWidth;
|
||||
MaxImageHeight = maxImageHeight;
|
||||
IsActive = isActive;
|
||||
FechaCreacion = fechaCreacion;
|
||||
FechaModificacion = fechaModificacion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating a new ProductType.
|
||||
/// Id=0 — DB assigns via IDENTITY.
|
||||
/// IsActive=true, FechaModificacion=null by default.
|
||||
/// AllowImages=false normalizes all 4 Max* fields to null.
|
||||
/// </summary>
|
||||
public static ProductType ForCreation(
|
||||
string nombre,
|
||||
bool hasDuration,
|
||||
bool requiresText,
|
||||
bool requiresCategory,
|
||||
bool isBundle,
|
||||
bool allowImages,
|
||||
int? maxImages,
|
||||
decimal? maxImageSizeMB,
|
||||
int? maxImageWidth,
|
||||
int? maxImageHeight,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
ValidateNombre(nombre);
|
||||
|
||||
var (mi, ms, mw, mh) = NormalizeMultimedia(allowImages, maxImages, maxImageSizeMB, maxImageWidth, maxImageHeight);
|
||||
|
||||
return new ProductType(
|
||||
id: 0,
|
||||
nombre: nombre,
|
||||
hasDuration: hasDuration,
|
||||
requiresText: requiresText,
|
||||
requiresCategory: requiresCategory,
|
||||
isBundle: isBundle,
|
||||
allowImages: allowImages,
|
||||
maxImages: mi,
|
||||
maxImageSizeMB: ms,
|
||||
maxImageWidth: mw,
|
||||
maxImageHeight: mh,
|
||||
isActive: true,
|
||||
fechaCreacion: timeProvider.GetUtcNow().UtcDateTime,
|
||||
fechaModificacion: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new ProductType with an updated Nombre and FechaModificacion.
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public ProductType WithRenamed(string nuevoNombre, TimeProvider timeProvider)
|
||||
{
|
||||
ValidateNombre(nuevoNombre);
|
||||
|
||||
return new ProductType(
|
||||
Id, nuevoNombre, HasDuration, RequiresText, RequiresCategory, IsBundle,
|
||||
AllowImages, MaxImages, MaxImageSizeMB, MaxImageWidth, MaxImageHeight,
|
||||
IsActive, FechaCreacion, timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new ProductType with updated flags, preserving multimedia fields.
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public ProductType WithUpdatedFlags(
|
||||
bool hasDuration,
|
||||
bool requiresText,
|
||||
bool requiresCategory,
|
||||
bool isBundle,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
return new ProductType(
|
||||
Id, Nombre, hasDuration, requiresText, requiresCategory, isBundle,
|
||||
AllowImages, MaxImages, MaxImageSizeMB, MaxImageWidth, MaxImageHeight,
|
||||
IsActive, FechaCreacion, timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new ProductType with updated multimedia limits.
|
||||
/// AllowImages=false normalizes all 4 Max* fields to null.
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public ProductType WithUpdatedMultimedia(
|
||||
bool allowImages,
|
||||
int? maxImages,
|
||||
decimal? maxImageSizeMB,
|
||||
int? maxImageWidth,
|
||||
int? maxImageHeight,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
var (mi, ms, mw, mh) = NormalizeMultimedia(allowImages, maxImages, maxImageSizeMB, maxImageWidth, maxImageHeight);
|
||||
|
||||
return new ProductType(
|
||||
Id, Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle,
|
||||
allowImages, mi, ms, mw, mh,
|
||||
IsActive, FechaCreacion, timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new ProductType with IsActive=false and FechaModificacion updated.
|
||||
/// Does NOT mutate the current instance.
|
||||
/// </summary>
|
||||
public ProductType WithDeactivated(TimeProvider timeProvider)
|
||||
{
|
||||
return new ProductType(
|
||||
Id, Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle,
|
||||
AllowImages, MaxImages, MaxImageSizeMB, MaxImageWidth, MaxImageHeight,
|
||||
isActive: false, FechaCreacion, timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
|
||||
// ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private static (int?, decimal?, int?, int?) NormalizeMultimedia(
|
||||
bool allow, int? mi, decimal? ms, int? mw, int? mh)
|
||||
=> allow ? (mi, ms, mw, mh) : (null, null, null, null);
|
||||
|
||||
private static void ValidateNombre(string nombre)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nombre))
|
||||
throw new ArgumentException(
|
||||
"El nombre del tipo de producto no puede estar vacío o ser solo espacios.",
|
||||
nameof(nombre));
|
||||
|
||||
if (nombre.Length > NombreMaxLength)
|
||||
throw new ArgumentException(
|
||||
$"El nombre del tipo de producto no puede superar los {NombreMaxLength} caracteres.",
|
||||
nameof(nombre));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when attempting to deactivate a ProductType that has active products. → HTTP 409
|
||||
/// </summary>
|
||||
public sealed class ProductTypeEnUsoException : DomainException
|
||||
{
|
||||
public int ProductTypeId { get; }
|
||||
public int ProductsActivos { get; }
|
||||
|
||||
public ProductTypeEnUsoException(int id, int productsActivos)
|
||||
: base($"El tipo de producto con id={id} no puede desactivarse: tiene {productsActivos} producto(s) activo(s) asociados.")
|
||||
{
|
||||
ProductTypeId = id;
|
||||
ProductsActivos = productsActivos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a combination of flags/multimedia is logically incoherent. → HTTP 422
|
||||
/// Defensive exception — PRD-001 normalizes instead of throwing.
|
||||
/// Reserved for future rules (e.g., PRD-004 IsBundle+HasDuration constraints).
|
||||
/// </summary>
|
||||
public sealed class ProductTypeFlagsIncoherentesException : DomainException
|
||||
{
|
||||
public string Reason { get; }
|
||||
|
||||
public ProductTypeFlagsIncoherentesException(string reason)
|
||||
: base($"Combinación de flags/multimedia inválida: {reason}")
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a ProductType with the same active name already exists. → HTTP 409
|
||||
/// </summary>
|
||||
public sealed class ProductTypeNombreDuplicadoException : DomainException
|
||||
{
|
||||
public string Nombre { get; }
|
||||
|
||||
public ProductTypeNombreDuplicadoException(string nombre)
|
||||
: base($"Ya existe un tipo de producto activo con el nombre '{nombre}'.")
|
||||
{
|
||||
Nombre = nombre;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace SIGCM2.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a requested ProductType does not exist. → HTTP 404
|
||||
/// </summary>
|
||||
public sealed class ProductTypeNotFoundException : DomainException
|
||||
{
|
||||
public int ProductTypeId { get; }
|
||||
|
||||
public ProductTypeNotFoundException(int id)
|
||||
: base($"El tipo de producto con id={id} no existe.")
|
||||
{
|
||||
ProductTypeId = id;
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user