refactor(bd): V023+V024 ChargeableCharConfig por ProductType + SP ReactivateWithGuard (PRC-001)

BREAKING: schema refactor pre-merge. Backend+frontend do not compile yet;
subsequent commits in this PR restore compilation. Acceptable only because
feature/PRC-001 is not yet merged to main.

- V023: drop MedioId + FK_Medio, add ProductTypeId + FK_ProductType, rename
  indexes, drop+create SPs InsertWithClose (now @ProductTypeId) and
  GetActiveForProductType (renamed from GetActiveForMedio). NEW SP
  ReactivateWithGuard (A+guard pattern for feature 3 of scope delta).
  Drop CK_Price_Positive, add CK_Price_NonNegative (>= 0 for opt-in billing).
- V024: reseed global rows with PricePerUnit = 0.0000 (opt-in billing).
- V023_ROLLBACK + V024_ROLLBACK scripts.
- SqlTestFixture: EnsureV023SchemaAsync, EnsureV024SeedAsync, renamed seed
  method signature (ProductTypeId=NULL + PricePerUnit=0), history table
  TablesToIgnore preserved. HardeningTests seeds dbo.ProductType (not Medio).
- MigrationTests: updated SP existence + column + FK + price assertions.
- RepositoryIntegrationTests + HardeningTests: SQL-level assertions updated;
  C# method/property renames deferred to Agent 2 (backend refactor).
This commit is contained in:
2026-04-21 10:35:38 -03:00
parent 5175cc1ece
commit 5c1675e59a
8 changed files with 1390 additions and 160 deletions

View File

