feat(api): RubrosController + integration tests e2e + audit verification (CAT-001)

This commit is contained in:
2026-04-18 20:05:20 -03:00
parent f8e9d18379
commit 5e2323e0bc
3 changed files with 836 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SIGCM2.Api.Authorization;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Rubros.Create;
using SIGCM2.Application.Rubros.Deactivate;
using SIGCM2.Application.Rubros.Dtos;
using SIGCM2.Application.Rubros.GetById;
using SIGCM2.Application.Rubros.GetTree;
using SIGCM2.Application.Rubros.Move;
using SIGCM2.Application.Rubros.Update;
namespace SIGCM2.Api.Controllers;
/// <summary>
/// CAT-001: Rubro N-ary tree management.
/// Read endpoints at /api/v1/rubros — require authentication (any role).
/// Write endpoints at /api/v1/admin/rubros — require 'catalogo:rubros:gestionar'.
/// </summary>
[ApiController]
public sealed class RubrosController : ControllerBase
{
private readonly IDispatcher _dispatcher;
public RubrosController(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
}
// ── READ endpoints ─────────────────────────────────────────────────────────
/// <summary>Returns the full Rubro tree. Requires authentication.</summary>
[HttpGet("api/v1/rubros/tree")]
[Authorize]
[ProducesResponseType(typeof(IReadOnlyList<RubroTreeNodeDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetRubroTree([FromQuery] bool incluirInactivos = false)
{
var query = new GetRubroTreeQuery(incluirInactivos);
var result = await _dispatcher.Send<GetRubroTreeQuery, IReadOnlyList<RubroTreeNodeDto>>(query);
return Ok(result);
}
/// <summary>Returns a single Rubro by id. Requires authentication.</summary>
[HttpGet("api/v1/rubros/{id:int}")]
[Authorize]
[ProducesResponseType(typeof(RubroDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetRubroById([FromRoute] int id)
{
var query = new GetRubroByIdQuery(id);
var result = await _dispatcher.Send<GetRubroByIdQuery, RubroDetailDto>(query);
return Ok(result);
}
// ── WRITE endpoints ────────────────────────────────────────────────────────
/// <summary>Creates a new Rubro. Requires catalogo:rubros:gestionar.</summary>
[HttpPost("api/v1/admin/rubros")]
[RequirePermission("catalogo:rubros:gestionar")]
[ProducesResponseType(typeof(RubroCreatedDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> CreateRubro([FromBody] CreateRubroRequest request)
{
var command = new CreateRubroCommand(
Nombre: request.Nombre ?? string.Empty,
ParentId: request.ParentId,
TarifarioBaseId: request.TarifarioBaseId);
var result = await _dispatcher.Send<CreateRubroCommand, RubroCreatedDto>(command);
return CreatedAtAction(nameof(GetRubroById), new { id = result.Id }, result);
}
/// <summary>Updates a Rubro's nombre. Requires catalogo:rubros:gestionar.</summary>
[HttpPut("api/v1/admin/rubros/{id:int}")]
[RequirePermission("catalogo:rubros:gestionar")]
[ProducesResponseType(typeof(RubroUpdatedDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> UpdateRubro([FromRoute] int id, [FromBody] UpdateRubroRequest request)
{
var command = new UpdateRubroCommand(
Id: id,
Nombre: request.Nombre ?? string.Empty);
var result = await _dispatcher.Send<UpdateRubroCommand, RubroUpdatedDto>(command);
return Ok(result);
}
/// <summary>Soft-deletes (deactivates) a Rubro. Requires catalogo:rubros:gestionar.</summary>
[HttpDelete("api/v1/admin/rubros/{id:int}")]
[RequirePermission("catalogo:rubros:gestionar")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> DeactivateRubro([FromRoute] int id)
{
var command = new DeactivateRubroCommand(id);
await _dispatcher.Send<DeactivateRubroCommand, RubroStatusDto>(command);
return NoContent();
}
/// <summary>Moves a Rubro to a new parent. Requires catalogo:rubros:gestionar.</summary>
[HttpPatch("api/v1/admin/rubros/{id:int}/mover")]
[RequirePermission("catalogo:rubros:gestionar")]
[ProducesResponseType(typeof(RubroMovedDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> MoveRubro([FromRoute] int id, [FromBody] MoveRubroRequest request)
{
var command = new MoveRubroCommand(
Id: id,
NuevoParentId: request.NuevoParentId,
NuevoOrden: request.NuevoOrden);
var result = await _dispatcher.Send<MoveRubroCommand, RubroMovedDto>(command);
return Ok(result);
}
}
// ── Request body records ──────────────────────────────────────────────────────
/// <summary>CAT-001: Create rubro request body.</summary>
public sealed record CreateRubroRequest(
string? Nombre,
int? ParentId,
int? TarifarioBaseId);
/// <summary>CAT-001: Update rubro request body.</summary>
public sealed record UpdateRubroRequest(
string? Nombre);
/// <summary>CAT-001: Move rubro request body.</summary>
public sealed record MoveRubroRequest(
int? NuevoParentId,
int NuevoOrden);