diff --git a/src/api/SIGCM2.Domain/Entities/ProductType.cs b/src/api/SIGCM2.Domain/Entities/ProductType.cs
new file mode 100644
index 0000000..ea3de1a
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Entities/ProductType.cs
@@ -0,0 +1,186 @@
+namespace SIGCM2.Domain.Entities;
+
+///
+/// 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).
+///
+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; }
+
+ ///
+ /// Full hydration constructor — used by the repository to reconstruct from DB rows.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+
+ ///
+ /// Returns a new ProductType with an updated Nombre and FechaModificacion.
+ /// Does NOT mutate the current instance.
+ ///
+ 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);
+ }
+
+ ///
+ /// Returns a new ProductType with updated flags, preserving multimedia fields.
+ /// Does NOT mutate the current instance.
+ ///
+ 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);
+ }
+
+ ///
+ /// Returns a new ProductType with updated multimedia limits.
+ /// AllowImages=false normalizes all 4 Max* fields to null.
+ /// Does NOT mutate the current instance.
+ ///
+ 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);
+ }
+
+ ///
+ /// Returns a new ProductType with IsActive=false and FechaModificacion updated.
+ /// Does NOT mutate the current instance.
+ ///
+ 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));
+ }
+}
diff --git a/src/api/SIGCM2.Domain/Exceptions/ProductTypeEnUsoException.cs b/src/api/SIGCM2.Domain/Exceptions/ProductTypeEnUsoException.cs
new file mode 100644
index 0000000..b3cf72b
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Exceptions/ProductTypeEnUsoException.cs
@@ -0,0 +1,17 @@
+namespace SIGCM2.Domain.Exceptions;
+
+///
+/// Thrown when attempting to deactivate a ProductType that has active products. → HTTP 409
+///
+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;
+ }
+}
diff --git a/src/api/SIGCM2.Domain/Exceptions/ProductTypeFlagsIncoherentesException.cs b/src/api/SIGCM2.Domain/Exceptions/ProductTypeFlagsIncoherentesException.cs
new file mode 100644
index 0000000..7901594
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Exceptions/ProductTypeFlagsIncoherentesException.cs
@@ -0,0 +1,17 @@
+namespace SIGCM2.Domain.Exceptions;
+
+///
+/// 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).
+///
+public sealed class ProductTypeFlagsIncoherentesException : DomainException
+{
+ public string Reason { get; }
+
+ public ProductTypeFlagsIncoherentesException(string reason)
+ : base($"Combinación de flags/multimedia inválida: {reason}")
+ {
+ Reason = reason;
+ }
+}
diff --git a/src/api/SIGCM2.Domain/Exceptions/ProductTypeNombreDuplicadoException.cs b/src/api/SIGCM2.Domain/Exceptions/ProductTypeNombreDuplicadoException.cs
new file mode 100644
index 0000000..79a4914
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Exceptions/ProductTypeNombreDuplicadoException.cs
@@ -0,0 +1,15 @@
+namespace SIGCM2.Domain.Exceptions;
+
+///
+/// Thrown when a ProductType with the same active name already exists. → HTTP 409
+///
+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;
+ }
+}
diff --git a/src/api/SIGCM2.Domain/Exceptions/ProductTypeNotFoundException.cs b/src/api/SIGCM2.Domain/Exceptions/ProductTypeNotFoundException.cs
new file mode 100644
index 0000000..0507af8
--- /dev/null
+++ b/src/api/SIGCM2.Domain/Exceptions/ProductTypeNotFoundException.cs
@@ -0,0 +1,15 @@
+namespace SIGCM2.Domain.Exceptions;
+
+///
+/// Thrown when a requested ProductType does not exist. → HTTP 404
+///
+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;
+ }
+}
diff --git a/tests/SIGCM2.Application.Tests/Domain/ProductTypes/ProductTypeExceptionsTests.cs b/tests/SIGCM2.Application.Tests/Domain/ProductTypes/ProductTypeExceptionsTests.cs
new file mode 100644
index 0000000..8b1f926
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Domain/ProductTypes/ProductTypeExceptionsTests.cs
@@ -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();
+ }
+
+ // ── 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();
+ }
+
+ // ── 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();
+ }
+
+ // ── 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();
+ }
+}
diff --git a/tests/SIGCM2.Application.Tests/Domain/ProductTypes/ProductTypeTests.cs b/tests/SIGCM2.Application.Tests/Domain/ProductTypes/ProductTypeTests.cs
new file mode 100644
index 0000000..3332b89
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Domain/ProductTypes/ProductTypeTests.cs
@@ -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();
+ }
+
+ [Fact]
+ public void ForCreation_NombreWhitespace_ThrowsArgumentException()
+ {
+ var act = () => ProductType.ForCreation(
+ " ", false, false, false, false, false,
+ null, null, null, null, FakeTime);
+
+ act.Should().Throw();
+ }
+
+ [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();
+ }
+
+ // ── 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));
+}