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:
@@ -6,8 +6,10 @@ using SIGCM2.Application.Common;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.Create;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.Deactivate;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.Delete;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.GetById;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.List;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.Reactivate;
|
||||
using SIGCM2.Application.Pricing.ChargeableChars.SchedulePrice;
|
||||
|
||||
namespace SIGCM2.Api.Controllers;
|
||||
@@ -39,7 +41,7 @@ public sealed class ChargeableCharConfigController : ControllerBase
|
||||
|
||||
/// <summary>
|
||||
/// Returns a paginated list of ChargeableCharConfig rows.
|
||||
/// Filters: medioId (optional, long?), activeOnly (bool, default true).
|
||||
/// Filters: productTypeId (optional, long?), activeOnly (bool, default true).
|
||||
/// Pagination: skip/take model mapped to page/pageSize — or use page/pageSize directly.
|
||||
/// Defaults: page=1, pageSize=20. Clamped: pageSize max 200.
|
||||
/// </summary>
|
||||
@@ -49,7 +51,7 @@ public sealed class ChargeableCharConfigController : ControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> List(
|
||||
[FromQuery] long? medioId,
|
||||
[FromQuery] long? productTypeId,
|
||||
[FromQuery] bool activeOnly = true,
|
||||
[FromQuery] int? page = null,
|
||||
[FromQuery] int? pageSize = null,
|
||||
@@ -74,7 +76,7 @@ public sealed class ChargeableCharConfigController : ControllerBase
|
||||
resolvedPageSize = Math.Min(pageSize ?? 20, 200);
|
||||
}
|
||||
|
||||
var query = new ListChargeableCharConfigQuery(medioId, activeOnly, resolvedPage, resolvedPageSize);
|
||||
var query = new ListChargeableCharConfigQuery(productTypeId, activeOnly, resolvedPage, resolvedPageSize);
|
||||
var result = await _dispatcher.Send<ListChargeableCharConfigQuery, PagedResult<ChargeableCharConfigDto>>(query);
|
||||
return Ok(result);
|
||||
}
|
||||
@@ -100,7 +102,7 @@ public sealed class ChargeableCharConfigController : ControllerBase
|
||||
// ── POST /api/v1/admin/chargeable-chars ───────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ChargeableCharConfig row. Closes the current active row for (MedioId, Symbol) if one exists.
|
||||
/// Creates a new ChargeableCharConfig row. Closes the current active row for (ProductTypeId, Symbol) if one exists.
|
||||
/// Returns 201 Created with Location header pointing to GET /{id}.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
@@ -113,7 +115,7 @@ public sealed class ChargeableCharConfigController : ControllerBase
|
||||
public async Task<IActionResult> Create([FromBody] CreateChargeableCharConfigRequest request)
|
||||
{
|
||||
var command = new CreateChargeableCharConfigCommand(
|
||||
request.MedioId,
|
||||
request.ProductTypeId,
|
||||
request.Symbol,
|
||||
request.Category,
|
||||
request.PricePerUnit,
|
||||
@@ -183,13 +185,57 @@ public sealed class ChargeableCharConfigController : ControllerBase
|
||||
new DeactivateChargeableCharConfigCommand(id));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ── PATCH /api/v1/admin/chargeable-chars/{id}/reactivate ─────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Reactivates a previously closed ChargeableCharConfig row (undo last deactivation).
|
||||
/// Guard rules (enforced by SP):
|
||||
/// - ALREADY_ACTIVE: target row is already active → 409
|
||||
/// - VIGENTE_EXISTS: a different active row exists for (ProductTypeId, Symbol) → 409
|
||||
/// - POSTERIOR_ROWS_EXIST: rows with higher ValidFrom exist after the target → 409
|
||||
/// </summary>
|
||||
[HttpPatch("{id:long}/reactivate")]
|
||||
[RequirePermission("tasacion:caracteres_especiales:gestionar")]
|
||||
[ProducesResponseType(typeof(ReactivateChargeableCharConfigResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Reactivate([FromRoute] long id)
|
||||
{
|
||||
var result = await _dispatcher.Send<ReactivateChargeableCharConfigCommand, ReactivateChargeableCharConfigResponse>(
|
||||
new ReactivateChargeableCharConfigCommand(id));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ── DELETE /api/v1/admin/chargeable-chars/{id} ───────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a ChargeableCharConfig row.
|
||||
/// NOTE: With SYSTEM_VERSIONING ON, the row is moved to the history table (temporal audit preserved).
|
||||
/// The row disappears from all current-state queries.
|
||||
/// Guard for "used in invoicing" is deferred to FAC-001 followup issue.
|
||||
/// Returns 200 + { id } consistent with the Deactivate pattern.
|
||||
/// </summary>
|
||||
[HttpDelete("{id:long}")]
|
||||
[RequirePermission("tasacion:caracteres_especiales:gestionar")]
|
||||
[ProducesResponseType(typeof(DeleteChargeableCharConfigResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete([FromRoute] long id)
|
||||
{
|
||||
var result = await _dispatcher.Send<DeleteChargeableCharConfigCommand, DeleteChargeableCharConfigResponse>(
|
||||
new DeleteChargeableCharConfigCommand(id));
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Request body records ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>PRC-001: Create ChargeableCharConfig request body.</summary>
|
||||
public sealed record CreateChargeableCharConfigRequest(
|
||||
long? MedioId,
|
||||
long? ProductTypeId,
|
||||
string Symbol,
|
||||
string Category,
|
||||
decimal PricePerUnit,
|
||||
|
||||
Reference in New Issue
Block a user