Skip to main content

Notas técnicas y consideraciones

Hallazgos observados al revisar el código. No son incidencias abiertas: son puntos a tener presentes antes de tocar el proyecto.

Configuración y despliegue

  • Puerto inconsistente en tres sitios. npm start ejecuta next start -p 80, por lo que el contenedor escucha en el 80, mientras el Dockerfile declara EXPOSE 3000 y el Deployment declara containerPort: 3000. Ambos valores son informativos en Kubernetes (el tráfico real depende del targetPort del Service, que vive fuera de este repo), pero la discrepancia es una trampa para cualquiera que cree un Service nuevo o depure conectividad. Conviene unificar en un solo puerto.
  • Versión de Node distinta en local y en la imagen. .nvmrc fija 22 y el Dockerfile construye con node:20-alpine en las dos etapas. Se compila y arranca, pero desarrollo y producción no ejecutan el mismo runtime.
  • La imagen no usa output: 'standalone'. next.config.ts no lo declara, así que la etapa runner copia node_modules completo (dependencias de desarrollo incluidas, tal como quedan tras npm ci). Activar standalone reduciría bastante el tamaño de la imagen y la superficie del contenedor.
  • No hay .env.example. Las 19 variables de entorno hay que deducirlas del código. .env.local está correctamente ignorado por git.
  • PDF_SIGNATURE_SECRET sin validación. Si falta, crypto.createHmac('sha256', '') no falla: se firman URLs con secreto vacío y todo parece funcionar. Un throw al arrancar sería preferible (lib/marketingCloud.tsx sí valida sus variables de esa forma).

Seguridad

  • URLs de PDF sin caducidad. El payload firmado (returnId, email, language) no incluye expiración, así que un enlace vale indefinidamente. La contrapartida es que rotar PDF_SIGNATURE_SECRET invalida todos los enlaces ya enviados por email, incluidos los de devoluciones en curso.
  • El email viaja en la URL del PDF, codificado en base64url (no cifrado). Cualquiera con el enlace puede leerlo.
  • HTML de terceros renderizado sin sanear. Las instrucciones y descripciones de los métodos de devolución llegan de robo-api y se inyectan con dangerouslySetInnerHTML (ThankYouPage, Step3ReturnMethodForm). Hoy el riesgo es bajo porque ese contenido lo edita el equipo interno, pero es un vector de XSS si alguna vez se abre su edición a más perfiles.
  • Credenciales de servicio compartidas. Todo el portal usa un único usuario de Auth0 (grant_type: password, scope customer). La autorización del cliente final se apoya únicamente en conocer la pareja número de pedido/devolución + email; no hay rate limiting propio en el portal frente a intentos de enumeración.
  • ContactKey en pruebas. En entornos no productivos, /api/trigger-journey sustituye data.EmailAddress por TEST_EMAIL, y como el ContactKey se deriva de ese campo, todos los eventos de prueba se agrupan bajo el mismo contacto en Marketing Cloud.

Datos y lógica de negocio

  • SKU de gastos de envío hardcodeado. itemIsShippingCost() compara contra el literal 'S00233' en lib/utils.tsx. Ese SKU se usa para excluir la línea de envío de la lista de artículos, del PDF y del cálculo del subtotal. Si cambia en el ERP, hay que cambiarlo aquí.
  • Dominio de producción hardcodeado. ThankYouPage construye el pdfUrl del journey como https://returns.hawkersco.com${pdfUrl}. En un entorno de desarrollo, el email llevaría un enlace a producción (el enlace sería válido, porque la firma no depende del host).
  • ?orderNum= contiene el número de devolución, no el del pedido (FormRetrieveCase). Herencia de una iteración anterior del formulario; renombrarlo obligaría a coordinar con quien genere esos enlaces (emails de Marketing Cloud, Service Cloud).
  • translations con doble formato. Los campos de traducción de robo-api llegan indistintamente como objeto o como string JSON, y el código hace typeof … === 'string' ? JSON.parse(…) : … en mapReturnMethod (lib/utils.tsx) y dos veces en getReturnPdf (método y estado). Normalizarlo en el backend eliminaría esa comprobación repetida.
  • Fecha de la devolución en la pantalla final. ThankYouPage muestra new Date().toLocaleDateString('es-ES', …) — la fecha del navegador con formato español fijo, en lugar del created que devuelve robo-api (que sí se guarda en el contexto y sí se usa en el journey).

