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 startejecutanext start -p 80, por lo que el contenedor escucha en el 80, mientras elDockerfiledeclaraEXPOSE 3000y elDeploymentdeclaracontainerPort: 3000. Ambos valores son informativos en Kubernetes (el tráfico real depende deltargetPortdelService, que vive fuera de este repo), pero la discrepancia es una trampa para cualquiera que cree unServicenuevo o depure conectividad. Conviene unificar en un solo puerto. - Versión de Node distinta en local y en la imagen.
.nvmrcfija 22 y elDockerfileconstruye connode:20-alpineen 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.tsno lo declara, así que la etaparunnercopianode_modulescompleto (dependencias de desarrollo incluidas, tal como quedan trasnpm ci). Activarstandalonereducirí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.localestá correctamente ignorado por git. PDF_SIGNATURE_SECRETsin validación. Si falta,crypto.createHmac('sha256', '')no falla: se firman URLs con secreto vacío y todo parece funcionar. Unthrowal arrancar sería preferible (lib/marketingCloud.tsxsí 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 rotarPDF_SIGNATURE_SECRETinvalida 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, scopecustomer). 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. ContactKeyen pruebas. En entornos no productivos,/api/trigger-journeysustituyedata.EmailAddressporTEST_EMAIL, y como elContactKeyse 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'enlib/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.
ThankYouPageconstruye elpdfUrldel journey comohttps://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).translationscon doble formato. Los campos de traducción de robo-api llegan indistintamente como objeto o como string JSON, y el código hacetypeof … === 'string' ? JSON.parse(…) : …enmapReturnMethod(lib/utils.tsx) y dos veces engetReturnPdf(método y estado). Normalizarlo en el backend eliminaría esa comprobación repetida.- Fecha de la devolución en la pantalla final.
ThankYouPagemuestranew Date().toLocaleDateString('es-ES', …)— la fecha del navegador con formato español fijo, en lugar delcreatedque 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.tsxrecorta el valor a dos letras y haceimport('../messages/<lang>.json')sin comprobar que esté entre los siete soportados. UnAccept-Languagecomonloru, sin cookie previa, provoca el import de un fichero inexistente. Otras partes del código (resolvePdfLocale,SelectLanguage) sí validan contraconstants/languages.ts; aquí falta esa misma comprobación. - Cambiar de idioma recarga la página completa. Al no usar routing por locale,
SelectLanguageescribe la cookie y llama awindow.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 desessionStorage.
Código muerto y dependencias
- Dependencias declaradas y no importadas en
app/:axios,date-fnsy@tanstack/react-table. Las peticiones se hacen confetchnativo y no hay tablas de datos ni manipulación de fechas con librería. - Dos sistemas de toast. El layout monta el
Toasterdesonner(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.tsxes una página con el textotest, 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 conrefunden las dependencias. EluseEffectque vuelve a pedir el detalle depende derefundy llama asetRefunddentro; lo único que evita un bucle infinito es la comparaciónJSON.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 depopstatese re-suscribe en cada cambio de paso (eluseEffectdepende decurrentStep) y suhandlePopStateinvocadestroyStep, 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.RefundProviderdevuelvenullhasta hidratarse. Es lo que evita desajustes servidor/cliente al leersessionStorage, pero implica que todo el árbol por debajo (incluido elFooter) no se renderiza en el primer paint. También contiene dosuseEffectque leen la misma clave desessionStorage, uno de ellos redundante.- Cachés de token en memoria de módulo. Tanto
lib/auth.tsxcomolib/marketingCloud.tsxguardan 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
lintni tests. Jenkins hacegcloud 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
Deploymenty espera a que mueran los pods antes de aplicar el nuevo (estrategiaRecreate, 1 réplica). Son unos segundos de 5xx en cada release.