fix: [TESIS-124] read the shipments listing filters as single values - #85
Conversation
`shipments#index` was the last listing reading `params[...]` straight. `?page[]=1` and `?per_page[]=1` answered 500, because `Array#to_i` does not exist, and `?status[foo]=bar` did too, from putting an `ActionController::Parameters` inside a `where`. `?order_id[]=1` was the one worth finding: it did not fail. `where` translated the array into an `IN`, so the listing filtered by several orders at once and answered 200 — an undeclared capability hidden behind a successful response. The card expected a 500 there; it was quieter than that. The unknown-versus-malformed distinction stays: `?status=inventado` still answers an empty list, which is the honest answer for a filter that matches nothing, and only a malformed shape is a 400. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
Sanntinat
left a comment
There was a problem hiding this comment.
Revisión — TESIS-124 (PR #85) · Parámetros de query de shipments#index
Revisión contra origin/master de proyecto-api, sobre TESIS-124-harden-shipments-query-params (03dab3b), con la card TESIS-124 (sin comentarios de alcance) y la descripción del PR al lado.
| Check | Resultado |
|---|---|
| Suite completa (corrida local, sobre la rama) | 1257 examples, 0 failures (6 nuevos) |
| RuboCop · Brakeman (local) | 256 archivos, sin ofensas · 0 warnings |
| CI (GitHub Actions) | scan_ruby · lint · test · validate-pr-title: success |
Rama vs master |
Al día (0 commits atrás) |
| Merge con los otros PRs abiertos de la API (#86, #87, #88) | Sin conflictos de texto; los cuatro juntos: 1403 examples, 0 failures |
✅ Lo que hace, lo hace bien
- Los cuatro parámetros de
shipments#indexpasan porscalar_param, el mismo helper deorders#index, y elrescue_fromdeMalformedParameterErrorya estaba enApplicationController: no hacía falta nada más. - Lo de
?order_id[]=1es el mejor hallazgo del PR. Lo reproduje sobre master: responde 200 y filtra por las dos órdenes con unIN. Con el PR, 400. Que un parámetro mal formado no rompa pero cambie la semántica de la consulta es peor que el 500, y está bien explicado en el comentario. - Se conserva la distinción que pedía la card:
?status=inventadosigue siendo 200 con lista vacía, y hay un ejemplo que la fija. - Los defaults no cambian:
?per_page=vacío sigue cayendo en 1, como antes, y unper_pageausente sigue siendo 20.
✅ Los specs tienen dientes. Los rompí uno por uno
Volví cada parámetro a params[...], de a uno, y corrí shipments_spec.rb:
| Rotura | Resultado |
|---|---|
page |
1 failure: returns 400 for a page that is not a single value |
per_page |
1 failure: returns 400 for a per_page that is not a single value |
status |
1 failure: returns 400 for a status that is not a single value |
order_id |
2 failures: el del 400 y says which parameter is wrong |
Cada rotura hace fallar su propio ejemplo, y ninguno pasa por construcción.
🔴 shipments#index no era el último listado que leía params[...] directo
La descripción dice que «era el último listado de api/v1 que leía params[...] directo». No es así: en master quedan dos más, y los dos tienen exactamente los dos síntomas que este PR corrige en envíos.
# failed_events_controller.rb
page = [params[:page].to_i, 1].max
per_page = params.fetch(:per_page, 20).to_i.clamp(1, 100)
events = events.where(event_type: params[:event_type]) if params[:event_type].present?
# stock_transfers_controller.rb
transfers = transfers.where(status: params[:status]) if params[:status].present?
transfers = transfers.where(product_id: params[:product_id]) if params[:product_id].present?Lo verifiqué con una sonda de request specs sobre la rama del PR (no la dejé en el repo):
failed-events page[] -> NoMethodError (500)
failed-events per_page[] -> NoMethodError (500)
failed-events event_type[] -> 200 (IN silencioso, como order_id)
failed-events event_type{} -> TypeError (500)
stock-transfers status{} -> TypeError (500)
stock-transfers status[] -> 200 (IN silencioso)
stock-transfers product_id[] -> 200 (IN silencioso)
shipments order_id[] -> 400 (este PR; sobre master: 200)
El primer criterio de la card es general: «Ningún listado de api/v1 responde 500 ante un parámetro de query mal formado». Con este PR sigue habiendo dos que sí. La card nombraba sólo envíos y productos porque se escribió mirando esos dos, pero el criterio es el que manda, y el propio PR se presenta como el que cierra el último.
Qué pediría: pasar por scalar_param los parámetros de failed_events#index (page, per_page, event_type; status ya se valida contra el enum y no rompe) y de stock_transfers#index (status, product_id), con un 400 por parámetro como los de envíos. Son los mismos cinco renglones que este PR ya resolvió.
Coordinación con #87 (TESIS-108): ese PR lleva el page/per_page de failed_events al concern Paginatable, que ya usa scalar_param. Si #87 entra primero, acá quedan event_type, status y product_id. Si entra este primero, el cálculo de página de failed_events se toca dos veces. Las dos son del mismo autor, así que conviene decidir el orden antes de seguir.
🟡 El segundo criterio queda a medias, y no por culpa de este PR
«Los tres índices usan el mismo helper, sin copias del cálculo de página»: el helper es el mismo, pero el cálculo de página sigue copiado en orders, products y shipments. Eso es TESIS-108. Sin embargo, #87 tampoco lo resuelve para envíos: su diff no toca shipments_controller.rb ni orders_controller.rb (lo detallo en su revisión). Así que el conflicto que anuncian los dos PRs en shipments#index no existe: simulé los merges en todos los órdenes y no hay ninguno. Esto no bloquea este PR, pero conviene saber que, con los dos mergeados, el criterio de la card sigue sin cumplirse.
Los criterios de la card
- Ningún listado de
api/v1responde 500 ante un parámetro de query mal formado. No se cumple: faltanfailed-eventsystock-transfers(ver la sonda). - [~] Los tres índices usan el mismo helper, sin copias del cálculo de página. Se cumple lo del helper. Las copias del cálculo quedan para TESIS-108.
- Hay un spec por parámetro que falla si alguien vuelve a leer
params[...]directo. Verificado rompiendo cada uno.
Veredicto
REQUEST CHANGES.
Lo que el PR hace está bien hecho, y el hallazgo del IN silencioso vale por sí mismo. Pido cambios por un solo motivo: el criterio central de la card no se cumple, y la descripción afirma lo contrario. Extender el mismo tratamiento a failed_events#index y stock_transfers#index, con sus specs de 400, lo cierra. Antes de hacerlo, conviene acordar el orden con #87.
The review probed the listings the card did not name and found the same two symptoms this PR fixes for shipments: `?event_type[]=x` and `?status[]=x` answer 200 while filtering with an IN, and a hash-shaped filter raises TypeError as a 500. The card's criterion is general — no listing of api/v1 answers 500 to a malformed query parameter — so `failed_events#index` and `stock_transfers#index` now read their filters with `scalar_param`, with a 400 spec per parameter. The status of failed events was not crashing, since it is validated against the enum, but a list-shaped status was silently ignored and the whole listing came back as if no filter had been sent. `page` and `per_page` of failed events stay untouched here: TESIS-108 moves them into the Paginatable concern, which already uses `scalar_param`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
Respuesta — TESIS-124 (review de Santiago, 24/sep)Commit en 🔴 No era el último listado que leía
|
| PR | Qué toca |
|---|---|
| #85 (este) | Sólo los filtros |
| #87 (TESIS-108) | Sólo la página de órdenes, envíos, failed-events y transferencias |
Así no se pisan, salvo en las dos líneas de shipments#index que #87 ahora sí toca —ahí el conflicto pasó a existir de verdad, y el que entre segundo se queda con el concern—. Orden: #86 → #87 → #85.
El segundo criterio
Con #87 en su forma nueva, el cálculo de página queda efectivamente en un solo archivo, que era lo que faltaba para cerrarlo.
Verificación: rspec → 1265 ejemplos, 0 fallas; RuboCop limpio en 256 archivos; Brakeman 0 warnings.
Gracias por sondear los listados que la card no nombraba — el hallazgo es de la misma familia que el del order_id[], y era el que faltaba para que el criterio fuera cierto.
🤖 Generated with Claude Code
Sanntinat
left a comment
There was a problem hiding this comment.
Re-revisión — TESIS-124 (PR #85) · Parámetros de query de los listados
Segunda vuelta sobre TESIS-124-harden-shipments-query-params (d7c9d30), contra origin/master de proyecto-api. La primera revisión pidió cambios porque failed-events y stock-transfers seguían leyendo params[...] directo.
| Check | Resultado |
|---|---|
| CI (GitHub Actions) | lint · scan_ruby · test · validate-pr-title: success |
Rama vs master |
2 commits atrás (TESIS-82 y TESIS-129). Mergea limpio |
| 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 | Con #87: shipments_controller.rb y stock_transfers_controller.rb (ver abajo). Con #88: failed_events_spec.rb y stock_transfers_spec.rb, de texto |
🔴 → ✅ Los dos listados que faltaban
failed_events#index lee event_type y status con scalar_param, y stock_transfers#index hace lo mismo con status y product_id. Me gustó que se sumara status de failed-events: no rompía, pero ?status[]=dead se ignoraba y devolvía el listado entero. Es la versión silenciosa del mismo problema.
Tienen dientes, en los dos archivos. Tomás lo mostró para transferencias; yo lo repetí para failed-events, volviendo sus tres lecturas a params[...]:
failed_events_spec.rb -> 19 examples, 4 failures (los cuatro nuevos)
Con #87 y #85 aplicados, no queda en api/v1 ningún filtro de listado leyendo params[...] directo (busqué params[:status], [:order_id], [:product_id], [:page] y [:event_type] en los controllers).
🟡 El reparto con #87 no es tan limpio como dice la respuesta
La respuesta dice que los dos PRs «no se pisan salvo en las dos líneas de shipments#index». Lo simulé en el orden propuesto y también choca stock_transfers_controller.rb. Ese conflicto trae una trampa: #87 movió los filtros a un privado nuevo, filtered_transfers, que sigue leyendo params[:status] y params[:product_id]. Si quien resuelva «se queda con el concern», como sugiere la respuesta, pierde el endurecimiento de este PR.
Lo probé resolviendo así a propósito: 7 ejemplos en rojo, cuatro de transferencias y tres de envíos. O sea que la suite lo atrapa, y no se perdería en silencio. Pero hay que saber qué hacer. Cómo lo resolví yo, y dejó todo en verde:
shipments_controller.rb: elindexde #87 (sin las dos líneas de página) y elfiltered_shipmentsde este PR.stock_transfers_controller.rb: elindexde #87 y, adentro defiltered_transfers, las lecturas conscalar_paramde este PR.
🟡 La descripción del PR sigue diciendo lo que ya no es cierto
La respuesta dice que la descripción «afirmaba lo contrario y estaba mal», pero no se editó. Todavía abre con «shipments#index era el último listado de api/v1 que leía params[...] directo», no nombra los dos listados nuevos y el conteo de verificación es el viejo (1257). Es lo que va a leer quien llegue al PR desde la card.
Los criterios de la card
- Ningún listado de
api/v1responde 500 ante un parámetro de query mal formado. Con #87 para la página y este PR para los filtros. - Los tres índices usan el mismo helper, sin copias del cálculo de página. Con #87, que lleva el cálculo al concern.
- Hay un spec por parámetro que falla si alguien vuelve a leer
params[...]directo. Verificado en los tres archivos.
Veredicto
APPROVE.
El 🔴 está resuelto, y lo verifiqué rompiéndolo. Lo que queda son dos cosas de proceso, ninguna del código: actualizar la descripción y resolver el conflicto con #87 como está arriba, que es el caso en que el orden de merge importa de verdad.
Brings in TESIS-82, TESIS-129 and TESIS-131. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
TESIS-108 landed, so the page calculation of shipments and transfers now lives in the concern and this card keeps only its half: the filters read through `scalar_param`. In the two request specs both sides stay — master's examples cover the filters that work, and this card's cover the 400 for a filter that is not a single value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
Ticket de Jira
https://proyectofinalfrlp.atlassian.net/browse/TESIS-124
Descripción
shipments#indexera el último listado deapi/v1que leíaparams[...]directo. Ahora sus cuatro parámetros —page,per_page,statusyorder_id— pasan porscalar_param, igual que órdenes y productos.El alcance salió más chico que el de la card, y un hallazgo salió distinto.
La card preveía endurecer también
products#index, pero eso ya entró con TESIS-62: lo verifiqué contra master antes de empezar y sus cinco parámetros ya usan el helper. Queda sólo envíos, que es lo que hace este PR.Lo que sí cambió respecto de lo que la card describía. La card daba por hecho que los cuatro parámetros devolvían 500. Los reproduje uno por uno sobre master:
?order_id[]=1no fallaba, y eso es peor que el 500.whererecibía el Array y lo traducía a unIN, así que el listado filtraba por varias órdenes a la vez y contestaba 200: una capacidad que nadie declaró, que nadie documentó y que nadie prueba, escondida detrás de una respuesta exitosa. Un 500 se ve; esto no.Lo que no cambia, porque la card pide conservarlo: un valor desconocido no es un valor mal formado.
?status=inventadosigue devolviendo lista vacía —la respuesta honesta para un filtro que no matchea nada— y sólo la forma mal armada es 400. Hay un ejemplo que lo fija, para que endurecer no se lleve puesta esa distinción.Y se responde 400 y no «se ignora el filtro»: descartarlo en silencio devolvería el listado entero, que es una respuesta plausible y equivocada.
shipments#indexusascalar_paramparapageyper_pagefiltered_shipmentslo usa parastatusyorder_idspec/requests/api/v1/shipments_spec.rb: uno por parámetro, uno que verifica que el mensaje nombra cuál está mal, y el del estado desconocidoNo hizo falta tocar el
rescue_from:MalformedParameterErrorya se rescata enApplicationController.Evidencia visual
N/A — es el manejo de parámetros de un endpoint existente.
Cómo probarlo
Precondición: sesión iniciada con un usuario del tenant
norte.GET /api/v1/shipments?page[]=1→ 400, con el mensaje nombrandopage. Antes: 500.?per_page[]=1y?status[foo]=bar.GET /api/v1/shipments?order_id[]=1&order_id[]=2→ 400. Antes: 200 filtrando por las dos órdenes.GET /api/v1/shipments?status=inventado→ 200 condatavacío, que es lo que tiene que seguir pasando.?status=in_transitsiguen igual.Verificación:
bundle exec rspec(1257 ejemplos, 0 fallas),bundle exec rubocop(247 archivos, sin ofensas) ybin/brakeman -q(0 warnings).Los specs tienen dientes: volví
scalar_param(:page)aparams[:page]y falló 1 ejemplo; volvíscalar_param(:order_id)aparams[:order_id]y fallaron 2. Ninguno pasa por construcción.Impacto y consideraciones
¿Introduce breaking changes?
Para un cliente que mandaba bien los parámetros, no. Sí deja de funcionar
?order_id[]=1&order_id[]=2, que filtraba por varias órdenes — pero eso nunca fue parte del contrato y el front no lo usa (fetchOrderShipmentmanda unorder_idescalar).¿Requiere nuevas variables de entorno?
No.
¿Afecta la arquitectura o genera un nuevo patrón?
No. Es aplicar a envíos el helper que ya usaban los otros dos índices. Con esto se cumple el criterio de la card de que los tres usen el mismo, sin copias del cálculo de página.
🤖 Generated with Claude Code
https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp