Skip to content

feat: [TESIS-142] show the shipping label in the order detail - #78

Merged
TomasMartin2004 merged 3 commits into
masterfrom
TESIS-142-print-the-shipping-label
Oct 5, 2026
Merged

TomasMartin2004 merged 3 commits into
masterfrom
TESIS-142-print-the-shipping-label

Conversation

@TomasMartin2004

@TomasMartin2004 TomasMartin2004 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

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::ConfirmDispatch guarda shipments.shipping_label_url desde TESIS-47 —la plantilla de Andreani mapea 'etiqueta.url' => 'shipping_label_url'— y ShipmentSerializer lo expone.

El front lo tiraba. La interfaz ApiShipment ni declaraba el campo:

interface ApiShipment {
  id, order_id, status, tracking_number, shipping_cost, courier, events
  // shipping_label_url ← no estaba
}

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:

Estado Qué muestra
Sin despachar «Se emite al despachar.» — la emite el courier (TESIS-47)
Despachado, sin etiqueta «El operador no devolvió la etiqueta.»
Con etiqueta Botón «Imprimir etiqueta»

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. Un javascript: en un href corre al hacer clic, en el navegador del operador, y hoy nada lo frena entre la respuesta del proveedor y la pantalla. printableLabel lo resuelve con una lista blanca de esquemas, va como función pura en utils/shipment.ts y 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 href sin 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.

printableLabel acepta undefined. El fixture de api.test.ts no traía el campo y reventaba con Cannot 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 getByText ambiguo.

Cómo probar

  1. npm run dev, entrar como admin@norte.com.
  2. Órdenes → la orden manual despachada (feat: A #6): el panel de Envío muestra «Imprimir etiqueta» con el enlace del courier.
  3. Una orden con envío sin despachar: el campo dice «Se emite al despachar.» y no hay enlace.

Verificación

  • npm run lint ✅
  • npm run test ✅ — 75 archivos, 693 tests (+10 sobre master, que tiene 683)
  • npm run build ✅ (tsc -b incluido)
  • npm run format:check ✅

Tests nuevos: ShippingLabelField (5: el enlace, target/rel, pendiente, sin envío y despachado sin etiqueta) y printableLabel (5: https, http, vacío/null, javascript:/data:, y relativa o texto suelto). Sumé labelUrl a los fixtures de Shipment de los otros cinco archivos de test.

Fuera de alcance

  • Previsualizar la etiqueta dentro de la app.
  • Generar una etiqueta propia cuando el courier no la devuelve.
  • Listado de envíos. Lo revisé contra el alcance: ningún RF lo pide —RF-21 a RF-25 son modelo, cotización, despacho, webhook y cron— y las únicas pantallas que la matriz nombra para la épica son «Enrutamiento Logístico» (RF-22) y «Detalle de Envío — Etiqueta» (RF-23), las dos por envío. E4b Módulo G compromete «consultar el historial completo de cada envío», que resuelve la tarjeta de ciclo de vida del detalle. GET /shipments se hizo en TESIS-113 para el KPI del panel, que lo consume con fetchCount.

🤖 Generated with Claude Code

https://claude.ai/code/session_012xAtddb53LRNVLuyTXyELp

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 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-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 href a la URL del courier, target="_blank" y rel="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 en GET /shipments/:id, y en la pantalla no queda ningún <a> con href javascript:: 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í que JAVASCRIPT: o un javascript: 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_ship pero 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)». master tiene 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, test y build en 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.

@TomasMartin2004
TomasMartin2004 merged commit 23dcc67 into master Oct 5, 2026
4 checks passed
@TomasMartin2004
TomasMartin2004 deleted the TESIS-142-print-the-shipping-label branch October 5, 2026 14:50
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