feat: paginación en GET /api/v1/products/{id}/prices (closes #47) #51

Merged
dmolinari merged 3 commits from feature/prd-003-prices-pagination into main 2026-04-19 23:08:32 +00:00
Owner

Summary

Implementa paginación OFFSET/FETCH en el endpoint GET /api/v1/products/{id}/prices. Breaking change limpio: contrato cambia de ProductPriceDto[]PagedResult<ProductPriceDto> con soporte de page y pageSize. Defaults server-side: page=1, pageSize=20 con clamping defensivo [1..100] en pageSize. Alineado con el único patrón paginado existente del repo (ListProducts).

Cambios por Capa

Backend (3 commits)

  • Query/Handler: GetProductPricesQuery agrega Page y PageSize; handler devuelve PagedResult<ProductPriceDto> con clamping defensivo
  • Repository: GetByProductIdAsync paginada con OFFSET/FETCH + COUNT (dos queries separadas, patrón ProductRepository)
  • Controller: query params int? page, int? pageSize; defaults 1/20; nueva permission [RequirePermission("catalogo:productos:gestionar")] (alinea con POST)
  • Tests: +6 tests backend (boundary cases: pageSize=100 exacto, pageSize=101→100, pageSize=1000→100, page=-5→1, pageSize=0→1; +1 no-overlap 30 items/2 páginas)

Frontend (1 commit)

  • API Hook: getProductPrices devuelve PagedResult<ProductPrice> con page/pageSize params; useProductPrices consumidor con queryKey granular ['product-prices', productId, page, pageSize]
  • Component: ProductPriceHistory renderiza prices.items + controles Anterior/Siguiente; estado local para currentPage; placeholderData: keepPreviousData para UX smooth
  • Invalidation: useAddProductPrice actualiza invalidateQueries key a ['product-prices', productId] (partial match invalida todas las páginas)
  • Tests: +20 tests frontend (§P.9 p1: Next enabled/Prev disabled; §P.10 click Next→refetch; §P.11 Prev always disabled; §P.12 Next disabled last page)

Hardening (1 commit)

  • Boundary tests exhaustivos (pageSize edge cases)
  • No-overlap test: 30 prices → 2 páginas disjuntas + unión=20 + DESC order validado
  • Grep check: 0 referencias a ProductPriceDto[] en código productivo
  • Cleanup: 0 TODOs introducidos

Test Results

Backend: 1424 tests (era 1418 pre-prd-003)

  • Api.Tests: 317 (+5 boundary)
  • Application.Tests: 1107 (+1 no-overlap)

Frontend: 472 tests (era 452 pre-prd-003)

  • ProductPriceHistory + hooks: +20 tests cubriendo §P.9–§P.12

Spec Compliance: 12/12 scenarios + 7 boundary tests — todos PASS

Pattern Refs

  • Backend: reutiliza PagedResult<T> de Application.Common + patrón OFFSET/FETCH + COUNT de ProductRepository.GetPagedAsync
  • Frontend: reutiliza PagedResult<T> de features/products/types.ts + keepPreviousData de useProducts hook

Closes #47

## Summary Implementa paginación OFFSET/FETCH en el endpoint GET `/api/v1/products/{id}/prices`. Breaking change limpio: contrato cambia de `ProductPriceDto[]` → `PagedResult<ProductPriceDto>` con soporte de `page` y `pageSize`. Defaults server-side: `page=1, pageSize=20` con clamping defensivo `[1..100]` en pageSize. Alineado con el único patrón paginado existente del repo (`ListProducts`). ## Cambios por Capa ### Backend (3 commits) - **Query/Handler**: `GetProductPricesQuery` agrega `Page` y `PageSize`; handler devuelve `PagedResult<ProductPriceDto>` con clamping defensivo - **Repository**: `GetByProductIdAsync` paginada con OFFSET/FETCH + COUNT (dos queries separadas, patrón ProductRepository) - **Controller**: query params `int? page, int? pageSize`; defaults 1/20; nueva permission `[RequirePermission("catalogo:productos:gestionar")]` (alinea con POST) - **Tests**: +6 tests backend (boundary cases: pageSize=100 exacto, pageSize=101→100, pageSize=1000→100, page=-5→1, pageSize=0→1; +1 no-overlap 30 items/2 páginas) ### Frontend (1 commit) - **API Hook**: `getProductPrices` devuelve `PagedResult<ProductPrice>` con `page/pageSize` params; `useProductPrices` consumidor con queryKey granular `['product-prices', productId, page, pageSize]` - **Component**: `ProductPriceHistory` renderiza `prices.items` + controles Anterior/Siguiente; estado local para `currentPage`; `placeholderData: keepPreviousData` para UX smooth - **Invalidation**: `useAddProductPrice` actualiza invalidateQueries key a `['product-prices', productId]` (partial match invalida todas las páginas) - **Tests**: +20 tests frontend (§P.9 p1: Next enabled/Prev disabled; §P.10 click Next→refetch; §P.11 Prev always disabled; §P.12 Next disabled last page) ### Hardening (1 commit) - Boundary tests exhaustivos (pageSize edge cases) - No-overlap test: 30 prices → 2 páginas disjuntas + unión=20 + DESC order validado - Grep check: 0 referencias a `ProductPriceDto[]` en código productivo - Cleanup: 0 TODOs introducidos ## Test Results **Backend**: 1424 tests (era 1418 pre-prd-003) - Api.Tests: 317 (+5 boundary) - Application.Tests: 1107 (+1 no-overlap) **Frontend**: 472 tests (era 452 pre-prd-003) - ProductPriceHistory + hooks: +20 tests cubriendo §P.9–§P.12 **Spec Compliance**: 12/12 scenarios + 7 boundary tests — todos PASS ## Pattern Refs - Backend: reutiliza `PagedResult<T>` de `Application.Common` + patrón OFFSET/FETCH + COUNT de `ProductRepository.GetPagedAsync` - Frontend: reutiliza `PagedResult<T>` de `features/products/types.ts` + `keepPreviousData` de `useProducts` hook --- Closes #47
dmolinari added 3 commits 2026-04-19 23:08:28 +00:00
- 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
dmolinari merged commit dd4d4a1673 into main 2026-04-19 23:08:32 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#51