feat: [TESIS-164] let the shipments listing be searched by tracking and order - #116
Conversation
Sanntinat
left a comment
There was a problem hiding this comment.
Revisión — TESIS-164 (PR #116) · Buscador del listado de envíos
Revisión de TESIS-164-shipments-search (04fd003), contra origin/master de proyecto-api.
| Check | Resultado |
|---|---|
| CI (GitHub Actions) | lint, scan_ruby, test y validate-pr-title en verde |
Rama vs master |
11 commits atrás y en conflicto (spec/requests/api/v1/api_contract_spec.rb). Ver abajo |
04fd003 aplicado sobre master (local) |
1753 ejemplos · 0 fallas · cobertura 99,42 % línea / 95,38 % rama · RuboCop 314 archivos sin ofensas · Brakeman 0 |
Bloqueante para mergear, no para aprobar: la rama arrastra tres PRs que ya entraron
La rama no sale de master: trae tres commits de otros PRs con sus IDs provisorios, que en master ya entraron con squash y otro SHA.
| En la rama | En master |
|---|---|
ea88536 [TESIS-999001] expose the stock status… |
3be2ff7 (#99) |
c1e7d25 [TESIS-999005] let a warehouse go… |
de07ba7 (#100) |
2cefa7b [TESIS-999017] refuse fractional order quantities… |
cc13721 (#105) |
Por eso el diff del PR muestra 18 archivos cuando el cambio de esta card son dos, y por eso choca con master en api_contract_spec.rb: ese spec cambió en master después de esos merges (TESIS-160 y TESIS-161).
Alcanza con quedarse sólo con el commit de la card:
git rebase --onto origin/master 2cefa7b TESIS-164-shipments-searchLo probé así: 04fd003 entra limpio sobre master y los números de la tabla de arriba son los de ese resultado.
✅ El buscador
- Las dos columnas, con
ILIKEysanitize_sql_like, el mismo patrón que órdenes y catálogo. El término viaja como parámetro (:pattern), así que no hay concatenación en el SQL. - El término vacío corta antes. Está bien visto que
NULL ILIKE '%%'esNULLy notrue: sin el corte, los envíos sin ningún código desaparecerían del listado normal. - El aislamiento entre empresas sigue en
policy_scope(Shipment), y hay un spec que lo fija con un envío gemelo de otra empresa (never reaches the shipment of another company). meta.totalcuenta las filas filtradas, y el término acota la pestaña de estado en vez de pisarla.
Los dientes, probados sobre master:
| Rotura | Falla |
|---|---|
Sacar sanitize_sql_like |
treats a literal % as text and not as a wildcard |
| Sacar el corte del término vacío | 10, entre ellos returns every shipment when the term is blank, including the one with no codes y todos los del listado sin buscar |
⚪ El motivo del left_outer en el comentario no es el que decide
El comentario de apply_search dice que un INNER «obligaría a Postgres a resolver el join también cuando no hay término». Pero con el return del término vacío, el join sólo existe cuando se busca, sea INNER o LEFT. Y como la FK es NOT NULL, los dos dan las mismas filas. left_outer está bien; lo que sobra es esa parte del motivo. El commit lo cuenta mejor: el return es lo que deja afuera al listado normal.
Los criterios de la card (parte de backend)
-
GET /api/v1/shipmentsaceptasearchcontratracking_numberyorders.external_order_id. -
ILIKE+sanitize_sql_like. - El join con
ordersesleft_outery sólo entra cuando hay término. -
meta.totalcuenta las filas que matchean. -
?search[foo]=barresponde 400 porscalar_param.
Veredicto
APPROVE, con el rebase como paso previo al merge.
El código de la card está bien y probado. Lo único que falta es sacar de la rama los tres commits que ya están en master, y eso no cambia ni una línea del cambio.
…nd order The listing had tabs and pagination but no way in. When a buyer calls, the operator holds one of two codes: the tracking number the courier issued, or the id the sales channel gave the order. Neither was reachable, so finding a shipment meant walking the pages by eye. GET /api/v1/shipments now takes `search` and runs it against both columns. ILIKE so case does not matter, and the term goes through sanitize_sql_like so a typed % is searched as text instead of returning the whole table. The join with orders is left_outer and only happens when there is a term. A shipment always has an order -- the FK is NOT NULL -- but an INNER here would make Postgres resolve the join on every unsearched listing, which is the normal case. For the same reason the blank term returns early: `NULL ILIKE '%%'` is NULL and not true, so applying the condition anyway would drop every shipment that has neither code yet from the plain listing. meta.total counts the matching rows, because the paginator and the table have to agree. A malformed `?search[foo]=bar` leaves through scalar_param with a 400, like the other filters of the endpoint. Out of scope, as in the orders search: ignoring accents and a GIN index with pg_trgm. Both need their own extension and travel together in their own card. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp
04fd003 to
4178eb3
Compare
Ticket de Jira
https://proyectofinalfrlp.atlassian.net/browse/TESIS-164
Descripción
El listado de envíos salió con pestañas por estado y paginado, pero sin buscador. Cuando llama un comprador, el operador tiene en la mano uno de dos códigos: el número de seguimiento que le dio el courier, o el id con el que el canal de venta nombra la orden. No se podía entrar por ninguno de los dos: encontrar un envío era recorrer las páginas a ojo.
GET /api/v1/shipmentsahora aceptasearchy lo cruza contra las dos columnas.ILIKEpara que la caja no importe, y el término pasa porsanitize_sql_likepara que un%tipeado se busque como texto en vez de devolver la tabla entera.Tres decisiones que quedaron escritas en el código:
ordersesleft_outery sólo entra cuando hay término. Un envío siempre tiene orden —la FK esNOT NULL—, pero unINNERacá obligaría a Postgres a resolver el join también en el listado sin buscar, que es el caso normal.NULL ILIKE '%%'no estruesinoNULL: aplicar la condición igual sacaría del listado normal a todo envío que todavía no tiene ninguno de los dos códigos (un alta manual sin despachar). Hay un spec que lo fija.meta.totalcuenta las filas que matchean, porque el paginador y la tabla tienen que decir lo mismo.Fuera de alcance, igual que en el buscador de órdenes: ignorar acentos y el índice GIN con
pg_trgm. Las dos piden su propia extensión y viajan juntas en su card.GET /api/v1/shipmentsaceptasearchcontrashipments.tracking_numberyorders.external_order_idstatusyorder_iden vez de reemplazarlos?search[foo]=barsale porscalar_paramcon 400, como el resto de los filtros del endpointEvidencia visual
N/A — es sólo API. El lado del front va en su propio PR de la misma card.
Cómo probar
bundle exec rspec spec/requests/api/v1/shipments_spec.rb(51 ejemplos).GET /api/v1/shipments?search=<parte del tracking>devuelve sólo ese envío.GET /api/v1/shipments?search=<parte del external_order_id>lo encuentra aunque todavía no esté despachado y no tenga tracking.GET /api/v1/shipments?search=%devuelve vacío, no la tabla entera.GET /api/v1/shipments?status=pending&search=<tracking de uno in_transit>devuelve vacío: el término acota la pestaña, no la pisa.GET /api/v1/shipments?search[foo]=barresponde 400 nombrando el parámetro.Impacto y consideraciones
¿Introduce breaking changes?
No.
searches opcional y sin él la respuesta es idéntica a la de hoy.¿Requiere nuevas variables de entorno?
No.
¿Afecta la arquitectura o genera un nuevo patrón?
No. Es el mismo patrón del buscador de órdenes (TESIS-52) y del catálogo:
ILIKE+sanitize_sql_like+ el total contado sobre el scope filtrado (ADR-015).Cobertura: 99,40 % línea · 95,28 % rama. RuboCop y Brakeman limpios, suite completa en 1.703 ejemplos.
🤖 Generated with Claude Code
https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp