Skip to content

feat: [TESIS-108] paginate every listing from one place - #87

Merged
TomasMartin2004 merged 7 commits into
masterfrom
TESIS-108-consistent-pagination
Sep 26, 2026
Merged

TomasMartin2004 merged 7 commits into
masterfrom
TESIS-108-consistent-pagination

Conversation

@TomasMartin2004

Copy link
Copy Markdown
Contributor

Ticket de Jira

https://proyectofinalfrlp.atlassian.net/browse/TESIS-108


⚠️ Apilado sobre #86 (TESIS-107)

La base de este PR no es master, es la rama de TESIS-107: esa card envuelve integrations#index, que acá además pasa a paginar. Hechas al revés habría que tocar el mismo controller dos veces, que es justo lo que la card pedía evitar. El diff que se ve acá es sólo lo de esta card.

Orden: api#86 → web#50 → éste.

📝 Descripción

El cálculo de página estaba copiado literal en cuatro controllers y ausente en otros tres, que devolvían la tabla entera. Api::V1::Paginatable pasa a ser el único lugar donde vive: cuántas filas se devuelven y cómo se arma el meta.

Los tres que no paginaban ahora paginan: warehouses, products/:id/mappings e integrations.

El default no es uno solo, y es a propósito. El front lee esos tres enteros —llenan selects: el picker de origen del alta manual, el de los modales de producto, los nodos del panel—, no tablas con paginador. Lo verifiqué antes de tocarlos: hay tres consumidores de /warehouses y ninguno pagina. Con el default de 20, a una empresa con más depósitos se le esconderían en silencio. Así que esos listados usan WHOLE_LIST_PER_PAGE (100) y las pantallas paginadas siguen con 20.

Lo que no cambia es el techo: 100 para todos. Ese es el criterio de la card —ningún listado devuelve una cantidad ilimitada de filas— y se cumple igual. Una empresa que lo pase se entera por meta.total, que va a ser mayor que las filas recibidas; es preferible a que un default de 20 se las esconda sin decir nada.

integrations también pagina. Es una tabla global y con pocas filas, y dejarla afuera era defendible —la card lo plantea así—, pero una excepción hay que volver a justificarla cada vez que alguien la lee. La regla vale más que el ahorro.

Un efecto que simplifica el ADR-015: ahora toda colección lleva meta, así que la regla del envelope pierde su cláusula «sólo si pagina». El ADR y la guía dicen la versión corta.

🛠️ Cambios

  • app/controllers/concerns/api/v1/paginatable.rb — nuevo. MAX_PER_PAGE, los dos defaults y paginate(scope, per_page:, total:), que devuelve [filas, meta].
  • products, failed_events, warehouses, product_mappings, integrations — todos pasan por el concern. products y warehouses pasan total: explícito porque sus scopes vienen agrupados y su .count devolvería un Hash.
  • spec/requests/api/v1/pagination_spec.rb — nuevo, 15 ejemplos.
  • api_contract_spec.rb, integrations_spec.rb, product_mappings_spec.rb — los ejemplos que fijaban «colección sin meta».
  • ADR-015 y docs/guidelines/architecture.md §4.0 — la regla, ahora sin excepción.

🧪 Cómo probarlo

Precondición: sesión iniciada con un usuario del tenant norte.

  1. GET /api/v1/warehouses → ahora trae meta, con hasta 100 depósitos. Antes: todos, sin meta.
  2. GET /api/v1/products/:id/mappings y GET /api/v1/integrations → ídem.
  3. ?page=0, ?page=-3, ?page=dos → primera página, sin romper.
  4. ?per_page=9999 → 100. ?per_page=0, ?per_page=-5, ?per_page=todas → 1.
  5. ?page=99 sobre 3 productos → data vacío y meta.total en 3.
  6. El listado de productos y el de órdenes siguen respondiendo igual que antes.

Verificación: bundle exec rspec (1267 ejemplos, 0 fallas), bundle exec rubocop (258 archivos, sin ofensas) y bin/brakeman -q (0 warnings).

El spec tiene dientes: le saqué la paginación a warehouses y los dos ejemplos que recorren todos los listados se pusieron en rojo, nombrando el endpoint —/api/v1/warehouses no trae el meta esperado—.

⚠️ Conflicto con #85 (TESIS-124)

Los dos tocan shipments#index. No hay conflicto de lógica: el concern se lleva el cálculo de page/per_page —usando scalar_param, que es lo que #85 pedía— y el endurecimiento de status y order_id de #85 queda intacto. Es un conflicto de texto en el mismo método, y el que mergee segundo lo resuelve quedándose con las dos cosas. Los dos PRs son míos.


