Repository navigation
feat: [TESIS-142] show the shipping label in the order detail - #78
Conversation
RF-23 commits "obtener el número de tracking y el link a la etiqueta de envío imprimible, almacenándolos en el registro del envío", and the traceability matrix of E4a §3 assigns it the screen "Detalle de Envío — Etiqueta". The backend holds up its end: ConfirmDispatch stores shipping_label_url since TESIS-47 — the Andreani template maps 'etiqueta.url' to it — and ShipmentSerializer exposes it. The front dropped it: ApiShipment did not even declare the field, so it arrived in the JSON and was discarded on translation. The label — the barcode slip that gets printed and stuck on the package — was visible nowhere. The shipping panel of the order detail now carries it, below the tracking number, with three states and no blanks: before dispatch it says the carrier issues it on dispatch, dispatched without one it says the carrier did not return it rather than leaving the field empty, and with one, the link. printableLabel narrows the URL to http(s) before it reaches an href. That URL is not written by the OMS: the carrier returns it and the template maps it verbatim, so a javascript: value there would run in the operator's browser on click. It lives in utils/shipment.ts as a pure function with its own tests, and a value that cannot be opened renders as no label at all, which is the truth from where the operator stands. The link opens in another tab with rel="noopener noreferrer": the destination is the carrier's site, not a route of the app. 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-142 (PR #78) · Mostrar la etiqueta de envío en el detalle de la orden
Revisión de TESIS-142-print-the-shipping-label (a1c006b), contra origin/master de proyecto-web. La hice junto con #76 y #77, y los tres los probé también mergeados entre sí.
| Check | Resultado |
|---|---|
| CI (GitHub Actions) | Los cuatro jobs en verde |
Rama vs master |
Al día. Mergea limpio |
| Local, la rama sola | lint, prettier y tsc -b limpios · 75 archivos · 693 tests · 0 fallas |
| Local, #76 + #77 + #78 mergeados | lint, prettier, tsc -b y build limpios · 75 archivos · 705 tests · 0 fallas. labelUrl es obligatorio en Shipment y #77 suma fixtures de envío: lo probé junto por eso, y compila |
| Navegador | Los tres estados y la URL hostil, contra la API de master. Detalle abajo |
| Conflictos con otros PRs | #77: sólo architecture.md, porque los dos suman una fila a la misma tabla y prettier realinea las columnas. Se resuelve quedándose con las dos filas |
✅ Los tres estados, y la defensa, verificados en el navegador
- Con etiqueta (#1, y una orden que despaché en la prueba de #77): «Imprimir etiqueta», con
hrefa la URL del courier,target="_blank"yrel="noopener noreferrer". - Sin despachar (un envío recién creado): «Se emite al despachar.», sin enlace.
- Despachado sin etiqueta (en la base de verificación le saqué la URL al envío de la #4, que tiene seguimiento): «El operador no devolvió la etiqueta.»
- URL hostil. Al envío de la #5 le guardé
javascript:alert(document.domain). La API la devuelve tal cual enGET /shipments/:id, y en la pantalla no queda ningún<a>conhrefjavascript:: el campo dice «El operador no devolvió la etiqueta.», que es lo que el PR propone para un valor que no se puede abrir.
✅ Las decisiones
- La lista blanca de esquemas, del lado del cliente, con el dato crudo intacto en el backend para auditar, es el reparto correcto.
new URL(...)normaliza el esquema a minúsculas y recorta los espacios, así queJAVASCRIPT:o unjavascript:con espacios adelante también quedan afuera. - Cambiar el texto pendiente a «Se emite al despachar.» para que no repita el «Pendiente de despacho» del seguimiento: dos textos iguales apilados se leen como un error de render.
✅ Los specs de la función y del componente tienen dientes
| Rotura | Falla |
|---|---|
printableLabel deja pasar cualquier esquema |
refuses a link that is not a web address, refuses something that is not an absolute url |
Sacar rel="noopener noreferrer" |
opens it in another tab without handing over this window |
| Invertir los textos de pendiente y sin etiqueta | los 3 de esos estados |
🟡 Que la frontera use printableLabel no lo prueba ningún test
En api.ts cambié labelUrl: printableLabel(shipment.shipping_label_url) por labelUrl: shipment.shipping_label_url y los 305 tests de features/orders siguen en verde. La función está probada, pero no que la traducción la use, y esa llamada es la defensa que vende el PR. El fixture de api.test.ts trae una URL https, que pasa igual con o sin el filtro.
Alcanza con un caso en api.test.ts con shipping_label_url: 'javascript:alert(1)' que espere labelUrl: null.
⚪ Menores
- «Cómo probar», paso 2. Dice que la orden manual despachada (#6) muestra «Imprimir etiqueta». Con los seeds actuales la #6 está
ready_to_shippero sin seguimiento ni etiqueta, así que muestra «Se emite al despachar.» La #1, la #4 y la #5 sí traen etiqueta. Seguramente en tu base la #6 está despachada de verdad. - Orden cancelada sin envío. El campo dice «Se emite al despachar.» en una orden que no se va a despachar. Es el mismo criterio que ya tenía el seguimiento («Pendiente de despacho»), así que no lo pido acá. Si algún día se distingue, que sea para los dos campos.
- La cuenta de tests del cuerpo. Dice «693 tests (+15)».
mastertiene 683, así que son +10. Los 693 están bien.
Los criterios de la card
- Un envío despachado muestra «Imprimir etiqueta» apuntando a la URL que devolvió el courier.
- Un envío sin despachar no muestra un enlace, y dice por qué.
- Un envío despachado sin etiqueta lo dice.
- Una URL que no sea http(s) no se pinta como enlace (verificado también con la API real devolviendo
javascript:). -
lint,testybuilden verde.
Veredicto
APPROVE.
Hace lo que pide RF-23 y la defensa funciona de punta a punta. El test de la frontera conviene sumarlo, porque es lo único que hoy deja romper esa defensa sin que nada se ponga en rojo, pero no bloquea el merge.
Descripción
RF-23 compromete «obtener el número de tracking y el link a la etiqueta de envío imprimible, almacenándolos en el registro del envío», y la matriz de trazabilidad de E4a §3 le asigna la pantalla «Detalle de Envío — Etiqueta».
El backend cumple su parte:
Shipments::ConfirmDispatchguardashipments.shipping_label_urldesde TESIS-47 —la plantilla de Andreani mapea'etiqueta.url' => 'shipping_label_url'— yShipmentSerializerlo expone.El front lo tiraba. La interfaz
ApiShipmentni declaraba el campo:Llegaba en el JSON y se descartaba al traducir al dominio. La etiqueta —el rótulo con el código de barras que se imprime y se pega al paquete— no se veía en ninguna pantalla.
Qué hace
Un campo «Etiqueta de envío» en el panel de datos del envío del detalle (S08), debajo del número de seguimiento, con tres estados y ningún hueco:
Decir los dos primeros en vez de dejar el campo vacío sigue el criterio que ya usaba
TrackingNumberField: un hueco se lee como un dato perdido.Decisiones que conviene mirar
El enlace se acota a http(s) antes de llegar a un
href. Esa URL no la escribe el OMS: la devuelve el courier y la plantilla la mapea tal cual. Unjavascript:en unhrefcorre al hacer clic, en el navegador del operador, y hoy nada lo frena entre la respuesta del proveedor y la pantalla.printableLabello resuelve con una lista blanca de esquemas, va como función pura enutils/shipment.tsy tiene sus tests. Un valor que no se puede abrir se muestra como si no hubiera etiqueta, que es la verdad desde donde está parado el operador.Es una línea de defensa del lado del cliente, no del backend: Rails guarda lo que el courier manda y eso está bien, porque el dato crudo es el que sirve para auditar. Lo que no corresponde es ponerlo en un
hrefsin mirarlo.Abre en otra pestaña con
rel="noopener noreferrer": el destino es el sitio del courier, no una ruta de la app, y no hay por qué entregarle la referencia a esta ventana.printableLabelaceptaundefined. El fixture deapi.test.tsno traía el campo y reventaba conCannot read properties of undefined. La API siempre lo manda —el serializer lo declara— pero una respuesta vieja o un fixture incompleto no tienen por qué tirar la traducción abajo. Aproveché y sumé el campo al fixture, así la traducción queda ejercitada de verdad.Cambié el texto del estado pendiente a «Se emite al despachar.» La primera versión decía «Pendiente de despacho», igual que el campo de tracking justo arriba: dos strings idénticos apilados se leen como un error de render, y además rompía tres tests por
getByTextambiguo.Cómo probar
npm run dev, entrar comoadmin@norte.com.Verificación
npm run lint✅npm run test✅ — 75 archivos, 693 tests (+10 sobremaster, que tiene 683)npm run build✅ (tsc -bincluido)npm run format:check✅Tests nuevos:
ShippingLabelField(5: el enlace,target/rel, pendiente, sin envío y despachado sin etiqueta) yprintableLabel(5: https, http, vacío/null,javascript:/data:, y relativa o texto suelto). SumélabelUrla los fixtures deShipmentde los otros cinco archivos de test.Fuera de alcance
GET /shipmentsse hizo en TESIS-113 para el KPI del panel, que lo consume confetchCount.🤖 Generated with Claude Code
https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp