feat(api): pagination on GET product prices (closes #47)

- GET /api/v1/products/{id}/prices now returns PagedResult<ProductPriceDto>
  with OFFSET/FETCH + COUNT via Dapper (two queries on same connection)
- Query params: ?page (default 1) and ?pageSize (default 20, max 100)
- Clamping: Math.Max(1, page) + Math.Clamp(pageSize, 1, 100) in handler
- Auth upgraded from [Authorize] to [RequirePermission("catalogo:productos:gestionar")]
- IProductPriceRepository.GetByProductIdAsync signature updated to paginated form
- AddProductPriceCommandHandler adapted to read back via page=1, pageSize=2
- TDD cycle: RED (tests updated to PagedResult shape) -> GREEN (implementation) -> REFACTOR
- Tests: 1418 total (1106 Application + 312 Api), 0 failures

closes #47
This commit is contained in:
2026-04-19 19:47:18 -03:00
parent da063ad677
commit 0dce3ee4ac
11 changed files with 425 additions and 98 deletions

View File

@@ -4,6 +4,7 @@ using NSubstitute;
using NSubstitute.ExceptionExtensions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Common;
using SIGCM2.Application.Products.Prices;
using SIGCM2.Application.Products.Prices.AddPrice;
using SIGCM2.Domain.Entities;
@@ -50,11 +51,10 @@ public class AddProductPriceCommandHandlerTests
_pricesRepo.AddAsync(1, Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
.Returns((10L, (long?)null));
_pricesRepo.GetByProductIdAsync(1, Arg.Any<CancellationToken>())
.Returns(new List<ProductPrice>
{
MakePrice(10, 1, 150m, Today)
}.AsReadOnly() as IReadOnlyList<ProductPrice>);
_pricesRepo.GetByProductIdAsync(1, Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<ProductPrice>(
new List<ProductPrice> { MakePrice(10, 1, 150m, Today) },
1, 2, 1));
_handler = new AddProductPriceCommandHandler(_pricesRepo, _productsRepo, _audit, _time);
}
@@ -110,12 +110,14 @@ public class AddProductPriceCommandHandlerTests
_pricesRepo.AddAsync(1, Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
.Returns((20L, (long?)5L)); // newId=20, closedId=5
_pricesRepo.GetByProductIdAsync(1, Arg.Any<CancellationToken>())
.Returns(new List<ProductPrice>
{
MakePrice(20, 1, 200m, Tomorrow),
MakePrice(5, 1, 150m, Today, pvt: Tomorrow.AddDays(-1))
}.AsReadOnly() as IReadOnlyList<ProductPrice>);
_pricesRepo.GetByProductIdAsync(1, Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new PagedResult<ProductPrice>(
new List<ProductPrice>
{
MakePrice(20, 1, 200m, Tomorrow),
MakePrice(5, 1, 150m, Today, pvt: Tomorrow.AddDays(-1))
},
1, 2, 2));
var result = await _handler.Handle(ValidCmd() with { Price = 200m, PriceValidFrom = Tomorrow });