Un core, dos vistas: KYB para bancos y onboarding para SaaS sin duplicar lógica

· 5 min read

# Un core, dos vistas: KYB para bancos y onboarding para SaaS sin duplicar lógica

El mismo dato, dos respuestas distintas

Cuando una empresa se da de alta, hay una única pregunta de fondo que arrastra a todas las demás: qué hace exactamente esa actividad y qué obligaciones fiscales genera. Lo que descubrí construyendo Conversor es que esa pregunta tiene al menos dos interlocutores que no se hablan entre sí y que esperan respuestas con formas completamente distintas.

Por un lado, un banco regional español. Cuando abre una cuenta a una empresa necesita hacer su verificación de negocio, lo que se llama KYB. No le interesa qué modelos tiene que presentar el autónomo en mayo. Quiere saber en qué sector de riesgo cae la actividad, de dónde salió ese dato y si la entidad reside en la UE.

Por el otro, un SaaS fiscal. Ese sí quiere exactamente lo otro: qué declaración de alta le toca, qué pasa con el IVA y la retención, qué régimen le conviene, el calendario de obligaciones y la cuota RETA.

Son dos respuestas que casi no comparten un solo campo. Y sin embargo salen de la misma operación de fondo: resolver una actividad económica en un perfil fiscal completo.

Mi primer impulso: dos endpoints, dos lógicas

Lo razonable, cuando dos clientes tan distintos te piden cosas tan distintas, es construirlo dos veces. Un endpoint para el banco, otro para el SaaS, cada uno con su propio código. Al fin y al cabo las respuestas no se parecen en nada: qué sentido tiene unificar lo que no comparte estructura.

Me parecía la decisión honesta. Si el banco necesita un sector de riesgo y un audit de fuentes, y el SaaS necesita la declaración de alta y un calendario, montar una capa común era forzar una abstracción antes de tiempo. Y las abstracciones prematuras son el clásico error que uno comete por no querer escribir dos veces la misma línea.

Así que empecé a escribir los dos endpoints por separado. Y fue entonces cuando apareció el error que me obligó a replantearlo todo.

El error: helpers en route.ts y un build que tsc no vio

Por comodidad, metí los helpers de proyección directamente en el fichero de la ruta, route.ts, y los exporté desde ahí. Quería reutilizar una función de mapeo en el otro endpoint o en un test, y me parecía lo natural: están en el mismo fichero, por qué no exportarlas.

El chequeo de tipos no dijo nada. tsc pasaba limpio. Todo compilaba en local. Y aun así, el build de Vercel se rompió.

La razón es una de esas restricciones de framework que no están en el compilador: Next.js App Router rechaza los exports que no son de Route en route.ts. El fichero de una ruta solo puede exportar lo que el router espera. Si exportas cualquier otra cosa, el build falla aunque TypeScript esté satisfecho.

Fue un error que me costó un buen rato localizar porque la señal venía del sitio equivocado. El compilador decía "bien", el deploy decía "no". Y el mensaje de error no apuntaba a mi función: apuntaba a una regla de convención que yo no conocía.

La solución: un core, dos vistas

La corrección me enseñó dos cosas a la vez, y las dos apuntaban al mismo sitio: lib/.

Primero, los helpers de proyección tienen que vivir en lib/, nunca exportarse desde route.ts. Es la regla que ahora respeto sin discutir. En lib/api/activity-profile.ts está el core: resolveActivity resuelve la actividad y assembleDossier monta el perfil fiscal completo.

Segundo, y esto es lo importante: una vez que el core vive fuera de las rutas, las dos vistas dejan de ser dos lógicas duplicadas y pasan a ser dos proyecciones delgadas sobre el mismo núcleo.

/api/v1/kyb/enrich es la vista del banco: devuelve fiscal_profile.sector_riesgo y lo acompaña de un audit con las fuentes, el data_version y la residencia UE. El banco no tiene que confiar en un número suelto; tiene de dónde salió y cuándo se generó.

/api/v1/onboarding/activity es la vista del SaaS: devuelve el alta_036, el IVA y la retención, el régimen recomendado, el calendario y la RETA. Es la respuesta que un producto de gestión necesita para arrancar el alta sin montar su propia capa de datos fiscales.

Las dos respuestas siguen sin parecerse en casi nada. Pero ahora comparten un único punto donde se resuelve la verdad fiscal. Si un día tengo que cambiar cómo se mapea un epígrafe del IAE, lo cambio una vez, en el core, y las dos vistas se corrigen solas.

Lo que aprendí

La lección no es "no dupliques código", que es un consejo barato. Es más concreta. La duplicación aquí no era solo un coste de mantenimiento: era un coste de corrección. Dos copias de "actividad a perfil fiscal" acaban divergiendo, y el día que divergen, el banco y el SaaS te devuelven respuestas distintas para la misma actividad. Eso no es un refactor pendiente, es un dato fiscal que deja de ser fiable.

Y la segunda lección: el compilador no es el único contrato que hay que respetar. Next.js tenía una regla de convención que TypeScript no podía ver, y me la explicó a base de romper el deploy. Desde entonces trato las restricciones del framework como parte del tipo: no basta con que tsc pase, el fichero de una ruta tiene su propio contrato de qué puede exportar.

El core resuelve una vez, las vistas proyectan y los helpers viven en lib/. Eso es todo, y me costó un build roto aprenderlo.

Si quieres ver los dos endpoints en detalle, los docs están abiertos: https://www.conversoriaecnae.es/api/v1/docs

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn