diff --git a/src/api/SIGCM2.Infrastructure/Persistence/ChargeableCharConfigRepository.cs b/src/api/SIGCM2.Infrastructure/Persistence/ChargeableCharConfigRepository.cs index dc5885e..5039a44 100644 --- a/src/api/SIGCM2.Infrastructure/Persistence/ChargeableCharConfigRepository.cs +++ b/src/api/SIGCM2.Infrastructure/Persistence/ChargeableCharConfigRepository.cs @@ -237,6 +237,11 @@ public sealed class ChargeableCharConfigRepository : IChargeableCharConfigReposi long id, CancellationToken ct = default) { + // IMPORTANT: the SP invocation and the subsequent SELECT MUST run on the SAME connection. + // Opening a second connection within the ambient TransactionScope would promote the + // transaction to DTC (distributed) — and MSDTC is typically not enabled on dev/prod + // servers. That promotion surfaces as an opaque 500 at the API boundary. Keeping both + // commands on a single SqlConnection keeps the tx as a local LTM (lightweight transaction). var p = new DynamicParameters(); p.Add("@Id", id, DbType.Int64); @@ -270,11 +275,20 @@ public sealed class ChargeableCharConfigRepository : IChargeableCharConfigReposi throw new ChargeableCharConfigReactivationNotAllowedException(id, "POSTERIOR_ROWS_EXIST"); } - // Fetch the reactivated row to return its current state. - var reactivated = await GetByIdAsync(id, ct); - return reactivated - ?? throw new ChargeableCharConfigInvalidException( - nameof(id), $"ChargeableCharConfig with Id={id} not found after reactivation (unexpected)."); + // Fetch the reactivated row on the SAME connection (avoids DTC promotion). + const string selectSql = """ + SELECT Id, ProductTypeId, Symbol, Category, PricePerUnit, ValidFrom, ValidTo, IsActive + FROM dbo.ChargeableCharConfig + WHERE Id = @Id + """; + + var row = await connection.QuerySingleOrDefaultAsync( + new CommandDefinition(selectSql, new { Id = id }, cancellationToken: ct)); + + return row is null + ? throw new ChargeableCharConfigInvalidException( + nameof(id), $"ChargeableCharConfig with Id={id} not found after reactivation (unexpected).") + : MapRow(row); } /// diff --git a/tests/SIGCM2.Application.Tests/Domain/Pricing/Exceptions/PricingExceptionTests.cs b/tests/SIGCM2.Application.Tests/Domain/Pricing/Exceptions/PricingExceptionTests.cs index a70b012..0a07afc 100644 --- a/tests/SIGCM2.Application.Tests/Domain/Pricing/Exceptions/PricingExceptionTests.cs +++ b/tests/SIGCM2.Application.Tests/Domain/Pricing/Exceptions/PricingExceptionTests.cs @@ -101,23 +101,23 @@ public sealed class PricingExceptionTests var activeVf = new DateOnly(2026, 4, 1); var ex = new ChargeableCharConfigForwardOnlyException( - medioId: 5, symbol: "$", newValidFrom: newVf, activeValidFrom: activeVf); + productTypeId: 5, symbol: "$", newValidFrom: newVf, activeValidFrom: activeVf); - ex.MedioId.Should().Be(5); + ex.ProductTypeId.Should().Be(5); ex.Symbol.Should().Be("$"); ex.NewValidFrom.Should().Be(newVf); ex.ActiveValidFrom.Should().Be(activeVf); } [Fact] - public void ChargeableCharConfigForwardOnlyException_NullMedioId_IsAllowed() + public void ChargeableCharConfigForwardOnlyException_NullProductTypeId_IsAllowed() { var ex = new ChargeableCharConfigForwardOnlyException( - medioId: null, symbol: "$", + productTypeId: null, symbol: "$", newValidFrom: new DateOnly(2026, 3, 1), activeValidFrom: new DateOnly(2026, 4, 1)); - ex.MedioId.Should().BeNull(); + ex.ProductTypeId.Should().BeNull(); } [Fact] @@ -127,7 +127,7 @@ public sealed class PricingExceptionTests var activeVf = new DateOnly(2026, 4, 1); var ex = new ChargeableCharConfigForwardOnlyException( - medioId: null, symbol: "$", newValidFrom: newVf, activeValidFrom: activeVf); + productTypeId: null, symbol: "$", newValidFrom: newVf, activeValidFrom: activeVf); ex.Message.Should().Contain("2026-03-01"); ex.Message.Should().Contain("2026-04-01"); @@ -137,7 +137,7 @@ public sealed class PricingExceptionTests public void ChargeableCharConfigForwardOnlyException_InheritsFromDomainException() { var ex = new ChargeableCharConfigForwardOnlyException( - medioId: null, symbol: "$", + productTypeId: null, symbol: "$", newValidFrom: new DateOnly(2026, 3, 1), activeValidFrom: new DateOnly(2026, 4, 1)); diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigHardeningTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigHardeningTests.cs index 8d06c96..e03fb0d 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigHardeningTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigHardeningTests.cs @@ -51,15 +51,15 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime // Seed two dedicated ProductTypes for override/fallback resolution tests. // V023: ChargeableCharConfig.ProductTypeId references dbo.ProductType(Id). _productType1Id = await conn.ExecuteScalarAsync(""" - INSERT INTO dbo.ProductType (Nombre, Codigo, Activo) + INSERT INTO dbo.ProductType (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages) OUTPUT INSERTED.Id - VALUES ('Hardening PT1 (override)', 'H_PT1', 1) + VALUES ('Hardening PT1 (override)', 0, 0, 0, 0, 0) """); _productType2Id = await conn.ExecuteScalarAsync(""" - INSERT INTO dbo.ProductType (Nombre, Codigo, Activo) + INSERT INTO dbo.ProductType (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages) OUTPUT INSERTED.Id - VALUES ('Hardening PT2 (fallback)', 'H_PT2', 1) + VALUES ('Hardening PT2 (fallback)', 0, 0, 0, 0, 0) """); } diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigRepositoryIntegrationTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigRepositoryIntegrationTests.cs index 8a70c9a..06a6ef1 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigRepositoryIntegrationTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Pricing/ChargeableCharConfigRepositoryIntegrationTests.cs @@ -58,9 +58,9 @@ public class ChargeableCharConfigRepositoryIntegrationTests : IAsyncLifetime await conn.OpenAsync(); _productTypeId = await conn.ExecuteScalarAsync(""" - INSERT INTO dbo.ProductType (Nombre, Codigo, Activo) + INSERT INTO dbo.ProductType (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages) OUTPUT INSERTED.Id - VALUES ('RepoIntegration PT1', 'RI_PT1', 1) + VALUES ('RepoIntegration PT1', 0, 0, 0, 0, 0) """); } @@ -263,9 +263,9 @@ public class ChargeableCharConfigRepositoryIntegrationTests : IAsyncLifetime await using var conn = new SqlConnection(TestConnectionStrings.AppTestDb); await conn.OpenAsync(); var otherPTId = await conn.ExecuteScalarAsync(""" - INSERT INTO dbo.ProductType (Nombre, Codigo, Activo) + INSERT INTO dbo.ProductType (Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle, AllowImages) OUTPUT INSERTED.Id - VALUES ('RepoIntegration PT2 NoOverride', 'RI_PT2', 1) + VALUES ('RepoIntegration PT2 NoOverride', 0, 0, 0, 0, 0) """); var rows = await repo.GetActiveForProductTypeAsync((long)otherPTId, asOf); diff --git a/tests/SIGCM2.TestSupport/SqlTestFixture.cs b/tests/SIGCM2.TestSupport/SqlTestFixture.cs index 4b07492..6bb7689 100644 --- a/tests/SIGCM2.TestSupport/SqlTestFixture.cs +++ b/tests/SIGCM2.TestSupport/SqlTestFixture.cs @@ -1522,11 +1522,30 @@ public sealed class SqlTestFixture : IAsyncLifetime await _connection.ExecuteAsync(setVersioning); await _connection.ExecuteAsync(createVigenteIndex); await _connection.ExecuteAsync(createQueryIndex); - await _connection.ExecuteAsync(createInsertSp); - await _connection.ExecuteAsync(alterInsertSp); - await _connection.ExecuteAsync(createGetActiveSp); - await _connection.ExecuteAsync(alterGetActiveSp); - await _connection.ExecuteAsync(seedV022); + + // Guard: only ALTER the V021-era SPs + seed with MedioId if the column still exists. + // If V023 already refactored the table in a prior run of the fixture on the same DB, + // MedioId is gone and these ALTERs would fail with "Invalid column name 'MedioId'". + // EnsureV023SchemaAsync (called right after) will re-install the SPs with @ProductTypeId. + const string hasMedioIdCheck = """ + SELECT CAST( + CASE WHEN EXISTS ( + SELECT 1 FROM sys.columns + WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig') + AND name = 'MedioId' + ) THEN 1 ELSE 0 END + AS BIT) + """; + var hasMedioId = await _connection.ExecuteScalarAsync(hasMedioIdCheck); + + if (hasMedioId) + { + await _connection.ExecuteAsync(createInsertSp); + await _connection.ExecuteAsync(alterInsertSp); + await _connection.ExecuteAsync(createGetActiveSp); + await _connection.ExecuteAsync(alterGetActiveSp); + await _connection.ExecuteAsync(seedV022); + } // Permission 'tasacion:caracteres_especiales:gestionar' and admin assignment // are seeded from SeedPermisosCanonicalAsync / SeedRolPermisosCanonicalAsync (post-respawn). }