using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SIGCM2.Api.Authorization; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Common; using SIGCM2.Application.Products.Create; using SIGCM2.Application.Products.Deactivate; using SIGCM2.Application.Products.GetById; using SIGCM2.Application.Products.List; using SIGCM2.Application.Products.Update; namespace SIGCM2.Api.Controllers; /// /// PRD-002: Product catalog management. /// Read endpoints at /api/v1/products — require authentication (any role). /// Write endpoints at /api/v1/admin/products — require 'catalogo:productos:gestionar'. /// [ApiController] public sealed class ProductsController : ControllerBase { private readonly IDispatcher _dispatcher; private readonly IValidator _createValidator; private readonly IValidator _updateValidator; public ProductsController( IDispatcher dispatcher, IValidator createValidator, IValidator updateValidator) { _dispatcher = dispatcher; _createValidator = createValidator; _updateValidator = updateValidator; } // ── READ endpoints ───────────────────────────────────────────────────────── /// Returns a paginated list of Products. Requires authentication. [HttpGet("api/v1/products")] [Authorize] [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task ListProducts( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] bool? activo = true, [FromQuery] string? search = null, [FromQuery] int? medioId = null, [FromQuery] int? productTypeId = null, [FromQuery] int? rubroId = null) { var query = new ListProductsQuery(page, pageSize, activo, search, medioId, productTypeId, rubroId); var result = await _dispatcher.Send>(query); return Ok(result); } /// Returns a single Product by id. Requires authentication. [HttpGet("api/v1/products/{id:int}")] [Authorize] [ProducesResponseType(typeof(ProductDetailDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetProductById([FromRoute] int id) { var query = new GetProductByIdQuery(id); var result = await _dispatcher.Send(query); return Ok(result); } // ── WRITE endpoints ──────────────────────────────────────────────────────── /// Creates a new Product. Requires catalogo:productos:gestionar. [HttpPost("api/v1/admin/products")] [RequirePermission("catalogo:productos:gestionar")] [ProducesResponseType(typeof(ProductCreatedDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task CreateProduct([FromBody] CreateProductRequest request) { var command = new CreateProductCommand( Nombre: request.Nombre ?? string.Empty, MedioId: request.MedioId, ProductTypeId: request.ProductTypeId, RubroId: request.RubroId, BasePrice: request.BasePrice, PriceDurationDays: request.PriceDurationDays); 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(GetProductById), new { id = result.Id }, result); } /// Updates a Product. Requires catalogo:productos:gestionar. [HttpPut("api/v1/admin/products/{id:int}")] [RequirePermission("catalogo:productos:gestionar")] [ProducesResponseType(typeof(ProductUpdatedDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task UpdateProduct([FromRoute] int id, [FromBody] UpdateProductRequest request) { var command = new UpdateProductCommand( Id: id, Nombre: request.Nombre ?? string.Empty, RubroId: request.RubroId, BasePrice: request.BasePrice, PriceDurationDays: request.PriceDurationDays); 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); } /// Soft-deletes (deactivates) a Product. Requires catalogo:productos:gestionar. [HttpDelete("api/v1/admin/products/{id:int}")] [RequirePermission("catalogo:productos:gestionar")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeactivateProduct([FromRoute] int id) { var command = new DeactivateProductCommand(id); await _dispatcher.Send(command); return NoContent(); } } // ── Request body records ────────────────────────────────────────────────────── /// PRD-002: Create Product request body. public sealed record CreateProductRequest( string? Nombre, int MedioId = 0, int ProductTypeId = 0, int? RubroId = null, decimal BasePrice = 0m, int? PriceDurationDays = null); /// PRD-002: Update Product request body. public sealed record UpdateProductRequest( string? Nombre, int? RubroId = null, decimal BasePrice = 0m, int? PriceDurationDays = null);