Impacto y consideraciones

¿Introduce breaking changes?
Para el contrato, no: meta es aditivo y data no cambia de forma. Para el comportamiento, sí en un caso: una empresa con más de 100 depósitos, mapeos o servicios deja de recibirlos todos en una respuesta. Es exactamente lo que la card pide.

¿Requiere nuevas variables de entorno?
No.

¿Afecta la arquitectura o genera un nuevo patrón?
Sí: un concern nuevo que todo listado tiene que usar. Documentado en architecture.md §4.0.


🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

TomasMartin2004 and others added 2 commits September 23, 2026 18:09
…re list

The API answered in four shapes because every card picked its own and nobody
wrote the rule down. The frontend already carried the symptom as a comment:
"index wraps in { data }, but show and update return the object bare". A
note like that is the tell that no rule exists.

The rule is now one line, in ADR-015: a collection travels wrapped, a single
resource travels bare, an error is always `{ "error": ... }`. Only
`integrations#index` had to change — it returned an array at the root.

That one mattered beyond consistency: an array at the root has nowhere to
put `meta`, so that endpoint could never start paginating without breaking
whoever reads it. TESIS-108 needs every collection to have that room.

Wrapping single resources too was the tidier rule, and it is written down in
the ADR as the alternative with the reason it was not taken: ten call sites
across four frontier files in the frontend, two of which TESIS-58 and
TESIS-61 are editing in live branches right now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
The page calculation was copied verbatim in four controllers and missing in
three others, which answered with the whole table. `Api::V1::Paginatable`
now owns both halves: how many rows come back and how `meta` is built.

`warehouses`, `mappings` and `integrations` start paginating. Those three
are read whole by the frontend — they fill selects, not tables with a pager
— so they take a default of 100 rather than the 20 of a paged screen. The
ceiling is the same for everyone, which is the point of the card: no listing
can answer an unbounded number of rows. A company past the ceiling finds out
through `meta.total`, which is better than a default of 20 hiding warehouses
in silence.

`integrations` paginates too. It is a global table with few rows and leaving
it out was defensible, but an exception has to be justified every time
someone reads it, and the rule is worth more than the saving.

With this, every collection carries `meta`, so the envelope rule from
ADR-015 loses its "only if it paginates" clause. The ADR and the guideline
say the simpler thing now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