Internacionalización

  • El locale no se valida antes de cargar los mensajes. i18n/request.tsx recorta el valor a dos letras y hace import('../messages/<lang>.json') sin comprobar que esté entre los siete soportados. Un Accept-Language como nl o ru, sin cookie previa, provoca el import de un fichero inexistente. Otras partes del código (resolvePdfLocale, SelectLanguage) sí validan contra constants/languages.ts; aquí falta esa misma comprobación.
  • Cambiar de idioma recarga la página completa. Al no usar routing por locale, SelectLanguage escribe la cookie y llama a window.location.reload(). Consecuencia: no hay URLs por idioma (ni indexación por idioma) y una recarga en mitad del wizard depende de que el estado se recupere de sessionStorage.

Código muerto y dependencias

  • Dependencias declaradas y no importadas en app/: axios, date-fns y @tanstack/react-table. Las peticiones se hacen con fetch nativo y no hay tablas de datos ni manipulación de fechas con librería.
  • Dos sistemas de toast. El layout monta el Toaster de sonner (components/ui/sonner.tsx), pero el repositorio conserva además el stack de toasts de shadcn/Radix (components/ui/toast.tsx, components/ui/toaster.tsx, hooks/use-toast.tsx), que solo se referencian entre sí. Es código muerto y arrastra la dependencia @radix-ui/react-toast.
  • app/test/page.tsx es una página con el texto test, mantenida a propósito para la validación del certificado de Google y excluida del Basic Auth. No borrarla sin confirmar que ya no se usa para eso.

Patrones frágiles

  • ReturnDetails: refresco con refund en las dependencias. El useEffect que vuelve a pedir el detalle depende de refund y llama a setRefund dentro; lo único que evita un bucle infinito es la comparación JSON.stringify(refund) !== JSON.stringify(returnDetails). Cualquier cambio en el mapeo que altere el orden de las claves o añada un campo volátil rompería esa igualdad y provocaría un bucle de peticiones.
  • StepContext: el listener de popstate se re-suscribe en cada cambio de paso (el useEffect depende de currentStep) y su handlePopState invoca destroyStep, que se define más abajo en el componente y no está en el array de dependencias. Funciona porque la función solo se ejecuta al recibir el evento, pero es delicado de modificar.
  • RefundProvider devuelve null hasta hidratarse. Es lo que evita desajustes servidor/cliente al leer sessionStorage, pero implica que todo el árbol por debajo (incluido el Footer) no se renderiza en el primer paint. También contiene dos useEffect que leen la misma clave de sessionStorage, uno de ellos redundante.
  • Cachés de token en memoria de módulo. Tanto lib/auth.tsx como lib/marketingCloud.tsx guardan el token en una variable de módulo. Con 1 réplica funciona bien; al escalar, cada pod gestionaría el suyo. El de Auth0 resta un buffer de 10 s a la expiración; el de Marketing Cloud no aplica buffer, por lo que puede intentar usar un token justo caducado.

Calidad y proceso

  • No hay tests en el repositorio (ni unitarios ni de integración) ni configuración de test runner. La cobertura E2E vive en Playwright Tests y depende de las clases qa-* presentes en el markup: renombrarlas o eliminarlas rompe esos tests de forma silenciosa desde este repo.
  • El pipeline no ejecuta lint ni tests. Jenkins hace gcloud builds submit, así que un error de TypeScript o de build sí detiene el despliegue, pero los avisos de ESLint no bloquean nada.
  • El despliegue tiene corte de servicio. El pipeline borra el Deployment y espera a que mueran los pods antes de aplicar el nuevo (estrategia Recreate, 1 réplica). Son unos segundos de 5xx en cada release.