diff --git a/tests/SIGCM2.Application.Tests/Products/Repository/ProductRepositoryTests.cs b/tests/SIGCM2.Application.Tests/Products/Repository/ProductRepositoryTests.cs
new file mode 100644
index 0000000..f812e45
--- /dev/null
+++ b/tests/SIGCM2.Application.Tests/Products/Repository/ProductRepositoryTests.cs
@@ -0,0 +1,205 @@
+using Dapper;
+using FluentAssertions;
+using Microsoft.Data.SqlClient;
+using SIGCM2.Application.Common;
+using SIGCM2.Domain.Entities;
+using SIGCM2.Infrastructure.Persistence;
+using SIGCM2.TestSupport;
+
+namespace SIGCM2.Application.Tests.Products.Repository;
+
+///
+/// PRD-002 — Integration tests for ProductRepository against SIGCM2_Test_App.
+/// Uses shared SqlTestFixture via [Collection("Database")] — fixture manages Respawn + seeds.
+/// Verifies full CRUD, paged listing, UQ constraint, and temporal history.
+///
+[Collection("Database")]
+public class ProductRepositoryTests : IAsyncLifetime
+{
+ private readonly SqlTestFixture _db;
+ private ProductRepository _repository = null!;
+ private int _defaultMedioId;
+ private int _defaultProductTypeId;
+
+ public ProductRepositoryTests(SqlTestFixture db)
+ {
+ _db = db;
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _db.ResetAndSeedAsync();
+
+ var factory = new SqlConnectionFactory(TestConnectionStrings.AppTestDb);
+ _repository = new ProductRepository(factory);
+
+ // Insert Medio and ProductType for use across all tests
+ await using var conn = new SqlConnection(TestConnectionStrings.AppTestDb);
+ await conn.OpenAsync();
+
+ _defaultMedioId = await conn.ExecuteScalarAsync("""
+ INSERT INTO dbo.Medio (Codigo, Nombre, Tipo, Activo)
+ OUTPUT INSERTED.Id
+ VALUES ('PR', 'Prueba Medio', 1, 1)
+ """);
+
+ _defaultProductTypeId = await conn.ExecuteScalarAsync("""
+ INSERT INTO dbo.ProductType (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages)
+ OUTPUT INSERTED.Id
+ VALUES ('Tipo Prueba', 0, 0, 0, 0, 0)
+ """);
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ private Product AProduct(string nombre = "Clasificado Test") =>
+ Product.ForCreation(
+ nombre: nombre,
+ medioId: _defaultMedioId,
+ productTypeId: _defaultProductTypeId,
+ rubroId: null,
+ basePrice: 100.50m,
+ priceDurationDays: null,
+ timeProvider: TimeProvider.System);
+
+ // ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
+
+ [Fact]
+ public async Task AddAsync_AndGetById_ReturnsAllFields()
+ {
+ var product = AProduct("Mi Producto");
+ var id = await _repository.AddAsync(product);
+ var result = await _repository.GetByIdAsync(id);
+
+ result.Should().NotBeNull();
+ result!.Id.Should().Be(id);
+ result.Nombre.Should().Be("Mi Producto");
+ result.MedioId.Should().Be(_defaultMedioId);
+ result.ProductTypeId.Should().Be(_defaultProductTypeId);
+ result.RubroId.Should().BeNull();
+ result.BasePrice.Should().Be(100.50m);
+ result.PriceDurationDays.Should().BeNull();
+ result.IsActive.Should().BeTrue();
+ result.FechaCreacion.Should().BeAfter(DateTime.MinValue);
+ result.FechaModificacion.Should().BeNull();
+ }
+
+ // ── GetByIdAsync null for unknown ─────────────────────────────────────────
+
+ [Fact]
+ public async Task GetByIdAsync_UnknownId_ReturnsNull()
+ {
+ var result = await _repository.GetByIdAsync(999999);
+
+ result.Should().BeNull();
+ }
+
+ // ── UpdateAsync ────────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateAsync_ChangesNombreAndBasePrice()
+ {
+ var id = await _repository.AddAsync(AProduct("Original"));
+ var product = await _repository.GetByIdAsync(id);
+ var updated = product!.WithUpdated("Renombrado", null, 200m, null, TimeProvider.System);
+
+ await _repository.UpdateAsync(updated);
+ var result = await _repository.GetByIdAsync(id);
+
+ result!.Nombre.Should().Be("Renombrado");
+ result.BasePrice.Should().Be(200m);
+ result.FechaModificacion.Should().NotBeNull();
+ }
+
+ // ── WithDeactivated creates history row ────────────────────────────────────
+
+ [Fact]
+ public async Task UpdateAsync_Deactivate_CreatesHistoryRow()
+ {
+ var id = await _repository.AddAsync(AProduct("Para Desactivar"));
+ var product = await _repository.GetByIdAsync(id);
+ var deactivated = product!.WithDeactivated(TimeProvider.System);
+
+ await _repository.UpdateAsync(deactivated);
+
+ // Verify temporal history: ProductType_History should have at least 1 row
+ await using var conn = new SqlConnection(TestConnectionStrings.AppTestDb);
+ await conn.OpenAsync();
+ var historyCount = await conn.ExecuteScalarAsync(
+ "SELECT COUNT(1) FROM dbo.Product_History WHERE Id = @Id", new { Id = id });
+ historyCount.Should().BeGreaterThanOrEqualTo(1);
+ }
+
+ // ── GetPagedAsync ─────────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task GetPagedAsync_DefaultQuery_ReturnsActiveProducts()
+ {
+ await _repository.AddAsync(AProduct("Producto A"));
+ await _repository.AddAsync(AProduct("Producto B"));
+
+ var result = await _repository.GetPagedAsync(new ProductsQuery(Page: 1, PageSize: 20, Activo: true));
+
+ result.Items.Should().HaveCountGreaterThanOrEqualTo(2);
+ result.Items.Should().AllSatisfy(p => p.IsActive.Should().BeTrue());
+ }
+
+ [Fact]
+ public async Task GetPagedAsync_FilterByMedioId_ReturnsOnlyMatching()
+ {
+ await _repository.AddAsync(AProduct("Producto Filtrado"));
+
+ var result = await _repository.GetPagedAsync(
+ new ProductsQuery(Page: 1, PageSize: 20, Activo: null, MedioId: _defaultMedioId));
+
+ result.Items.Should().AllSatisfy(p => p.MedioId.Should().Be(_defaultMedioId));
+ }
+
+ // ── ExistsByNombreAsync ────────────────────────────────────────────────────
+
+ [Fact]
+ public async Task ExistsByNombreAsync_ExistingActiveProduct_ReturnsTrue()
+ {
+ var nombre = "Nombre Unico Test";
+ await _repository.AddAsync(AProduct(nombre));
+
+ var result = await _repository.ExistsByNombreAsync(nombre, _defaultMedioId, _defaultProductTypeId);
+
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task ExistsByNombreAsync_ExcludeSelf_ReturnsFalse()
+ {
+ var nombre = "Nombre Self Excluido";
+ var id = await _repository.AddAsync(AProduct(nombre));
+
+ var result = await _repository.ExistsByNombreAsync(nombre, _defaultMedioId, _defaultProductTypeId, excludeId: id);
+
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task ExistsByNombreAsync_NonExisting_ReturnsFalse()
+ {
+ var result = await _repository.ExistsByNombreAsync("Nombre Que No Existe XYZ", _defaultMedioId, _defaultProductTypeId);
+
+ result.Should().BeFalse();
+ }
+
+ // ── UQ index: deactivated allows reuse of name ────────────────────────────
+
+ [Fact]
+ public async Task ExistsByNombreAsync_DeactivatedProduct_ReturnsFalse_AllowsReuse()
+ {
+ var nombre = "Nombre Reutilizable";
+ var id = await _repository.AddAsync(AProduct(nombre));
+ var product = await _repository.GetByIdAsync(id);
+ await _repository.UpdateAsync(product!.WithDeactivated(TimeProvider.System));
+
+ // After deactivation, name should be available again
+ var result = await _repository.ExistsByNombreAsync(nombre, _defaultMedioId, _defaultProductTypeId);
+
+ result.Should().BeFalse();
+ }
+}