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:
@@ -8,7 +8,7 @@ using Xunit;
|
||||
namespace SIGCM2.Application.Tests.Infrastructure.Pricing;
|
||||
|
||||
/// <summary>
|
||||
/// PRC-001 Batch 7 — Integration hardening tests for ChargeableCharConfig.
|
||||
/// PRC-001 — Integration hardening tests for ChargeableCharConfig.
|
||||
/// Covers cross-cutting concerns not addressed by individual layer batches:
|
||||
///
|
||||
/// T7.1 Concurrency: SemaphoreSlim barrier forces genuine parallel race on
|
||||
@@ -18,9 +18,11 @@ namespace SIGCM2.Application.Tests.Infrastructure.Pricing;
|
||||
///
|
||||
/// T7.3 FOR SYSTEM_TIME AS OF: temporal snapshot query at T0 returns pre-close state.
|
||||
///
|
||||
/// T7.4 Per-medio + global fallback resolution via GetActiveForMedioAsync:
|
||||
/// - ELDIA override for '$' → per-medio row returned at priority
|
||||
/// - ELPLATA (no override) → global fallback returned
|
||||
/// T7.4 Per-ProductType + global fallback resolution via GetActiveForProductTypeAsync:
|
||||
/// - ProductType1 override for '$' → per-PT row returned at priority
|
||||
/// - ProductType2 (no override) → global fallback returned
|
||||
///
|
||||
/// V023 scope delta: MedioId → ProductTypeId. Seeds use dbo.ProductType rows.
|
||||
///
|
||||
/// All tests run against SIGCM2_Test_App (Database collection + SqlTestFixture).
|
||||
/// Each test seeds its own unique symbols to avoid cross-test interference.
|
||||
@@ -29,8 +31,10 @@ namespace SIGCM2.Application.Tests.Infrastructure.Pricing;
|
||||
public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
{
|
||||
private readonly SqlTestFixture _db;
|
||||
private int _eldiaId;
|
||||
private int _elplataId;
|
||||
// V023 scope delta: renamed from _eldiaId/_elplataId to ProductType-based IDs.
|
||||
// These are ProductType IDs (FK to dbo.ProductType), not Medio IDs.
|
||||
private int _productType1Id; // has per-PT override (was ELDIA)
|
||||
private int _productType2Id; // no override, falls back to global (was ELPLATA)
|
||||
|
||||
public ChargeableCharConfigHardeningTests(SqlTestFixture db)
|
||||
{
|
||||
@@ -44,17 +48,18 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
await using var conn = new SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await conn.OpenAsync();
|
||||
|
||||
// Seed two dedicated medios: ELDIA (has per-medio override) and ELPLATA (no override).
|
||||
_eldiaId = await conn.ExecuteScalarAsync<int>("""
|
||||
INSERT INTO dbo.Medio (Codigo, Nombre, Tipo, Activo)
|
||||
// Seed two dedicated ProductTypes for override/fallback resolution tests.
|
||||
// V023: ChargeableCharConfig.ProductTypeId references dbo.ProductType(Id).
|
||||
_productType1Id = await conn.ExecuteScalarAsync<int>("""
|
||||
INSERT INTO dbo.ProductType (Nombre, IsActive)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES ('HARD_ELDIA', 'ELDIA Hardening', 1, 1)
|
||||
VALUES ('Hardening PT1 (override)', 1)
|
||||
""");
|
||||
|
||||
_elplataId = await conn.ExecuteScalarAsync<int>("""
|
||||
INSERT INTO dbo.Medio (Codigo, Nombre, Tipo, Activo)
|
||||
_productType2Id = await conn.ExecuteScalarAsync<int>("""
|
||||
INSERT INTO dbo.ProductType (Nombre, IsActive)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES ('HARD_ELPLA', 'ELPLATA Hardening', 1, 1)
|
||||
VALUES ('Hardening PT2 (fallback)', 1)
|
||||
""");
|
||||
}
|
||||
|
||||
@@ -126,27 +131,28 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
successes.Should().Be(1, "exactly one concurrent InsertWithClose must succeed");
|
||||
failures.Should().Be(2, "the other two concurrent inserts must fail with SqlException");
|
||||
|
||||
// Verify post-race state: exactly 1 vigente row for (NULL, '¢')
|
||||
// Verify post-race state: exactly 1 vigente row for (ProductTypeId=NULL, '¢')
|
||||
// V023: MedioId → ProductTypeId; global fallback = ProductTypeId IS NULL
|
||||
await using var verifyConn = new SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await verifyConn.OpenAsync();
|
||||
|
||||
var vigente = await verifyConn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.ChargeableCharConfig
|
||||
WHERE MedioId IS NULL
|
||||
WHERE ProductTypeId IS NULL
|
||||
AND Symbol = @Symbol
|
||||
AND ValidTo IS NULL
|
||||
AND IsActive = 1
|
||||
""", new { Symbol = symbol });
|
||||
|
||||
vigente.Should().Be(1,
|
||||
"filtered unique index UX_ChargeableCharConfig_Vigente must prevent more than 1 vigente row per (MedioId, Symbol)");
|
||||
"filtered unique index UX_ChargeableCharConfig_Vigente must prevent more than 1 vigente row per (ProductTypeId, Symbol)");
|
||||
|
||||
// No duplicates: total row count must also be 1 (only the winner was inserted)
|
||||
var total = await verifyConn.ExecuteScalarAsync<int>("""
|
||||
SELECT COUNT(1)
|
||||
FROM dbo.ChargeableCharConfig
|
||||
WHERE MedioId IS NULL
|
||||
WHERE ProductTypeId IS NULL
|
||||
AND Symbol = @Symbol
|
||||
""", new { Symbol = symbol });
|
||||
|
||||
@@ -260,115 +266,127 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// T7.4 — Per-medio + global fallback resolution via GetActiveForMedioAsync
|
||||
// T7.4 — Per-ProductType + global fallback resolution via GetActiveForProductTypeAsync
|
||||
//
|
||||
// V023 scope delta: MedioId → ProductTypeId in table + SP.
|
||||
//
|
||||
// Scenario:
|
||||
// - Global '$' at price 1.00 (seed row from ResetAndSeedAsync / canonical V022 seed)
|
||||
// - ELDIA-specific '$' at price 5.00 effective from today (per-medio override)
|
||||
// - ELPLATA has no override for '$'
|
||||
// - Global '$' at price 0.00 (seed row from ResetAndSeedAsync / canonical V022+V024 seed)
|
||||
// - ProductType1-specific '$' at price 5.00 effective from 2026-01-01 (per-PT override)
|
||||
// - ProductType2 has no override for '$'
|
||||
//
|
||||
// GetActiveConfigForMedioAsync(ELDIA, today) → '$' = 5.00 (per-medio override wins)
|
||||
// GetActiveConfigForMedioAsync(ELPLATA, today) → '$' = 1.00 (global fallback)
|
||||
// GetActiveConfigForProductTypeAsync(PT1, today) → '$' = 5.00 (per-PT override wins)
|
||||
// GetActiveConfigForProductTypeAsync(PT2, today) → '$' = 0.00 (global fallback)
|
||||
//
|
||||
// NOTE: C# method calls (GetActiveForMedioAsync, GetActiveConfigForMedioAsync) will be
|
||||
// renamed in Agent 2 (Backend refactor). These tests will FAIL COMPILATION until Agent 2.
|
||||
// SQL-level assertions in this test (the ExecInsertWithCloseAsync helper) are already
|
||||
// updated for V023 (@ProductTypeId param). The C# repo/service method calls are left as-is.
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfigForMedio_EldiaOverride_WinsOverGlobal()
|
||||
public async Task GetActiveConfigForProductType_Override_WinsOverGlobal()
|
||||
{
|
||||
var asOf = new DateOnly(2026, 6, 1);
|
||||
|
||||
// Seed per-medio override for ELDIA: '$' at 5.00 effective from 2026-01-01
|
||||
// Seed per-PT override for ProductType1: '$' at 5.00 effective from 2026-01-01
|
||||
await using var seedConn = new SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await seedConn.OpenAsync();
|
||||
|
||||
await ExecInsertWithCloseAsync(seedConn, _eldiaId, "$", "Currency", 5.0000m, new DateTime(2026, 1, 1));
|
||||
await ExecInsertWithCloseAsync(seedConn, _productType1Id, "$", "Currency", 5.0000m, new DateTime(2026, 1, 1));
|
||||
|
||||
// Build the repository + service (same as application layer usage)
|
||||
// Build the repository + service (C# method will be renamed in Agent 2)
|
||||
var repo = BuildRepository();
|
||||
var rows = await repo.GetActiveForMedioAsync((long)_eldiaId, asOf);
|
||||
var rows = await repo.GetActiveForMedioAsync((long)_productType1Id, asOf); // TODO Agent 2: rename to GetActiveForProductTypeAsync
|
||||
|
||||
// The per-medio '$' must be returned
|
||||
// The per-PT '$' must be returned
|
||||
var dollarRow = rows.FirstOrDefault(r => r.Symbol == "$");
|
||||
dollarRow.Should().NotBeNull("ELDIA has a per-medio '$' override — SP must return it");
|
||||
dollarRow.Should().NotBeNull("ProductType1 has a per-PT '$' override — SP must return it");
|
||||
|
||||
dollarRow!.MedioId.Should().Be(_eldiaId,
|
||||
"the per-medio row (MedioId = ELDIA) must take priority over the global row");
|
||||
dollarRow!.MedioId.Should().Be(_productType1Id, // TODO Agent 2: rename to ProductTypeId
|
||||
"the per-PT row (ProductTypeId = PT1) must take priority over the global row");
|
||||
|
||||
dollarRow.PricePerUnit.Should().Be(5.0000m,
|
||||
"ELDIA override has price 5.00, not the global 1.00");
|
||||
"ProductType1 override has price 5.00, not the global 0.00");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfigForMedio_ElplataNoOverride_FallsBackToGlobal()
|
||||
public async Task GetActiveConfigForProductType_NoOverride_FallsBackToGlobal()
|
||||
{
|
||||
var asOf = new DateOnly(2026, 6, 1);
|
||||
|
||||
// ELPLATA has no per-medio rows — the canonical global seed from ResetAndSeedAsync
|
||||
// provides '$' at global price.
|
||||
// ProductType2 has no per-PT rows — the canonical global seed from ResetAndSeedAsync
|
||||
// provides '$' at global price (0.0000 after V024).
|
||||
var repo = BuildRepository();
|
||||
var rows = await repo.GetActiveForMedioAsync((long)_elplataId, asOf);
|
||||
var rows = await repo.GetActiveForMedioAsync((long)_productType2Id, asOf); // TODO Agent 2: rename to GetActiveForProductTypeAsync
|
||||
|
||||
// Must have at least the global '$' from seed
|
||||
rows.Should().NotBeEmpty("canonical seed provides global rows active as of 2026-06-01");
|
||||
|
||||
var dollarRow = rows.FirstOrDefault(r => r.Symbol == "$");
|
||||
dollarRow.Should().NotBeNull("global '$' must be returned for ELPLATA (no override exists)");
|
||||
dollarRow.Should().NotBeNull("global '$' must be returned for ProductType2 (no override exists)");
|
||||
|
||||
dollarRow!.MedioId.Should().BeNull(
|
||||
"ELPLATA has no override — the returned row must be the global row (MedioId = NULL)");
|
||||
dollarRow!.MedioId.Should().BeNull( // TODO Agent 2: rename to ProductTypeId
|
||||
"ProductType2 has no override — the returned row must be the global row (ProductTypeId = NULL)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetActiveConfigForMedio_ServiceLayer_AppliesPriorityCorrectly()
|
||||
public async Task GetActiveConfigForProductType_ServiceLayer_AppliesPriorityCorrectly()
|
||||
{
|
||||
// End-to-end: IChargeableCharConfigService resolves the final dictionary.
|
||||
// Seed ELDIA override for '%' (percentage) at 3.00; global '%' at 2.00 (from V022 seed).
|
||||
// Seed ProductType1 override for '%' (percentage) at 3.00; global '%' at 0.00 (from V024 seed).
|
||||
var asOf = new DateOnly(2026, 6, 1);
|
||||
|
||||
await using var seedConn = new SqlConnection(TestConnectionStrings.AppTestDb);
|
||||
await seedConn.OpenAsync();
|
||||
|
||||
await ExecInsertWithCloseAsync(seedConn, _eldiaId, "%", "Percentage", 3.0000m, new DateTime(2026, 1, 1));
|
||||
await ExecInsertWithCloseAsync(seedConn, _productType1Id, "%", "Percentage", 3.0000m, new DateTime(2026, 1, 1));
|
||||
|
||||
// Build the service (wraps repo with priority resolution)
|
||||
// TODO Agent 2: rename GetActiveConfigForMedioAsync → GetActiveConfigForProductTypeAsync
|
||||
var service = BuildService();
|
||||
|
||||
var eldiaConfig = await service.GetActiveConfigForMedioAsync((long)_eldiaId, asOf);
|
||||
var elplataConfig = await service.GetActiveConfigForMedioAsync((long)_elplataId, asOf);
|
||||
var pt1Config = await service.GetActiveConfigForMedioAsync((long)_productType1Id, asOf); // TODO Agent 2
|
||||
var pt2Config = await service.GetActiveConfigForMedioAsync((long)_productType2Id, asOf); // TODO Agent 2
|
||||
|
||||
// ELDIA: '%' must come from per-medio override at 3.00
|
||||
eldiaConfig.Should().ContainKey("%",
|
||||
"ELDIA has a per-medio override for '%'");
|
||||
eldiaConfig["%"].PricePerUnit.Should().Be(3.0000m,
|
||||
"per-medio '%' at 3.00 must override the global 2.00 for ELDIA");
|
||||
// ProductType1: '%' must come from per-PT override at 3.00
|
||||
pt1Config.Should().ContainKey("%",
|
||||
"ProductType1 has a per-PT override for '%'");
|
||||
pt1Config["%"].PricePerUnit.Should().Be(3.0000m,
|
||||
"per-PT '%' at 3.00 must override the global 0.00 for ProductType1");
|
||||
|
||||
// ELPLATA: '%' must come from global fallback
|
||||
// Note: ChargeableCharSnapshot only exposes Category + PricePerUnit (not MedioId).
|
||||
// We verify via price: global '%' is seeded at 2.00 by V022.
|
||||
elplataConfig.Should().ContainKey("%",
|
||||
"global '%' from canonical seed must appear in ELPLATA's resolved config");
|
||||
// V022 seeds global '%' at 1.0000 (placeholder — see V022__seed_chargeable_char_config.sql)
|
||||
elplataConfig["%"].PricePerUnit.Should().Be(1.0000m,
|
||||
"ELPLATA falls back to the global '%' at 1.00 (V022 seed placeholder price)");
|
||||
// ProductType2: '%' must come from global fallback
|
||||
// Note: ChargeableCharSnapshot only exposes Category + PricePerUnit (not MedioId/ProductTypeId).
|
||||
// We verify via price: global '%' is seeded at 0.0000 by V024 (opt-in billing, was 1.0000 in V022).
|
||||
pt2Config.Should().ContainKey("%",
|
||||
"global '%' from canonical seed must appear in ProductType2's resolved config");
|
||||
// V024 resets global '%' to 0.0000 (opt-in billing — V022 placeholder 1.0000 replaced)
|
||||
pt2Config["%"].PricePerUnit.Should().Be(0.0000m,
|
||||
"ProductType2 falls back to the global '%' at 0.00 (V024 seed opt-in billing price)");
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Helper: calls usp_ChargeableCharConfig_InsertWithClose directly via SQL.
|
||||
/// V023 scope delta: parameter renamed from @MedioId to @ProductTypeId.
|
||||
/// </summary>
|
||||
private static async Task<long> ExecInsertWithCloseAsync(
|
||||
SqlConnection conn,
|
||||
int? medioId,
|
||||
int? productTypeId,
|
||||
string symbol,
|
||||
string category,
|
||||
decimal pricePerUnit,
|
||||
DateTime validFrom)
|
||||
{
|
||||
var p = new DynamicParameters();
|
||||
p.Add("@MedioId", medioId, System.Data.DbType.Int32);
|
||||
p.Add("@Symbol", symbol, System.Data.DbType.String);
|
||||
p.Add("@Category", category, System.Data.DbType.String);
|
||||
p.Add("@PricePerUnit", pricePerUnit, System.Data.DbType.Decimal, precision: 18, scale: 4);
|
||||
p.Add("@ValidFrom", validFrom, 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);
|
||||
p.Add("@ProductTypeId", productTypeId, System.Data.DbType.Int32); // V023: was @MedioId
|
||||
p.Add("@Symbol", symbol, System.Data.DbType.String);
|
||||
p.Add("@Category", category, System.Data.DbType.String);
|
||||
p.Add("@PricePerUnit", pricePerUnit, System.Data.DbType.Decimal, precision: 18, scale: 4);
|
||||
p.Add("@ValidFrom", validFrom, 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 conn.ExecuteAsync(
|
||||
"dbo.usp_ChargeableCharConfig_InsertWithClose",
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,19 @@ namespace SIGCM2.Application.Tests.Infrastructure.Pricing;
|
||||
///
|
||||
/// All tests run against the real DB via SqlTestFixture (Database collection).
|
||||
/// Canonical seed (4 global symbols: $, %, !, ¡) is loaded by ResetAndSeedAsync().
|
||||
/// Tests that mutate specific (MedioId, Symbol) pairs clean their own state before mutating.
|
||||
/// Tests that mutate specific (ProductTypeId, Symbol) pairs clean their own state before mutating.
|
||||
///
|
||||
/// V023 scope delta: MedioId → ProductTypeId. C# method/property renames (InsertWithCloseAsync
|
||||
/// medioId: param, GetActiveForMedioAsync, entity.MedioId) are deferred to Agent 2 (Backend refactor).
|
||||
/// This class will FAIL COMPILATION after Agent 2 renames the domain layer — expected.
|
||||
///
|
||||
/// Spec coverage:
|
||||
/// T4.1 InsertWithCloseAsync — first insert for symbol → new row, returns Id
|
||||
/// T4.2 InsertWithCloseAsync — with existing vigente → closes previous, inserts new
|
||||
/// T4.3 InsertWithCloseAsync — backdate attempt → ThrowsForwardOnlyException
|
||||
/// T4.4 InsertWithCloseAsync — system versioning captures history row after mutation
|
||||
/// T4.5 GetActiveForMedioAsync — medio has override → returns both medio and global rows
|
||||
/// T4.6 GetActiveForMedioAsync — no medio override → returns only global rows
|
||||
/// T4.5 GetActiveForProductTypeAsync — PT has override → returns both PT and global rows
|
||||
/// T4.6 GetActiveForProductTypeAsync — no PT override → returns only global rows
|
||||
/// T4.7 ListAsync — paginates (skip/take)
|
||||
/// T4.8 CountAsync — filters by activeOnly
|
||||
/// T4.9 GetByIdAsync — missing → returns null
|
||||
|
||||
Reference in New Issue
Block a user