using FluentValidation; using Microsoft.AspNetCore.Mvc; using SIGCM2.Api.Authorization; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Common; using SIGCM2.Application.PuntosDeVenta.Create; using SIGCM2.Application.PuntosDeVenta.Deactivate; using SIGCM2.Application.PuntosDeVenta.GetById; using SIGCM2.Application.PuntosDeVenta.List; using SIGCM2.Application.PuntosDeVenta.Reactivate; using SIGCM2.Application.PuntosDeVenta.Update; namespace SIGCM2.Api.Controllers; /// /// ADM-008: PuntoDeVenta management endpoints at /api/v1/admin/puntos-de-venta. /// All endpoints require permission 'administracion:puntos_de_venta:gestionar'. /// [ApiController] [Route("api/v1/admin/puntos-de-venta")] public sealed class PuntosDeVentaController : ControllerBase { private readonly IDispatcher _dispatcher; private readonly IValidator _createValidator; private readonly IValidator _updateValidator; public PuntosDeVentaController( IDispatcher dispatcher, IValidator createValidator, IValidator updateValidator) { _dispatcher = dispatcher; _createValidator = createValidator; _updateValidator = updateValidator; } /// Creates a new punto de venta. Requires administracion:puntos_de_venta:gestionar. [HttpPost] [RequirePermission("administracion:puntos_de_venta:gestionar")] [ProducesResponseType(typeof(PuntoDeVentaCreatedDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task CreatePuntoDeVenta([FromBody] CreatePuntoDeVentaRequest request) { var command = new CreatePuntoDeVentaCommand( MedioId: request.MedioId ?? 0, NumeroAFIP: request.NumeroAFIP ?? 0, Nombre: request.Nombre ?? string.Empty, Descripcion: request.Descripcion); var validation = await _createValidator.ValidateAsync(command); if (!validation.IsValid) { var errors = validation.Errors .GroupBy(e => e.PropertyName) .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); return BadRequest(new { errors }); } var result = await _dispatcher.Send(command); return CreatedAtAction(nameof(GetPuntoDeVentaById), new { id = result.Id }, result); } /// Lists puntos de venta with optional filters. [HttpGet] [RequirePermission("administracion:puntos_de_venta:gestionar")] [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task ListPuntosDeVenta( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] int? medioId = null, [FromQuery] bool? activo = null) { if (page < 1) return BadRequest(new { error = "page must be >= 1" }); if (pageSize < 1) return BadRequest(new { error = "pageSize must be >= 1" }); var query = new ListPuntosDeVentaQuery(page, pageSize, medioId, activo); var result = await _dispatcher.Send>(query); return Ok(result); } /// Gets a single punto de venta by id. [HttpGet("{id:int}")] [RequirePermission("administracion:puntos_de_venta:gestionar")] [ProducesResponseType(typeof(PuntoDeVentaDetailDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetPuntoDeVentaById([FromRoute] int id) { var query = new GetPuntoDeVentaByIdQuery(id); var result = await _dispatcher.Send(query); return Ok(result); } /// Updates a punto de venta's editable fields. [HttpPut("{id:int}")] [RequirePermission("administracion:puntos_de_venta:gestionar")] [ProducesResponseType(typeof(PuntoDeVentaUpdatedDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task UpdatePuntoDeVenta([FromRoute] int id, [FromBody] UpdatePuntoDeVentaRequest request) { var command = new UpdatePuntoDeVentaCommand( Id: id, Nombre: request.Nombre ?? string.Empty, NumeroAFIP: request.NumeroAFIP ?? 0, Descripcion: request.Descripcion); var validation = await _updateValidator.ValidateAsync(command); if (!validation.IsValid) { var errors = validation.Errors .GroupBy(e => e.PropertyName) .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); return BadRequest(new { errors }); } var result = await _dispatcher.Send(command); return Ok(result); } /// Deactivates a punto de venta. [HttpPost("{id:int}/deactivate")] [RequirePermission("administracion:puntos_de_venta:gestionar")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeactivatePuntoDeVenta([FromRoute] int id) { var command = new DeactivatePuntoDeVentaCommand(id); await _dispatcher.Send(command); return NoContent(); } /// Reactivates a punto de venta (only if parent Medio is active). [HttpPost("{id:int}/reactivate")] [RequirePermission("administracion:puntos_de_venta:gestionar")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task ReactivatePuntoDeVenta([FromRoute] int id) { var command = new ReactivatePuntoDeVentaCommand(id); await _dispatcher.Send(command); return NoContent(); } } // ── Request body records ────────────────────────────────────────────────────── /// ADM-008: Create punto de venta request body. public sealed record CreatePuntoDeVentaRequest( int? MedioId, short? NumeroAFIP, string? Nombre, string? Descripcion); /// ADM-008: Update punto de venta request body. public sealed record UpdatePuntoDeVentaRequest( string? Nombre, short? NumeroAFIP, string? Descripcion);