@@ -6,14 +6,15 @@ using Xunit;
namespace SIGCM2.Application.Tests.Infrastructure.Pricing;
/// <summary>
/// PRC-001 Batch 1 (RED) — Integration tests for V020/V021/V022 migrations.
/// PRC-001 — Integration tests for V020/V021/V022/V023/V024 migrations.
/// These tests verify the BD schema applied against SIGCM2_Test_App:
/// - V020: permission 'tasacion:caracteres_especiales:gestionar' exists.
/// - V021: dbo.ChargeableCharConfig table + SYSTEM_VERSIONING + filtered UX + SPs.
/// - V021: dbo.ChargeableCharConfig table + SYSTEM_VERSIONING + SPs (initial MedioId shape).
/// - V022: 4 global seed rows ($, %, !, ¡) exist and are active.
/// - V023 (scope delta): MedioId → ProductTypeId refactor + ReactivateWithGuard SP.
/// - V024 (scope delta): global seed PricePerUnit reset to 0.0000 (opt-in billing).
///
/// Tests are tagged [RED] until V020+V021+V022 are applied (Batch 1 GREEN step).
/// After GREEN, all tests in this class should pass.
/// After GREEN all tests pass. SqlTestFixture applies V023+V024 during initialization.
/// </summary>
[Collection("Database")]
public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
@@ -104,19 +105,19 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
[Fact]
public async Task usp_InsertWithClose_FirstPrice_ClosedIsNull()
{
// Cleanup
// Cleanup (use ProductTypeId IS NULL after V023 refactor)
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE MedioId IS NULL AND Symbol = N'@'");
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'@'");
var p = new DynamicParameters();
p.Add("@MedioId", null, System.Data.DbType.Int32);
p.Add("@Symbol", "@", System.Data.DbType.String);
p.Add("@Category", "Other",System.Data.DbType.String);
p.Add("@PricePerUnit", 1.5m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p.Add("@NewId", dbType: System.Data.DbType.Int64,
p.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p.Add("@Symbol", "@", System.Data.DbType.String);
p.Add("@Category", "Other",System.Data.DbType.String);
p.Add("@PricePerUnit", 1.5m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync(
@@ -132,7 +133,7 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Cleanup
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'@' AND MedioId IS NULL");
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'@' AND ProductTypeId IS NULL");
}
[Fact]
@@ -140,17 +141,17 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
{
// Seed primer activo
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE MedioId IS NULL AND Symbol = N'€'");
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'€'");
var p1 = new DynamicParameters();
p1.Add("@MedioId", null, System.Data.DbType.Int32);
p1.Add("@Symbol", "€", System.Data.DbType.String);
p1.Add("@Category", "Currency", System.Data.DbType.String);
p1.Add("@PricePerUnit", 1.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p1.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p1.Add("@NewId", dbType: System.Data.DbType.Int64,
p1.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p1.Add("@Symbol", "€", System.Data.DbType.String);
p1.Add("@Category", "Currency", System.Data.DbType.String);
p1.Add("@PricePerUnit", 1.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p1.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p1.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p1.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p1.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
@@ -159,14 +160,14 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Insertar segundo (debe cerrar el primero)
var p2 = new DynamicParameters();
p2.Add("@MedioId", null, System.Data.DbType.Int32);
p2.Add("@Symbol", "€", System.Data.DbType.String);
p2.Add("@Category", "Currency", System.Data.DbType.String);
p2.Add("@PricePerUnit", 2.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p2.Add("@ValidFrom", new DateTime(2026, 2, 1), System.Data.DbType.Date);
p2.Add("@NewId", dbType: System.Data.DbType.Int64,
p2.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p2.Add("@Symbol", "€", System.Data.DbType.String);
p2.Add("@Category", "Currency", System.Data.DbType.String);
p2.Add("@PricePerUnit", 2.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p2.Add("@ValidFrom", new DateTime(2026, 2, 1), System.Data.DbType.Date);
p2.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p2.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p2.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
@@ -187,24 +188,24 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Cleanup
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'€' AND MedioId IS NULL");
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'€' AND ProductTypeId IS NULL");
}
[Fact]
public async Task usp_InsertWithClose_ForwardOnlyViolation_Throws50409()
{
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE MedioId IS NULL AND Symbol = N'£'");
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'£'");
var p1 = new DynamicParameters();
p1.Add("@MedioId", null, System.Data.DbType.Int32);
p1.Add("@Symbol", "£", System.Data.DbType.String);
p1.Add("@Category", "Currency", System.Data.DbType.String);
p1.Add("@PricePerUnit", 1.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p1.Add("@ValidFrom", new DateTime(2026, 3, 1), System.Data.DbType.Date);
p1.Add("@NewId", dbType: System.Data.DbType.Int64,
p1.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p1.Add("@Symbol", "£", System.Data.DbType.String);
p1.Add("@Category", "Currency", System.Data.DbType.String);
p1.Add("@PricePerUnit", 1.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p1.Add("@ValidFrom", new DateTime(2026, 3, 1), System.Data.DbType.Date);
p1.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p1.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p1.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
@@ -212,14 +213,14 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Intento con ValidFrom <= activo.ValidFrom → debe lanzar 50409
var p2 = new DynamicParameters();
p2.Add("@MedioId", null, System.Data.DbType.Int32);
p2.Add("@Symbol", "£", System.Data.DbType.String);
p2.Add("@Category", "Currency", System.Data.DbType.String);
p2.Add("@PricePerUnit", 2.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p2.Add("@ValidFrom", new DateTime(2026, 3, 1), System.Data.DbType.Date); // igual → viola forward-only
p2.Add("@NewId", dbType: System.Data.DbType.Int64,
p2.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p2.Add("@Symbol", "£", System.Data.DbType.String);
p2.Add("@Category", "Currency", System.Data.DbType.String);
p2.Add("@PricePerUnit", 2.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p2.Add("@ValidFrom", new DateTime(2026, 3, 1), System.Data.DbType.Date); // igual → viola forward-only
p2.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p2.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p2.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
var act = async () => await _connection.ExecuteAsync(
@@ -232,52 +233,53 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Cleanup
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'£' AND MedioId IS NULL");
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'£' AND ProductTypeId IS NULL");
}
[Fact]
public async Task usp_InsertWithClose_MedioNull_GlobalFallback_Works()
public async Task usp_InsertWithClose_ProductTypeNull_GlobalFallback_Works()
{
// V023: global fallback is now ProductTypeId IS NULL (was MedioId IS NULL)
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE MedioId IS NULL AND Symbol = N'¥'");
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'¥'");
var p = new DynamicParameters();
p.Add("@MedioId", null, System.Data.DbType.Int32);
p.Add("@Symbol", "¥", System.Data.DbType.String);
p.Add("@Category", "Currency", System.Data.DbType.String);
p.Add("@PricePerUnit", 1.25m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p.Add("@NewId", dbType: System.Data.DbType.Int64,
p.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p.Add("@Symbol", "¥", System.Data.DbType.String);
p.Add("@Category", "Currency", System.Data.DbType.String);
p.Add("@PricePerUnit", 1.25m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p,
commandType: System.Data.CommandType.StoredProcedure);
var newId = p.Get<long?>("@NewId");
newId.Should().BeGreaterThan(0, "global insert (MedioId NULL) debe funcionar");
newId.Should().BeGreaterThan(0, "global insert (ProductTypeId NULL) debe funcionar");
// Cleanup
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'¥' AND MedioId IS NULL");
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'¥' AND ProductTypeId IS NULL");
}
[Fact]
public async Task SystemVersioning_UpdateOnClose_ProducesHistoryRow()
{
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE MedioId IS NULL AND Symbol = N'#'");
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'#'");
var p1 = new DynamicParameters();
p1.Add("@MedioId", null, System.Data.DbType.Int32);
p1.Add("@Symbol", "#", System.Data.DbType.String);
p1.Add("@Category", "Other", System.Data.DbType.String);
p1.Add("@PricePerUnit", 1.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p1.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p1.Add("@NewId", dbType: System.Data.DbType.Int64,
p1.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p1.Add("@Symbol", "#", System.Data.DbType.String);
p1.Add("@Category", "Other", System.Data.DbType.String);
p1.Add("@PricePerUnit", 1.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p1.Add("@ValidFrom", new DateTime(2026, 1, 1), System.Data.DbType.Date);
p1.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p1.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p1.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
@@ -286,14 +288,14 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Close it by inserting a newer version
var p2 = new DynamicParameters();
p2.Add("@MedioId", null, System.Data.DbType.Int32);
p2.Add("@Symbol", "#", System.Data.DbType.String);
p2.Add("@Category", "Other", System.Data.DbType.String);
p2.Add("@PricePerUnit", 2.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p2.Add("@ValidFrom", new DateTime(2026, 6, 1), System.Data.DbType.Date);
p2.Add("@NewId", dbType: System.Data.DbType.Int64,
p2.Add("@ProductTypeId", null, System.Data.DbType.Int32); // V023: was @MedioId
p2.Add("@Symbol", "#", System.Data.DbType.String);
p2.Add("@Category", "Other", System.Data.DbType.String);
p2.Add("@PricePerUnit", 2.0m, System.Data.DbType.Decimal, precision: 18, scale: 4);
p2.Add("@ValidFrom", new DateTime(2026, 6, 1), System.Data.DbType.Date);
p2.Add("@NewId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
p2.Add("@ClosedId", dbType: System.Data.DbType.Int64,
p2.Add("@ClosedId", dbType: System.Data.DbType.Int64,
direction: System.Data.ParameterDirection.Output);
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
@@ -309,13 +311,25 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
// Cleanup
await _connection.ExecuteAsync(
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'#' AND MedioId IS NULL");
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'#' AND ProductTypeId IS NULL");
}
// ── V021: SP — usp_ChargeableCharConfig_GetActiveForMedio ────────────
// ── V023 scope delta: SP — usp_ChargeableCharConfig_GetActiveForProductType ────────────
[Fact]
public async Task V021_SP_GetActiveForMedio_Exists()
public async Task V023_SP_GetActiveForProductType_Exists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.objects
WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_GetActiveForProductType')
AND type = 'P'");
exists.Should().Be(1, "V023 debe crear usp_ChargeableCharConfig_GetActiveForProductType (renamed from GetActiveForMedio)");
}
[Fact]
public async Task V023_SP_GetActiveForMedio_NoLongerExists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
@@ -323,7 +337,81 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_GetActiveForMedio')
AND type = 'P'");
exists.Should().Be(1, "usp_ChargeableCharConfig_GetActiveForMedio debe existir");
exists.Should().Be(0, "V023 debe eliminar usp_ChargeableCharConfig_GetActiveForMedio (renamed to GetActiveForProductType)");
}
[Fact]
public async Task V023_SP_ReactivateWithGuard_Exists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.objects
WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_ReactivateWithGuard')
AND type = 'P'");
exists.Should().Be(1, "V023 debe crear usp_ChargeableCharConfig_ReactivateWithGuard (new SP for feature 3)");
}
// ── V023 scope delta: column ProductTypeId replaces MedioId ─────────
[Fact]
public async Task V023_Column_ProductTypeId_Exists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.columns
WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')
AND name = 'ProductTypeId'");
exists.Should().Be(1, "V023 debe agregar columna ProductTypeId a dbo.ChargeableCharConfig");
}
[Fact]
public async Task V023_Column_MedioId_NoLongerExists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.columns
WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')
AND name = 'MedioId'");
exists.Should().Be(0, "V023 debe eliminar columna MedioId de dbo.ChargeableCharConfig");
}
[Fact]
public async Task V023_FK_ProductType_Exists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.foreign_keys
WHERE parent_object_id = OBJECT_ID('dbo.ChargeableCharConfig')
AND referenced_object_id = OBJECT_ID('dbo.ProductType')");
exists.Should().Be(1, "V023 debe crear FK de ChargeableCharConfig a ProductType");
}
[Fact]
public async Task V023_Check_Price_NonNegative_Exists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.check_constraints
WHERE name = 'CK_ChargeableCharConfig_Price_NonNegative'
AND parent_object_id = OBJECT_ID('dbo.ChargeableCharConfig')");
exists.Should().Be(1, "V023 debe crear CK_ChargeableCharConfig_Price_NonNegative (>= 0 para opt-in billing)");
}
[Fact]
public async Task V023_Check_Price_Positive_NoLongerExists()
{
var exists = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*)
FROM sys.check_constraints
WHERE name = 'CK_ChargeableCharConfig_Price_Positive'
AND parent_object_id = OBJECT_ID('dbo.ChargeableCharConfig')");
exists.Should().Be(0, "V023 debe eliminar CK_ChargeableCharConfig_Price_Positive (reemplazado por NonNegative)");
}
// ── V020: Permission existence ────────────────────────────────────────
@@ -339,17 +427,17 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
"V020 debe insertar el permiso 'tasacion:caracteres_especiales:gestionar'");
}
// ── V022: Seed rows ───────────────────────────────────────────────────
// ── V022: Seed rows (after V023 refactor: use ProductTypeId IS NULL) ─────
[Fact]
public async Task V022_Seeds_AtLeastFourGlobalRows()
{
var count = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*) FROM dbo.ChargeableCharConfig
WHERE MedioId IS NULL AND Symbol IN (N'$', N'%', N'!', N'¡')
WHERE ProductTypeId IS NULL AND Symbol IN (N'$', N'%', N'!', N'¡')
AND ValidTo IS NULL AND IsActive = 1");
count.Should().Be(4, "V022 debe sembrar exactamente 4 filas globales: $, %, !, ¡");
count.Should().Be(4, "V022 debe sembrar exactamente 4 filas globales: $, %, !, ¡ (global = ProductTypeId IS NULL tras V023)");
}
[Fact]
@@ -357,9 +445,39 @@ public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
{
var inactiveCount = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*) FROM dbo.ChargeableCharConfig
WHERE MedioId IS NULL AND Symbol IN (N'$', N'%', N'!', N'¡')
WHERE ProductTypeId IS NULL AND Symbol IN (N'$', N'%', N'!', N'¡')
AND IsActive = 0");
inactiveCount.Should().Be(0, "todas las filas de seed deben tener IsActive = 1");
}
// ── V024 scope delta: global seed prices = 0.0000 (opt-in billing) ───────
[Fact]
public async Task V024_GlobalSeedRows_HaveZeroPrice()
{
var nonZeroCount = await _connection.ExecuteScalarAsync<int>(@"
SELECT COUNT(*) FROM dbo.ChargeableCharConfig
WHERE ProductTypeId IS NULL
AND Symbol IN (N'$', N'%', N'!', N'¡')
AND ValidTo IS NULL
AND PricePerUnit <> 0.0000");
nonZeroCount.Should().Be(0,
"V024 debe resetear todas las filas de seed global a PricePerUnit = 0.0000 (opt-in billing)");
}
[Fact]
public async Task V024_GlobalSeedRows_AllHaveZeroPriceExact()
{
var rows = await _connection.QueryAsync<decimal>(@"
SELECT PricePerUnit FROM dbo.ChargeableCharConfig
WHERE ProductTypeId IS NULL
AND Symbol IN (N'$', N'%', N'!', N'¡')
AND ValidTo IS NULL");
rows.Should().AllSatisfy(price =>
price.Should().Be(0.0000m,
"V024 seed: cada fila global debe tener PricePerUnit = 0.0000 exacto"));
}
}