Catalizadora × Truly Nolen International · 3 de septiembre de 2026 · verificado contra el sistema de TNI

El prospecto de GlobalLink, dentro del sistema de TNI

Qué hay que hacer, quién lo hace y en qué orden; el cruce campo por campo; y cómo lo vive el vendedor. Lo demás, en anexos.

Todo lo que dice esta página sobre el sistema de TNI se leyó hoy de su base de datos real: catálogos, columnas, tipos, largos, obligatoriedad, y cómo TNI mismo inscribe sus prospectos en la oficina de Guatemala.

0resumen

Resumen ejecutivo

En dos frasesCada prospecto que un vendedor trabaje en GlobalLink aparecerá en el sistema de TNI como un cliente potencial, con su vendedor y su historial, y lo que TNI haga con él volverá a GlobalLink cada noche. Además, desde el sistema de TNI se entrará a GlobalLink con un clic, sin otra contraseña.
La decisión
Cliente en estado Potencial

El prospecto se inscribe en la ficha de clientes de TNI en estado 5, que es como Guatemala ya inscribe 1.268 prospectos. Cabe entero en el permiso de escritura ya redactado.

Qué pone TNI
4 accesos, 3 respuestas, 1 pestaña

Firma, usuario de escritura, acceso de red y una columna; tres confirmaciones sobre su modelo; y la pestaña con la página que emite el pase de acceso.

Qué construye Catalizadora
10 tareas en 4 bloques

Tres arrancan hoy sin depender de TNI. La escritura real es lo último y se enciende de a un paso.

Lo que encontramos hoy
3 defectos antes de escribir

El código tomaba el estado 4 por Potencial, y es «cancelado por el cliente». El mapeo omitía columnas obligatorias y escribía dos que no existen. El modo rápido del espejo está apagado desde agosto.

Cómo se verificó
En vivo y con revisor

Todo lo que se dice de TNI se leyó hoy de su base real. Cuatro pasadas de un revisor independiente sobre cuatro versiones.

Estado hoy
Nada sale todavía

105 avisos esperan en la cola desde agosto y nunca se escribió una fila en TNI. Es a propósito: no hay escritura sin firma.

Cómo leer esto.CEO: secciones 0 y 1.CTO y equipo: todo.Auditoría: 8 y 9.Cada sección abre con una explicación en lenguaje llano y sigue con el detalle técnico.
El plan, en una frase

El prospecto se crea en la ficha de clientes de TNI en estado 5, Potencial; cada avance es un renglón «Lead» en su bitácora; al ganar, nace el servicio en etapa 7. Lo que TNI cambie vuelve cada noche por el espejo.

  • Cabe entero en el permiso de escritura ya acordado: altas en Clientes, Contactos, Servicios y Propuestas. Nunca una modificación.
  • Es como TNI ya trabaja: Guatemala tiene 1.268 clientes en estado 5 y registra sus gestiones de venta como renglones «Lead».
  • El acceso desde su sistema lleva el número de empleado en el pase, y eso ata solo cada vendedor.
  • Hoy no sale nada hacia TNI: 105 avisos esperan en la cola desde agosto y nunca se escribió una fila. Encender es lo último, y se hace de a un paso.
↑ volver al resumen
1sección

El plan: quién responde por qué, qué depende de qué, y cinco días

Las seis preguntas que tiene que contestar este documento, en orden: de quién es cada cosa, qué falta, qué sigue para cada uno, cuál es el plan, qué bloquea a qué, y cómo se hace en una semana.

En lenguaje llanoCatalizadora construye y opera todo el mecanismo. TNI da cuatro permisos, responde tres preguntas y agrega una pestaña. Diez de las dieciocho tareas no dependen de nadie y arrancan hoy; lo único que no podemos hacer solos es escribir en el sistema de TNI, que necesita su firma y su usuario.

1.1 · De quién es cada cosa

Catalizadora responde por el mecanismo

  1. ConstruirEl aviso que sale cuando nace o avanza un prospecto, el mapeo a las columnas de TNI, la comprobación de duplicados, el paso de vuelta y el punto de entrada del acceso. Diez tareas.
  2. OperarEl programa que escribe corre en nuestro servidor, con topes de velocidad, reintentos, auditoría de cada intento y alerta si la cola se atasca.
  3. Responder por los datosQue lo que se escriba sea correcto, que no se duplique, que no se pierda, y que lo que TNI cambie se refleje. Si algo falla, se apaga desde nuestro lado en menos de 30 minutos.
  4. InformarUn reporte mensual con altas por tabla, tasa de éxito, backlog e incidentes, como fija el addendum.
  5. No responde porNada que ocurra dentro del sistema de TNI: su flujo de facturación, sus estados, sus usuarios. No modificamos ni borramos nada suyo, nunca.

TNI responde por los permisos y por su sistema

  1. AutorizarLa firma del addendum es el interruptor legal y técnico: sin ella no se escribe una sola fila.
  2. Dar accesoUn usuario de escritura acotado a cuatro tablas, el acceso de red a ese usuario, y una columna nueva que impide duplicados.
  3. Confirmar tres convencionesCómo quieren que se inscriba el prospecto: estado, tipo de cliente y de dónde decir que viene. Son tres respuestas cortas, no un desarrollo.
  4. Su sistemaLa pestaña en el menú, la página que emite el pase de acceso con su llave, y la equivalencia entre sus cargos y nuestros roles.
  5. No responde porNingún desarrollo del lado de GlobalLink, ninguna carga de datos, ninguna pantalla nueva. Sus prospectos aparecen en las pantallas que ya usa.

1.2 · Qué falta y qué sigue, para cada uno

QuiénLo próximo que tiene que hacerCuánto le tomaQué desbloquea
Truly Nolen International · cuatro cosas, ninguna de más de una hora de trabajo
TNI Quien conozca el modelo de datosResponder las tres confirmaciones de R5: estado y tipo del prospecto, qué ponen en identidad cuando no la tienen, y cómo marcar que el prospecto viene de GlobalLink30 minutosCierra el mapeo. Es lo más barato y lo que más desbloquea.
TNI DirecciónFirmar el addendum COT-0003-A1 v2, ya redactadoUna firmaHabilita toda la escritura real
TNI Administrador de base de datosCrear el usuario de escritura, abrir la red y agregar la columna anti-duplicados (R2, R3, R4), con las sentencias que están en la sección 21 horaPermite la primera escritura
TNI Equipo del sistema de gestiónLa pestaña, la página que emite el pase y la llave pública, y la tabla de equivalencia de cargos (R6, R7, R8)1 a 2 díasHabilita el acceso con un clic. Es una vía aparte: no bloquea la escritura.
Catalizadora · empieza hoy, sin esperar nada
CatalizadoraCorregir el mapeo contra el esquema real y el estado 5 (C1, C10)Día 1Sin esto, la primera escritura falla
CatalizadoraLos avisos que salen cuando el prospecto nace y avanza (C2, C3)Día 2Empieza a acumularse lo que se va a escribir
CatalizadoraDuplicados, números devueltos y bitácora completa (C4, C5, C6)Día 3Permite la prueba en sombra
CatalizadoraEl paso de vuelta (C7)Día 4Cierra el circuito de los dos sentidos
CatalizadoraEncender y verificar (C8), y el acceso (C9)Día 5Prospectos reales en el sistema de TNI
El único camino que no controlamos. Escribir en el sistema de TNI necesita tres cosas suyas: la firma, el usuario y la red. Todo lo demás lo construimos y lo probamos sin tocar su sistema. Si esas tres llegan esta semana, el viernes hay prospectos reales en TNI; si llegan después, el encendido toma dos horas el día que lleguen.

1.3 · Qué depende de qué

CAMINO DE LA ESCRITURA C1 · C10 · C2 Mapeo real, estado 5 y los avisos no depende de nadie C3 · C4 · C5 · C6 Datos mínimos, duplicados, ids devueltos, bitácora necesita R5 · las 3 confirmaciones Prueba en sombra El programa muestra qué escribiría, sin escribir verificable sin tocar TNI C8 · Encendido Primera escritura real, de a un paso necesita R1 · R2 · R3 Prospectos reales en el sistema de TNI R5 · Tres confirmaciones 30 minutos de un DBA R1 · R2 · R3 Firma, usuario de escritura y acceso de red R4 · Columna única recomendada, no bloquea VÍA PARALELA · EL ACCESO CON UN CLIC · NO BLOQUEA NI ES BLOQUEADA POR LA ESCRITURA R6 · R7 · R8 Pestaña, página que emite el pase, llave y cargos C9 · Punto de entrada Se construye y se prueba antes, contra un emisor de laboratorio Entrar con un clic desde el sistema de gestión, sin segunda contraseña El número de empleado que trae el pase resuelve además quién es el vendedor en cada prospecto que se escribe.
Sólo dos cosas bloquean: las tres confirmaciones, que abren el mapeo completo, y la firma con los accesos, que abren la escritura real. Todo lo demás avanza en paralelo.

1.4 · El plan de cinco días

Cinco días hábiles, contados desde que TNI responde las tres confirmaciones. El objetivo del quinto día es que un prospecto creado por un vendedor de Guatemala aparezca en el sistema de TNI, con su vendedor y su bitácora, y que lo que TNI cambie vuelva esa noche.

DíaCatalizadoraTNIAl terminar el día
1
lunes
Mapeo contra el esquema real: las cuatro columnas obligatorias que faltan, las dos que no existen fuera, los largos y la fecha correcta. Corrección del estado 5 en la vista y en el código C1 · C10Recibe este documento. Responde las tres confirmaciones. Inicia la firma R5 · R1El mapeo escribe columnas que existen. Nada sale todavía hacia TNI.
2
martes
Los avisos: cuando el prospecto nace con dueño, cuando se contacta, cuando agenda, cuando se propone, cuando se gana o se pierde. Regla de datos mínimos C2 · C3Crea el usuario de escritura y abre el acceso de red R2 · R3Cada prospecto nuevo deja su aviso en la cola, retenido.
3
miércoles
Comprobación de duplicados contra la cartera de la oficina, guardado de los números que devuelve TNI, y la bitácora completa al espejo C4 · C5 · C6Agrega la columna anti-duplicados R4. Empieza la pestaña y la página del pase R6 · R7Primera corrida en sombra: el programa dice, fila por fila, qué escribiría en TNI. Se revisa con ustedes.
4
jueves
El paso de vuelta: lo que TNI cambie en la ficha, la bitácora o los servicios se aplica sobre el negocio. Revisión de la sombra contra veinte negocios reales C7Entrega la equivalencia de cargos y, si está lista, la llave pública R8 · R7Informe de sombra: las veinte filas exactas que se escribirían, para aprobación.
5
viernes
Encendido gradual con el documento firmado en el servidor: cinco prospectos reales de Guatemala, verificación en la pantalla de TNI, y apagado inmediato si algo no cuadra C8Confirma en su pantalla que los cinco prospectos están bienProspectos reales en el sistema de TNI, con vendedor y bitácora. La cola se abre al resto.

Si TNI responde esta semana

  • El viernes hay prospectos reales en su sistema, verificados en su pantalla.
  • El acceso con un clic queda probado contra el emisor de laboratorio, y se conecta a su llave el día que la publiquen.
  • El lunes siguiente se abre la cola completa y empieza el reporte semanal.

Si TNI se demora

  • Los cinco días entregan igual todo lo construido y probado en sombra: se ve exactamente qué se escribiría, sin escribir.
  • El encendido pasa a ser un trámite de dos horas el día que lleguen la firma y los accesos.
  • Lo único que no puede adelantarse es la verificación en su pantalla, que necesita filas reales.
Lo que puede mover la fecha

Las tres confirmaciones de TNI son el primer bloqueo: sin ellas el mapeo queda incompleto y la sombra no sirve para aprobar. La firma y los accesos son el segundo: sin ellos no hay escritura real, por decisión contractual y no técnica. Nada más del plan depende de terceros.

1.5 · Las dieciocho tareas, en orden

El detalle de cada una: las de TNI en la sección 2, las de Catalizadora en la 3.

TareaDónde, exactamenteQuiénCuándo
Bloque 1 · no depende de TNI · los negocios empiezan a encolarse sin salir hacia TNI
C1Rehacer el mapeo de alta contra el esquema real: sumar las cuatro columnas obligatorias que faltan, quitar las dos que no existen, recortar a los largos reales, poner la fecha de creaciónwriteback_mapping.py · mapping.ts · docs/32 · prueba en eval-mapeo-datos.pyCatalizadoraBloque 1
C2Los cinco avisos nuevos en la cola: alta con dueño, contactado, propuesta enviada, perdido, reactivado. Un índice único por evento y el candado en la función de reclamomigración nueva, molde de la 302 · app.dequeue_writeback_pendingCatalizadoraBloque 1
C10Corregir el estado 4 tomado como Potencial: es el 5gold.customer_mirror_opportunities · customer-mirror.ts · icp.jsonl · docs/57CatalizadoraBloque 1
Bloque 2 · verificable en sombra · necesita las confirmaciones de TNI
C3Datos mínimos: si falta teléfono, dirección o localidad el aviso queda retenido con motivo, y se libera cuando el dato llegaapp.writeback_outbox · estado held + held_reasonCatalizadoraBloque 2
C4Duplicados antes de escribir: teléfono exacto y nombre normalizado contra la cartera de la oficina; si existe, se enlaza y no se creabin/writeback.py · criterio de ya-es-cliente.tsCatalizadoraBloque 2
C5Guardar los números que devuelve TNI: cliente, contacto, servicio, propuestaapp.writeback_outbox +3 columnas · app.leads.payload · id_cliente_creado a la nocheCatalizadoraBloque 2
C6La bitácora de TNI viaja completa al espejo: siete columnas máspopulate 02 · silver.contactos +3 columnasCatalizadoraBloque 2
T4La columna anti-duplicados con índice único, en cuatro tablas y tres basesidempotency_key · requerimiento R4TNIBloque 2
T5Tres confirmaciones: estado 5 y tipo 1 para el prospecto; qué ponen en Identidad cuando no la tienen; «GlobalLink» como referencia nueva o «Otro»AuxStatusClientes · AuxTiposClientes · AuxTiposReferenciasTNIBloque 2
Bloque 3 · no depende de TNI
C7El paso de vuelta: cada noche, lo que TNI cambió en la ficha, la bitácora y los servicios se aplica sobre el negocioapp.reconciliar_prospectos_con_tni() · después de tn-espejo-base@CatalizadoraBloque 3
Bloque 4 · primera escritura real · se enciende de a un paso
T1Firmar la autorización; nosotros la depositamos en el servidor/etc/tn-globallink/writeback-addendum-firmadoTNIBloque 4
T2Usuario de escritura: sólo altas, cuatro tablas, tres basesuser_globallink_writer · requerimiento R2TNIBloque 4
T3Acceso de red a ese usuario desde nuestra única direcciónpuerto del SQL Server · IP por canal privadoTNIBloque 4
C8Operar el programa de escritura: cada 15 minutos, topes, reintentos, auditoría, alerta si se atascatn-writeback.timer · TN_WRITEBACK_APLICAR=1 · tn-alert-email.shCatalizadoraBloque 4
En paralelo · el acceso desde su sistema
C9El punto de entrada que recibe el pase, verifica y abre sesión; alta automática por número de empleadoapp/(auth)/exchange/route.ts · proxy.ts · public.users.id_empleado_tni · 2 tablas nuevasCatalizadoraParalelo
T6La pestaña «Truly Nolen GlobalLink» en su sistema, con el logo y el fragmento que entregamosplantilla del menú del sistema de gestiónTNIParalelo
T7La página que emite el pase firmado, y la llave pública publicadauna página nueva en su sitio · dbo.EmpleadosTNIParalelo
T8Qué cargo de su tabla de empleados equivale a cada rol de GlobalLinkapp.tni_cargo_rol, la llenan ellosTNIParalelo
↑ volver al resumen
2sección

Ocho requerimientos, cada uno con qué, por qué, exactamente cómo, y cómo lo verificamos

Están escritos para que su equipo técnico los ejecute sin preguntarnos nada, y para que un tercero pueda auditar que se cumplieron. Los valores que faltan (contraseña, dirección de nuestro servidor) viajan por canal privado, nunca por este documento.

En lenguaje llanoLo que TNI tiene que hacer o responder, escrito para que su equipo técnico lo ejecute sin preguntarnos. Cuatro son permisos, tres son respuestas y uno es una pestaña en su sistema.
  1. R1 · Firmar el addendum de escritura COT-0003-A1 v2

    Qué: la firma de José Lutz sobre el PDF ya redactado, que autoriza altas en cuatro tablas de las tres bases productivas.

    Por qué: el Term Sheet, cláusula 1, fija modalidad de sólo lectura; sin el addendum, escribir es incumplimiento.

    Exactamente cómo: nos devuelven el PDF firmado; nosotros lo depositamos en el servidor de la plataforma como /etc/tn-globallink/writeback-addendum-firmado. El programa de escritura comprueba la existencia y el tamaño de ese archivo antes de cada corrida; sin archivo, no emite ninguna alta.

    Cómo lo verificamos: el registro de cada corrida deja addendum_file_exists=true; el primer informe mensual lo cita.

    Cuándo: bloque 4, antes de la primera escritura real.

  2. R2 · Crear el usuario de escritura user_globallink_writer

    Qué: un login de SQL Server distinto del lector actual, con permiso de alta y lectura sobre exactamente cuatro tablas, en Truly, TrulyNew y TrulyRuso. Ningún permiso sobre TrulyGestion.

    Por qué: separar lectura de escritura permite revocar una sin tocar la otra, y acota el daño posible de cualquier incidente a altas en cuatro tablas. El SELECT sobre esas mismas cuatro tablas hace falta para que el alta devuelva el número asignado (OUTPUT INSERTED).

    Exactamente cómo:

    -- una vez, en el servidor
    CREATE LOGIN user_globallink_writer WITH PASSWORD = '…', CHECK_POLICY = ON, CHECK_EXPIRATION = OFF;
    -- en cada una de las tres bases productivas
    CREATE USER user_globallink_writer FOR LOGIN user_globallink_writer;
    GRANT INSERT, SELECT ON dbo.Clientes   TO user_globallink_writer;
    GRANT INSERT, SELECT ON dbo.Contactos  TO user_globallink_writer;
    GRANT INSERT, SELECT ON dbo.Servicios  TO user_globallink_writer;
    GRANT INSERT, SELECT ON dbo.Propuestas TO user_globallink_writer;
    DENY UPDATE, DELETE ON dbo.Clientes   TO user_globallink_writer;
    DENY UPDATE, DELETE ON dbo.Contactos  TO user_globallink_writer;
    DENY UPDATE, DELETE ON dbo.Servicios  TO user_globallink_writer;
    DENY UPDATE, DELETE ON dbo.Propuestas TO user_globallink_writer;
    -- nada más: sin EXECUTE, sin acceso a dbo.Usuarios ni dbo.Empleados, sin db_datawriter

    La contraseña se entrega por la bóveda de secretos de Catalizadora, no por correo ni chat; nosotros la guardamos en /etc/tn-globallink/sqlserver-writer.env con permisos 600.

    Cómo lo verificamos: desde nuestro servidor, con ese usuario, una consulta fn_my_permissions sobre las cuatro tablas tiene que devolver INSERT y SELECT y nada más, y un intento de UPDATE tiene que fallar con error 229. Ese resultado queda en el registro de la primera corrida.

    Cuándo: bloque 4, dentro de los 10 días hábiles siguientes a la firma, como fija la cláusula 4 del addendum.

  3. R3 · Habilitar el acceso de red al usuario de escritura

    Qué: permitir conexiones al puerto del SQL Server desde una única dirección IP, la del servidor de la plataforma, para ese login. La dirección se entrega por canal privado; es la misma desde la que hoy se conecta el usuario lector.

    Exactamente cómo: regla de firewall que admita esa IP/32 al puerto de la instancia; si el login se restringe por origen, la misma IP.

    Cómo lo verificamos: conexión de prueba desde nuestro servidor con el usuario de escritura, en modo sombra (sin insertar), registrada en el log de la corrida.

    Cuándo: bloque 4, junto con R2.

  4. R4 · Agregar la columna anti-duplicados idempotency_key

    Qué: una columna nueva, nula por omisión, con índice único filtrado, en dbo.Clientes, dbo.Contactos, dbo.Servicios y dbo.Propuestas, en las tres bases.

    Por qué: si nuestro programa reintenta un alta que sí llegó (corte de red después del INSERT), el motor rechaza la segunda con el índice único y no se duplica el cliente. Es la garantía de motor; sin ella, la única defensa es nuestra búsqueda previa.

    Exactamente cómo:

    -- repetir para Contactos, Servicios y Propuestas; en Truly, TrulyNew y TrulyRuso
    ALTER TABLE dbo.Clientes ADD idempotency_key UNIQUEIDENTIFIER NULL;
    CREATE UNIQUE NONCLUSTERED INDEX UX_Clientes_idempotency_key
      ON dbo.Clientes (idempotency_key) WHERE idempotency_key IS NOT NULL;

    Impacto: ninguno sobre datos ni pantallas existentes: las filas actuales quedan en nulo y el índice filtrado no las incluye. Su aplicación no necesita conocer la columna. Nota sobre el texto del addendum: la cláusula 4 la describe como NOT NULL con UNIQUE; esa forma rompería las filas existentes, que no tienen clave. La forma de acá, nula con índice filtrado, cumple el mismo fin sin tocar el histórico; conviene dejarlo escrito en la firma.

    Cómo lo verificamos: la columna aparece en el catálogo que leemos cada noche, y el primer reintento controlado en modo real devuelve el error 2601 esperado.

    Cuándo: bloque 2. Si no está cuando se encienda la escritura, el programa usa la marca en observaciones como única defensa, y lo dice en el informe.

  5. R5 · Confirmar tres convenciones de su modelo de datos

    Qué: tres respuestas cortas de quien conoce su sistema, sobre datos que leímos de su base y necesitamos escribir igual que ustedes.

    5a · Estado y tipo del prospecto. Leímos que el prospecto es IdStatusCliente = 5 («Potential»), y que Guatemala tiene 1.268 clientes así. Para IdTipoCliente proponemos 1 («Creative», venta activa) porque el prospecto sale de prospección; su oficina de Guatemala registra el 72 % de sus potenciales como 2 («Office»). ¿Confirman 5 y qué tipo quieren para los prospectos que vienen de GlobalLink?

    5b · Identidad. Su sistema llena Clientes.Identidad en el 100 % de los prospectos. Nosotros la tenemos sólo cuando el registro oficial del país la trae. ¿Qué escriben cuando no la conocen, o aceptan nulo?

    5c · Referencia. Para que el prospecto diga de dónde vino, proponemos IdTipoReferencia = 6 («Other») con ReferenciaOtra = 'TN GlobalLink · Rocket' o '· Manual'. Mejor aún sería una fila nueva «GlobalLink» en AuxTiposReferencias, que ustedes agregan y nosotros leemos del espejo. ¿Cuál prefieren?

    Cómo lo verificamos: los tres valores quedan como constantes del mapeo, con su origen citado, y aparecen en el primer alta de prueba.

    Cuándo: bloque 2, antes de probar el alta en sombra.

  6. R6 · La pestaña «Truly Nolen GlobalLink» en el sistema de gestión

    Qué: una pestaña más en la barra de menú, a la derecha de «Reports», visible para los perfiles que ustedes decidan. Nosotros entregamos el logo en PNG y SVG y el fragmento HTML.

    Exactamente cómo: la pestaña apunta a la página de R7, dentro de su propio sitio; no a GlobalLink. Fragmento de referencia, adaptable a su plantilla:

    <a class="tab" href="globallink.aspx" target="_blank" rel="noopener">
      <img src="img/tn-globallink.png" alt="" width="16" height="16"> Truly Nolen GlobalLink ↗
    </a>

    Cómo lo verificamos: con el usuario de prueba que ya tenemos, la pestaña aparece y abre la página de R7.

    Cuándo: en paralelo con el resto.

  7. R7 · La página que emite el pase de acceso, y la llave pública

    Qué: una página nueva en su sitio (por ejemplo /proposals/TG/globallink.aspx) que, para el usuario con sesión, arme un pase firmado y lo envíe a GlobalLink; y la llave pública publicada en una dirección fija.

    Por qué: es lo que permite entrar a GlobalLink sin segunda contraseña y sin que ninguna contraseña viaje. Su llave privada nunca sale de su servidor; nosotros sólo verificamos.

    Exactamente cómo: la especificación completa está en la sección 8 de esta página: contenido del pase, algoritmo, vigencia, formato de la llave pública, formulario de envío y errores. En resumen: JWT firmado con RS256 (llave RSA de 2048 bits o más), claims iss, aud, sub, iat, exp, nbf, jti, email, name, country, role, franchise_id, language, vigencia de 300 segundos, y envío por POST en un formulario que se envía solo. La llave pública, en formato JWKS, en https://www.trulynoleninternational.com/.well-known/jwks.json o en la dirección fija que ustedes definan.

    Datos que la página lee de dbo.Empleados: IdEmpleado (va en sub), Nombre y Apellido (en name), Email, IdPais (convertido a ISO de dos letras en country), IdOficina (en franchise_id) e IdCargo (convertido con la tabla de R8 en role).

    Si no pueden publicar una llave pública en su plataforma, la variante con secreto compartido (HS256) usa exactamente el mismo pase; cambia sólo la firma, y el secreto se intercambia por la bóveda. Lo decide su equipo.

    Cómo lo verificamos: un pase de prueba emitido por su página, verificado por nuestro punto de entrada en la dirección de pruebas, con las once comprobaciones de la sección 8.5 en verde y el registro de auditoría escrito.

    Cuándo: en paralelo. Del lado nuestro el punto de entrada se prueba antes con un emisor de laboratorio; el día que su llave esté publicada sólo cambia una dirección de configuración.

  8. R8 · La equivalencia entre sus cargos y los roles de GlobalLink

    Qué: una tabla de dos columnas: cada IdCargo de dbo.Empleados que vaya a entrar, y el rol de GlobalLink que le corresponde.

    Los siete roles de GlobalLink y qué pueden hacer: tni_admin ve toda la red · country_admin ve su país y reparte trabajo entre oficinas · auditor sólo lectura de toda la red · franchise_owner, franchise_admin y franchise_manager ven su oficina y reparten trabajo · franchise_user es el vendedor: ve y trabaja sus negocios. La especificación de mayo nombraba un rol tni_regional que no existe; su equivalente es country_admin.

    Exactamente cómo: nos mandan la tabla (hoja de cálculo o correo); nosotros la cargamos en app.tni_cargo_rol. Un cargo sin fila en esa tabla no entra: el pase se rechaza con un mensaje claro, no se adivina un rol.

    Cómo lo verificamos: un pase de prueba por cada cargo de la tabla abre GlobalLink con el rol esperado.

    Cuándo: en paralelo, antes de la primera prueba de acceso.

Lo que TNI no tiene que hacer. Ninguna pantalla nueva en su sistema para ver los prospectos: aparecen en Clientes, en la bitácora de cada cliente y en Sales > Services, como los suyos. Ningún cambio en su aplicación por la columna anti-duplicados. Ninguna carga de datos: todo lo que se escribe nace en GlobalLink.
↑ volver al resumen
3sección

Diez tareas, cada una con archivo, columna o sentencia

Las de TNI están en la sección 2. Éstas son las nuestras, en el orden de la tabla.

En lenguaje llanoCómo se construye cada pieza de nuestro lado, archivo por archivo. Es la parte para el equipo técnico; el CEO puede saltarla.

Catalizadora · 10 tareas

  1. Rehacer el mapeo de alta contra el esquema realHoy omite cuatro columnas obligatorias y escribe un nombre que no existe.
    Detalle

    Archivos: infra/workers/sqlserver_snapshot/lib/writeback_mapping.py (la lista COLS_CLIENTE y la función map_lead_to_tni_cliente), su espejo apps/web/lib/writeback/mapping.ts, y la tabla del addendum docs/32.

    Columnas de Clientes que pasa a mandar, en este orden: Cliente · IdOficina · IdStatusCliente=5 · IdTipoCliente=1 · CuentaNacional=0 · CuentaInternacional=0 · IdVendedor · IdTipoReferencia=6 · ReferenciaOtra · Direccion · Localidad · Provincia · Telefono · Movil · Email · Web · Rubro · Identidad · ContactoAdmin · ContactoAdminEmail · ContactoAdminTelefono · FechaAlta · Observaciones. Se quitan ContactoVentas, ContactoVentasEmail y ContactoVentasTel.

    Recortes al largo real: Cliente 100 · Localidad 500 · Telefono 150 · Movil 50 · Email 50 · Web 150 · Identidad 50 · ContactoAdmin 150 · ContactoAdminEmail 50 · ContactoAdminTelefono 150 · ReferenciaOtra 50. FechaAlta pasa de fecha_cierre a fecha_creacion, como fecha local de la oficina a las 00:00.

    Contactos (COLS_CONTACTO): IdCliente · IdOficina · IdMotivoContacto=1 · FechaContacto · HoraContacto · FechaProgramada · HoraInicio · HoraFin · IdVendedor · VentaDirecta=1 · Observaciones. Las horas se arman como datetime(1900,1,1,hh,mm).

    Servicios (COLS_SERVICIO): IdCliente · IdContacto=0 · IdVendedor · IdStatusServicio=1 · IdEtapaServicio=7 · VentaDirecta=1 · FechaAlta · FechaProgramada · HoraInicio=08:00 · HoraFin=09:00 · Observaciones. Propuestas (COLS_PROPUESTA): IdServicio · FechaPropuesta. Se quita Observaciones, que no existe en esa tabla y hoy se escribe.

    La prueba que faltaba: infra/bin/eval-mapeo-datos.py hoy recorre sólo infra/scripts/populate; se le suma infra/workers/sqlserver_snapshot/lib y una segunda regla: para cada tabla que se inserta, toda columna que el catálogo declare obligatoria y no sea de identidad tiene que estar en la lista. El piso vive en infra/eval/mapeo-datos-piso.json. Y tests/probar-writeback-mapping.py deja de afirmar sólo ausencias.

  2. Los avisos nuevos en la colaProspecto creado, contactado, propuesta enviada, perdido, reactivado. Cita y ganado ya existen.
    Detalle

    Una migración nueva en infra/schemas/, la siguiente al 426, con el molde de la 302.

    Disparador del alta, app.trg_lead_dueno_writeback: AFTER UPDATE OF status, assigned_rep_user_id ON app.leads. Dispara cuando la fila queda con dueño por primera vez: NEW.status = 'assigning_rep' (Rocket) o NEW.assigned_rep_user_id IS NOT NULL (vendedor), y antes no lo tenía. Va en UPDATE y no en INSERT porque el negocio se inserta en «new» y el dueño se escribe después, dentro de la misma transacción (lib/leads/dueno-al-nacer.ts). Encola evento = 'prospecto_creado' con: id_lead, prospecto_nombre, id_compania, id_contacto, id_oficina, pais_target, origen, metodo, destino, assigned_rep_user_id, fecha_creacion, ciudad, y de app.companias: industria, sitio_web, direccion, telefono, email; de app.contactos: nombre, email, telefono_e164, movil.

    Disparador de hitos, app.trg_lead_hito_writeback: AFTER UPDATE OF status. contacted → contactado · negotiation → propuesta_enviada (con proposal_amount_usd) · lost → perdido (con lost_reason) · new viniendo de lost → reactivado. La cita la produce ya la 302 (lead_potencial, con scheduled_at de app.lead_appointments) y el ganado la 043 (cierre_ganado).

    Un aviso por hito: un índice único parcial por evento, uq_writeback_outbox_<evento> ON app.writeback_outbox (id_lead) WHERE payload->>'evento' = '<evento>', como el uq_writeback_outbox_potencial de la 302. Reactivado admite repetición: su índice incluye la fecha.

    Orden: el programa no escribe un hito de un negocio cuyo aviso de alta no esté en synced; lo deja en pending con reintento a 15 minutos.

    Candado: app.dequeue_writeback_pending cambia el booleano p_incluir_potencial por p_eventos text[], por omisión sólo cierre_ganado; el cron pasa la lista completa sólo con la bandera tn:write-back encendida (lib/flags.ts, variable TN_FF_WRITE_BACK).

    Pruebas: un test unitario que lee el .sql y compara los literales de los eventos con lib/writeback/potencial.ts (ya existe ese patrón para lead_potencial) y un eval «todo negocio con dueño tiene su aviso de alta».

  3. La regla de datos mínimosTNI exige nombre, dirección, localidad y teléfono. Un prospecto sin eso no se encola.
    Detalle

    En la misma migración: el CHECK de app.writeback_outbox.status (hoy pending · processing · synced · failed · dlq) suma held, y una columna held_reason text. El disparador del alta encola en held con el motivo («falta teléfono») cuando falte alguno de los tres. Un disparador sobre app.companias y app.leads.ciudad pasa la fila a pending cuando el dato llega, sea por el enriquecimiento o por la ficha. La ficha del negocio muestra el motivo mientras esté retenido.

  4. La consulta de duplicados antes de escribirTeléfono exacto y nombre normalizado, acotados a la oficina.
    Detalle

    En bin/writeback.py, antes del INSERT y después de la búsqueda por marca GLOBALLINK que ya hace: (1) teléfono en dígitos, en forma internacional y local, igual exacto contra silver.clientes.telefono y movil, en estados 1 y 5, con el criterio que lib/sales/ya-es-cliente.ts ya aplica para rotular «ya es cliente», pero acotado a la oficina en vez de al país; (2) nombre normalizado con app.normalize_brand_name() igual exacto contra el nombre del cliente en esa oficina. No se usa el parecido por trigramas de customer-mirror.ts: acusa en falso. Si hay coincidencia: no se crea; el aviso queda synced con el id encontrado, el negocio guarda ese id, y se escribe un solo renglón «Lead» en la bitácora de TNI que dice «prospecto vinculado a cliente existente desde GlobalLink».

  5. Guardar los números que devuelve TNICliente, contacto, servicio y propuesta.
    Detalle

    app.writeback_outbox ya tiene tni_cliente_id; se suman tni_contacto_id, tni_servicio_id y tni_propuesta_id, enteros. Ojo con app.leads.id_cliente_creado: es una clave foránea a silver.clientes (migración 013), así que no se puede escribir en el momento del alta: la fila recién existe en el espejo a la noche. El número queda de inmediato en el aviso y en app.leads.payload.tni_cliente_id, y el paso de vuelta completa id_cliente_creado cuando el espejo trae la fila. Una vista app.v_lead_tni junta las tres cosas para la ficha.

  6. La bitácora de TNI viaja completa al espejoHoy se copian 5 de sus 21 columnas.
    Detalle

    infra/scripts/populate/02-silver-clientes-contactos.py, el INSERT a silver.contactos: sumar IdVendedor, IdMotivoContacto, HoraContacto, FechaProgramada, HoraInicio, HoraFin y VentaDirecta. silver.contactos ya tiene id_vendedor, hora_contacto, id_motivo_contacto y venta_directa; le faltan fecha_programada, hora_inicio y hora_fin: una migración las agrega. Sin esto, la vuelta no sabe quién hizo la visita ni cuándo estaba programada.

  7. El paso de vuelta después del espejoFicha, bitácora y servicios contra los negocios, cada noche.
    Detalle

    Una función en la base, app.reconciliar_prospectos_con_tni(), que corre al final de la carga nocturna de cada base (infra/systemd/tn-espejo-base@.service; la unidad tn-espejo-diario está retirada). Para cada negocio con número de cliente en TNI: aplica las reglas de la sección «Cruce de vuelta» leyendo silver.clientes, silver.contactos y silver.servicios; escribe la historia en app.lead_activity con actor_label = 'TNI' y los tipos que ya existen (stage_change, field_change, note, call_logged); cambia app.leads.status sólo hacia adelante, respetando la máquina de estados de lib/leads/stage-transitions.ts; ignora todo renglón de bitácora cuyas observaciones empiecen con GLOBALLINK. Deja su conteo en el log del espejo, para distinguir «corrió y no había cambios» de «no corrió».

  8. Operar el programa de escrituraTopes, reintentos, auditoría, alerta.
    Detalle

    Ya existen infra/systemd/tn-writeback.service y .timer, apagados a propósito. El timer corre cada 15 minutos; al encender, con --aplicar sólo si existe el documento firmado en el servidor y la variable TN_WRITEBACK_APLICAR=1. Alerta por infra/bin/tn-alert-email.sh si hay más de 50 avisos pendientes durante más de 30 minutos, que es lo que fija el addendum. Cada intento queda en app.writeback_log.

  9. El punto de entrada del accesoMás el logo, el fragmento y la especificación para su equipo.
    Detalle

    Ruta nueva apps/web/app/(auth)/exchange/route.ts, POST, declarada pública en apps/web/proxy.ts. Verifica con la biblioteca jose que la aplicación ya usa: llave pública remota con caché de una hora, emisor y destinatario fijos, tolerancia de 30 segundos, vida máxima de 5 minutos. El número único se guarda en una tabla nueva app.sso_pases_vistos (jti, visto_en) con limpieza a los 10 minutos. Busca o crea en public.users por external_sub, y llena una columna nueva public.users.id_empleado_tni con el número de empleado del pase. El rol sale de una tabla nueva app.tni_cargo_rol (id_cargo, role) que llena TNI. Abre sesión con signSession y setSessionCookie de lib/auth/session.ts, registra en app.user_sessions y deja el intento en audit.events.

    Configuración, por la bóveda: TNI_IDP_ISSUER y TNI_IDP_JWKS_URL. Prueba de punta a punta contra infra/mock-idp, que ya emite pases con este formato. Entrega a TNI: el logo, el fragmento HTML de la pestaña y del formulario de envío, y specs/JWT-SPEC.md actualizada con la dirección de hoy.

  10. Corregir el número de estado en el código de GlobalLinkHoy lee el 4 como «Potencial» y es «Cancelado por el cliente».
    Detalle

    Dónde vive el número equivocado: en la vista materializada gold.customer_mirror_opportunities, cuyo CASE etiqueta el 4 como «oportunidad entre países» y manda el 5, el potencial real, a «otro»; eso sí cambia el resultado de esa vista. En apps/web/lib/sales/customer-mirror.ts el 4 está en el comentario de la línea 140, y la lógica de la línea 182 sólo distingue el 1, así que ahí no cambia el comportamiento. También en el caso icp-004 de apps/web/evals/golden/icp.jsonl y en docs/57. Una migración recrea la vista con el 5, y un test unitario fija el catálogo: 1 · 2 · 3 · 4 · 5 con sus etiquetas.

Bloque 1
Tareas 1, 2 y 10 de CatalizadoraMapeo real, avisos, corrección del estado. Los negocios empiezan a encolarse sin salir hacia TNI.
no depende de TNI
Bloque 2
Tareas 3 a 6 de Catalizadora, y tareas 4 y 5 de TNIDatos mínimos, duplicados, números devueltos, bitácora completa al espejo; la columna anti-duplicados y las tres confirmaciones. Verificable en modo sombra.
confirmaciones de TNI
Bloque 3
Tarea 7El paso de vuelta, cada noche.
no depende de TNI
Bloque 4
Tarea 8 de Catalizadora, y tareas 1 a 3 de TNIFirma, usuario de escritura, acceso de red. Primera escritura real, encendida de a un paso.
accesos de TNI
En paralelo
Tarea 9 y tareas 6 a 8 de TNIEl acceso desde su sistema. Se prueba contra el emisor de laboratorio.
llave y pestaña
4sección

Negocio, compañía y contacto: qué es cada una, cómo se enlazan y a dónde va cada campo

En GlobalLink un prospecto no es una sola fila. La compañía es la empresa; el contacto es una persona de esa empresa; el negocio es la oportunidad que un vendedor trabaja con esa empresa, con una persona señalada. Una compañía puede tener varios contactos y varios negocios a lo largo del tiempo. TNI no separa así: su ficha de cliente junta empresa y hasta tres personas, y la oportunidad vive en la bitácora y en el servicio.

En lenguaje llanoEn GlobalLink la empresa, sus personas y la oportunidad de venta son tres cosas distintas. En el sistema de TNI viven juntas en una ficha. Acá se ve cómo se acomoda cada campo.
GLOBALLINK · TRES ENTIDADES TNI · CUATRO TABLAS Compañía · app.companias nombre · industria · sitio_web · direccion telefono · email · whatsapp · pais_iso una por empresa y país · nombre normalizado como llave 1 → N Contactos · app.contactos nombre · cargo · email · telefono_e164 movil · movil_tiene_whatsapp · linkedin_url varias personas por compañía · id_compania 1 → N Negocio · app.leads id_lead · id_compania · id_contacto id_oficina · pais_target · ciudad status · assigned_rep_user_id origen · metodo · score fecha_creacion · fecha_cierre proposal_amount_usd · lost_reason id_cliente_creado · payload la oportunidad: una compañía, una persona señalada, un vendedor, una etapa. Puede haber varios en el tiempo Copias planas en el negocio (contacto_nombre, contacto_email, contacto_telefono, contacto_movil) las escribe un solo mecanismo desde la entidad; la entidad manda (TN-2032). Negocio ← app.lead_appointments (la cita) · app.lead_activity (la historia) · app.writeback_outbox (los avisos) dbo.Clientes · una fila por negocio, deduplicada por compañía ← compañía: Cliente, Rubro, Direccion, Localidad, Telefono, Email, Web ← contacto señalado: ContactoAdmin, su correo, su teléfono, Movil dbo.Contactos · un renglón «Lead» por hito del negocio ← negocio: fechas, vendedor, cita, marca GLOBALLINK con el id del negocio dbo.Servicios + dbo.Propuestas · al ganar ← negocio: vendedor, fecha de cierre, etapa 7; la propuesta cuelga del servicio Los otros contactos de la compañía no viajan: la ficha de TNI tiene tres roles fijos (Admin, Ventas, Técnico) y el prospecto llena sólo el administrativo, como hace TNI. Un segundo negocio de la misma compañía reutiliza el IdCliente: no crea otra ficha.
La compañía y la persona señalada se funden en una fila de Clientes. El negocio no tiene tabla propia en TNI: es su bitácora, y al ganar, su servicio y su propuesta.
Entidad · campo en GlobalLinkQué es y de dónde saleA dónde va en TNICuándo
Compañía · app.companias · una por empresa y país; nace en el padrón o al crear el negocio (lib/leads/compania-del-negocio.ts)
nombre · nombre_normRazón social o marca; el normalizado es la llave que evita duplicar la misma empresaClientes.Cliente (100)alta
pais_iso · oficina_idPaís de dos letras y oficina que la encontróno viaja: el país va en la oficina y en la base
industriaGiro, del hallazgo o del enriquecimientoClientes.Rubroalta
direccionDirección física, del hallazgoClientes.Direccion · obligatoriaalta
telefonoTeléfono principal de la empresaClientes.Telefono (150) · obligatoriaalta
emailCorreo general de la empresaClientes.Email (50)alta
sitio_webSitioClientes.Web (150)alta
whatsappNúmero de WhatsApp descubierto para la empresa (mig 378)no viaja: Clientes no tiene columna de WhatsApp
enrichment_status · enrichment_payload · enriched_atEstado y resultado del enriquecimiento; de acá salen ciudad, departamento e identidad fiscal cuando el padrón las traeClientes.Localidad · Provincia · Identidadalta
creado_por_subQuién la dio de alta (mig 408)no viaja
Contacto · app.contactos · varias personas por compañía; el negocio señala una con id_contacto (mig 407)
nombre · cargoLa persona y su puestoClientes.ContactoAdmin (150) · el cargo no tiene columnaalta
emailCorreo de la personaClientes.ContactoAdminEmail (50)alta
telefono_e164Teléfono de la persona, en formato internacionalClientes.ContactoAdminTelefono (150) · en formato localalta
movil · movil_tiene_whatsappMóvil editado a mano y si tiene WhatsApp (mig 409)Clientes.Movil (50)alta
whatsapp · linkedin_url · instagram_url · x_url · fuente_urlCanales descubiertos y de dónde salió el dato (mig 378, 319)no viajan
los demás contactos de la compañíaPersonas no señaladas por el negociono viajan: la ficha tiene tres roles fijos
Negocio · app.leads · la oportunidad; señala compañía (id_compania, mig 369) y contacto (id_contacto, mig 407)
id_leadIdentificador del negocioClientes.Observaciones y Contactos.Observaciones, como marca GLOBALLINKalta y cada hito
id_oficinaOficina dueña, del alcance del usuarioIdOficina en las cuatro tablas, y la base de destinosiempre
assigned_rep_user_idEl vendedor dueño → número de empleado de TNIClientes.IdVendedor · Contactos.IdVendedor · Servicios.IdVendedorsiempre
statusEtapa: new · contacted · appointment_scheduled · negotiation · won · lost · tni_syncedno viaja como valor: cada cambio produce un renglón en Contactos; Clientes queda en estado 5cada hito
origen · metodoFuente del dato (13 valores) y lente con que se descubrió (mig 377)Clientes.Observaciones · IdTipoReferencia 6 + ReferenciaOtraalta
fecha_creacionCuándo nacióClientes.FechaAltaalta
ciudadLocalidad, del enriquecimientoClientes.Localidad (500) · obligatoriaalta
app.lead_appointments.scheduled_atLa cita agendadaContactos.FechaProgramada · HoraInicio · HoraFincita
proposal_amount_usdMonto propuestoContactos.Observaciones «propuesta enviada · USD …»propuesta
fecha_cierreCuándo se ganóServicios.FechaAlta · FechaProgramada · Propuestas.FechaPropuestaganado
lost_reasonPor qué se perdióContactos.Observaciones «perdido · motivo»perdido
id_cliente_creado · payload.tni_cliente_idEl número que TNI devolvió: la llave de la vuelta← Clientes.IdClientevuelta
score · win_probability · next_action · notasDatos de trabajo del vendedorno viajan
Tres reglas de la ramificación. Una compañía con un cliente ya existente en TNI no genera otra ficha: el negocio nuevo reutiliza ese IdCliente y sólo agrega renglones de bitácora. Un cambio de persona señalada en el negocio no modifica la ficha de TNI (no hay UPDATE): se registra como renglón de bitácora y, si TNI cambia el contacto de su lado, vuelve por el espejo. Y el padrón de prospección (app.derived_prospects) no viaja nunca: sólo lo que se convirtió en negocio.
↑ volver al resumen ↑ volver al resumen
5sección

Campo por campo, de GlobalLink a TNI

Columnas, tipos, largos y obligatoriedad leídos hoy del catálogo del SQL Server de TNI en sus tres bases productivas. Las convenciones (qué valor ponen, qué llenan siempre) se midieron sobre los 1.268 prospectos de la oficina de Guatemala, sus 1.364 gestiones de venta de los últimos 365 días y sus 919 servicios de los últimos 180 días.

