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).
484 lines
21 KiB
C#
484 lines
21 KiB
C#
using Dapper;
|
|
using FluentAssertions;
|
|
using Microsoft.Data.SqlClient;
|
|
using Xunit;
|
|
|
|
namespace SIGCM2.Application.Tests.Infrastructure.Pricing;
|
|
|
|
/// <summary>
|
|
/// 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 + 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).
|
|
///
|
|
/// After GREEN all tests pass. SqlTestFixture applies V023+V024 during initialization.
|
|
/// </summary>
|
|
[Collection("Database")]
|
|
public sealed class ChargeableCharConfigMigrationTests : IAsyncLifetime
|
|
{
|
|
private SqlConnection _connection = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_connection = new SqlConnection(TestConnectionStrings.AppTestDb);
|
|
await _connection.OpenAsync();
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _connection.CloseAsync();
|
|
await _connection.DisposeAsync();
|
|
}
|
|
|
|
// ── V021: Table existence ─────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task V021_Table_ChargeableCharConfig_Exists()
|
|
{
|
|
var exists = await _connection.ExecuteScalarAsync<int>(
|
|
"SELECT COUNT(*) FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')");
|
|
|
|
exists.Should().Be(1, "V021 debe crear dbo.ChargeableCharConfig");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task V021_Table_ChargeableCharConfig_HasSystemVersioning()
|
|
{
|
|
var temporalType = await _connection.ExecuteScalarAsync<int?>(@"
|
|
SELECT temporal_type
|
|
FROM sys.tables
|
|
WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig')");
|
|
|
|
temporalType.Should().Be(2, "SYSTEM_VERSIONING = ON requiere temporal_type = 2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task V021_HistoryTable_ChargeableCharConfigHistory_Exists()
|
|
{
|
|
var exists = await _connection.ExecuteScalarAsync<int>(
|
|
"SELECT COUNT(*) FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ChargeableCharConfig_History')");
|
|
|
|
exists.Should().Be(1, "SYSTEM_VERSIONING debe crear dbo.ChargeableCharConfig_History");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task V021_FilteredUniqueIndex_UX_ChargeableCharConfig_Vigente_Exists()
|
|
{
|
|
var exists = await _connection.ExecuteScalarAsync<int>(@"
|
|
SELECT COUNT(*)
|
|
FROM sys.indexes
|
|
WHERE name = 'UX_ChargeableCharConfig_Vigente'
|
|
AND object_id = OBJECT_ID('dbo.ChargeableCharConfig')");
|
|
|
|
exists.Should().Be(1, "UX_ChargeableCharConfig_Vigente filtered index debe existir");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task V021_CoverIndex_IX_ChargeableCharConfig_Query_Exists()
|
|
{
|
|
var exists = await _connection.ExecuteScalarAsync<int>(@"
|
|
SELECT COUNT(*)
|
|
FROM sys.indexes
|
|
WHERE name = 'IX_ChargeableCharConfig_Query'
|
|
AND object_id = OBJECT_ID('dbo.ChargeableCharConfig')");
|
|
|
|
exists.Should().Be(1, "IX_ChargeableCharConfig_Query covering index debe existir");
|
|
}
|
|
|
|
// ── V021: SP — usp_ChargeableCharConfig_InsertWithClose ──────────────
|
|
|
|
[Fact]
|
|
public async Task V021_SP_InsertWithClose_Exists()
|
|
{
|
|
var exists = await _connection.ExecuteScalarAsync<int>(@"
|
|
SELECT COUNT(*)
|
|
FROM sys.objects
|
|
WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_InsertWithClose')
|
|
AND type = 'P'");
|
|
|
|
exists.Should().Be(1, "usp_ChargeableCharConfig_InsertWithClose debe existir");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task usp_InsertWithClose_FirstPrice_ClosedIsNull()
|
|
{
|
|
// Cleanup (use ProductTypeId IS NULL after V023 refactor)
|
|
await _connection.ExecuteAsync(
|
|
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'@'");
|
|
|
|
var p = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
await _connection.ExecuteAsync(
|
|
"dbo.usp_ChargeableCharConfig_InsertWithClose",
|
|
p,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
|
|
var newId = p.Get<long?>("@NewId");
|
|
var closedId = p.Get<long?>("@ClosedId");
|
|
|
|
newId.Should().BeGreaterThan(0, "primer insert debe devolver NewId > 0");
|
|
closedId.Should().BeNull("primer insert no cierra nada — ClosedId debe ser NULL");
|
|
|
|
// Cleanup
|
|
await _connection.ExecuteAsync(
|
|
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'@' AND ProductTypeId IS NULL");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task usp_InsertWithClose_HappyPath_ClosesAndCreates()
|
|
{
|
|
// Seed primer activo
|
|
await _connection.ExecuteAsync(
|
|
"DELETE FROM dbo.ChargeableCharConfig WHERE ProductTypeId IS NULL AND Symbol = N'€'");
|
|
|
|
var p1 = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
var firstId = p1.Get<long?>("@NewId")!.Value;
|
|
|
|
// Insertar segundo (debe cerrar el primero)
|
|
var p2 = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
|
|
var newId = p2.Get<long?>("@NewId")!.Value;
|
|
var closedId = p2.Get<long?>("@ClosedId");
|
|
|
|
newId.Should().BeGreaterThan(firstId);
|
|
closedId.Should().Be(firstId, "el primer activo debe ser cerrado");
|
|
|
|
// Verify el activo cerrado tiene ValidTo = 2026-01-31
|
|
var closedValidTo = await _connection.ExecuteScalarAsync<DateTime?>(
|
|
"SELECT ValidTo FROM dbo.ChargeableCharConfig WHERE Id = @Id",
|
|
new { Id = firstId });
|
|
closedValidTo.Should().Be(new DateTime(2026, 1, 31),
|
|
"ValidTo del cerrado = ValidFrom(nuevo) - 1 día");
|
|
|
|
// Cleanup
|
|
await _connection.ExecuteAsync(
|
|
"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 ProductTypeId IS NULL AND Symbol = N'£'");
|
|
|
|
var p1 = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
|
|
// Intento con ValidFrom <= activo.ValidFrom → debe lanzar 50409
|
|
var p2 = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
var act = async () => await _connection.ExecuteAsync(
|
|
"dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
|
|
await act.Should().ThrowAsync<SqlException>()
|
|
.Where(ex => ex.Number == 50409,
|
|
"ValidFrom igual al activo debe generar THROW 50409 (forward-only)");
|
|
|
|
// Cleanup
|
|
await _connection.ExecuteAsync(
|
|
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'£' AND ProductTypeId IS NULL");
|
|
}
|
|
|
|
[Fact]
|
|
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 ProductTypeId IS NULL AND Symbol = N'¥'");
|
|
|
|
var p = new DynamicParameters();
|
|
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,
|
|
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 (ProductTypeId NULL) debe funcionar");
|
|
|
|
// Cleanup
|
|
await _connection.ExecuteAsync(
|
|
"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 ProductTypeId IS NULL AND Symbol = N'#'");
|
|
|
|
var p1 = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p1,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
var firstId = p1.Get<long?>("@NewId")!.Value;
|
|
|
|
// Close it by inserting a newer version
|
|
var p2 = new DynamicParameters();
|
|
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,
|
|
direction: System.Data.ParameterDirection.Output);
|
|
|
|
await _connection.ExecuteAsync("dbo.usp_ChargeableCharConfig_InsertWithClose", p2,
|
|
commandType: System.Data.CommandType.StoredProcedure);
|
|
|
|
// The UPDATE on the closed row should produce a history row
|
|
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
|
"SELECT COUNT(*) FROM dbo.ChargeableCharConfig_History WHERE Id = @Id",
|
|
new { Id = firstId });
|
|
|
|
historyCount.Should().BeGreaterThanOrEqualTo(1,
|
|
"UPDATE en SYSTEM_VERSIONED table debe producir al menos 1 fila en history");
|
|
|
|
// Cleanup
|
|
await _connection.ExecuteAsync(
|
|
"DELETE FROM dbo.ChargeableCharConfig WHERE Symbol = N'#' AND ProductTypeId IS NULL");
|
|
}
|
|
|
|
// ── V023 scope delta: SP — usp_ChargeableCharConfig_GetActiveForProductType ────────────
|
|
|
|
[Fact]
|
|
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(*)
|
|
FROM sys.objects
|
|
WHERE object_id = OBJECT_ID('dbo.usp_ChargeableCharConfig_GetActiveForMedio')
|
|
AND type = 'P'");
|
|
|
|
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 ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task V020_Permission_tasacion_caracteres_especiales_gestionar_Exists()
|
|
{
|
|
var exists = await _connection.ExecuteScalarAsync<int>(@"
|
|
SELECT COUNT(*) FROM dbo.Permiso
|
|
WHERE Codigo = 'tasacion:caracteres_especiales:gestionar'");
|
|
|
|
exists.Should().Be(1,
|
|
"V020 debe insertar el permiso 'tasacion:caracteres_especiales:gestionar'");
|
|
}
|
|
|
|
// ── 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 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: $, %, !, ¡ (global = ProductTypeId IS NULL tras V023)");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task V022_AllSeedRowsHaveIsActive_True()
|
|
{
|
|
var inactiveCount = await _connection.ExecuteScalarAsync<int>(@"
|
|
SELECT COUNT(*) FROM dbo.ChargeableCharConfig
|
|
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"));
|
|
}
|
|
}
|