<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Cervera Cobeñas</title>
    <description>The latest articles on DEV Community by Michael Cervera Cobeñas (@cm7chael).</description>
    <link>https://dev.to/cm7chael</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176069%2F7bf03a79-18d9-4f13-84c2-1b30f99185c0.jpg</url>
      <title>DEV Community: Michael Cervera Cobeñas</title>
      <link>https://dev.to/cm7chael</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cm7chael"/>
    <language>en</language>
    <item>
      <title>Microfrontends con Angular y Native Federation: lo que aprendí llevando más de 15 módulos a producción</title>
      <dc:creator>Michael Cervera Cobeñas</dc:creator>
      <pubDate>Fri, 16 Oct 2026 20:04:14 +0000</pubDate>
      <link>https://dev.to/cm7chael/microfrontends-con-angular-y-native-federation-lo-que-aprendi-llevando-mas-de-15-modulos-a-59km</link>
      <guid>https://dev.to/cm7chael/microfrontends-con-angular-y-native-federation-lo-que-aprendi-llevando-mas-de-15-modulos-a-59km</guid>
      <description>&lt;p&gt;Hace unos meses me tocó llevar una plataforma corporativa a una arquitectura de microfrontends: un portal que carga más de 15 módulos, cada uno con su propio repositorio, su propio equipo y su propio despliegue. Hoy esos módulos salen a producción por separado, sin coordinar ventanas de despliegue entre equipos.&lt;/p&gt;

&lt;p&gt;No voy a explicar qué es un microfrontend, de eso ya hay bastante escrito. Quiero contar lo que no sale en los tutoriales, lo que solo aparece cuando el segundo, el quinto y el décimo módulo ya están en producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primero: cuándo NO usar microfrontends
&lt;/h2&gt;

&lt;p&gt;Empiezo por aquí porque es la pregunta que más me hacen y la respuesta honesta es "casi nunca al inicio".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Te conviene&lt;/th&gt;
&lt;th&gt;No te conviene&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Varios equipos que necesitan desplegar sin esperarse&lt;/td&gt;
&lt;td&gt;Un solo equipo trabajando en un solo producto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dominios de negocio claramente separados (ventas, calidad, RRHH)&lt;/td&gt;
&lt;td&gt;Pantallas que comparten casi todo el estado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Módulos que crecen a ritmos distintos&lt;/td&gt;
&lt;td&gt;Un producto que todavía está buscando su forma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Una plataforma que va a vivir años&lt;/td&gt;
&lt;td&gt;Un MVP que tiene que salir en semanas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Si estás en la columna de la derecha, un monolito bien modularizado te da el 80 % del beneficio con el 20 % del costo. Los microfrontends son, sobre todo, una decisión de organización. Pagan cuando el cuello de botella es la coordinación entre equipos, no la calidad del código.&lt;/p&gt;

&lt;h2&gt;
  
  
  La arquitectura, en una imagen
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────────────────┐
   usuario ───────▶│ HOST (portal) │
                    │ · login único (OIDC) │
                    │ · menú según permisos │
                    │ · lee el manifiesto │
                    └──────────────┬───────────────┘
                                   │ carga en tiempo de ejecución
            ┌──────────────────────┼──────────────────────┐
            ▼ ▼ ▼
     remoteEntry.json remoteEntry.json remoteEntry.json
     ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
     │ Ventas │ │ Calidad │ │ RRHH │
     │ repo propio │ │ repo propio │ │ repo propio │
     │ deploy │ │ deploy │ │ deploy │
     │ propio │ │ propio │ │ propio │
     └──────────────┘ └──────────────┘ └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El stack es Angular con &lt;strong&gt;Native Federation&lt;/strong&gt; (&lt;code&gt;@angular-architects/native-federation&lt;/code&gt;). Es el sucesor de Module Federation para el builder moderno de Angular basado en esbuild: en lugar de depender de webpack, usa los estándares del navegador (módulos ES e import maps).&lt;/p&gt;

&lt;p&gt;El host no conoce a los módulos en tiempo de compilación. Los descubre al arrancar, desde un manifiesto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// federation.manifest.json (uno por ambiente)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ventas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://ventas.ejemplo.com/remoteEntry.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;calidad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://calidad.ejemplo.com/remoteEntry.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// main.ts del host&lt;/span&gt;
&lt;span class="nf"&gt;initFederation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;federation.manifest.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./bootstrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// rutas del host&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ventas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loadChildren&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;loadRemoteModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;remoteName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ventas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exposedModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hasta aquí es lo que trae cualquier tutorial. Lo que sigue lo aprendí en producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que rompe en producción (y cómo lo resolví)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Las rutas absolutas dentro de un módulo
&lt;/h3&gt;

&lt;p&gt;Un módulo se desarrolla solo, en su propio puerto, donde &lt;code&gt;/detalle/42&lt;/code&gt; funciona perfecto. Embebido en el portal, ese módulo vive bajo &lt;code&gt;/ventas&lt;/code&gt;, y &lt;code&gt;/detalle/42&lt;/code&gt; manda al usuario a una ruta que no existe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La regla:&lt;/strong&gt; dentro de un módulo, toda navegación es relativa. &lt;code&gt;routerLink="detalle/42"&lt;/code&gt; y &lt;code&gt;router.navigate(['detalle', id], { relativeTo: route })&lt;/code&gt;. Es una línea en la guía del equipo y una revisión en cada pull request, pero ahorra el bug más repetido de toda la plataforma.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. El CORS de &lt;code&gt;remoteEntry.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;El host pide el &lt;code&gt;remoteEntry.json&lt;/code&gt; y los chunks de cada módulo desde otro dominio. Si el servidor del módulo no responde con &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, el navegador bloquea la carga y el usuario ve una pantalla en blanco sin ningún error útil en la interfaz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La regla:&lt;/strong&gt; el CORS de los archivos estáticos del módulo es parte de la plantilla del proyecto, no algo que cada equipo descubre por su cuenta. Todo módulo nuevo nace con la configuración de su servidor web ya resuelta.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. La caché que esconde la versión nueva
&lt;/h3&gt;

&lt;p&gt;Despliegas, todo sale en verde, y el usuario sigue viendo la versión anterior. El culpable casi siempre es &lt;code&gt;remoteEntry.json&lt;/code&gt;: es el único archivo cuyo nombre no cambia entre versiones, así que si el navegador o un CDN lo guarda, apunta a chunks viejos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La regla:&lt;/strong&gt; &lt;code&gt;remoteEntry.json&lt;/code&gt; se sirve sin caché; los chunks, que llevan un hash en el nombre, se pueden cachear para siempre. Así cada despliegue llega al usuario en la siguiente carga, sin pedirle a nadie que borre su caché.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Un manifiesto por ambiente… y la vez que se mezclaron
&lt;/h3&gt;

&lt;p&gt;Cada ambiente (pruebas y producción) tiene su propio manifiesto con las URLs de sus módulos. Una vez, una construcción manual horneó los valores por defecto y el portal de producción terminó cargando módulos de pruebas. El síntoma fue un error de CORS; la causa, un ambiente mezclado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La regla:&lt;/strong&gt; el pipeline valida, antes de construir, que todas las URLs correspondan al ambiente destino, y aborta si no. Desde entonces ese error ya no puede repetirse, porque el pipeline no lo deja pasar.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Una sola versión del Design System en todos los módulos
&lt;/h3&gt;

&lt;p&gt;Los módulos comparten librerías: Angular, el router y el Design System propio que usan 17 aplicaciones. Native Federation las comparte como &lt;em&gt;singleton&lt;/em&gt;, pero si un módulo trae otra versión del Design System, el portal muestra dos estilos distintos en la misma pantalla o, peor, componentes que se rompen al navegar entre módulos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La regla:&lt;/strong&gt; cada cambio en el Design System se publica como paquete versionado y se actualiza en &lt;em&gt;todos&lt;/em&gt; los módulos a la vez, no solo en el que lo necesitaba. Y no basta con subir el rango en el &lt;code&gt;package.json&lt;/code&gt;: si la imagen se construye con &lt;code&gt;npm ci&lt;/code&gt;, manda el &lt;em&gt;lockfile&lt;/em&gt;, así que hay que reinstalar y versionar el lock también.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Un módulo en el menú que todavía no existe
&lt;/h3&gt;

&lt;p&gt;El portal arma su menú desde un catálogo de módulos. Registrar un módulo antes de que esté desplegado en todos los ambientes hace que el portal intente cargar un &lt;code&gt;remoteEntry.json&lt;/code&gt; que no existe, y esa ruta queda rota.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La regla:&lt;/strong&gt; cada entrada del catálogo tiene un indicador de disponibilidad. El módulo aparece en el menú con una página de "próximamente" hasta que su servicio existe donde corre el portal.&lt;/p&gt;

&lt;h2&gt;
  
  
  El login: uno solo, en el host
&lt;/h2&gt;

&lt;p&gt;La decisión que más simplificó todo: &lt;strong&gt;solo el host autentica&lt;/strong&gt;. El portal maneja el inicio de sesión con OIDC (Keycloak) y los módulos reciben el token ya resuelto. Ningún módulo define su propia autorización ni su propio login.&lt;/p&gt;

&lt;p&gt;Eso tiene dos efectos. Para el usuario, un solo inicio de sesión para toda la plataforma. Para los equipos, un módulo nuevo no empieza escribiendo pantallas de login, sino pantallas de negocio. Los permisos viajan en el token y el menú se arma según ellos.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Si cada módulo resuelve la seguridad a su manera, tienes tantos modelos de seguridad como módulos. Centralizarla es la única forma de poder auditarla.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Qué haría distinto si empezara hoy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El Design System antes que el segundo módulo.&lt;/strong&gt; Cada componente que un equipo construye "mientras tanto" es un componente que después hay que migrar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Las barreras del pipeline desde el día uno.&lt;/strong&gt; La validación de ambientes la agregué después de un incidente. Pudo estar desde el inicio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatizar la actualización del Design System.&lt;/strong&gt; Actualizar una librería en más de 15 repositorios a mano no escala: es exactamente el tipo de tarea que debe hacer un script, no una persona.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una plantilla de módulo.&lt;/strong&gt; CORS, rutas relativas, configuración de federación y pipeline listos al crear el repositorio. Cada regla de este artículo debería ser un archivo de la plantilla, no un párrafo de la guía.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Para cerrar
&lt;/h2&gt;

&lt;p&gt;Los microfrontends funcionan cuando resuelven un problema de organización: equipos que necesitan avanzar sin esperarse. La parte técnica —federación, manifiestos, rutas— se resuelve en una semana. Lo difícil es todo lo demás: que cada regla quede escrita, que las que importan se conviertan en barreras automáticas y que cualquier persona del equipo pueda sostener la plataforma con lo que está documentado.&lt;/p&gt;

&lt;p&gt;Si estás evaluando microfrontends para tu equipo o ya estás peleando con alguno de estos problemas, escríbeme por &lt;a href="https://www.linkedin.com/in/cm7chael/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. Me interesa saber cómo lo están resolviendo otros.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Publicado originalmente en &lt;a href="https://cm7chael.github.io/portafolio/blog/microfrontends-angular-native-federation-lecciones-produccion.html" rel="noopener noreferrer"&gt;mi blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>microfrontends</category>
      <category>architecture</category>
      <category>spanish</category>
    </item>
    <item>
      <title>SAP Business One con una sola puerta: cómo lo integré en 17 aplicaciones sin que ninguna le hable directo</title>
      <dc:creator>Michael Cervera Cobeñas</dc:creator>
      <pubDate>Sat, 10 Oct 2026 22:23:48 +0000</pubDate>
      <link>https://dev.to/cm7chael/sap-business-one-con-una-sola-puerta-como-lo-integre-en-17-aplicaciones-sin-que-ninguna-le-hable-14j5</link>
      <guid>https://dev.to/cm7chael/sap-business-one-con-una-sola-puerta-como-lo-integre-en-17-aplicaciones-sin-que-ninguna-le-hable-14j5</guid>
      <description>&lt;p&gt;Cuando una empresa usa SAP Business One y empieza a construir sus propias aplicaciones, lo primero que pasa es que cada una se conecta a SAP por su cuenta. La de ventas consulta clientes, la de compras consulta proveedores, la de recursos humanos consulta empleados. Cada una con sus credenciales, sus consultas y su forma de manejar los errores.&lt;/p&gt;

&lt;p&gt;Con dos aplicaciones se puede vivir así. Con diecisiete, no. En la plataforma que lidero ninguna aplicación habla con SAP directamente, y cuento cómo está armado y qué fui aprendiendo al integrarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una sola puerta hacia SAP
&lt;/h2&gt;