En lenguaje llanoCada dato que sale de GlobalLink y en qué columna del sistema de TNI aterriza, con su tipo, su largo y si es obligatorio. Leído hoy del catálogo real.
va existe de los dos lados arreglar el código de hoy lo omite o lo escribe mal crear acá Catalizadora crear allá TNI
En GlobalLinkTransformaciónEn TNIEstado
Alta del prospecto → dbo.Clientes · una fila por negocio · las marcadas «obligatoria» no admiten nulo
app.leads.prospecto_nombretextoRecorte a 100 caracteresClientenvarchar(100) · obligatoriava
app.leads.id_oficinaentero · lleva la base adentroBase = id ÷ 1.000.000.000 (0 Truly · 1 TrulyNew · 2 TrulyRuso). IdOficina = restoIdOficinaint · obligatoriava
constanteSiempre 5. TNI: 1 Active · 2 Inactive · 3 Cancelled by TN · 4 Cancelled x customer · 5 PotentialIdStatusClienteint · obligatoriaarreglar hoy no se manda
constante1, «Creative / venta activa», porque el prospecto sale de prospección activa. Es una decisión nuestra, no la práctica de la 679: ahí el 72 % de los potenciales está en 2 «Office / venta receptiva» y el 24 % en 1. TNI confirmaIdTipoClienteint · obligatoriaarreglar hoy no se manda confirmar
constantes0 y 0 por omisión: no sabemos si es cuenta nacional. TNI llena los dos en el 100 % de sus prospectos y marca cuenta nacional en el 41 %CuentaNacional · CuentaInternacionalsmallintcrear acá
app.leads.assigned_rep_user_iduuid · el dueño del negocio→ número de empleado de TNI. Llega solo con el acceso desde su sistema (el pase trae el IdEmpleado); mientras tanto, cruce por correo contra silver.empleadosIdVendedorint → dbo.Empleados · TNI lo llena en el 97 %crear acá
destino del negociorocket · vendedor6 «Other / Otro» + texto «TN GlobalLink · Rocket» o «· Manual». Mejor aún: una fila «GlobalLink» nueva en el catálogo, que agrega TNIIdTipoReferencia · ReferenciaOtraint · nvarchar(50)crear acá catálogo
app.companias.direcciontextoTal cual. Si falta, el prospecto no se encolaDireccionnvarchar(max) · obligatoriaarreglar hoy no se manda
app.leads.ciudadtexto · del enriquecimientoTal cual. Si falta, el prospecto no se encolaLocalidadnvarchar(500) · obligatoria en Truly y TrulyNew, opcional en TrulyRusoarreglar hoy no se manda
enriquecimientodepartamentoTal cual, si existeProvincianvarchar(50)va
app.companias.telefonotextoFormato local del país, sin prefijo internacional, como guarda TNI. Si falta, el prospecto no se encolaTelefononvarchar(150) · obligatoriava
app.contactos.moviltextoFormato local, recorte a 50Movilnvarchar(50)va
app.companias.emailtextoRecorte a 50 caracteres: el campo de TNI es cortoEmailnvarchar(50)va
app.companias.sitio_webtextoRecorte a 150Webnvarchar(150)va
app.companias.industriatextoTal cualRubronvarchar(max) · TNI lo llena en el 3 %va
enriquecimiento del padrónNIT, RFC, RUT, CUITSólo si el registro oficial lo trajo. TNI la llena en el 100 % de sus prospectos: hay que preguntarles qué ponen cuando no la tienenIdentidadnvarchar(50)confirmar
app.contactos.nombre · email · telefono_e164la persona que decideAl contacto administrativo, que es el que TNI llena en el 100 % de sus prospectos; el de ventas sólo en el 7 %. El teléfono pasa a formato local, como el de la empresaContactoAdmin · ContactoAdminEmail · ContactoAdminTelefononvarchar(150) · (50) · (150)arreglar hoy va a ContactoVentas
app.leads.fecha_creacionfecha y hora con zonaFecha local de la oficina a las 00:00, que es como TNI la guarda. Hoy el código usa la fecha de cierreFechaAltadatetimearreglar
app.leads.id_lead · origen · metodometodo existe desde la migración 377GLOBALLINK:<id_lead> · origen=… · metodo=…Observacionesnvarchar(max)va
app.writeback_outbox.id_outboxuuid del avisoTal cualidempotency_keyuniqueidentifier · columna nueva con índice únicocrear allá
Cada hito → dbo.Contactos · un renglón «Lead» por aviso, como TNI registra sus visitas de venta
app.leads.id_cliente_creadoResto de la división por baseIdClienteint · obligatoriacrear acá
app.leads.id_oficinaMisma reglaIdOficinaint · obligatoriava
constanteSiempre 1 «Lead / Visita de venta». Los otros motivos del catálogo son «Callback» (2) y «Felicitaciones» (4); el reclamo va en otra columna, IdTipoReclamo, que queda vacíaIdMotivoContactoint · obligatoriaarreglar hoy no se manda
fecha del hitoFecha local a las 00:00FechaContactodatetime · obligatoriava
hora del hitoHora local sobre la fecha base 1900-01-01, que es la convención de TNIHoraContactodatetime · obligatoriaarreglar hoy no se manda
app.lead_appointments.scheduled_aten la cita; en los demás hitos, la fecha del hitoFecha a las 00:00; inicio y fin sobre 1900-01-01; por omisión 08:00 a 09:00, como TNI. TNI los llena en el 100 %FechaProgramada · HoraInicio · HoraFindatetimecrear acá
app.leads.assigned_rep_user_idNúmero de empleado, como arribaIdVendedorint · TNI 100 %crear acá
constanteSiempre 1. TNI: 1.358 de 1.364VentaDirectasmallintcrear acá
tipo de aviso + detalleGLOBALLINK:<id_lead>:<hito> · detalleObservacionesnvarchar(max) en Truly y TrulyNew, ntext en TrulyRusova
app.writeback_outbox.id_outboxTal cualidempotency_keycolumna nuevacrear allá
Ganado → dbo.Servicios y dbo.Propuestas · diez columnas obligatorias en Servicios sin contar la de identidad; el código de hoy manda dos
app.leads.id_cliente_creadoResto de la división por baseServicios.IdClienteint · obligatoriava
constante0. En los 919 servicios recientes de la 679 el contacto enlazado es 0; la columna es obligatoria, así que ése es el valor que TNI escribe. No se leyeron claves foráneasServicios.IdContactoint · obligatoriaarreglar
app.leads.assigned_rep_user_idNúmero de empleadoServicios.IdVendedorint · obligatoriaarreglar
constantesEstado 1 «Active»; etapa 7 «Proposal Accepted, Schedule Service», la etapa en la que están los servicios de clientes potenciales de la 679 (21 de 23); venta directa 1IdStatusServicio · IdEtapaServicio · VentaDirectaint · int · smallint · obligatoriasarreglar
app.leads.fecha_cierreFecha a las 00:00; programada el mismo día; horas 08:00 a 09:00 sobre 1900-01-01FechaAlta · FechaProgramada · HoraInicio · HoraFindatetime · obligatoriasarreglar
IdServicio devueltoTal cualPropuestas.IdServicioint · la única obligatoria en Truly y TrulyRuso; TrulyNew exige además IdTipoEdificio, Contrato y Periodicova TrulyNew
marca de trazabilidadNo existe columna de observaciones en Propuestas: la marca va en el servicio y en la clave anti-duplicadosPropuestas.ObservacionesNO EXISTE · hoy el código la escribearreglar
Lo que no se escribe. CodigoCliente lo numera la oficina (14 % de sus prospectos lo tienen). ContactoVentas y ContactoTecnico son otros roles. Naturaleza, IdZona, Sucursal los completa la oficina cuando los conoce. El país no existe como columna: va implícito en la oficina y en la base.
↑ volver al resumen
6sección

Lo que TNI cambia, y cómo llega a GlobalLink

El espejo nocturno ya copia la ficha de clientes, la bitácora de contactos y los servicios de TNI, completos, cada noche. Existe un modo que trae sólo las filas que cambiaron, con la marca que pone el motor de SQL Server en cada modificación; se midió en agosto y está apagado desde el 12 de agosto por un defecto en su marca de avance. Lo que falta para la vuelta es el paso siguiente: comparar lo que llegó contra los negocios y aplicar el cambio.

En lenguaje llanoCada cosa que TNI cambie en su sistema, y qué efecto tiene en GlobalLink a la mañana siguiente.
Cambió en TNILlega por el espejo aEfecto en GlobalLinkEstado
La llave que une los dos lados
Clientes.IdClientelo asignó TNI al crearsilver.clientes.id_clientecon la base sumadaSe busca el negocio por app.leads.id_cliente_creado. Sin esa columna escrita no hay vuelta posiblecrear acá
La ficha del cliente
Clientes.IdStatusCliente5 → 1 · TNI lo activósilver.clientes.id_status_clienteEl negocio pasa a ganado si no lo estaba, y luego a sincronizado. Fecha de cierre: la de TNIcrear acá
Clientes.IdStatusCliente→ 3 «Cancelled by TN» · → 4 «Cancelled x customer»silver.clientes.id_status_clienteEl negocio pasa a perdido, con el motivo que TNI eligiócrear acá
Clientes.IdStatusCliente→ 2 «Inactive»silver.clientes.id_status_clienteSe anota en la línea de tiempo y se avisa al gerente: un prospecto no debería volverse inactivocrear acá
Clientes.IdVendedorcambiósilver.clientes.id_vendedorSe reasigna el dueño por el número de empleado. Si ese empleado no tiene usuario en GlobalLink, se avisa al gerente de la oficinacrear acá
Clientes.Telefono · Movil · Email · Direccion · Localidadsilver.clientes.telefono · movil · email · direccion · localidadSe actualizan la empresa y el contacto en GlobalLink. TNI manda sobre los datos del cliente una vez creadocrear acá
Clientes.ContactoAdmin · Email · Telefonosilver.clientes.contacto_admin · _email · _telSe actualiza la persona de contacto del negociocrear acá
La bitácora de contactos
Contactosrenglón nuevo para ese IdCliente, sin la marca GLOBALLINKsilver.contactoshoy trae 5 columnas; sumar IdVendedor, IdMotivoContacto, HoraContacto, FechaProgramada, HoraInicio, HoraFin, VentaDirectaEntra a la línea de tiempo como «visita o llamada registrada en TNI por …». Si el negocio estaba en «nuevo», pasa a «contactado»crear acá
Contactosrenglón con la marca GLOBALLINKsilver.contactosSe ignora: lo escribimos nosotros. Sin esta regla, cada hito volvería como si TNI lo hubiera anotadocrear acá
Los servicios
Servicios.IdEtapaServicio→ 8 «Service Started»silver.servicios.id_etapa_servicioEl negocio pasa a ganado y sincronizado, aunque la ficha siga en 5crear acá
Servicios.IdStatusServicio→ 3 o 4, canceladosilver.servicios.id_status_servicioEl negocio pasa a perdido, con el motivocrear acá
Serviciosservicio nuevo para ese IdCliente sin la marcasilver.serviciosSe contrató por fuera de GlobalLink: el negocio pasa a ganado con la fecha del serviciocrear acá
Cuándo. La base de Guatemala se fotografía a las 04:00 UTC, las 22:00 de Guatemala (TrulyNew a las 03:30 y TrulyRuso a las 03:00). El cambio hecho en TNI durante el día se ve en GlobalLink a la mañana siguiente. Para una cadencia más corta hace falta reactivar el modo que trae sólo lo que cambió: medido en agosto, son 541 filas de clientes por día en Truly y 23 en TrulyNew; TrulyRuso no se midió.
↑ volver al resumen
7sección

Un vendedor, un prospecto, catorce pasos

Carlos vende en la oficina de Guatemala, que ya tiene 1.268 clientes en estado Potencial inscritos a mano en el sistema de TNI. Así es su día con los dos sistemas conectados.

En lenguaje llanoUn vendedor real de Guatemala, de principio a fin, con lo que ve en cada pantalla y de quién es cada paso.
Catalizadora lo construye y lo opera Catalizadora TNI lo pone o lo hace TNI ya existe funciona hoy
  1. 1
    Carlos entra al TNI Management System, como todos los días

    Con su usuario de siempre. Ese usuario vive en la tabla de empleados de TNI, con su oficina y su cargo. Nada cambia en su sistema.

    TNI ya existe
  2. 2
    Hace clic en la pestaña «Truly Nolen GlobalLink»

    Su sistema arma un pase con su número de empleado, su nombre, su país, su oficina y su cargo, lo firma, y lo manda a GlobalLink. Dura cinco minutos y sirve una vez.

    TNI Management System
    HomeReports ▾Truly Nolen GlobalLink ↗
    TNI pone la pestaña y la página que emite el pase · Catalizadora entrega el logo, el fragmento y la especificación
  3. 3
    GlobalLink lo recibe ya identificado

    Verifica la firma y abre su tablero de Guatemala, oficina 679, con permisos de vendedor. Como el pase trae su número de empleado, GlobalLink ya sabe quién es Carlos en TNI: no hace falta cruzar nada a mano.

    TN GlobalLink Oficina 679 · Carlos
    Entró desde el sistema de gestión.
    Catalizadora construye el punto de entrada que verifica el pase
  4. 4
    Prospecta

    En «Prospectar» busca por sector, por zona o deja que el sistema le proponga. Encuentra el Hotel Los Volcanes, con dirección, teléfono, correo y la persona que decide.

    Catalizadora ya existe
  5. 5
    Decide cómo trabajarlo: Modo Manual o Truly Rocket

    Modo Manual: se lo queda él. Truly Rocket: el bot hace los primeros toques y le entrega la cita. En los dos casos el negocio nace con dueño.

    Prospecto · Hotel Los Volcanes
    Hotel Los VolcanesHotelería · Zona 10, Guatemala · +502 2xxx xxxxContacto: María Pérez, gerente de operaciones
    Enviar a Truly RocketModo Manual
    Catalizadora ya existe
  6. 6
    En segundos nace el negocio en GlobalLink; en minutos, el cliente en TNI

    GlobalLink crea la empresa, la persona y el negocio, y deja el aviso en la cola. El programa de escritura elige la base de Guatemala, comprueba que el hotel no exista ya en la cartera de la 679, y crea la ficha en «Clientes», que es donde Guatemala da de alta a sus prospectos en 2026 (su módulo «Gestión de Ventas / Prospectos» no recibe altas desde noviembre de 2025). La crea exactamente como TNI crea las suyas: estado 5 Potencial, Carlos como vendedor, la persona como contacto administrativo, referencia «Otro: TN GlobalLink», y el tipo de cliente que TNI confirme (proponemos «venta activa»). TNI devuelve el número de cliente y queda guardado en el negocio.

    Catalizadora el aviso, el programa, el mapeo · TNI el usuario de escritura y la columna anti-duplicados
  7. 7
    Carlos lo ve en TNI, en su lista de clientes

    Vuelve a su sistema, filtra por estado Potencial y ahí está, con su nombre como vendedor. Igual que los otros 1.268 potenciales de la oficina.

    TNI Management System > Ventas > Clientes · Status: Potencial
    IDOficinaCódigoClienteVendedorLocalidadStatus
    588xxxGUATEHOTEL LOS VOLCANES
    14 calle 3-20 zona 10
    MORALES, C.GuatemalaPotencial
    587997GUATECLINICA SANTA LUCIA
    Av. Reforma 12-01 zona 9
    RAMIREZ, A.GuatemalaPotencial
    Reconstrucción con las columnas y colores reales de la pantalla «Clientes» de su sistema: ID, Oficina, Código, Cliente con dirección, Vendedor, Localidad y Status en amarillo para Potencial. El código lo asigna la oficina.
    TNI su pantalla de siempre; nada que construir
  8. 8
    Primer contacto

    Carlos llama o escribe desde GlobalLink; si eligió Rocket, el bot manda el primer correo. En TNI aparece un renglón en la bitácora del cliente con motivo «Lead», que es como TNI registra sus gestiones de venta: las 1.364 del último año en la 679 llevan ese motivo, 1.358 de ellas marcadas como venta directa.

    Catalizadora
  9. 9
    Agenda la visita técnica

    La agende él o la agende el bot. En TNI aparece un renglón «Lead» con fecha programada y hora de inicio y fin, que es exactamente la forma en que TNI anota una visita de venta. Es el momento en que el prospecto pasa a «potencial» según lo acordado en la reunión del 10 de agosto, que quedó escrito en la migración 302.

    TNI Management System > Customer: HOTEL LOS VOLCANES > Contacts
    DateReasonScheduledSales PersonNotes
    04/09/2026Lead04/09/2026 08:00–09:00Carlos M.GLOBALLINK · contactado · correo
    05/09/2026Lead09/09/2026 10:00–11:00Carlos M.GLOBALLINK · cita agendada
    Catalizadora
  10. 10
    Hace la visita y anota el resultado en TNI, como hace hoy

    Carlos registra la visita en el sistema de gestión, no en GlobalLink. Esa noche, el espejo trae ese renglón y GlobalLink lo pone en la línea de tiempo del negocio.

    Negocio · Hotel Los Volcanes · línea de tiempo
    • 9 sep
      Visita registrada en TNI por Carlos M.«Recorrido de cocina y bodega. Interés en plan mensual.»
    • 5 sepCita agendada para el 9 de septiembre, 10:00
    • 4 sepCorreo enviado · primer contacto
    • 4 sepNegocio creado · Modo Manual · cliente TNI n.º 588.214
    TNI Carlos usa su sistema · Catalizadora el paso de vuelta que lee el espejo y lo aplica
  11. 11
    Manda la propuesta

    Desde GlobalLink, con el monto. En TNI: otro renglón «Lead» en la bitácora con «propuesta enviada · USD 1.800 anuales».

    Catalizadora
  12. 12
    Gana

    Carlos cierra el negocio en GlobalLink. El programa crea en TNI el servicio en la etapa «Propuesta aceptada, programar servicio», la etapa en la que están los servicios de los clientes potenciales de la oficina, y la propuesta colgando de él. Aparece en el reporte Sales > Services > List de la oficina. La ficha sigue en Potencial: activarla es de TNI.

    TNI - List of Services · Office=Guatemala, Service Status=Active · Sorted by Office
    ServiceIdIdCustomerCustomerSales PersonAddressZoneStatusServiceStartDateEndDateFrequencyValor
    1035xxx588xxxHOTEL LOS VOLCANESMORALES C.14 CALLE 3-20 ZONA 10ZONA 10Active09/15/202609/14/2027Monthly150.00
    Reconstrucción sobre la pantalla real capturada el 3 de septiembre con el usuario de prueba: las once columnas y el encabezado son los del reporte; la fila es de ejemplo.
    Catalizadora ya existe, ajustando las columnas obligatorias
  13. 13
    TNI programa, inicia y factura

    Con su flujo de siempre: el servicio pasa a «iniciado» y la ficha de Potencial a Activo. Nadie de Catalizadora toca esa ficha ni ese servicio.

    TNI ya existe
  14. 14
    Esa noche, GlobalLink cierra el ciclo

    El espejo trae el cambio y el negocio queda como sincronizado con TNI. Si Carlos no lo había cerrado, se cierra solo. Si en cambio TNI cancela el servicio o da de baja la ficha, el negocio pasa a perdido, con el motivo que TNI puso.

    Catalizadora
↑ volver al resumen
8sección

Especificación del pase de acceso, para construir y para auditar

Base: la especificación acordada entre los equipos técnicos el 5 de mayo de 2026, actualizada con la dirección real de la plataforma, los roles reales y dos endurecimientos: el pase viaja por POST y el rol se resuelve por una tabla de equivalencias. Lo que sigue es el contrato completo.

En lenguaje llanoCómo se entra a GlobalLink desde el sistema de TNI sin escribir otra contraseña, y por qué es seguro. Escrito para poder construirlo y auditarlo.
👤 PABLO ESTRADA | TNI | 🔒 Close | [TRULY]
TNI Management System
Home
Reports ▾
Truly Nolen GlobalLink ↗
TNI Management System > Sales > Services > List

Cabecera y pestañas copiadas de la pantalla real del 3 de septiembre; la pestaña nueva es la propuesta.

8.1 · Las partes y sus direcciones

ElementoValorQuién lo pone
Emisor (iss)https://www.trulynoleninternational.com propuesto; TNI confirmaTNI · el valor que confirmen se configura en GlobalLink y debe coincidir letra por letra en cada pase
Llave pública (JWKS)https://www.trulynoleninternational.com/.well-known/jwks.json propuesto; TNI confirmaTNI · o la dirección fija que definan; se configura una vez en GlobalLink
Página emisora/proposals/TG/globallink.aspx nombre a elección de TNITNI · requiere sesión del sistema de gestión
Destinatario (aud)tn-globallinkFijo
Punto de entradaPOST https://app.trulynolen.tech/globalink/auth/exchangeCatalizadora · público, sin sesión previa, declarado en apps/web/proxy.ts
Destino tras entrarhttps://app.trulynolen.tech/globalink/ el tablero de la oficina del usuarioCatalizadora
Página de errorhttps://app.trulynolen.tech/globalink/auth/exchange/error?motivo=…Catalizadora · con enlace de vuelta al sistema de gestión

8.2 · El pase: cabecera y contenido

Un JWT (RFC 7519) firmado con RS256 (RSA con SHA-256, llave de 2048 bits como mínimo). Cabecera y contenido:

// cabecera
{ "alg": "RS256", "typ": "JWT", "kid": "tni-2026-09" }
// contenido · ejemplo de un vendedor de Guatemala
{
  "iss": "https://www.trulynoleninternational.com",
  "aud": "tn-globallink",
  "sub": "22059",                      // dbo.Empleados.IdEmpleado, como texto
  "iat": 1788500000,                   // emitido, segundos Unix UTC
  "nbf": 1788500000,                   // no antes de · igual a iat
  "exp": 1788500300,                   // vence · iat + 300 como máximo
  "jti": "5f1c9b2e-7a3d-4e0b-9c61-2f8d4a7b3c10",  // único por pase, UUID v4
  "email": "carlos.morales@trulynolen.com.gt",   // dbo.Empleados.Email
  "name": "Carlos Morales",            // Nombre + Apellido
  "country": "gt",                     // ISO 3166-1 alfa-2 en minúsculas, desde IdPais
  "franchise_id": "679",               // dbo.Empleados.IdOficina, como texto
  "role": "franchise_user",            // resuelto con la tabla de cargos (R8)
  "language": "es"                     // ISO 639-1 · opcional
}
CampoTipoObligatorioRegla
isstextoIgual al emisor configurado. Cualquier otro valor: rechazo.
audtextoExactamente tn-globallink.
subtextoEl IdEmpleado de TNI. Es la identidad estable: si cambia el correo o el nombre, la cuenta sigue siendo la misma.
iat · nbf · expenterosí · no · síSegundos Unix en UTC. exp − iat ≤ 300. Tolerancia de reloj: 30 segundos en cada extremo.
jtitextoUUID v4, nuevo en cada pase. GlobalLink lo guarda 10 minutos y rechaza repeticiones.
emailtextoRFC 5322. Se guarda y se usa para avisos; no es la llave de la cuenta.
nametextoUTF-8, hasta 200 caracteres.
countrytextoDos letras minúsculas. Tiene que existir como país activo en GlobalLink; si no, rechazo con motivo pais_no_habilitado.
franchise_idtextosi el rol es de oficinaEl IdOficina de TNI, como texto. Tiene que existir en el espejo y pertenecer al país del pase.
roletextoUno de los siete roles de GlobalLink: tni_admin · country_admin · auditor · franchise_owner · franchise_admin · franchise_manager · franchise_user. El tni_regional de la especificación de mayo no existe: es country_admin. Si TNI prefiere no resolver el rol en su página, puede mandar cargo con el IdCargo y GlobalLink lo resuelve con la tabla de R8.
languagetextonoUno de los diez idiomas de la plataforma; si falta, el del país.

8.3 · La llave pública

Documento JWKS (RFC 7517) en la dirección fija, servido por HTTPS con TLS 1.2 o superior, cacheable. Cada llave lleva kid, kty: "RSA", use: "sig", alg: "RS256", n y e. Para rotar, TNI publica la llave nueva junto a la vieja, empieza a firmar con la nueva, y retira la vieja después de 10 minutos; GlobalLink guarda el documento una hora y lo vuelve a pedir cuando llega un kid que no conoce. La llave privada vive en el servidor de TNI (almacén de certificados de Windows o equivalente) y nunca en código ni en variables de entorno.

8.4 · Cómo se envía el pase

La página emisora de TNI devuelve un documento HTML mínimo con un formulario que se envía solo. El pase viaja en el cuerpo, no en la dirección: así no queda en el historial del navegador, ni en el Referer, ni en los registros de ningún servidor intermedio.

<!doctype html><meta charset="utf-8"><title>Entrando a GlobalLink…</title>
<form id="f" method="post" action="https://app.trulynolen.tech/globalink/auth/exchange">
  <input type="hidden" name="token" value="eyJhbGciOiJSUzI1NiIs…">
  <noscript><button>Entrar a GlobalLink</button></noscript>
</form>
<script>document.getElementById("f").submit();</script>

Cabeceras de la respuesta de esa página: Cache-Control: no-store y Referrer-Policy: no-referrer. Un pase nunca se escribe en un log del lado de TNI.

8.5 · Qué hace GlobalLink al recibirlo, en orden, y qué devuelve si falla

#ComprobaciónSi fallaMotivo registrado
1El cuerpo trae token con forma de JWT (tres partes) y menos de 8 KB.400pase_malformado
2La cabecera dice alg: RS256 y trae kid. Ningún otro algoritmo se acepta, incluido none.401algoritmo_no_admitido
3Existe una llave con ese kid en el JWKS (de caché o refrescado una vez) y la firma verifica.401firma_invalida · kid_desconocido
4iss y aud coinciden con lo configurado.401emisor_o_destinatario_incorrecto
5nbf ≤ ahora + 30 s, exp > ahora − 30 s, exp − iat ≤ 300.401pase_vencido · pase_demasiado_largo
6jti no visto en los últimos 10 minutos. Se inserta en app.sso_pases_vistos en la misma transacción; si ya estaba, es un reintento o un replay.401pase_repetido
7country existe y está activo; si el rol es de oficina, franchise_id existe en silver.oficinas y pertenece a ese país.403pais_no_habilitado · oficina_no_habilitada
8role es uno de los siete; o cargo tiene fila en app.tni_cargo_rol.403rol_no_admitido · cargo_sin_equivalencia
9Cuenta: se busca en public.users por id_empleado_tni = sub. Si no existe, se crea con external_sub = 'tni:' + sub, correo, nombre, rol, oficina y país del pase. Si existe, se actualizan nombre, correo, rol, oficina y país, y si estaba inactiva se rechaza.403cuenta_desactivada
10Sesión: cookie tn_session firmada por GlobalLink (HS256, secreto propio), HttpOnly, Secure en producción, SameSite=Lax, Path=/, vida de 8 horas que se renueva con actividad, ligada a una fila en app.user_sessions con su jti, dispositivo, IP y ubicación aproximada. Es la misma sesión que abre el login normal.
11Respuesta: 303 See Other al tablero de la oficina. El pase no se guarda en ningún lado; ya no sirve.

Cada intento, exitoso o no, escribe una fila en audit.events con action = 'auth.exchange', la IP y el agente de usuario en sus columnas, y dentro de details el resultado, el motivo, el sub y el jti. El pase mismo nunca se registra; el correo y el nombre se escriben sólo en esa tabla de auditoría, no en los registros de aplicación. Limite: 30 intentos por minuto por IP; por encima, 429 sin detalle.

8.6 · Qué ve la persona si algo falla

Una página en el idioma del pase, o en español si no lo trae, con un texto por motivo, sin jerga y sin datos técnicos: «Este acceso venció; vuelve al sistema de gestión y haz clic otra vez» · «Tu oficina todavía no está habilitada en GlobalLink; avisa a tu gerente» · «Tu cargo no tiene un rol asignado en GlobalLink; avisa a TNI». Siempre con el enlace de vuelta al sistema de gestión. Los detalles técnicos quedan en la auditoría, no en pantalla.

8.7 · Altas, bajas y cambios de usuarios

  • Alta: automática en el primer acceso, con los datos del pase. No hay invitación ni contraseña de GlobalLink.
  • Cambio: nombre, correo, oficina y rol se actualizan en cada acceso desde el pase. TNI manda.
  • Baja: cuando TNI da de baja al empleado, su página deja de emitirle pases y el acceso se corta solo; además, cada noche el espejo trae Empleados.FechaBaja y GlobalLink desactiva la cuenta y revoca sus sesiones abiertas. Ese motivo de revocación no existe hoy en app.user_sessions (admite cierre de sesión, revocación remota, revocación por administrador, vencimiento y fusión de oficinas): la migración del acceso agrega employee_offboarded.
  • Convivencia con el login normal: una cuenta creada por pase puede recibir después una contraseña de GlobalLink si TNI lo pide; una cuenta creada a mano queda ligada al empleado la primera vez que entra por pase con el mismo correo.

8.8 · Qué entrega cada lado, y cómo se prueba

TNI entrega

  • La dirección del JWKS y un pase de prueba firmado con la llave real, para un usuario de prueba de Guatemala.
  • La página emisora en su sitio y la pestaña en el menú (R6, R7).
  • La tabla de equivalencia de cargos (R8).
  • Un contacto técnico para la prueba conjunta.

Catalizadora entrega

  • El punto de entrada en producción con las once comprobaciones, y el mismo en la dirección de pruebas.
  • Esta especificación como documento versionado (specs/JWT-SPEC.md, versión 2), el logo en PNG y SVG, y el fragmento de la pestaña y del formulario.
  • Un emisor de laboratorio (infra/mock-idp) para que su equipo pueda probar su página contra un punto de entrada real antes de publicar la llave. Hoy ese emisor y la especificación de mayo todavía usan el rol tni_regional; se actualizan a los siete roles reales junto con la versión 2 de la especificación, antes de la primera prueba.
  • El registro de auditoría consultable por el rol auditor.
#Prueba de aceptaciónResultado esperado
P1Pase válido de un vendedor de GuatemalaEntra al tablero de la oficina 679 con rol de vendedor; fila en auditoría con resultado ok
P2El mismo pase enviado dos vecesLa segunda vez: 401 pase_repetido
P3Pase con exp a 10 minutos401 pase_demasiado_largo
P4Pase firmado con otra llave, o con alg: none401 firma_invalida o algoritmo_no_admitido
P5Pase con un cargo sin fila en la tabla403 cargo_sin_equivalencia, con la página de error y el enlace de vuelta
P6Pase de un empleado nuevoSe crea la cuenta y entra; public.users.id_empleado_tni queda escrito
P7Pase de un empleado con fecha de baja en el espejo403 cuenta_desactivada; sus sesiones anteriores revocadas
P8JWKS inaccesible durante la pruebaSi hay caché vigente, entra; si no, 503 con mensaje y reintento sugerido; nunca se acepta un pase sin verificar
↑ volver al resumen
9sección

Para quien necesite el fondo

En lenguaje llanoEl fondo: diagramas, sentencias, reglas de conflicto y la lista completa de lo que se verificó hoy contra la base de TNI.
Los dos sentidos, en un diagrama
Anexo · Los dos sentidos

Lo que GlobalLink escribe en minutos, y lo que TNI cambia vuelve cada noche

El prospecto vive en la ficha de clientes de TNI en estado 5, Potencial. Cada avance es un renglón «Lead» en su bitácora. Al ganar, nace el servicio en la etapa que TNI usa para eso. Lo que TNI cambie vuelve por el espejo.

GLOBALLINK SISTEMA DE GESTIÓN DE TNI TIEMPO ↓ 1 · Nace el prospecto con dueño «Enviar a Truly Rocket» o «Modo Manual» + vendedor app.leads · aviso prospecto_creado minutos Alta en dbo.Clientes IdStatusCliente 5 · IdTipoCliente 1 · IdVendedor · ContactoAdmin devuelve IdCliente → queda guardado en el negocio IdCliente → app.leads.id_cliente_creado 2 · Primer contacto correo, WhatsApp o llamada; por Rocket o por el vendedor Renglón «Lead» en dbo.Contactos IdMotivoContacto 1 · FechaContacto · HoraContacto · IdVendedor 3 · Cita agendada la agende el bot o el vendedor · el «Potencial» de la reunión del 10 de agosto Renglón «Lead» con la visita programada FechaProgramada · HoraInicio · HoraFin · VentaDirecta 1 4 · Propuesta enviada el negocio entra a negociación, con monto Renglón «Lead» en dbo.Contactos Observaciones «propuesta enviada · USD …» 5 · Ganado el vendedor cierra en GlobalLink aviso cierre_ganado · ya construido, con columnas por ajustar Alta en dbo.Servicios y dbo.Propuestas IdEtapaServicio 7 · IdStatusServicio 1 · IdVendedor · fechas la ficha sigue en 5: TNI la activa cuando inicia y factura de vuelta · espejo nocturno · sólo lo que cambió A · TNI inicia el servicio y activa al cliente Servicios.IdEtapaServicio → 8 · Clientes.IdStatusCliente 5 → 1 su flujo de siempre; nosotros no tocamos la ficha ni el servicio esa noche El negocio queda cerrado y sincronizado app.leads.status → won → tni_synced si el vendedor no lo había cerrado, se cierra solo, con la fecha de TNI B · Alguien anota una visita o llamada en TNI renglón nuevo en Contactos, sin la marca GLOBALLINK los que llevan la marca son nuestros y se ignoran al volver Aparece en la línea de tiempo del negocio app.lead_activity · «visita registrada en TNI por …» si estaba en «nuevo», pasa a «contactado» C · TNI cambia el vendedor, el teléfono o el contacto Clientes.IdVendedor · Telefono · Email · ContactoAdmin* la ficha de TNI manda sobre los datos del cliente Se reasigna el dueño y se actualiza la ficha app.leads.assigned_rep_user_id · app.contactos · app.companias cada cambio queda anotado como «cambio hecho en TNI» D · TNI cancela o da de baja Clientes.IdStatusCliente → 3 o 4 · Servicios.IdStatusServicio → 3 o 4 El negocio pasa a perdido app.leads.status → lost · motivo «cancelado por TN» o «por el cliente» se puede reactivar desde GlobalLink: vuelve a nacer como en el paso 1 6 · Perdido desde GlobalLink con motivo obligatorio; la ficha de TNI no se toca Renglón «Lead» en dbo.Contactos Observaciones «perdido · motivo» · TNI decide si da de baja Ida: cola de salida + programa de escritura, en minutos. Vuelta: espejo nocturno completo, cada 24 h.
Seis momentos de ida escriben en TNI sin modificar nunca una fila existente. Cuatro cambios de vuelta se detectan por el espejo y actualizan el negocio en GlobalLink. Todo cabe en las cuatro tablas ya autorizadas.
↑ volver al resumen
Qué detona qué
Anexo · Qué detona qué

Cada momento del negocio, y lo que produce en TNI

Cada detonante deja un aviso en la cola de salida. El programa de escritura lo toma, elige la base por la oficina y escribe. Si falla, reintenta; si se repite, no duplica.

Momento en GlobalLinkAviso en la colaQué se escribe en TNIQué vuelve
El negocio queda con dueño
Rocket o vendedor, en la misma transacción del alta
prospecto_creadonuevoAlta en dbo.Clientes: estado 5, tipo 1, vendedor, contacto administrativo, referencia, y todos los campos del cruce de idaIdClienteapp.leads.id_cliente_creado
Primer contacto
el negocio pasa a «contactado»
contactadonuevoRenglón «Lead» en dbo.Contactos: fecha, hora, vendedor, canal en observacionesIdContacto, para auditoría
Cita agendada
por el bot o a mano
lead_potencialya existeRenglón «Lead» con FechaProgramada, HoraInicio y HoraFin de la visitaIdContacto → se guarda: es el contacto de la venta
Propuesta enviada
el negocio pasa a «negociación»
propuesta_enviadanuevoRenglón «Lead» con el monto en observacionesIdContacto
Ganadocierre_ganadoya existeAlta en dbo.Servicios en etapa 7 «Propuesta aceptada, programar servicio», estado activo, que es la etapa en la que están 21 de los 23 servicios de clientes potenciales de la 679; alta en dbo.Propuestas colgando del servicio. La ficha sigue en 5.IdServicio, IdPropuesta. Cuando TNI inicia y factura, el cambio vuelve por el espejo y cierra el ciclo.
Perdido
con motivo
perdidonuevoRenglón «Lead» con el motivoNada. TNI decide si da de baja la ficha.
Reactivado
de perdido vuelve a nuevo
reactivadonuevoRenglón «Lead». No se crea otra ficha: se reutiliza el IdCliente guardado.Nada
Un aviso por hito, nunca dos. Cada tipo de aviso llevará un índice único por negocio en la cola; hoy sólo lo tiene el de cita. Los avisos se procesan en orden de creación, así que el alta siempre llega antes que el primer renglón de bitácora. Y un prospecto sin datos mínimos no se encola: TNI exige nombre, dirección, localidad y teléfono; si faltan, el enriquecimiento los completa o el vendedor los escribe en la ficha.
Nuestro lado en Supabase: dónde vive cada cosa
Anexo · Nuestro lado en Supabase

Dónde vive cada cosa en Supabase

GlobalLink corre sobre un único proyecto de Supabase, con esquemas separados por función. Esta es la ruta de un prospecto por ese proyecto, de ida y de vuelta, con la tabla y la columna exactas de cada paso.

SUPABASE · PROYECTO ÚNICO esquema app app.leads app.companias · app.contactos app.lead_activity app.lead_appointments public.users disparadores la cola de salida app.writeback_outbox app.writeback_log un aviso por hito, con su payload y el número que devolvió TNI esquema silver · el espejo de TNI, sólo lectura silver.clientes · silver.contactos · silver.servicios silver.oficinas · silver.paises_ops · silver.empleados silver.aux_status_clientes · aux_tipos_clientes · aux_motivos_contactos los identificadores llevan la base sumada: id + base × mil millones app.reconciliar_prospectos_con_tni() · cada noche nuestro servidor lee la cola por conexión directa a Postgres y escribe en TNI bin/writeback.py lib/writeback_mapping.py lib/sqlserver_insert.py fotografía TNI cada noche y carga el esquema silver bin/snapshot.py → bronce scripts/populate/02… → silver tn-espejo-base@Truly · 04:00 UTC foto completa cada noche SQL SERVER DE TNI Truly · TrulyNew · TrulyRuso dbo.Clientes dbo.Contactos dbo.Servicios · dbo.Propuestas dbo.Empleados altas desde nuestro servidor lectura completa cada noche INSERT SELECT
La aplicación nunca habla con TNI: escribe en el esquema app y la base encola. El único que toca el SQL Server es el programa del servidor, en los dos sentidos. El esquema silver es de sólo lectura desde la aplicación por regla contractual.
Esquema · tabla · columnaPara qué sirve en este flujoExisteCambio
El negocio y sus entidades
app.leads.id_leaduuidEl identificador del prospecto. Viaja a TNI en la marca de observaciones
app.leads.status · assigned_rep_user_id · id_oficina · pais_target · origen · metodo · ciudad · fecha_creacion · fecha_cierre · proposal_amount_usd · lost_reasonLo que leen los disparadores para armar cada aviso. metodo entró con la migración 377 y no está en el volcado base más viejo mig 013, 040, 377
app.leads.id_compania → app.companiasnombre, industria, sitio_web, direccion, telefono, emailLa empresa. Alimenta Cliente, Rubro, Web, Direccion, Telefono, Email mig 319, 369
app.leads.id_contacto → app.contactosnombre, email, telefono_e164, movilLa persona. Alimenta ContactoAdmin, su correo y su teléfono, y Movil mig 319, 407, 409
app.lead_appointments.scheduled_atLa visita. Alimenta FechaProgramada, HoraInicio y HoraFin del renglón de cita
app.leads.id_cliente_creadobigint · clave foránea a silver.clientesEl número de cliente en TNI, con la base sumada. Se completa cuando el espejo trae la fila mig 013escribirla
app.leads.payload.tni_cliente_idEl mismo número, de inmediato, sin esperar al espejo
La cola de salida
app.writeback_outboxid_outbox, id_lead, payload, status, attempts, next_retry_at, tni_cliente_id, synced_atUn aviso por hito. payload->>'evento' dice cuál. id_outbox es la clave de idempotencia que viaja a TNI mig 043, 143, 302status «held», held_reason, tni_contacto_id, tni_servicio_id, tni_propuesta_id
app.writeback_logCada intento, con resultado, duración y error
app.trg_lead_won_writeback · app.trg_lead_potencial_writebackLos dos disparadores de hoy: ganado y cita mig 043, 302
app.trg_lead_dueno_writeback · app.trg_lead_hito_writebackLos disparadores nuevos: alta con dueño, contactado, propuesta, perdido, reactivadonomigración nueva
app.dequeue_writeback_pending · app.reap_stale_writebackReclamo atómico con plazo de 15 minutos y recuperación de huérfanosp_eventos text[]
La vuelta
silver.clientesid_cliente, id_status_cliente, id_vendedor, telefono, movil, email, direccion, localidad, contacto_admin, contacto_admin_email, contacto_admin_tel, fecha_bajaLa ficha de TNI, cada noche
silver.contactosid_cliente, id_vendedor, id_motivo_contacto, fecha_contacto, hora_contacto, venta_directa, observacionesLa bitácora de TNI. Hoy el populate llena 5 columnasfecha_programada, hora_inicio, hora_fin + populate
silver.serviciosid_cliente, id_status_servicio, id_etapa_servicio, fecha_altaLos servicios de TNI
app.reconciliar_prospectos_con_tni()Aplica el cruce de vuelta sobre app.leads, app.lead_activity, app.companias y app.contactosnomigración nueva
app.lead_activitytype: stage_change, field_change, note, call_logged · actor_labelLa línea de tiempo del negocio, donde entra lo que TNI anotó
Personas, oficinas y catálogos
public.usersid, external_sub, email, name, role, oficina_id, pais_idEl directorio de usuarios. external_sub recibe el número de empleado del paseid_empleado_tni
app.tni_cargo_rol · app.sso_pases_vistosLa equivalencia cargo → rol que llena TNI, y los números de pase ya usadosnomigración nueva
silver.oficinasid_oficina, id_pais · silver.paises_opsiso_alpha_2 · silver.paises_del_grupo()La oficina y su país; la validación de que el país del negocio corresponde a la oficina mig 200
silver.empleadosid_empleado, email, id_oficina, id_cargoLos empleados de TNI, para el cruce por correo mientras no haya pase
silver.aux_status_clientes · aux_tipos_clientes · aux_motivos_contactos · aux_tipos_referencias · aux_status_servicios · aux_etapas_serviciosLos catálogos de TNI en 16 idiomas. De acá salen las etiquetas que muestra GlobalLink
lib/flags.ts · tn:write-backTN_FF_WRITE_BACKLa bandera que deja salir los avisos hacia TNI. Hoy apagada
Cómo se inscribe: las sentencias
Anexo · Cómo se inscribe

Las tres sentencias que escribe el programa

Sólo altas, con parámetros, en la base que corresponde a la oficina. El envoltorio de conexión rechaza cualquier otra instrucción antes de abrir la conexión. Las columnas son las reales; los valores fijos son los que TNI usa.

-- 1 · el alta del prospecto · una sola vez por negocio
INSERT INTO dbo.Clientes
  ([Cliente], [IdOficina], [IdStatusCliente], [IdTipoCliente], [CuentaNacional], [CuentaInternacional],
   [IdVendedor], [IdTipoReferencia], [ReferenciaOtra],
   [Direccion], [Localidad], [Provincia], [Telefono], [Movil], [Email], [Web], [Rubro], [Identidad],
   [ContactoAdmin], [ContactoAdminEmail], [ContactoAdminTelefono],
   [FechaAlta], [Observaciones], [idempotency_key])
OUTPUT INSERTED.IdCliente
VALUES
  (@nombre, @id_oficina, 5, 1, 0, 0,
   @id_empleado, 6, 'TN GlobalLink · Rocket',
   @direccion, @localidad, @provincia, @telefono, @movil, @email, @web, @rubro, @identidad,
   @contacto_nombre, @contacto_email, @contacto_telefono,
   @fecha_alta, 'GLOBALLINK:' + @id_lead + ' · origen=' + @origen + ' · metodo=' + @metodo, @id_aviso);

-- 2 · cada hito · un renglón «Lead» en la bitácora
INSERT INTO dbo.Contactos
  ([IdCliente], [IdOficina], [IdMotivoContacto], [FechaContacto], [HoraContacto],
   [FechaProgramada], [HoraInicio], [HoraFin], [IdVendedor], [VentaDirecta], [Observaciones], [idempotency_key])
OUTPUT INSERTED.IdContacto
VALUES
  (@id_cliente, @id_oficina, 1, @fecha, @hora,
   @fecha_visita, @hora_inicio, @hora_fin, @id_empleado, 1,
   'GLOBALLINK:' + @id_lead + ':' + @hito + ' · ' + @detalle, @id_aviso);

-- 3 · al ganar · el servicio en la etapa donde TNI crea los suyos, y la propuesta colgando
INSERT INTO dbo.Servicios
  ([IdCliente], [IdContacto], [IdVendedor], [IdStatusServicio], [IdEtapaServicio], [VentaDirecta],
   [FechaAlta], [FechaProgramada], [HoraInicio], [HoraFin], [Observaciones], [idempotency_key])
OUTPUT INSERTED.IdServicio
VALUES
  (@id_cliente, 0, @id_empleado, 1, 7, 1,
   @fecha_cierre, @fecha_cierre, '1900-01-01 08:00', '1900-01-01 09:00', 'GLOBALLINK:' + @id_lead + ':servicio', @id_aviso);

-- Propuestas NO tiene columna Observaciones: la traza va en el servicio y en la clave
INSERT INTO dbo.Propuestas ([IdServicio], [FechaPropuesta], [idempotency_key])
OUTPUT INSERTED.IdPropuesta
VALUES (@id_servicio, @fecha_cierre, @id_aviso);
-- en TrulyNew la tabla exige además IdTipoEdificio, Contrato y Periodico: se resuelve con TNI antes de escribir ahí
Antes de escribir
Se pregunta si ya existe

Primero por la marca GLOBALLINK en observaciones. Después por teléfono exacto o nombre normalizado contra la cartera de esa oficina en el espejo, con el criterio que GlobalLink ya usa para rotular «ya es cliente», acotado a la oficina. Si existe, se enlaza el número y no se crea.

Si se repite
No duplica

El aviso lleva su propio identificador. Con la clave única que agrega TNI, el motor rechaza el segundo intento. Mientras no exista, la marca en observaciones cumple la misma función.

Si falla
Reintenta y avisa

Cinco reintentos con espera creciente. Un fallo por dato inválido no se reintenta: queda marcado y visible. Cada intento, con su resultado, se guarda en la bitácora de escritura.

Velocidad
Diez altas por minuto por tabla

En horario laboral de la oficina; treinta fuera de horario. Topes mensuales acordados: cinco mil clientes, doce mil renglones de bitácora, ocho mil servicios, tres mil propuestas.

Quién manda cuando los dos lados cambian lo mismo
Anexo · Quién manda

Cuando los dos lados cambian lo mismo

Regla 1
TNI manda sobre el cliente

Estado de la ficha, vendedor asignado, teléfono, correo, dirección, etapa del servicio. Lo que TNI cambie ahí se adopta en GlobalLink y queda anotado como cambio hecho en TNI.

Regla 2
GlobalLink manda sobre la conversación

Contactado, cita, negociación: esas etapas no existen en la ficha de TNI. Se registran allá como renglones de bitácora, pero se deciden acá.

Regla 3
Activar es de TNI, cerrar es de los dos

Un negocio se cierra como ganado cuando el vendedor lo cierra en GlobalLink o cuando TNI activa la ficha o inicia el servicio, lo que pase primero. Sólo TNI pasa la ficha de Potencial a Activo.

Regla 4
Nunca una modificación desde acá

GlobalLink sólo agrega filas en TNI. Si algo tiene que cambiar en una ficha ya creada, lo cambia TNI y vuelve por el espejo.

Qué se verificó hoy, y contra qué
Anexo · Qué se verificó hoy, y contra qué

Lo que esta página afirma sobre TNI se leyó de su base, no de un documento

AfirmaciónCómo se comprobóResultado
Guatemala vive en la base Truly, y el número 679 se repite en otra baseConteo en vivo de la oficina 679 en las tres basesTruly: 14.941 clientes, última alta ayer. TrulyNew: el mismo número 679 es otra oficina, de otro país, con 701 clientes. TrulyRuso: no existe. Por eso el número de oficina nunca viaja sin su base
El estado de prospecto es el 5Lectura del catálogo de estados en el espejo, tres idiomas1 Active · 2 Inactive · 3 Cancelled by TN · 4 Cancelled x customer · 5 Potential. La documentación y una vista de GlobalLink decían 4.
TNI ya inscribe prospectos asíConteo por estado en la oficina 679, leído en vivo del SQL Server; el espejo de anoche da 1.2691.268 clientes en estado 5; 2.484 activos; 11.124 inactivos
Columnas, tipos, largos y obligatoriedad de las cuatro tablasCatálogo del SQL Server en las tres bases productivas, con las columnas de identidad; Guatemala vive en TrulyEn Truly, la base de Guatemala, sin contar las columnas de identidad: Clientes 79 columnas, 7 obligatorias. Contactos 21, 5. Servicios 78, 10. Propuestas 174, 1. TrulyNew agrega columnas (Clientes 80, Contactos 22, Servicios 80, Propuestas 178) y en Propuestas exige tres más
Qué llena TNI en un prospectoPorcentaje de llenado por columna sobre los 1.268 potenciales de la 679Nombre, dirección, localidad, teléfono, contacto administrativo, identidad y fecha al 100 %. Vendedor 97 %. Contacto de ventas 7 %. Móvil 33 %, correo 28 %
Cómo registra TNI una gestión de venta1.364 renglones de bitácora con motivo «Lead» de los últimos 365 días en la 6791.358 con motivo «Lead» y venta directa 1. Fecha, hora, visita programada y vendedor al 100 %. Observaciones 17 %
En qué etapa están los servicios de TNI919 servicios dados de alta en los últimos 180 días en la 679, cruzados con el estado del clienteSólo 23 pertenecen a clientes potenciales, y de ésos 21 están en etapa 7. Del total, la mayoría ya está en etapa 8, iniciado. Es la etapa actual, no la de creación. El contacto enlazado es 0 en el 100 %, y como la columna es obligatoria, ese 0 es lo que TNI escribe
Los prospectos de Guatemala entran en «Clientes», no en el módulo «Prospectos» de su sistemaAltas por mes en ClientesTemporarios (el módulo Prospectos) y en Clientes para la 679Prospectos: 409 filas en la 679, la última alta en noviembre de 2025, 7 % convertidas. Clientes: entre 55 y 134 altas por mes en 2026. Medido en vivo el 3 de septiembre, desde el servidor que aloja el espejo
La hora se guarda sobre la fecha 1900-01-01Formato de los últimos renglones de bitácora y de serviciosConfirmado: 1900-01-01 08:00:00
El mapeo de escritura actual fallaríaComparación del código contra el catálogoOmite IdStatusCliente, IdTipoCliente, Direccion y Localidad, obligatorias; escribe ContactoVentasTel, que no existe; en Contactos omite HoraContacto e IdMotivoContacto; en Servicios manda 2 de las 10 obligatorias; en Propuestas escribe Observaciones, que no existe
Los usuarios del sistema de gestión son empleadosCatálogo de dbo.EmpleadosTiene usuario y contraseña como columnas obligatorias, junto con país, oficina y cargo. El pase de acceso puede llevar el número de empleado
El espejo puede traer sólo lo que cambióMedición del 5 de agosto sobre dos fotos completas, en el repositorio, y el estado actual en el README del workerLa marca de motor capturó el 100 % de los cambios en clientes, contactos y servicios en Truly y TrulyNew. El modo está apagado desde el 12 de agosto; hoy el espejo es completo
Las pantallas. Con el usuario de prueba se entró al sistema de gestión el 3 de septiembre: ese perfil sólo ve el menú Reports (Offices, Employees, Customers, Sales, Services, Inventory, Events, Control Panel, TNI Indicator, TNI Alertas) y, dentro de él, la lista de servicios; las pantallas de Clientes y del módulo Prospectos redirigen al inicio. La cabecera con las pestañas y el reporte de servicios del paso 12 son copia de lo visto; la lista de clientes del paso 7 y la bitácora del paso 9 siguen siendo reconstrucciones con las columnas documentadas. Para capturarlas hace falta un usuario con el perfil de vendedor. Sin verificar: qué escribe TNI en Identidad cuando no la conoce.