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:
@@ -7,24 +7,24 @@ namespace SIGCM2.Application.Abstractions.Persistence;
|
||||
/// Implemented by ChargeableCharConfigRepository (Dapper) in Infrastructure.
|
||||
///
|
||||
/// InsertWithCloseAsync: invokes usp_ChargeableCharConfig_InsertWithClose which atomically
|
||||
/// closes any active row for (MedioId, Symbol) and inserts the new row.
|
||||
/// closes any active row for (ProductTypeId, Symbol) and inserts the new row.
|
||||
///
|
||||
/// GetActiveForMedioAsync: invokes usp_ChargeableCharConfig_GetActiveForMedio which returns
|
||||
/// both per-medio rows AND global (MedioId IS NULL) rows for the given asOfDate.
|
||||
/// The Application service applies the per-medio > global priority rule.
|
||||
/// GetActiveForProductTypeAsync: invokes usp_ChargeableCharConfig_GetActiveForProductType which
|
||||
/// returns both per-ProductType rows AND global (ProductTypeId IS NULL) rows for the given asOfDate.
|
||||
/// The Application service applies the per-ProductType > global priority rule.
|
||||
/// </summary>
|
||||
public interface IChargeableCharConfigRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes usp_ChargeableCharConfig_InsertWithClose inside the ambient TransactionScope.
|
||||
/// Closes any active row matching (MedioId, Symbol) and inserts a new one.
|
||||
/// Closes any active row matching (ProductTypeId, Symbol) and inserts a new one.
|
||||
/// Returns the Id of the newly inserted row.
|
||||
/// Throws:
|
||||
/// - ChargeableCharConfigForwardOnlyException on SQL THROW 50409
|
||||
/// - ChargeableCharConfigInvalidException on SQL THROW 50404 (not-found guard)
|
||||
/// </summary>
|
||||
Task<long> InsertWithCloseAsync(
|
||||
long? medioId,
|
||||
long? productTypeId,
|
||||
string symbol,
|
||||
string category,
|
||||
decimal price,
|
||||
@@ -33,20 +33,20 @@ public interface IChargeableCharConfigRepository
|
||||
|
||||
/// <summary>
|
||||
/// Returns all active rows whose [ValidFrom, ValidTo] window covers the given asOfDate
|
||||
/// for the specified medio, including global rows (MedioId IS NULL).
|
||||
/// The SP returns both per-medio AND global rows — callers apply priority.
|
||||
/// for the specified ProductType, including global rows (ProductTypeId IS NULL).
|
||||
/// The SP returns both per-ProductType AND global rows — callers apply priority.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<ChargeableCharConfig>> GetActiveForMedioAsync(
|
||||
long medioId,
|
||||
Task<IReadOnlyList<ChargeableCharConfig>> GetActiveForProductTypeAsync(
|
||||
long productTypeId,
|
||||
DateOnly asOfDate,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns paginated rows filtered by MedioId and IsActive.
|
||||
/// Returns paginated rows filtered by ProductTypeId and IsActive.
|
||||
/// Skip = (page - 1) * pageSize computed by the caller.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<ChargeableCharConfig>> ListAsync(
|
||||
long? medioId,
|
||||
long? productTypeId,
|
||||
bool activeOnly,
|
||||
int skip,
|
||||
int take,
|
||||
@@ -56,7 +56,7 @@ public interface IChargeableCharConfigRepository
|
||||
/// Returns total row count for the given filters (used for pagination metadata).
|
||||
/// </summary>
|
||||
Task<int> CountAsync(
|
||||
long? medioId,
|
||||
long? productTypeId,
|
||||
bool activeOnly,
|
||||
CancellationToken ct = default);
|
||||
|
||||
@@ -76,4 +76,29 @@ public interface IChargeableCharConfigRepository
|
||||
long id,
|
||||
DateOnly today,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes usp_ChargeableCharConfig_ReactivateWithGuard.
|
||||
/// Guard rules (enforced by SP):
|
||||
/// 50410 → target row is already active → ChargeableCharConfigReactivationNotAllowedException(ALREADY_ACTIVE)
|
||||
/// 50411 → a vigente active row exists for (ProductTypeId, Symbol) → ChargeableCharConfigReactivationNotAllowedException(VIGENTE_EXISTS)
|
||||
/// 50412 → posterior rows exist after target row → ChargeableCharConfigReactivationNotAllowedException(POSTERIOR_ROWS_EXIST)
|
||||
/// 50404 → row not found → ChargeableCharConfigInvalidException
|
||||
/// On success: re-opens the row (IsActive=true, ValidTo=NULL) and returns the reactivated entity.
|
||||
/// </summary>
|
||||
Task<ChargeableCharConfig> ReactivateAsync(
|
||||
long id,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Physically deletes the row with the given Id from dbo.ChargeableCharConfig (current state).
|
||||
/// NOTE: Since SYSTEM_VERSIONING is ON, SQL Server moves the row to the history table with
|
||||
/// SysEndTime set to the delete time. The row disappears from all current-state queries but
|
||||
/// remains queryable via FOR SYSTEM_TIME. Temporal audit trail is preserved.
|
||||
/// Future guard for "used in invoicing" is deferred to FAC-001 followup issue.
|
||||
/// Throws KeyNotFoundException if the row does not exist.
|
||||
/// </summary>
|
||||
Task DeleteAsync(
|
||||
long id,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user