&lt;p&gt;Toda la comunicación con SAP pasa por un &lt;strong&gt;gateway&lt;/strong&gt;: un servicio en .NET que es el único que tiene las credenciales de SAP. Por dentro usa dos caminos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El Service Layer&lt;/strong&gt; , la API oficial de SAP Business One, para crear documentos y para las lecturas estándar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lectura directa a HANA&lt;/strong&gt; , con procedimientos almacenados, para las consultas pesadas que por Service Layer tomarían muchas llamadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y tampoco las aplicaciones llaman al gateway. Solo lo llama el backend del portal, y cada módulo le pide los datos al portal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; módulo (ventas, compras, rrhh…)
        │ token del usuario
        ▼
 backend del portal ──── tablas espejo (maestros)
        │ mismo token
        ▼
 gateway SAP (único con credenciales)
        │
        ▼
 SAP Business One (Service Layer + HANA)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El token del usuario viaja en cada salto. Así el gateway sabe quién pidió cada cosa, y ningún módulo necesita una cuenta de servicio de SAP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los maestros se copian, no se consultan
&lt;/h2&gt;

&lt;p&gt;La decisión que más ordenó todo fue esta: los &lt;strong&gt;maestros&lt;/strong&gt; de SAP (clientes, empleados, artículos, proveedores, cuentas contables, departamentos) se sincronizan en la base de datos del portal como &lt;strong&gt;tablas espejo&lt;/strong&gt;. Los módulos leen esas tablas a través de la API del portal. Nunca consultan SAP en vivo para un maestro.&lt;/p&gt;

&lt;p&gt;La sincronización tiene un solo punto de entrada por maestro, un &lt;code&gt;POST /sync&lt;/code&gt; que es lo único que llama al gateway. Se dispara de dos formas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Con un botón "Sincronizar con SAP" en la configuración del portal, solo para administradores.&lt;/li&gt;
&lt;li&gt;Con una tarea programada todas las madrugadas, que corre todos los maestros en secuencia. Si uno falla, los demás siguen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Por dentro es un &lt;em&gt;upsert&lt;/em&gt; por lotes, y lo que desaparece de SAP se marca como inactivo en vez de borrarse, porque puede haber registros en el portal que todavía apuntan a él. La corrida completa, unos 35 mil registros, toma menos de un minuto.&lt;/p&gt;

&lt;p&gt;¿Por qué copiar en lugar de consultar?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Velocidad.&lt;/strong&gt; Un buscador de clientes no puede esperar a SAP en cada tecla.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disponibilidad.&lt;/strong&gt; Si SAP está en mantenimiento, el portal sigue funcionando con los datos de la noche anterior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un solo lugar para corregir.&lt;/strong&gt; Si cambia cómo se lee un maestro, se cambia en la sincronización y todos los módulos lo reciben igual.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En vivo solo va lo que tiene que estar al segundo: el stock, el costo de un artículo, el tipo de cambio y la creación de documentos.&lt;/p&gt;

&lt;p&gt;Para que esto se mantenga, quedó escrito como regla, con el patrón paso a paso para agregar un maestro nuevo: el endpoint en el gateway, la tabla espejo y su sincronización en el portal, la pantalla con el botón, y el módulo leyendo del portal. Cualquiera del equipo puede agregar uno siguiendo esos pasos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que aprendí integrando
&lt;/h2&gt;

&lt;h3&gt;
  
  
  El Service Layer corta en 20 sin avisar
&lt;/h3&gt;

&lt;p&gt;Una consulta pedía 100 órdenes y llegaban 20. El Service Layer pagina de a 20 por defecto y deja el resto detrás de un &lt;code&gt;@odata.nextLink&lt;/code&gt;. No da error ni advertencia, y el &lt;code&gt;count&lt;/code&gt; de la respuesta sí trae el total real. Lo descubrimos porque un usuario decía que sus órdenes no estaban en SAP, cuando en realidad estaban en la página dos.&lt;/p&gt;

&lt;p&gt;Hay que pedir el tamaño de página explícitamente (la cabecera &lt;code&gt;Prefer: odata.maxpagesize&lt;/code&gt;) o seguir el &lt;code&gt;nextLink&lt;/code&gt;. Y del lado de quien pagina, la condición de corte tiene que ser el &lt;code&gt;count&lt;/code&gt;, no "el lote vino más chico de lo que pedí", porque con este comportamiento eso corta en la primera vuelta.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;DocTotal&lt;/code&gt; no está en la moneda del documento
&lt;/h3&gt;

&lt;p&gt;En los documentos de SAP Business One, &lt;code&gt;DocTotal&lt;/code&gt; es el total en &lt;strong&gt;moneda local&lt;/strong&gt;. &lt;code&gt;DocCurrency&lt;/code&gt; dice en qué moneda se emitió. Mostrar &lt;code&gt;DocTotal&lt;/code&gt; con el símbolo de &lt;code&gt;DocCurrency&lt;/code&gt; hacía que una orden en dólares apareciera con el monto en soles, o sea más del triple.&lt;/p&gt;

&lt;p&gt;El importe en la moneda del documento es &lt;code&gt;DocTotalFc&lt;/code&gt;, pero cuando el documento está en moneda local SAP lo deja en 0, y un 0 se puede leer como "cero dólares". Por eso el gateway lo publica como &lt;code&gt;null&lt;/code&gt; en ese caso, y quien lo consume usa &lt;code&gt;DocTotal&lt;/code&gt; cuando no hay otro. Lo llamamos "moneda extranjera" y no "dólares", porque una parte de las compras están en euros.&lt;/p&gt;

&lt;p&gt;Hubo un segundo tropiezo con esto mismo: el Service Layer solo devuelve los campos que pides en el &lt;code&gt;$select&lt;/code&gt;. Agregamos &lt;code&gt;DocTotalFc&lt;/code&gt; al modelo pero no a la lista de campos de la consulta, así que llegaba vacío siempre. Eso no lo detecta el compilador, solo se ve llamando a SAP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un error de validación disfrazado de caída
&lt;/h3&gt;

&lt;p&gt;Una alerta de errores 5xx sonó un día por varios 502 seguidos. No era SAP caído: alguien había pedido un costeo sin elegir cliente, el gateway respondió 400 diciendo que faltaba el cliente, y la función que hacía las lecturas convertía cualquier respuesta no exitosa en 502. Las escrituras sí propagaban bien el 400, las lecturas no.&lt;/p&gt;

&lt;p&gt;Ese error tenía dos costos: el usuario veía "SAP no responde" cuando el problema era un campo vacío, y la métrica que dispara las alertas se llenaba de falsos 5xx. Ahora las lecturas propagan los 400, 409 y 422 tal cual, con el mensaje del backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  El SQL de SAP también es código
&lt;/h3&gt;

&lt;p&gt;Los procedimientos almacenados que lee el gateway se crearon directamente en HANA y no estaban en ningún repositorio. Y el mapeo de sus resultados es tolerante a propósito: busca las columnas por nombre, y si una columna cambia de nombre, el campo llega &lt;code&gt;null&lt;/code&gt; o &lt;code&gt;0&lt;/code&gt; sin ningún error.&lt;/p&gt;

&lt;p&gt;O sea, alguien podía cambiar un procedimiento y una pantalla empezaba a mostrar ceros sin que nada fallara. La regla que acordamos: el SQL vive con el código que se rompe si cambia. Cada procedimiento que llama el gateway se versiona en su repositorio, en el mismo cambio que el código que lo lee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué haría distinto si empezara hoy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versionar el SQL de SAP desde el primer procedimiento.&lt;/strong&gt; Recuperarlo después, cuando ya hay decenas en uso, es arqueología.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pruebas contra una copia de la compañía.&lt;/strong&gt; Los dos tropiezos del &lt;code&gt;DocTotal&lt;/code&gt; se ven recién llamando a SAP. Una prueba que corra contra una base de respaldo los habría atrapado antes de producción.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una sola función de paginación en el gateway.&lt;/strong&gt; El corte en 20 apareció en varias consultas a la vez porque cada una paginaba a su manera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La moneda explícita en cada monto.&lt;/strong&gt; Ningún campo de dinero sin su moneda al lado, aunque parezca obvio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Si estás por integrar con SAP Business One
&lt;/h2&gt;

&lt;p&gt;Antes de escribir la primera consulta, decide quién es el único que habla con SAP. Si cada aplicación se conecta por su cuenta, después cuesta mucho juntar todo en un solo lugar. Y separa desde el inicio lo que se copia (los maestros) de lo que va en vivo: esa decisión define cuánto depende tu plataforma de que SAP esté disponible.&lt;/p&gt;

&lt;p&gt;Si estás integrando con SAP Business One, escríbeme por &lt;a href="https://www.linkedin.com/in/cm7chael/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. Me interesa saber cómo lo resolvieron otros equipos, sobre todo con el Service Layer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Publicado originalmente en &lt;a href="https://cm7chael.github.io/portafolio/blog/integrar-sap-business-one-gateway-tablas-espejo.html" rel="noopener noreferrer"&gt;mi blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sap</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>spanish</category>
    </item>
  </channel>
</rss>
