feat(api): RubrosController + integration tests e2e + audit verification (CAT-001)
This commit is contained in:
151
src/api/SIGCM2.Api/Controllers/RubrosController.cs
Normal file
151
src/api/SIGCM2.Api/Controllers/RubrosController.cs
Normal 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);
|
||||
Reference in New Issue
Block a user