170 lines
7.4 KiB
C#
170 lines
7.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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'.
|
|
/// </summary>
|
|
[ApiController]
|
|
public sealed class ProductsController : ControllerBase
|
|
{
|
|
private readonly IDispatcher _dispatcher;
|
|
private readonly IValidator<CreateProductCommand> _createValidator;
|
|
private readonly IValidator<UpdateProductCommand> _updateValidator;
|
|
|
|
public ProductsController(
|
|
IDispatcher dispatcher,
|
|
IValidator<CreateProductCommand> createValidator,
|
|
IValidator<UpdateProductCommand> updateValidator)
|
|
{
|
|
_dispatcher = dispatcher;
|
|
_createValidator = createValidator;
|
|
_updateValidator = updateValidator;
|
|
}
|
|
|
|
// ── READ endpoints ─────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Returns a paginated list of Products. Requires authentication.</summary>
|
|
[HttpGet("api/v1/products")]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(PagedResult<ProductListItemDto>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
public async Task<IActionResult> 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<ListProductsQuery, PagedResult<ProductListItemDto>>(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>Returns a single Product by id. Requires authentication.</summary>
|
|
[HttpGet("api/v1/products/{id:int}")]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(ProductDetailDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> GetProductById([FromRoute] int id)
|
|
{
|
|
var query = new GetProductByIdQuery(id);
|
|
var result = await _dispatcher.Send<GetProductByIdQuery, ProductDetailDto>(query);
|
|
return Ok(result);
|
|
}
|
|
|
|
// ── WRITE endpoints ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Creates a new Product. Requires catalogo:productos:gestionar.</summary>
|
|
[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<IActionResult> 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<CreateProductCommand, ProductCreatedDto>(command);
|
|
return CreatedAtAction(nameof(GetProductById), new { id = result.Id }, result);
|
|
}
|
|
|
|
/// <summary>Updates a Product. Requires catalogo:productos:gestionar.</summary>
|
|
[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<IActionResult> 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<UpdateProductCommand, ProductUpdatedDto>(command);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>Soft-deletes (deactivates) a Product. Requires catalogo:productos:gestionar.</summary>
|
|
[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<IActionResult> DeactivateProduct([FromRoute] int id)
|
|
{
|
|
var command = new DeactivateProductCommand(id);
|
|
await _dispatcher.Send<DeactivateProductCommand, ProductStatusDto>(command);
|
|
return NoContent();
|
|
}
|
|
}
|
|
|
|
// ── Request body records ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>PRD-002: Create Product request body.</summary>
|
|
public sealed record CreateProductRequest(
|
|
string? Nombre,
|
|
int MedioId = 0,
|
|
int ProductTypeId = 0,
|
|
int? RubroId = null,
|
|
decimal BasePrice = 0m,
|
|
int? PriceDurationDays = null);
|
|
|
|
/// <summary>PRD-002: Update Product request body.</summary>
|
|
public sealed record UpdateProductRequest(
|
|
string? Nombre,
|
|
int? RubroId = null,
|
|
decimal BasePrice = 0m,
|
|
int? PriceDurationDays = null);
|