refactor+feat(backend): ChargeableCharConfig por ProductType + Reactivate + Delete endpoints (PRC-001)
Part A — MedioId → ProductTypeId rename across all C# layers:
Domain, Application, Infrastructure, API, all test projects.
Solution was non-compilable after BD refactor (5c1675e); now compiles clean (0 errors).
Part B — PATCH /api/v1/admin/chargeable-chars/{id}/reactivate:
ReactivateChargeableCharConfigCommand/Handler, SP guard maps 50410/50411/50412
→ ChargeableCharConfigReactivationNotAllowedException(Reason) → HTTP 409.
Part C — DELETE /api/v1/admin/chargeable-chars/{id}:
DeleteChargeableCharConfigCommand/Handler, physical DELETE on SYSTEM_VERSIONED table.
KeyNotFoundException → 404 via ExceptionFilter.
Tests: +30 unit tests (TDD RED→GREEN). All 1266 unit tests pass.
This commit is contained in:
@@ -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<int>("""
|
||||
INSERT INTO dbo.ProductType (Nombre, IsActive)
|
||||
INSERT INTO dbo.ProductType (Nombre, Codigo, Activo)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES ('Hardening PT1 (override)', 1)
|
||||
VALUES ('Hardening PT1 (override)', 'H_PT1', 1)
|
||||
""");
|
||||
|
||||
_productType2Id = await conn.ExecuteScalarAsync<int>("""
|
||||
INSERT INTO dbo.ProductType (Nombre, IsActive)
|
||||
INSERT INTO dbo.ProductType (Nombre, Codigo, Activo)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES ('Hardening PT2 (fallback)', 1)
|
||||
VALUES ('Hardening PT2 (fallback)', 'H_PT2', 1)
|
||||
""");
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// T7.1 — Concurrency: only one winner survives the race
|
||||
//
|
||||
// Three parallel connections try to InsertWithClose for the same (MedioId=null, Symbol).
|
||||
// Three parallel connections try to InsertWithClose for the same (ProductTypeId=null, Symbol).
|
||||
// The SP uses SERIALIZABLE + UPDLOCK + HOLDLOCK, so only one can commit.
|
||||
// The other two must receive SqlException (50409, 2601, 2627, or deadlock 1205).
|
||||
//
|
||||
// After resolution: exactly 1 vigente row exists for (MedioId=NULL, Symbol).
|
||||
// After resolution: exactly 1 vigente row exists for (ProductTypeId=NULL, Symbol).
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
@@ -94,7 +94,7 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
await conn.OpenAsync();
|
||||
|
||||
var p = new DynamicParameters();
|
||||
p.Add("@MedioId", null, System.Data.DbType.Int32);
|
||||
p.Add("@ProductTypeId", null, System.Data.DbType.Int32);
|
||||
p.Add("@Symbol", symbol, System.Data.DbType.String);
|
||||
p.Add("@Category", category, System.Data.DbType.String);
|
||||
p.Add("@PricePerUnit", price, System.Data.DbType.Decimal, precision: 18, scale: 4);
|
||||
@@ -278,10 +278,6 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
// 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]
|
||||
@@ -297,13 +293,13 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
|
||||
// Build the repository + service (C# method will be renamed in Agent 2)
|
||||
var repo = BuildRepository();
|
||||
var rows = await repo.GetActiveForMedioAsync((long)_productType1Id, asOf); // TODO Agent 2: rename to GetActiveForProductTypeAsync
|
||||
var rows = await repo.GetActiveForProductTypeAsync((long)_productType1Id, asOf);
|
||||
|
||||
// The per-PT '$' must be returned
|
||||
var dollarRow = rows.FirstOrDefault(r => r.Symbol == "$");
|
||||
dollarRow.Should().NotBeNull("ProductType1 has a per-PT '$' override — SP must return it");
|
||||
|
||||
dollarRow!.MedioId.Should().Be(_productType1Id, // TODO Agent 2: rename to ProductTypeId
|
||||
dollarRow!.ProductTypeId.Should().Be(_productType1Id,
|
||||
"the per-PT row (ProductTypeId = PT1) must take priority over the global row");
|
||||
|
||||
dollarRow.PricePerUnit.Should().Be(5.0000m,
|
||||
@@ -318,7 +314,7 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
// 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)_productType2Id, asOf); // TODO Agent 2: rename to GetActiveForProductTypeAsync
|
||||
var rows = await repo.GetActiveForProductTypeAsync((long)_productType2Id, asOf);
|
||||
|
||||
// Must have at least the global '$' from seed
|
||||
rows.Should().NotBeEmpty("canonical seed provides global rows active as of 2026-06-01");
|
||||
@@ -326,7 +322,7 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
var dollarRow = rows.FirstOrDefault(r => r.Symbol == "$");
|
||||
dollarRow.Should().NotBeNull("global '$' must be returned for ProductType2 (no override exists)");
|
||||
|
||||
dollarRow!.MedioId.Should().BeNull( // TODO Agent 2: rename to ProductTypeId
|
||||
dollarRow!.ProductTypeId.Should().BeNull(
|
||||
"ProductType2 has no override — the returned row must be the global row (ProductTypeId = NULL)");
|
||||
}
|
||||
|
||||
@@ -343,11 +339,10 @@ public sealed class ChargeableCharConfigHardeningTests : IAsyncLifetime
|
||||
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 pt1Config = await service.GetActiveConfigForMedioAsync((long)_productType1Id, asOf); // TODO Agent 2
|
||||
var pt2Config = await service.GetActiveConfigForMedioAsync((long)_productType2Id, asOf); // TODO Agent 2
|
||||
var pt1Config = await service.GetActiveConfigForProductTypeAsync((long)_productType1Id, asOf);
|
||||
var pt2Config = await service.GetActiveConfigForProductTypeAsync((long)_productType2Id, asOf);
|
||||
|
||||
// ProductType1: '%' must come from per-PT override at 3.00
|
||||
pt1Config.Should().ContainKey("%",
|
||||
|
||||
Reference in New Issue
Block a user