@Sanntinat Sanntinat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisión — TESIS-108 (PR #87) · Paginación consistente en todos los listados

Revisión de TESIS-108-consistent-pagination (4a57fd2) contra su base, TESIS-107-response-envelope (#86), con la card TESIS-108 y el comentario de alcance de Tomás en Jira (23/sep) al lado.

Check Resultado
Suite completa (corrida local, sobre la rama) 1267 examples, 0 failures (15 nuevos en pagination_spec.rb)
RuboCop · Brakeman (local, sobre el merge de los 4 PRs de la API) 266 archivos, sin ofensas · 0 warnings
CI (GitHub Actions) scan_ruby · lint · test · validate-pr-title: success
Base Apilado sobre #86: el diff es sólo esta card
Merge con #85 y #88 Sin conflictos de texto; los cuatro juntos: 1403 examples, 0 failures

✅ El concern está bien hecho

  • Api::V1::Paginatable es corto y claro: techo (MAX_PER_PAGE), los dos defaults y paginate, que devuelve [filas, meta]. Usa scalar_param, así que en failed_events el ?page[]=1 pasa de 500 a 400 de paso.
  • Los dos defaults están bien argumentados. Verifiqué que los tres consumidores de /warehouses en el front leen el listado entero para llenar selects. Con un default de 20, a una empresa con más depósitos se le esconderían sin aviso.
  • El total: explícito en products y warehouses, porque sus scopes vienen agrupados, está bien visto y comentado.
  • integrations pagina y lo dice. La card pedía que, si se dejaba afuera, quedara escrito el motivo. Se eligió no dejarlo afuera, que es más simple.

🔴 Un listado sigue devolviendo filas sin límite: GET /stock-transfers

stock_transfers#index no pasa por el concern: sigue siendo render json: { data: ... } sobre el scope entero. Lo verifiqué con una sonda de request spec:

GET /api/v1/stock-transfers?per_page=1  -> 200, filas=3, claves=["data"]

Ignora per_page y no trae meta. El primer criterio de la card es «Ningún listado de api/v1 puede devolver una cantidad ilimitada de filas», y la guía (§4.0) afirma ahora que «ningún listado devuelve una cantidad ilimitada de filas».

¿Por qué no lo detectó el spec que «recorre todos los listados»? Porque la lista está escrita a mano y stock-transfers no está en ella:

['/api/v1/products', '/api/v1/warehouses', '/api/v1/orders', '/api/v1/shipments',
 '/api/v1/failed-events', '/api/v1/integrations', "/api/v1/products/#{product.id}/mappings"]

La tabla de la card tampoco lo nombraba, así que es fácil que se haya escapado. Pero el criterio es general, y el ADR y la guía lo dan por cumplido.

🔴 El cálculo de página sigue en tres lugares, no en uno

El segundo criterio es «El cálculo de página vive en un solo archivo». El concern lo tiene, pero orders#index y shipments#index siguen con su propia copia:

# orders_controller.rb:37-38 (y shipments_controller.rb:26-27, igual)
page = [scalar_param(:page).to_i, 1].max
per_page = (scalar_param(:per_page) || 20).to_i.clamp(1, 100)

El clamp(1, 100) suelto es justo el número que la card pedía convertir en constante. pagination_spec.rb no lo detecta, porque esos dos listados devuelven un meta con la misma forma.

La descripción dice otra cosa. En la sección de conflicto con #85: «el concern se lleva el cálculo de page/per_page» de shipments#index». El diff no toca shipments_controller.rb`. Por eso no hay conflicto con #85: simulé los merges en todos los órdenes y entran limpios. Y la cuenta del comentario del concern («copiada literal en cuatro controllers») incluye a órdenes y envíos, que quedaron afuera.

Qué pediría: pasar orders#index, shipments#index y stock_transfers#index por paginate (este último con el default de 20, que es una pantalla de listado) y sumar stock-transfers a la lista del spec. Si #85 entra antes, en envíos queda sólo reemplazar las dos líneas de página.

🟡 «No hay colección sin meta» tiene excepciones que el ADR no nombra

El ADR queda con la versión corta: «No hay colección sin meta: desde TESIS-108 todas paginan». Además de stock-transfers, la sonda muestra:

GET /api/v1/orders/provinces     -> 200 claves=["data"]
GET /api/v1/products/categories  -> 200 claves=["data"]

Y las cotizaciones (POST /orders/:id/quotes) también responden { data } sin meta. Ninguna de las tres tiene sentido paginarla: son catálogos fijos o el resultado de una acción. Pero entonces la regla tiene que decirlo («los listados de registros paginan; los catálogos fijos van en data sin meta»). Si no, el consumidor que confíe en el ADR va a leer meta.total donde no existe.

🟡 Menores

  • El comentario de alcance en Jira repite lo del conflicto con #85 en shipments#index. Conviene corregirlo junto con la descripción.
  • integrations arma index_by sobre todas las integraciones de la empresa aunque pagine los servicios. No cambia nada con 100 filas de techo, pero no queda alineado con la página.

Los criterios de la card

  • Ningún listado de api/v1 puede devolver una cantidad ilimitada de filas. No se cumple: stock-transfers.
  • El cálculo de página vive en un solo archivo. No se cumple: orders y shipments conservan su copia.
  • page y per_page fuera de rango se acotan en vez de romper. Los 15 ejemplos de pagination_spec.rb cubren los bordes que pide la card.
  • El frontend sigue funcionando: meta es aditivo y data no cambia de forma.

Veredicto

REQUEST CHANGES.

El concern es el correcto y las decisiones de defaults están bien pensadas. Pero dos de los cuatro criterios no se cumplen, y la documentación (ADR, guía, descripción y el comentario del propio concern) afirma que sí. El arreglo es mecánico: tres controllers más por el concern y una ruta más en el spec. Y como este PR fija una regla para todos los listados, vale la pena que la regla sea cierta el día que entra.

TomasMartin2004 and others added 3 commits September 24, 2026 21:38
The review found that two of the four card criteria were not met while
the docs said they were.

- `stock_transfers#index` returned the whole scope: no ceiling, no
  meta. It was missing from the list the sweeping spec walks, which is
  why nothing caught it. Both are fixed.
- `orders#index` and `shipments#index` kept their own copy of the page
  calculation, including the loose `clamp(1, 100)` the card asked to
  turn into a constant. They go through the concern now.

The rule in the ADR and the guide now names what does not paginate and
why: fixed vocabularies (provinces, categories) and the result of an
action (quotes) travel in `data` without `meta`, because their length
is decided by the code and not by the company's data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
The review found the fourth shape the ADR says no longer exists:
`auth/register` answered both of its failures as `{ errors: [...] }` —
plural, array — with a comment claiming it did so to spare the frontend
two formats. It is a single `error` string now, and the contract spec
covers both paths, which is why nothing caught it before.

The ADR also said three things that were not true: that
`api_contract_spec.rb` breaks on any new shape (it only covers the
endpoints it lists), that integrations was the only endpoint to change,
and that two files were being edited in live branches — TESIS-58 and
TESIS-61 merged today. The rule now also states that `error` may carry
extra data, as the 409 of optimistic locking does with
`current_version`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
The base picked up the registration fix and the ADR corrections. One
conflict, in the ADR: the rule about `error` comes from TESIS-107 and
the one about what carries `meta` from this card, so both stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
@TomasMartin2004

Copy link
Copy Markdown
Contributor Author

Respuesta — TESIS-108 (review de Santiago, 24/sep)

Commits en TESIS-108-consistent-pagination: df0ce50 (los arreglos) y 8594b2a (merge de la base, #86, que también se movió).

Los dos 🔴 eran ciertos y eran los criterios centrales de la card. Resueltos.

🔴 stock-transfers devolvía la tabla entera

Confirmado con tu sonda. Ahora pasa por paginate con el default de 20 —es una pantalla de listado, no un select— y el filtrado por estado y producto se fue a un privado (filtered_transfers), así que el index queda en dos líneas.

Y lo que importa: la ruta entró en la lista del spec. Esa lista escrita a mano era la razón por la que nada lo detectó. Lo verifiqué al revés: le saqué la paginación a transferencias y los dos ejemplos que recorren los listados se ponen en rojo nombrando la ruta —/api/v1/stock-transfers no trae el meta esperado—.

🔴 El cálculo de página seguía en tres lugares

También cierto, y la cuenta del comentario del concern estaba mal (decía cuatro controllers, contando órdenes y envíos, que no había tocado). orders#index y shipments#index pasan por el concern: se van las dos copias de [scalar_param(:page).to_i, 1].max y el clamp(1, 100) suelto, que era justo el número que la card pedía volver constante.

Quedó de yapa: shipments#index leía params[:page] directo, así que ahora un ?page[]=1 ahí contesta 400 en vez de 500.

⚠️ El conflicto con #85: antes no existía, ahora sí

Tenías razón en que el conflicto que anunciaba no existía —mi diff no tocaba shipments_controller.rb—. Con este cambio sí lo toca, así que el conflicto pasa a ser real. Lo dividí para que sea mecánico:

PR Qué toca
#85 (TESIS-124) Sólo los filtros: event_type de failed-events, status y product_id de transferencias
#87 (este) Sólo la página: page/per_page de órdenes, envíos, failed-events y transferencias

No se pisan salvo en las dos líneas de shipments#index, donde el que entre segundo se queda con el concern. Orden sugerido: #86 → #87 → #85.

🟡 Las excepciones de «no hay colección sin meta»

Cierto, y la sonda las nombra bien. El ADR y la guía ahora dicen qué no pagina y por qué:

La regla corta es si el largo lo decide la empresa, pagina; si lo decide el código, no.

Con la tabla de las tres: provincias, categorías y cotizaciones.

🟡 Los menores

  • El comentario de alcance en Jira: corregido con el reparto de arriba.
  • integrations armando el index_by sobre todas las integraciones: lo dejo. Con el techo de 100 no cambia nada medible y tocarlo pide reordenar el PORO; queda anotado para TESIS-89.

Verificación: CI=true rspec → 1269 ejemplos, 0 fallas; RuboCop limpio en 258 archivos.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

@Sanntinat Sanntinat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-revisión — TESIS-108 (PR #87) · Paginación de todos los listados

Segunda vuelta sobre TESIS-108-consistent-pagination (8594b2a), contra origin/master de proyecto-api. La primera revisión pidió cambios porque stock-transfers devolvía la tabla entera y porque el cálculo de página seguía copiado en órdenes y envíos.

Check Resultado
CI (GitHub Actions) lint · scan_ruby · test · validate-pr-title: success
Rama vs master 2 commits atrás. Conflicto en integrations_controller.rb con TESIS-82 (ver abajo)
Merge en el orden propuesto (master → #86 → #87 → #85), resuelto a mano 1372 examples, 0 failures · RuboCop 271 archivos sin ofensas
Conflictos con otros PRs #86: ADR-015. #85: shipments_controller.rb y stock_transfers_controller.rb. #90: integrations_controller.rb (el de master, que #90 ya trae)

🔴 → ✅ stock-transfers pagina, y el spec lo sabe

stock_transfers#index pasa por paginate con el default de 20, y la ruta entró a la lista de pagination_spec.rb. Lo verifiqué al revés: le saqué la paginación y caen los dos ejemplos que recorren los listados.

pagination_spec.rb -> 15 examples, 2 failures
  every listing of the API answers with a meta that carries page, per_page and total
  every listing of the API respects the ceiling everywhere

Sobre el default de 20: me fijé si algún consumidor iba a quedar recortado. El front todavía no llama a /stock-transfers, así que hoy no afecta a nadie. Cuando alguien lo consuma para el «+N Incoming» de la ficha, va a tener que pedir con product_id y leer meta.total. Vale una línea en la card que lo consuma.

🔴 → ✅ El cálculo de página vive en un solo archivo

Órdenes y envíos pasan por el concern. Busqué en app/ cualquier clamp(1, .offset( o to_i, 1].max fuera de paginatable.rb, y no queda ninguno. También recorrí las rutas GET de colección con el master de hoy (que trae TESIS-82 y TESIS-129): todos los index de api/v1 incluyen Paginatable, y los que no paginan son los tres que el ADR nombra, además de /me y /tenant-config, que son recursos.

🟡 → ✅ Las excepciones de «no hay colección sin meta»

La tabla del ADR-015 con la regla corta («si el largo lo decide la empresa, pagina; si lo decide el código, no») es mejor de lo que yo había pedido. Cuando entre POST /quotes (#90, TESIS-131), va a ser una cuarta fila de la misma familia que las cotizaciones de una orden. Eso me toca a mí sumarlo en mi PR.

⚠️ Antes de mergear: tres conflictos, todos mecánicos

  1. Con master, en integrations_controller.rb. TESIS-82 cambió el skip_after_action (ahora sólo verify_policy_scoped, con un comentario de por qué el listado no pasa por Pundit) y agregó authorize en el update. Se resuelve con el skip y el comentario de master más el include Paginatable y el index de este PR.
  2. Con #86, en el ADR-015. Esta rama mergeó #86 antes de su último commit (479afb6). Volviendo a mergear TESIS-107-response-envelope se va.
  3. Con #85. No es sólo shipments#index: también choca stock_transfers_controller.rb, y ahí filtered_transfers (de este PR) todavía lee params[:status] y params[:product_id]. El que entre segundo tiene que llevar los scalar_param de #85 adentro de filtered_transfers. Si no, caen 7 ejemplos de #85. Lo detallo en su re-revisión.

Resolviendo así los tres, la combinación de los tres PRs sobre master queda en verde.

🟡 Menor, que queda como estaba

El index_by de integrations sobre todas las integraciones de la empresa: de acuerdo con dejarlo para TESIS-89.

Los criterios de la card

  • Ningún listado de api/v1 puede devolver una cantidad ilimitada de filas. Recorrido sobre las rutas actuales.
  • El cálculo de página vive en un solo archivo. Verificado con búsqueda.
  • Todas las colecciones de registros traen meta, y las que no lo traen están nombradas en el ADR.
  • Un spec recorre los listados y falla si uno deja de paginar. Verificado rompiendo transferencias.

Veredicto

APPROVE.

Los dos 🔴 están resueltos y verificados, y el concern quedó como la única definición de la paginación. Antes del merge hay que actualizar la rama contra master y contra #86, y coordinar con #85 lo de filtered_transfers.

@TomasMartin2004
TomasMartin2004 changed the base branch from TESIS-107-response-envelope to master September 26, 2026 01:36
TomasMartin2004 and others added 2 commits September 25, 2026 22:50
TESIS-107 landed on master, so the envelope half of the conflicts is
settled there and this card keeps its own half: every listing of
records carries `meta`, and the rule names what does not paginate.

`integrations#index` takes both sides: the Pundit comment that came
with TESIS-82 and the pagination of this card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
Brings in TESIS-93, whose `order(:id)` settles the intermittent spec
that made the previous push fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
@TomasMartin2004
TomasMartin2004 merged commit 5de846a into master Sep 26, 2026
4 checks passed
@TomasMartin2004
TomasMartin2004 deleted the TESIS-108-consistent-pagination branch September 26, 2026 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants