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é necesitamos de TNI
Tres cosas nos bloquean

La firma del addendum, un usuario de escritura y el acceso de red. Sólo eso nos detiene. Las decisiones de datos y la equivalencia de cargos ya están resueltas de nuestro lado: se les informan, no se les preguntan. Quedan además dos objetos de base de datos que sólo ellos pueden crear —la columna anti-duplicados y la tabla del acceso—, pero ninguno de los dos nos detiene: el primero es opcional y el segundo va con el acceso, que no tiene fecha.

Qué construye Catalizadora
El motor completo

Diez tareas en cuatro bloques más una vía paralela. Nueve de las diez no dependen de TNI y arrancan hoy; la décima es el encendido, que espera la firma y los dos accesos. 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. Seis pasadas de un revisor independiente; las dos últimas, sobre esta versión.

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 se apoya en el número de empleado de su propia base, 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 firma una autorización, da dos accesos técnicos y, cuando pueda, agrega una pestaña. No tiene que decidir nada. Nueve de las diez tareas nuestras 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 tres cosas, y sólo tres

  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 y el acceso de red a ese usuario. Son las dos únicas cosas técnicas que nos detienen, porque nadie fuera de TNI puede crearlas. Hay otros dos objetos que también sólo ellos pueden crear —la columna anti-duplicados de R4 y la tabla del acceso de R7—, pero ninguno bloquea: el primero es opcional y el segundo va sin fecha.
  3. No tiene que decidir nadaLas convenciones de inscripción y la equivalencia de cargos ya están decididas y medidas contra su propia base. Se les informan en la sección 2 con el porqué de cada una. Si alguna no les cuadra, se cambia con un correo; mientras tanto avanzamos.
  4. Su sistema, cuando puedanLa pestaña y la página del acceso con un clic: una tabla y dos a tres horas, con el código ya escrito en R7. No corre prisa y no bloquea nada: mientras no exista, la gente entra a GlobalLink con su usuario y contraseña, como hoy.
  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 · tres cosas nos bloquean. Nada que investigar, nada que decidir.
TNI Quien conozca el modelo de datosNada pendiente. Las convenciones de inscripción y la equivalencia de cargos ya están decididas y documentadas en la sección 2; se les informan para que las conozcan, no para que las aprueben.Leerlas: 15 minNada. Si quieren cambiar alguna, un correo y se cambia.
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 y abrir la red (R2, R3), con las sentencias ya escritas en la sección 2. La columna anti-duplicados (R4) es opcional y puede esperar.30 minutosPermite la primera escritura. Junto con la firma, es lo único que nos bloquea.
TNI Equipo del sistema de gestiónCuando puedan y sin prisa: la pestaña y la página del acceso (R6, R7), con el código ya escrito. No hay fecha comprometida para esto.2 a 3 horasSólo la comodidad de entrar con un clic. Mientras no exista, la gente entra con su usuario y contraseña de GlobalLink.
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, y sólo tres: la firma, el usuario y la red. Nadie fuera de TNI puede darlas. Todo lo demás —el motor, las convenciones, los duplicados, la vuelta, el acceso de nuestro lado— lo construimos, lo decidimos y lo probamos sin depender de ellos y 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 no depende de nadie 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 · Convenciones decididas · sólo se informan 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 Pestaña y página de acceso. Sin esto se entra con contraseña C9 · Punto de entrada Se construye y se prueba antes, sin depender de su página Entrar con un clic desde el sistema de gestión, sin segunda contraseña El número de empleado que resuelve el acceso resuelve además quién es el vendedor en cada prospecto que se escribe.
Sólo una cosa bloquea: la firma con los dos 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 hoy. No esperan ninguna respuesta de TNI: lo único que puede correr la fecha del quinto día es que la firma y los dos accesos no hayan llegado. 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 e inicia la firma del addendum. Pide media hora a su administrador de base de datos para R2 y R3 R1 · R2 · R3El 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 · C6Si quieren y pueden, sin compromiso de fecha: la columna opcional y empezar la pestaña y la página de acceso R4 · R6 · R7Primera corrida en sombra: el programa dice, fila por fila, qué escribiría en TNI. Se lo compartimos.
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 C7Nada pendiente. Si quieren, prueban la página de acceso contra nuestra dirección de pruebas R7Informe de sombra: las veinte filas exactas que se escribirían. Se lo compartimos para que lo vean; no esperamos aprobación para seguir.
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 C8Si quieren, miran en su pantalla los cinco prospectos. La cola se abre igual: la apertura no queda condicionada a una confirmación suya.Prospectos reales en el sistema de TNI, con vendedor y bitácora. La cola se abre al resto.

Si la firma y los accesos llegan esta semana

  • El viernes hay prospectos reales en su sistema, verificados en su pantalla.
  • El acceso con un clic queda probado de nuestro lado y esperando su página; mientras tanto la gente entra con usuario y contraseña.
  • El lunes siguiente se abre la cola completa y empieza el reporte semanal.

Si se demoran

  • Los cinco días entregan igual el motor completo, construido y probado en sombra: se ve exactamente qué se escribiría, sin escribir. No se pospone ninguna tarea nuestra.
  • 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

Sólo hay un bloqueo, y es la firma con los dos accesos: sin ellos no hay escritura real, por decisión contractual y no técnica. Ninguna otra cosa de TNI mueve la fecha, porque ninguna otra está esperando una respuesta suya. 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 · no depende 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 bases. Opcional: la protección ya corre de nuestro ladoidempotency_key · requerimiento R4TNIBloque 2
T5Las convenciones de inscripción: estado 5 y tipo 1, «C/F» en Identidad cuando no se conoce, y «Otro» con el texto «TN GlobalLink» como referencia. Decidido: se informa a TNI, no se le preguntaAuxStatusClientes · AuxTiposClientes · AuxTiposReferenciasCatalizadoraBloque 1
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 la credencial, hace las once comprobaciones 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 identifica al usuario y guarda la credencialuna página nueva en su sitio · dbo.EmpleadosTNIParalelo
T8Qué cargo de su tabla de empleados equivale a cada rol de GlobalLink. Decidido sobre sus 43 cargos; TNI puede objetar una filaapp.tni_cargo_rol, la llenamos nosotrosCatalizadoraParalelo
↑ volver al resumen
2sección

Especificación de implementación para el equipo de TNI

Ocho puntos, de los cuales sólo tres nos bloquean. Cada uno trae el objeto exacto, la sentencia lista para ejecutar cuando la hay, la consulta de verificación con el resultado que tiene que devolver, el criterio de aceptación y lo que no hay que hacer. Ningún valor, nombre ni ruta queda librado a interpretación.

En lenguaje llanoLo que TNI tiene que hacer o responder. Tres nos bloquean —la firma, un permiso de base de datos y una regla de red, media hora en total— y cinco no: dos son decisiones que ya tomamos y les informamos, una es una mejora opcional en su base, y dos son cambios en su sistema que pueden hacer cuando quieran. Ningún desarrollo nuevo salvo la pestaña y la página que la acompaña.

2.0 · El entorno, para que no haya duda de dónde

ElementoValor exactoCómo lo sabemos
InstanciaVMDBAPPDevuelto por SELECT @@SERVERNAME el 3 de septiembre de 2026
MotorMicrosoft SQL Server 2019 (RTM-GDR) 15.0.2180.2SELECT @@VERSION
PuertoTCP 11433El que ya usa el usuario lector
Usuario lector actualuser_globallinkCon permiso de sólo lectura; no se modifica ni se reemplaza
Bases productivas donde se escribeTruly · TrulyNew · TrulyRusoLas tres que TNI confirmó como productivas el 31 de julio de 2026
Base prohibidaTrulyGestionCopia de pruebas. Nuestro programa la rechaza por nombre antes de conectarse
Tablas destinodbo.Clientes · dbo.Contactos · dbo.Servicios · dbo.PropuestasLas cuatro autorizadas por el addendum; ninguna otra
Columnas de identidadIdCliente · IdContacto · IdServicio · IdPropuestaSon IDENTITY; no se insertan, se recuperan con OUTPUT INSERTED
GuatemalaIdOficina 679 en la base Truly · IdPais 244 · CodigoPais «GTB»Medido: 14.941 clientes y alta de ayer. Ojo: en TrulyNew el número 679 es otra oficina, de otro país
Dirección desde la que escribimosuna sola IP, /32 se entrega por canal privado, no por este documentoEs la misma desde la que hoy se conecta el usuario lector

2.1 · Lo que ya hicimos, para no pedírselo a ustedes

Antes de escribir esta lista de requerimientos fuimos a su base y averiguamos todo lo que se podía averiguar sin molestarlos. Esta sección existe para que quede claro que no les estamos trasladando el trabajo de investigación: lo que queda en la sección 2 es lo que, por permisos o por autoridad, sólo puede salir de ustedes.

Lo que había que averiguarLo que hicimos nosotros, y cuándoLo que les habría costado a ustedes
Con qué estado se inscribe un prospectoLeímos su catálogo AuxStatusClientes el 3 de septiembre y contamos los clientes de la oficina 679 por estado. Resultado: el prospecto es el estado 5, «Potential». Nuestro propio código decía 4, que en su catálogo es «Cancelado por el cliente»; lo corregimos de nuestro lado antes de escribirles.Una consulta y una revisión de código que no era suya
Cómo llenan los campos en la prácticaMedimos las convenciones reales sobre 1.268 prospectos de Guatemala, 1.364 gestiones de venta de los últimos 365 días y 919 servicios de los últimos 180. Sabemos qué llenan siempre, qué dejan vacío y con qué valores.Varias horas de alguien que conozca el sistema
Qué columnas son obligatoriasLeímos el catálogo de columnas de sus tres bases productivas: nombre, tipo, largo y obligatoriedad de cada una. Encontramos que nuestro mapeo omitía cuatro columnas obligatorias de Clientes —más dos de Contactos y ocho de Servicios— y escribía dos nombres de columna que no existen. Lo arreglamos nosotros; ninguna de esas fallas llegó a su base.Un rechazo en la primera escritura y una ida y vuelta de diagnóstico
Qué cargo corresponde a qué permisoLeímos su catálogo de 43 cargos con la cantidad de empleados activos de cada uno, y les entregamos la equivalencia ya llena (R8) para los 10 que tienen gente: 2.018 personas con rol y 2.738 deliberadamente fuera: 2.667 técnicos de campo y 71 «Otro».Diseñar un modelo de permisos desde cero
Cómo se identifica cada paísComprobamos que su código de país no es el estándar internacional en 13 de sus 239 países —Guatemala es «GTB»—. Por eso el acceso no les pide que manden el país: lo leemos de su base y lo traducimos nosotros.Una fuente de errores silenciosos, difíciles de ver
Si el acceso necesita permisos nuevosConsultamos los permisos efectivos del usuario de lectura que ya nos dieron y confirmamos que su alcance es a nivel de base: la tabla nueva del acceso queda cubierta sin darnos ningún permiso adicional. Por eso R7 no pide un GRANT para nosotros.Una solicitud de permisos y su revisión de seguridad
Cómo se escribe la página del accesoLa escribimos nosotros, completa, en las dos tecnologías que conviven en su portal: ASP clásico y ASP.NET. Está en R7, lista para copiar, con la consulta de verificación de cada paso.Diseñar el mecanismo y escribirlo desde cero
Lo que sí les toca, y por qué no podemos hacerlo nosotros. Tres cosas, y sólo tres: nadie fuera de TNI puede firmar un addendum, crear un usuario en su base de datos ni abrir su red. Ésas nos bloquean de verdad. Las otras cinco de esta sección no: dos son decisiones que ya tomamos y les informamos, una es una mejora opcional en su base, y dos son cambios en su sistema que pueden hacer cuando quieran, porque mientras tanto la gente entra con usuario y contraseña.

2.2 · Los ocho puntos, agrupados por lo que de verdad nos detiene

No son ocho pedidos equivalentes. Tres nos bloquean y cinco no. La diferencia importa: nadie va a estar esperando una respuesta suya sobre las cinco de abajo, porque ya están decididas y en construcción.

#Qué esQuién lo ejecutaEsfuerzoQué pasa si no llega
Nos bloquea · sin estas tres no se escribe una sola fila, y nadie fuera de TNI puede darlas
R1Addendum firmadoDirecciónuna firmaSe detiene toda la escritura. Es una barrera contractual y además un candado en el programa: sin el PDF en el servidor, no emite ninguna alta.
R2Usuario de escritura en las tres basesAdministrador de base de datos20 minNo hay con qué conectarse a insertar. El usuario de lectura que ya tenemos no sirve, a propósito.
R3Acceso de red para ese usuarioRedes o el mismo DBA10 minNo se llega al servidor. Se comprueba en un minuto con la consulta de R3.
No nos detiene · dos son decisiones ya tomadas que se les informan, y una es una mejora opcional en su base
R4Columna idempotency_key en cuatro tablas por baseAdministrador de base de datos15 minNada. Sin ella, la protección contra duplicados la hacemos con una marca de texto, que ya está construida y funciona. La columna es más limpia para ustedes, no para nosotros.
R5Las convenciones con que se inscribe el prospectoleerlas: 15 minNada. Escribimos con lo decidido y queda registrado en cada alta. Si prefieren otro valor, un correo y se cambia en un minuto.
R8Equivalencia de cargos con el rol de GlobalLinkleerla: 15 minNada. La aplicamos como está propuesta. Un cargo que no esté en la tabla no entra, que es el comportamiento seguro.
En su sistema, cuando puedan · sin fecha comprometida
R6Pestaña en el menú del sistema de gestiónEquipo del portal1 hNada se detiene. La gente entra a GlobalLink con su usuario y contraseña, como hoy.
R7Página que identifica al usuario, con el código ya escritoEquipo del portal2 a 3 hNada se detiene. Es la comodidad de entrar con un clic; nuestro lado queda construido y probado esperándola.
Lo que pedimos, en una frase: firmen el addendum y pidan a su administrador de base de datos media hora para R2 y R3. Con eso arrancamos. Todo lo demás de esta sección son decisiones que ya tomamos para no hacerles perder tiempo, y trabajo en su sistema que puede esperar el tiempo que necesiten.

R1 · Addendum de escritura firmado

Dirección de TNIEsfuerzo: una firmaBloquea: toda la escrituraAutorización · sólo la puede dar TNI

Objetivo. Autorizar por escrito las altas descritas en el addendum COT-0003-A1 versión 2, que ya está redactado y no requiere cambios de nuestro lado.

Qué se firma. El documento «Addendum COT-0003-A1 · Autorización de write-back limitado, versión 2 del 20 de agosto de 2026». Firman José Lutz por Truly Nolen International y Pablo Estrada por AI Catalysts. Autoriza únicamente altas en cuatro tablas de tres bases, prohíbe modificar y borrar, y puede suspenderse por correo con efecto inmediato.

Qué hacemos nosotros con él. Depositamos el PDF firmado en el servidor de la plataforma, en la ruta /etc/tn-globallink/writeback-addendum-firmado. El programa de escritura verifica que ese archivo exista y no esté vacío antes de cada corrida; si no está, no emite ninguna alta y lo deja escrito en su registro.

Criterio de aceptación. PDF firmado por ambas partes, en cualquier formato de firma. No hace falta plataforma de firma electrónica.

Una salvedad, para que quede escrita. El texto del addendum describe idempotency_key como columna obligatoria. En la práctica se aplica como nula con índice único filtrado (R4) —de otro modo fallaría sobre las filas que ya existen— y, además, R4 pasó a ser opcional. No cambia el alcance autorizado ni las tablas afectadas; si prefieren corregir esa línea antes de firmar, lo hacemos en el día.

Qué no hace falta. Ningún anexo técnico nuevo: el memo que lo acompaña ya está entregado.

R2 · Usuario de escritura user_globallink_writer

Administrador de base de datosEsfuerzo: 20 minutosBloquea: la primera escrituraPermiso · las sentencias van escritas abajo

Objetivo. Un login nuevo, distinto del lector, que sólo pueda insertar en cuatro tablas de las tres bases productivas.

Por qué uno nuevo y no ampliar el actual. Para poder revocar la escritura sin tocar la lectura, y para que cualquier auditoría vea en el catálogo de permisos exactamente qué puede hacer cada uno.

Objetos a crear. Un login de servidor y tres usuarios de base, uno por base:

ObjetoNombre exactoDóndePermisos
LOGINuser_globallink_writerinstancia VMDBAPPninguno a nivel servidor
USERuser_globallink_writerTrulyINSERT y SELECT en cuatro tablas; DENY UPDATE y DELETE en las mismas
USERuser_globallink_writerTrulyNewigual
USERuser_globallink_writerTrulyRusoigual

Por qué también SELECT. Porque el alta devuelve el número asignado con OUTPUT INSERTED.IdCliente, y sin permiso de lectura sobre esa tabla el motor rechaza la cláusula. El SELECT se limita a esas cuatro tablas; el resto de la base queda fuera.

Script, listo para ejecutar. La contraseña la generan ustedes y nos la entregan por la bóveda de secretos, nunca por correo ni por chat:

/* ---------- 1) una sola vez, en la instancia ---------- */
USE [master];
GO
CREATE LOGIN [user_globallink_writer]
  WITH PASSWORD = N'<la que ustedes generen>',
       CHECK_POLICY = ON,
       CHECK_EXPIRATION = OFF,
       DEFAULT_DATABASE = [Truly];
GO

/* ---------- 2) repetir el bloque completo en Truly, TrulyNew y TrulyRuso ---------- */
USE [Truly];   -- luego [TrulyNew], luego [TrulyRuso]
GO
CREATE USER [user_globallink_writer] FOR LOGIN [user_globallink_writer];
GO
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];
GO
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];
GO

Verificación. Ejecutar en cada base conectado como ese usuario. Devuelve una fila por tabla y permiso, incluidas las denegaciones: tienen que salir dieciséis filas — ocho GRANT de INSERT y SELECT, y ocho DENY de UPDATE y DELETE. Las ocho de DENY son la prueba de que la restricción quedó puesta, no un defecto:

SELECT o.name AS tabla, p.permission_name, p.state_desc
FROM   sys.database_permissions p
JOIN   sys.objects o ON o.object_id = p.major_id
WHERE  p.grantee_principal_id = DATABASE_PRINCIPAL_ID('user_globallink_writer')
ORDER BY o.name, p.permission_name;

-- y la prueba negativa: esto TIENE que fallar con error 229
UPDATE dbo.Clientes SET Observaciones = Observaciones WHERE 1 = 0;

Criterio de aceptación. Las dieciséis filas —ocho concedidas y ocho denegadas— y el error 229 en la prueba negativa. Nosotros repetimos la misma verificación desde nuestro servidor y la dejamos en el registro de la primera corrida.

Qué no hacer. No agregar el usuario a db_datawriter ni a ningún rol fijo: eso le daría escritura sobre toda la base. No darle EXECUTE sobre procedimientos. No crearlo en TrulyGestion. No reutilizar la contraseña del lector.

R3 · Acceso de red para el usuario de escritura

Redes o el mismo DBAEsfuerzo: 10 minutosBloquea: la primera escrituraPermiso · una regla de firewall

Objetivo. Que las conexiones de ese usuario, desde nuestra única dirección, lleguen al puerto del motor.

ParámetroValor
Origenuna sola IP con máscara /32 · se entrega por canal privado · es la misma del lector actual
Destinoinstancia VMDBAPP
Puerto y protocoloTCP 11433
Sentidoentrante; no hace falta ninguna regla de salida hacia nosotros

Cómo se comprueba, y por qué hacen falta los dos lados. Esta regla es la única de las tres que no se puede verificar desde dentro de su red: una prueba de conexión lanzada desde su propio servidor devuelve «conectado» con la regla puesta o sin ella, porque no atraviesa el firewall. Se comprueba en dos pasos:

1 · Ustedes, que el motor escucha. Desde el servidor de base de datos, confirma que el puerto es el correcto y que la instancia responde. No prueba la regla:

Test-NetConnection -ComputerName localhost -Port 11433
# TcpTestSucceeded : True → el motor escucha en 11433

2 · Nosotros, que la regla deja entrar. Abrimos una conexión desde la IP autorizada con el usuario de R2, en modo sombra, sin insertar nada, y les confirmamos el resultado el mismo día. Es lo único que prueba de verdad que la lista blanca quedó bien.

Criterio de aceptación. Nuestra conexión entra desde la IP autorizada, y falla desde cualquier otra. Ese par —entra una, no entran las demás— es lo que demuestra que es una lista blanca y no una apertura general. Se lo pasamos por escrito el mismo día.

Qué no hacer. No abrir el puerto a rangos amplios ni a Internet. Si su política exige VPN en lugar de lista blanca, decirlo y coordinamos: el programa corre en un servidor fijo y puede usar un túnel.

R4 · Columna idempotency_key con índice único filtrado

Administrador de base de datosEsfuerzo: 15 minutosOpcional · mejora para ustedes, no la necesitamos

Objetivo. Que si nuestro programa reintenta un alta que en realidad ya entró (corte de red justo después del INSERT), sea el propio motor de SQL Server el que rechace la segunda.

Esto es opcional y no lo estamos esperando. La protección contra duplicados ya está resuelta de nuestro lado sin tocar su base: antes de insertar comparamos contra la cartera de la oficina y marcamos cada alta con el identificador del negocio en Observaciones, de modo que un reintento se reconoce y no se repite. Funciona, y es lo que va a estar corriendo desde el primer día. La columna es mejor para ustedes, no para nosotros: pone la garantía en el motor en vez de en nuestro código, y deja el rastro en una columna consultable en lugar de dentro de un texto libre. Si su administrador la agrega, la usamos; si no, no pasa nada.

Objetos a crear. Doce columnas y doce índices: cuatro tablas por cada una de las tres bases.

TablaColumnaTipoÍndice
dbo.Clientesidempotency_keyUNIQUEIDENTIFIER NULLUX_Clientes_idempotency_key · único, filtrado
dbo.Contactosidempotency_keyUNIQUEIDENTIFIER NULLUX_Contactos_idempotency_key
dbo.Serviciosidempotency_keyUNIQUEIDENTIFIER NULLUX_Servicios_idempotency_key
dbo.Propuestasidempotency_keyUNIQUEIDENTIFIER NULLUX_Propuestas_idempotency_key

Por qué nula y con índice filtrado, y no obligatoria. El texto del addendum la describe como obligatoria. Aplicarla así fallaría: las filas que ya existen no tienen clave, y una columna obligatoria sin valor por omisión no se puede agregar a una tabla con datos. La forma de acá deja el histórico intacto, el índice sólo mira las filas que traen clave, y el efecto anti-duplicado es el mismo. Es una mejora sobre el texto firmado, no una desviación que haya que documentar: el addendum autoriza altas, y esta columna sólo las hace más seguras. Como además es opcional, no cambia nada de lo firmado.

Script, listo para ejecutar. Repetir el bloque completo en Truly, TrulyNew y TrulyRuso:

USE [Truly];   -- luego [TrulyNew], luego [TrulyRuso]
GO
ALTER TABLE [dbo].[Clientes]   ADD [idempotency_key] UNIQUEIDENTIFIER NULL;
ALTER TABLE [dbo].[Contactos]  ADD [idempotency_key] UNIQUEIDENTIFIER NULL;
ALTER TABLE [dbo].[Servicios]  ADD [idempotency_key] UNIQUEIDENTIFIER NULL;
ALTER TABLE [dbo].[Propuestas] ADD [idempotency_key] UNIQUEIDENTIFIER NULL;
GO
CREATE UNIQUE NONCLUSTERED INDEX [UX_Clientes_idempotency_key]
  ON [dbo].[Clientes]   ([idempotency_key]) WHERE [idempotency_key] IS NOT NULL;
CREATE UNIQUE NONCLUSTERED INDEX [UX_Contactos_idempotency_key]
  ON [dbo].[Contactos]  ([idempotency_key]) WHERE [idempotency_key] IS NOT NULL;
CREATE UNIQUE NONCLUSTERED INDEX [UX_Servicios_idempotency_key]
  ON [dbo].[Servicios]  ([idempotency_key]) WHERE [idempotency_key] IS NOT NULL;
CREATE UNIQUE NONCLUSTERED INDEX [UX_Propuestas_idempotency_key]
  ON [dbo].[Propuestas] ([idempotency_key]) WHERE [idempotency_key] IS NOT NULL;
GO

Impacto medido. ALTER TABLE ADD de una columna nula es un cambio de metadatos: no reescribe las filas. Sobre Clientes de Truly son 464.278 filas y la operación es inmediata. Los cuatro índices se crean vacíos, porque ninguna fila existente tiene clave. Su aplicación no necesita conocer la columna: no aparece en ningún SELECT * de pantalla porque es nueva y nula.

Verificación. En cada base, tiene que devolver cuatro filas con is_nullable = 1 y cuatro índices has_filter = 1:

SELECT t.name AS tabla, c.name AS columna, c.is_nullable, ty.name AS tipo
FROM   sys.columns c
JOIN   sys.tables  t  ON t.object_id = c.object_id
JOIN   sys.types   ty ON ty.user_type_id = c.user_type_id
WHERE  c.name = 'idempotency_key'
ORDER BY t.name;

SELECT t.name AS tabla, i.name AS indice, i.is_unique, i.has_filter, i.filter_definition
FROM   sys.indexes i
JOIN   sys.tables  t ON t.object_id = i.object_id
WHERE  i.name LIKE 'UX_%_idempotency_key'
ORDER BY t.name;

Criterio de aceptación. Cuatro columnas y cuatro índices por base, doce y doce en total. Nosotros lo confirmamos porque la columna aparece en el catálogo que leemos cada noche.

Qué no hacer. No ponerla obligatoria. No crear el índice sin el filtro WHERE … IS NOT NULL: sin él, un solo valor nulo por tabla sería admitido y el resto rechazado. No agregarla a TrulyGestion.

R5 · Las convenciones con que vamos a escribir

Decisión de CatalizadoraSu esfuerzo: leerlo, 15 minutosNo bloquea nadaDecidido · para su información, no requiere respuesta

Objetivo. Que el prospecto que escribimos se vea exactamente como los que inscribe su gente. Estas tres convenciones ya están decididas y en construcción: el 3 de septiembre leímos su catálogo y medimos cómo llenan los campos sobre los 1.268 prospectos de la oficina de Guatemala, y elegimos en consecuencia. Esta sección existe para que sepan exactamente qué vamos a escribir y por qué, con la consulta al lado para que puedan comprobarlo. No estamos esperando una respuesta. Si alguna de las tres no les cuadra, un correo y se cambia: es una constante del programa, cuesta un minuto.

5aEstado y tipo con que se inscribe un prospecto
Lo que medimos en su baseEn la oficina 679 hay 1.268 clientes con IdStatusCliente = 5, que su catálogo AuxStatusClientes llama «Potential». De ésos, 908 tienen IdTipoCliente = 2 «Office», 303 tienen 1 «Creative» y 57 tienen 3 «Other».
Lo que vamos a escribirIdStatusCliente = 5 y IdTipoCliente = 1 «Creative».
Por quéEl estado no admite duda. El tipo sí: elegimos «Creative» porque el prospecto sale de prospección activa, no de una llamada entrante, aunque su práctica mayoritaria sea «Office». Es el único punto donde nos apartamos de lo que hace su gente hoy, y por eso lo señalamos.
Estado de la decisiónTomada. Se escribe así desde la primera alta. Cambiarla después es una constante y una corrida de corrección.
-- comprueben lo anterior en un minuto
USE [Truly];
SELECT s.IdStatusCliente, s.StatusCliente_01 AS ingles, s.StatusCliente_02 AS espanol,
       COUNT(c.IdCliente) AS clientes_en_la_679
FROM   dbo.AuxStatusClientes s
LEFT   JOIN dbo.Clientes c ON c.IdStatusCliente = s.IdStatusCliente AND c.IdOficina = 679
GROUP  BY s.IdStatusCliente, s.StatusCliente_01, s.StatusCliente_02
ORDER  BY s.IdStatusCliente;
5bQué va en Identidad cuando no se conoce el número fiscal
Lo que medimos en su baseLos 1.268 prospectos de la 679 tienen Identidad con contenido: el 100 %. La columna es obligatoria y no admite nulos.
Nuestra situaciónSólo tenemos el número fiscal cuando el registro oficial del país lo publica. En Guatemala no siempre ocurre.
Lo que vamos a escribirEl texto fijo C/F cuando no lo tengamos, y el número real cuando sí.
Por quéEs la abreviatura de «consumidor final», que ya se usa en la facturación de la región, y no inventa un número que después nadie sabe si es real. Descartamos dejarlo vacío porque la columna es obligatoria, y descartamos un provisional con formato propio porque ensucia su base.
Estado de la decisiónTomada. Si prefieren otro texto fijo, se cambia con un correo.
-- comprueben que la columna se llena siempre, y con qué
USE [Truly];
SELECT COUNT(*) AS prospectos,
       SUM(CASE WHEN LTRIM(RTRIM(ISNULL(Identidad,''))) = '' THEN 1 ELSE 0 END) AS sin_identidad
FROM   dbo.Clientes
WHERE  IdOficina = 679 AND IdStatusCliente = 5;
5cCómo se marca que el prospecto viene de GlobalLink
Lo que medimos en su baseSu catálogo AuxTiposReferencias tiene diez filas: TV, Web, Publicidad vía pública, Otro, Referencia de otro cliente, Auto de Truly Nolen, Radio, Páginas amarillas, Referencia de otro vendedor y Redes sociales. Ninguna sirve para distinguir el origen nuevo.
Lo que vamos a escribirIdTipoReferencia = 6 «Otro», y en ReferenciaOtra el texto TN GlobalLink · Rocket o TN GlobalLink · Manual según de dónde venga.
Por quéNo les obliga a tocar su catálogo. La alternativa mejor para ustedes es agregar una fila «GlobalLink» a AuxTiposReferencias: el origen quedaría contable en sus reportes en vez de escondido en un texto libre. Si la agregan, denos el número y usamos ése; el cambio de nuestro lado es de un minuto.
Estado de la decisiónTomada, con la opción «Otro». Si agregan la fila «GlobalLink» a su catálogo, avísennos el número y pasamos a usarlo: para ustedes es mejor, porque el origen queda contable en sus reportes.

Cómo lo van a ver. Antes de escribir nada en firme les mostramos la primera alta de prueba con estos tres valores puestos, sobre un prospecto real de Guatemala. Ahí se ve el resultado, no la promesa.

Por qué no se lo preguntamos. Porque ya teníamos con qué decidirlo: su propia base. Devolverles una pregunta que nosotros podíamos contestar sería trasladarles trabajo y sumar una espera al proyecto. Lo único que de verdad no podemos hacer sin ustedes es escribir, y eso depende de R1, R2 y R3.

R6 · Pestaña «Truly Nolen GlobalLink» en el menú

Equipo del portalEsfuerzo: 1 horaBloquea: el acceso con un clicSu sistema · cuando puedan, sin fecha

Objetivo. Una pestaña más en la barra donde hoy están «Home» y «Reports», a la derecha de ellas.

Va junto con R7 y, como aquélla, no bloquea nada ni tiene fecha comprometida.

Dónde. En la plantilla que dibuja esa barra. En la instalación actual la barra se arma en /proposals/TG/includes/inc_menu_superior.asp, que es el archivo que aparece en el error cuando una página se abre fuera de su flujo. Si la plantilla real es otra, es la que corresponda: el requisito es que la pestaña salga en todas las pantallas del sistema.

Qué entregamos nosotros. El icono del ratón de Truly Nolen en SVG y en PNG de 16 y 32 píxeles (el de 32 es para pantallas de alta densidad), y este fragmento, adaptable a los estilos de su barra:

<a class="tab" href="/proposals/TG/globallink.asp"   <!-- .aspx si la hacen en ASP.NET -->
   target="_blank" rel="noopener noreferrer">
  <img src="/proposals/TG/img/tn-globallink.png" width="16" height="16" alt="" />
  Truly Nolen GlobalLink &#8599;
</a>

Las dos rutas van absolutas, y esto importa. Este fragmento vive en un archivo que se incluye desde todas las pantallas del sistema. Una ruta relativa se resuelve contra la dirección de la pantalla que la pide, no contra la del archivo incluido: desde /proposals/TG/reports/… el enlace apuntaría a /proposals/TG/reports/globallink.asp, que no existe, y el icono no cargaría. Con la barra inicial funciona igual desde cualquier pantalla. Si su instalación cuelga de otra carpeta, es esa ruta con barra inicial.

Reglas. La pestaña apunta a la página de R7, dentro de su propio sitio, nunca directamente a GlobalLink. Se abre en una pestaña nueva, para que la persona no pierda lo que estaba haciendo. La flecha avisa que sale del sistema.

Quién la ve. Ésta sí es decisión suya, porque es su menú y su gente. Nuestra recomendación, si no quieren pensarlo: los mismos perfiles que hoy ven el módulo de ventas. Si prefieren empezar sólo con Guatemala, basta condicionar la pestaña a IdPais = 244 mientras dure el piloto.

Criterio de aceptación. Con el usuario de prueba que ya tenemos, la pestaña aparece y abre la página de R7.

R7 · La página que identifica al usuario y lo envía a GlobalLink

Equipo del portalEsfuerzo: 2 a 3 horasBloquea: entrar sin segunda contraseñaSu sistema · cuando puedan, sin fecha

Objetivo. Que un empleado que ya inició sesión en el sistema de gestión llegue a GlobalLink identificado, sin escribir otra contraseña. Junto con la pestaña de R6, es el único desarrollo nuevo del lado de TNI: una tabla y una página.

Esto no bloquea nada, y no hay fecha comprometida. Mientras la página no exista, cada persona entra a GlobalLink con su usuario y contraseña, que es como se entra hoy. Lo que aporta es la comodidad de un clic y que nadie tenga que recordar una segunda contraseña. Lo describimos con este nivel de detalle —el código completo, en las dos tecnologías de su portal— justamente para que cuando su equipo tenga el hueco, sean dos o tres horas y no un proyecto.

El método elegido: credencial de un solo uso. La página genera un número aleatorio, lo guarda en una tabla junto con el número de empleado y una caducidad de dos minutos, y lo envía a GlobalLink. GlobalLink lo busca en esa misma tabla con el usuario de lectura que ya tiene, comprueba que siga vigente y que no se haya usado antes, y abre la sesión. Sin criptografía, sin librerías, sin llaves, sin permisos nuevos.

Lo elegimos por tres razones: funciona igual en ASP clásico y en ASP.NET, y su portal tiene las dos cosas; no obliga a su equipo a manejar llaves ni rotaciones; y no requiere que nos den ningún permiso adicional, porque nuestro usuario lector ya puede leer cualquier tabla de la base. Si prefieren un token firmado, las dos variantes están al final de este requerimiento.

EMPLEADO globallink.asp SQL SERVER DE TNI GLOBALLINK 1 · clic en la pestaña 2 · INSERT con el IdEmpleado de la sesión devuelve la credencial 3 · el navegador la reenvía por POST, sin pasar por la barra de direcciones 4 · SELECT con el usuario lector empleado, oficina, país, cargo 5 · sesión abierta
Cinco pasos. Los dos primeros ocurren en el servidor de TNI; el cuarto es una consulta que GlobalLink ya sabe hacer contra su base.

Paso 1 · Crear la tabla

Dónde. En las tres bases productivas: Truly, TrulyNew y TrulyRuso. Si su portal usa una sola base para todos los países, créenla sólo ahí y avísennos cuál es.

Qué se crea. Una tabla y un índice. Ninguna otra cosa.

ColumnaTipoPara qué
TokenUNIQUEIDENTIFIER NOT NULLLa credencial. La genera el motor con NEWID(). Es la clave primaria
IdEmpleadoINT NOT NULLQuién pidió el acceso. Sale de la sesión del portal
CreadoDATETIME2(0) NOT NULLCuándo se pidió. Lo pone el motor
VenceDATETIME2(0) NOT NULLDos minutos después. Pasado eso, la credencial no sirve
OrigenNVARCHAR(45) NULLLa dirección desde la que se pidió. Opcional, ayuda a auditar
USE [Truly];   -- después repetir igual en [TrulyNew] y en [TrulyRuso]
GO
CREATE TABLE [dbo].[GlobalLinkAccesos] (
    [Token]      UNIQUEIDENTIFIER NOT NULL CONSTRAINT [DF_GLA_Token]  DEFAULT NEWID(),
    [IdEmpleado] INT              NOT NULL,
    [Creado]     DATETIME2(0)     NOT NULL CONSTRAINT [DF_GLA_Creado] DEFAULT SYSUTCDATETIME(),
    [Vence]      DATETIME2(0)     NOT NULL,
    [Origen]     NVARCHAR(45)     NULL,
    CONSTRAINT [PK_GlobalLinkAccesos] PRIMARY KEY CLUSTERED ([Token])
);
GO
CREATE NONCLUSTERED INDEX [IX_GlobalLinkAccesos_Vence]
    ON [dbo].[GlobalLinkAccesos] ([Vence]);
GO

Verificación del paso 1. Tiene que devolver cinco filas, una por columna:

SELECT c.name AS columna, t.name AS tipo, c.max_length, c.is_nullable
FROM   sys.columns c
JOIN   sys.types   t ON t.user_type_id = c.user_type_id
WHERE  c.object_id = OBJECT_ID('dbo.GlobalLinkAccesos')
ORDER  BY c.column_id;

Cuánto ocupa. Una fila por acceso, unos 60 bytes. Con 2.018 empleados entrando dos veces por día son unas 4.000 filas diarias, que la propia página va borrando. La tabla nunca pasa de unos pocos megabytes.

Paso 2 · Darle permiso de escritura al usuario del portal

Qué usuario es. El que ya usa el sistema de gestión para leer y escribir en la base: el que figura en la cadena de conexión del portal. No es el nuestro. Si no saben cuál es, esta consulta lo muestra mientras alguien navega el sistema:

SELECT DISTINCT s.login_name, s.program_name, DB_NAME(s.database_id) AS base
FROM   sys.dm_exec_sessions s
WHERE  s.is_user_process = 1 AND DB_NAME(s.database_id) IN ('Truly','TrulyNew','TrulyRuso');

El permiso. Sólo sobre la tabla nueva: insertar, leer y borrar. No hace falta modificar. El SELECT es imprescindible aunque la página no consulte nada, porque el INSERT usa OUTPUT INSERTED.Token para recuperar la credencial recién creada, y sin permiso de lectura sobre esa tabla el motor rechaza la cláusula:

GRANT INSERT, SELECT, DELETE ON [dbo].[GlobalLinkAccesos] TO [<el usuario del portal>];
GO

Verificación del paso 2. Ejecutar conectado como ese usuario; tiene que insertar y devolver un identificador:

INSERT INTO dbo.GlobalLinkAccesos (IdEmpleado, Vence)
OUTPUT INSERTED.Token
VALUES (22059, DATEADD(minute, 2, SYSUTCDATETIME()));
-- borrar la fila de prueba
DELETE FROM dbo.GlobalLinkAccesos WHERE IdEmpleado = 22059;

Sobre nuestro lado: no hace falta darnos nada. Nuestro usuario user_globallink pertenece al rol db_datareader y tiene permiso de consulta a nivel de base de datos, verificado el 3 de septiembre de 2026. Ese permiso alcanza automáticamente a cualquier tabla nueva.

Paso 3 · Averiguar dos cosas de su sistema

La página necesita dos datos que sólo su equipo conoce. Este paso no es programar: es abrir el código del portal y anotarlos.

3a · Cómo identifica el sistema al empleado que tiene la sesión abierta. En la cabecera de cada pantalla aparece el nombre de la persona, por ejemplo «PABLO ESTRADA». Ese texto sale de una variable de sesión. Búsquenla en el archivo que dibuja esa cabecera, que en la instalación actual es /proposals/TG/includes/inc_menu_superior.asp, o en la página de inicio de sesión, buscando Session(. Puede llamarse Session("IdEmpleado"), Session("Usuario") u otra cosa.

Los dos casos posibles, y qué hacer en cada uno:

Lo que guarda la sesiónQué usa la página
El número de empleado, por ejemplo 22059Se usa directo. Es el caso simple.
El nombre de usuario con el que entró, por ejemplo Pablo2026La página lo convierte con una consulta a su propia tabla de empleados: SELECT IdEmpleado FROM dbo.Empleados WHERE Usuario = @usuario AND FechaBaja IS NULL. El filtro por fecha de baja no es opcional: es lo que hace que un empleado dado de baja deje de recibir credenciales. El código de abajo trae esta variante lista.

3b · La cadena de conexión que usa el portal. En ASP clásico suele estar en un Application("…") definido en global.asa, o en un archivo de configuración incluido en todas las páginas. En ASP.NET está en web.config, dentro de connectionStrings. La página nueva usa exactamente la misma; no se crea ninguna conexión nueva ni se guarda ninguna contraseña más.

Paso 4 · Crear la página

Dónde. En la misma carpeta que el resto del sistema, con el nombre globallink.asp si la hacen en ASP clásico, o globallink.aspx si la hacen en ASP.NET. No hace falta cambiar ninguna configuración de IIS: es una página más de la aplicación que ya existe.

Versión ASP clásico. Completa. Las dos líneas con la flecha son las únicas que hay que ajustar con lo que anotaron en el paso 3:

<%@ Language="VBScript" %>
<%
Option Explicit
Response.Buffer = True
Response.AddHeader "Cache-Control", "no-store"
Response.AddHeader "Referrer-Policy", "no-referrer"

Dim usuario, idEmpleado, cn, cmd, rs, token

' ►►► AJUSTAR (paso 3a): la variable de sesión que ya usa el sistema.
'     Si guardan el número de empleado, usen esa y salten el bloque de conversión.
usuario = Session("Usuario")

If IsEmpty(usuario) Or usuario = "" Then
    Response.Redirect "login.asp"
End If

Set cn = Server.CreateObject("ADODB.Connection")
' ►►► AJUSTAR (paso 3b): la misma cadena de conexión que usa el resto del sistema
cn.Open Application("ConnectionString")

' ── conversión de nombre de usuario a número de empleado ────────────────
'    Si la sesión ya trae el número, borren este bloque y hagan:
'    idEmpleado = Session("IdEmpleado")
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cn
cmd.CommandText = "SELECT IdEmpleado FROM dbo.Empleados " & _
                  "WHERE Usuario = ? AND FechaBaja IS NULL"
cmd.Parameters.Append cmd.CreateParameter("u", 200, 1, 50, usuario)   ' adVarWChar
Set rs = cmd.Execute

If rs.EOF Then
    rs.Close : cn.Close
    Response.Write "<p>Su usuario no tiene un empleado activo asociado. " & _
                   "Avise a la administración de TNI.</p>"
    Response.End
End If

idEmpleado = rs("IdEmpleado")
rs.Close : Set rs = Nothing : Set cmd = Nothing

' ── limpieza de credenciales viejas · no falla si no hay ninguna ────────
cn.Execute "DELETE FROM dbo.GlobalLinkAccesos " & _
           "WHERE Vence < DATEADD(day, -1, SYSUTCDATETIME())"

' ── generar la credencial ───────────────────────────────────────────────
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cn
cmd.CommandType = 1                                        ' adCmdText
cmd.CommandText = "SET NOCOUNT ON; " & _
                  "INSERT INTO dbo.GlobalLinkAccesos (IdEmpleado, Vence, Origen) " & _
                  "OUTPUT INSERTED.Token " & _
                  "VALUES (?, DATEADD(minute, 2, SYSUTCDATETIME()), ?)"
cmd.Parameters.Append cmd.CreateParameter("id", 3, 1, 4, CLng(idEmpleado))          ' adInteger
cmd.Parameters.Append cmd.CreateParameter("ip", 200, 1, 45, _
                                          Request.ServerVariables("REMOTE_ADDR"))  ' adVarWChar

Set rs = cmd.Execute
token = rs("Token")

rs.Close : Set rs = Nothing : Set cmd = Nothing
cn.Close : Set cn = Nothing
%>
<!doctype html>
<html><head><meta charset="utf-8"><title>Entrando a Truly Nolen GlobalLink…</title></head>
<body style="font-family:Arial,Helvetica,sans-serif;padding:40px;color:#333">
  <p>Entrando a Truly Nolen GlobalLink…</p>
  <form id="f" method="post"
        action="https://app.trulynolen.tech/globalink/auth/exchange">
    <input type="hidden" name="credencial" value="<%= token %>" />
    <noscript><button type="submit">Continuar</button></noscript>
  </form>
  <script>document.getElementById('f').submit();</script>
</body></html>

Versión ASP.NET. Mismo comportamiento. Archivo globallink.aspx.cs:

using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;

public partial class GlobalLink : System.Web.UI.Page
{
    protected string Token = "";

    protected void Page_Load(object sender, EventArgs e)
    {
        Response.AppendHeader("Cache-Control", "no-store");
        Response.AddHeader("Referrer-Policy", "no-referrer");

        // ►►► AJUSTAR (paso 3a)
        object usuario = Session["Usuario"];
        if (usuario == null || string.IsNullOrEmpty(usuario.ToString()))
        { Response.Redirect("~/login.asp"); return; }

        // ►►► AJUSTAR (paso 3b)
        string cs = ConfigurationManager.ConnectionStrings["Truly"].ConnectionString;

        using (var cn = new SqlConnection(cs))
        {
            cn.Open();

            int idEmpleado;
            using (var q = new SqlCommand(
                "SELECT IdEmpleado FROM dbo.Empleados WHERE Usuario = @u AND FechaBaja IS NULL", cn))
            {
                q.Parameters.Add("@u", SqlDbType.NVarChar, 50).Value = usuario.ToString();
                object r = q.ExecuteScalar();
                if (r == null)
                { Response.Write("Su usuario no tiene un empleado activo asociado."); Response.End(); return; }
                idEmpleado = Convert.ToInt32(r);
            }

            using (var limpia = new SqlCommand(
                "DELETE FROM dbo.GlobalLinkAccesos WHERE Vence < DATEADD(day, -1, SYSUTCDATETIME())", cn))
                limpia.ExecuteNonQuery();

            using (var ins = new SqlCommand(
                "INSERT INTO dbo.GlobalLinkAccesos (IdEmpleado, Vence, Origen) " +
                "OUTPUT INSERTED.Token " +
                "VALUES (@id, DATEADD(minute, 2, SYSUTCDATETIME()), @ip)", cn))
            {
                ins.Parameters.Add("@id", SqlDbType.Int).Value = idEmpleado;
                ins.Parameters.Add("@ip", SqlDbType.NVarChar, 45).Value =
                    (object)Request.UserHostAddress ?? DBNull.Value;
                Token = ((Guid)ins.ExecuteScalar()).ToString();
            }
        }
    }
}

Y el globallink.aspx que lo acompaña. Va con CodeFile, que es lo que corresponde cuando se sueltan los dos archivos en la carpeta sin recompilar nada. Si su portal se compila a una DLL (un Web Application Project), cambien CodeFile por CodeBehind y recompilen; con el atributo equivocado el servidor responde Could not load type 'GlobalLink':

<%@ Page Language="C#" AutoEventWireup="true"
         CodeFile="globallink.aspx.cs" Inherits="GlobalLink" %>
<!doctype html>
<html><head><meta charset="utf-8"><title>Entrando a Truly Nolen GlobalLink…</title></head>
<body style="font-family:Arial,Helvetica,sans-serif;padding:40px;color:#333">
  <p>Entrando a Truly Nolen GlobalLink…</p>
  <form id="f" method="post"
        action="https://app.trulynolen.tech/globalink/auth/exchange">
    <input type="hidden" name="credencial" value="<%= Token %>" />
    <noscript><button type="submit">Continuar</button></noscript>
  </form>
  <script>document.getElementById('f').submit();</script>
</body></html>

Paso 5 · Probar la página sola, antes de conectarla

Este paso se hace sin nosotros y sin salir de su red. Sirve para confirmar que la sesión y la conexión funcionan antes de involucrar a GlobalLink.

  1. Cambiar el destino del formulario, de forma temporal

    Reemplacen la línea action="https://app.trulynolen.tech/…" por action="globallink.asp" y agreguen debajo del formulario, también temporal: <p>Credencial generada: <%= token %></p> (en la versión ASP.NET, <%= Token %>). Quiten el <script> que envía solo, para poder ver la pantalla.

  2. Entrar al sistema con un usuario real y abrir la página

    Con una sesión iniciada, ir a /proposals/TG/globallink.asp. Tiene que aparecer la credencial en pantalla, con forma de 3f2504e0-4f89-41d3-9a0c-0305e82c3301.

  3. Comprobar que la fila quedó bien

    En la base, esta consulta tiene que devolver la fila recién creada, con el número de empleado correcto y una caducidad dos minutos en el futuro:

    SELECT TOP 5 a.Token, a.IdEmpleado, e.Nombre, e.Apellido,
           a.Creado, a.Vence, a.Origen
    FROM   dbo.GlobalLinkAccesos a
    JOIN   dbo.Empleados e ON e.IdEmpleado = a.IdEmpleado
    ORDER  BY a.Creado DESC;
  4. Volver a dejar la página como estaba

    Restaurar el destino real, quitar la línea que muestra la credencial y devolver el <script>. Importante: la credencial no debe quedar visible en pantalla en la versión definitiva.

Paso 6 · Conectar con GlobalLink, primero contra pruebas

Nos avisan y les habilitamos una dirección de pruebas. Cambian el destino del formulario por esa dirección y abren la página con una sesión real. La respuesta es una pantalla en texto claro que dice qué comprobación pasó y cuál falló, con este formato:

1 · credencial bien formada .......... sí
2 · encontrada en la base de TNI ..... sí   (base: Truly)
3 · vigente .......................... sí   (vence en 108 segundos)
4 · no usada antes ................... sí
5 · dentro del tope de 5 minutos ..... sí   (creada hace 12 segundos)
6 · empleado activo .................. sí   (IdEmpleado 22059)
7 · país habilitado .................. sí   (IdPais 244 → Guatemala)
8 · oficina habilitada ............... sí   (IdOficina 679)
9 · cargo con rol asignado ........... sí   (IdCargo 8 → vendedor)
RESULTADO ............................ acceso concedido

Son las nueve comprobaciones que su equipo puede reproducir con una credencial de prueba. Las dos últimas de la sección 8.4 no aparecen en el informe porque sólo se ven con una cuenta real: la 10 crea o actualiza la cuenta —y rechaza con cuenta_desactivada si estaba dada de baja de nuestro lado— y la 11 abre la sesión. Cuando las nueve líneas den «sí», cambian el destino por la dirección definitiva y quedó. Nosotros confirmamos del otro lado que la sesión abrió con el usuario correcto.

Paso 7 · Agregar la pestaña

Es el requerimiento R6: una entrada más en la barra de menú que apunte a la página del paso 4, con la extensión que corresponda a la tecnología que hayan elegido. Con eso el circuito queda cerrado para el usuario final.

Si algo falla · qué significa cada error

Lo que ve el equipoQué significaCómo se arregla
Se redirige a login.aspLa variable de sesión del paso 3a no es la correcta, o está vacíaVerificar el nombre exacto en el archivo de la cabecera del portal
Invalid object name 'dbo.GlobalLinkAccesos'La tabla no existe en la base a la que se conecta el portalCrearla también en esa base; el paso 1 pide las tres
The INSERT permission was deniedFalta el permiso del paso 2 para el usuario del portalEjecutar el GRANT del paso 2 en esa base
The SELECT permission was deniedEl GRANT del paso 2 se ejecutó sin SELECT. La cláusula OUTPUT INSERTED lo necesita.Volver a ejecutar el GRANT completo del paso 2
«Su usuario no tiene un empleado activo asociado»El nombre de usuario de la sesión no existe en dbo.Empleados, o tiene fecha de bajaRevisar el empleado; si su login no usa esa tabla, avisarnos y ajustamos la consulta
La pantalla queda en blancoUn error de ASP que no se está mostrandoActivar temporalmente los mensajes de error detallados en esa página
GlobalLink responde «credencial no encontrada»Buscamos en las tres bases y no está: la página escribió en otra base, o la credencial ya vencióDecirnos en qué base escribió y la ampliamos a esa
GlobalLink responde «credencial ya usada»Se reenvió la misma; es el comportamiento correctoVolver a entrar desde la pestaña genera una nueva

Preguntas que suelen aparecer

¿Viaja alguna contraseña?Ninguna. Ni la de TNI ni la de GlobalLink. La credencial es un número aleatorio sin significado.
¿Y si alguien intercepta la credencial?Tiene dos minutos y un solo uso. Además viaja dentro del cuerpo del envío, cifrado por HTTPS, y nunca aparece en la barra de direcciones ni en los registros del servidor.
¿Pueden entrar a GlobalLink sin pasar por nuestro sistema?No con este método. La credencial sólo la genera su página, para alguien que ya tiene sesión en el sistema de gestión.
¿Qué pasa si damos de baja a un empleado?La consulta del paso 4 filtra por FechaBaja IS NULL, así que su página deja de emitir credenciales. Además nosotros leemos esa fecha cada noche y desactivamos la cuenta.
¿Necesitan acceso a nuestro sistema?No. Su página sólo escribe en su propia base y envía un formulario.
¿Hay que abrir algún puerto?No. Es el navegador del empleado el que envía el formulario, no el servidor de TNI.
¿Y si el empleado usa dos oficinas?Entra con la que tenga en dbo.Empleados. Si en el futuro hacen falta varias, se resuelve del lado de GlobalLink sin tocar su página.
¿Se puede probar en un ambiente de pruebas de ustedes?Sí. Si tienen una copia del portal, la página funciona igual apuntando a nuestra dirección de pruebas.

Lista de verificación de R7

PasoQué queda hechoCómo se comprueba
1Tabla e índice creados en las tres basesLa consulta de columnas devuelve cinco filas por base
2Permiso de inserción y borrado al usuario del portalEl INSERT de prueba devuelve una credencial
3Anotados el nombre de la variable de sesión y la cadena de conexiónEscritos en el ticket, para el paso 4
4Página creada con las dos líneas ajustadasSe abre sin error con una sesión iniciada
5Prueba local: aparece la credencial y la fila en la tablaLa consulta ordenada por fecha muestra la fila
6Prueba contra nuestra dirección de pruebasLas nueve líneas del informe en «sí»
7Pestaña en el menú, apuntando a la páginaUn empleado entra a GlobalLink desde su sistema

Criterio de aceptación de R7. Un empleado real de Guatemala hace clic en la pestaña y llega a GlobalLink con su nombre, su oficina y su rol, sin escribir contraseña. Y un segundo envío de la misma credencial es rechazado.

Qué no hacer. No poner la credencial en la dirección. No alargar la caducidad más allá de cinco minutos. No dejar la credencial visible en pantalla en la versión definitiva. No guardar la contraseña del empleado en ningún campo. No reutilizar una credencial ya enviada.

Si prefieren un token firmado en lugar de la credencial en tabla

Las dos variantes conservan todo lo demás igual: la misma pestaña, el mismo envío por formulario, los mismos identificadores crudos, la misma dirección de destino. Cambia sólo qué se manda en el campo credencial.

Variante con secreto compartido. Requiere ASP.NET. El secreto se intercambia una vez por la bóveda y se guarda cifrado en web.config. Nos ahorra la consulta a su base, a cambio de manejar un secreto:

using System;
using System.Security.Cryptography;
using System.Text;

static string B64Url(byte[] b) =>
    Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_');

string CrearPase(int idEmpleado, int idPais, int idOficina, int idCargo,
                 string nombre, string email, string secreto)
{
    long ahora = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    string cabecera = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
    string cuerpo =
        "{\"iss\":\"https://www.trulynoleninternational.com\"," +
        "\"aud\":\"tn-globallink\",\"sub\":\"" + idEmpleado + "\"," +
        "\"name\":\"" + nombre.Replace("\"", "") + "\"," +
        "\"email\":\"" + (email ?? "") + "\"," +
        "\"tni_id_pais\":" + idPais + ",\"tni_id_oficina\":" + idOficina +
        ",\"tni_id_cargo\":" + idCargo + "," +
        "\"iat\":" + ahora + ",\"nbf\":" + ahora + ",\"exp\":" + (ahora + 300) + "," +
        "\"jti\":\"" + Guid.NewGuid() + "\"}";
    string sinFirma = B64Url(Encoding.UTF8.GetBytes(cabecera)) + "." +
                      B64Url(Encoding.UTF8.GetBytes(cuerpo));
    using (var h = new HMACSHA256(Encoding.UTF8.GetBytes(secreto)))
        return sinFirma + "." + B64Url(h.ComputeHash(Encoding.UTF8.GetBytes(sinFirma)));
}

Variante con par de llaves. Es la especificación acordada en mayo, con la llave pública publicada en una dirección fija en formato JWKS. Si algún día se prefiriera este camino, el contenido del token y la rotación de llaves están descritos acá y las comprobaciones 6 a 11 de la sección 8.4 valen igual; cambian sólo las cinco primeras. No es lo que se va a construir. Ver la sección 8.

using Microsoft.IdentityModel.Tokens;      // NuGet: System.IdentityModel.Tokens.Jwt
using System.IdentityModel.Tokens.Jwt;

var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(llavePrivada);        // del almacén de certificados del servidor
var cred = new SigningCredentials(new RsaSecurityKey(rsa) { KeyId = "tni-2026-09" },
                                  SecurityAlgorithms.RsaSha256);
var token = new JwtSecurityToken(
    issuer:   "https://www.trulynoleninternational.com",
    audience: "tn-globallink",
    claims:   new[] {
        new Claim("sub", idEmpleado.ToString()),
        new Claim("name", nombre),
        new Claim("email", email ?? ""),
        new Claim("tni_id_pais",    idPais.ToString()),
        new Claim("tni_id_oficina", idOficina.ToString()),
        new Claim("tni_id_cargo",   idCargo.ToString()),
        new Claim("jti", Guid.NewGuid().ToString())
    },
    notBefore: DateTime.UtcNow,
    expires:   DateTime.UtcNow.AddMinutes(5),
    signingCredentials: cred);
string pase = new JwtSecurityTokenHandler().WriteToken(token);

R8 · La equivalencia de cargos que vamos a aplicar

Decisión de CatalizadoraSu esfuerzo: leerla, 15 minutosNo bloquea nadaDecidido · para su información

Objetivo. Definir qué puede hacer en GlobalLink cada cargo de su tabla de empleados. Ya está definida y la vamos a cargar así: leímos su catálogo de 43 cargos del espejo con la cantidad de empleados activos de cada uno. Diez tienen gente, y son los que van en la tabla de abajo, cada uno con su rol asignado. Los otros 33 no tienen ningún empleado activo, así que quedan sin rol: el día que TNI le asigne alguien a uno de ellos, esa persona no entrará hasta que agreguemos la fila, que es una línea en una tabla nuestra. No hace falta que preparen nada ni que respondan. La publicamos acá porque es una decisión sobre permisos de su gente y ustedes tienen que poder verla y objetarla; si una fila no corresponde, un correo y se corrige.

IdCargoCargo en su sistemaActivosRol propuesto en GlobalLinkQué podría hacer
1TNI15tni_adminVer toda la red, administrar
2Franquiciado107franchise_ownerSu oficina completa, repartir trabajo
3Gerente General288franchise_adminSu oficina completa, repartir trabajo
4Gerente de Ventas28franchise_managerSu oficina, repartir trabajo
6Gerente de Operaciones44franchise_managerSu oficina, repartir trabajo
5Gerente Técnico67franchise_userVer y trabajar sus negocios
7Administrativo458franchise_userVer y trabajar sus negocios
8Vendedor1.011franchise_userVer y trabajar sus negocios
9Técnico2.667sin accesoNo entra a GlobalLink
10Otro71sin accesoSe decide caso por caso si hace falta

Con esta propuesta entran 2.018 personas de la red —la suma de la columna «Activos» de las filas con rol— y quedan fuera los 2.667 técnicos de campo, que no usan un sistema de ventas. Los números salen de su propia tabla de empleados, contando sólo los que no tienen fecha de baja. Su catálogo tiene 43 cargos; los 33 que no aparecen acá no tienen ningún empleado activo.

Qué hacemos con esto. La cargamos tal cual en una tabla nuestra, app.tni_cargo_rol. Un cargo que no esté en la lista no entra: el acceso se rechaza con un mensaje claro y nunca se adivina un rol. Cambiar una fila después es una línea en esa tabla, sin desplegar nada.

Lo que sí les pedimos que miren. Dos filas, porque son decisiones de negocio y no técnicas: los 2.667 técnicos de campo, que dejamos fuera porque no usan un sistema de ventas, y los 71 «Otro», que quedan fuera por precaución. Si alguno de esos grupos debería entrar, dígannos y lo cambiamos.

2.3 · Lista de verificación para cerrar

Sólo las tres primeras filas tienen que ocurrir para que el proyecto avance. Las demás se marcan cuando pasen, sin fecha y sin esperar a nadie.

#Qué tiene que ocurrirCómo lo confirmamos
R1PDF del addendum firmado por ambas partesQueda en el servidor; el registro de cada corrida lo cita
R2Aviso de usuario creado; contraseña por la bóvedaConsulta de permisos y prueba negativa desde nuestro servidor
R3Aviso de red habilitadaConexión de prueba en modo sombra, sin insertar
R4Opcional. Si la agregan, aviso de columnas e índices creadosAparecen en el catálogo que leemos cada noche
R5Nada de su parte. Las convenciones ya están puestas.Se ven en la primera alta de prueba, que les mostramos
R6Pestaña visible en el menúCon el usuario de prueba que ya tenemos
R7Tabla creada y página publicadaLas nueve líneas del informe de pruebas en «sí», y una entrada real
R8Nada de su parte. La equivalencia ya está cargada.Un acceso por cargo abre con el rol esperado
Un solo canal para dudas. Cualquier consulta sobre esta especificación se responde el mismo día. Lo que no está escrito acá no está asumido: preguntarlo antes de improvisar.

2.4 · Los traspasos: qué recibe cada lado antes de poder empezar

Ningún punto de esta sección arranca en frío. Para cada uno: lo que ya tienen en la mano, lo que nos devuelven, y qué se detiene si no llega. Fíjense en la última columna: sólo tres filas detienen el proyecto; otras dos posponen una comodidad. Casi todo lo de «lo que ya les dimos» está en este documento; las tres excepciones, que van por canal aparte, son la IP de origen de R3, el logo de R6 y la dirección de pruebas de R7.

#Lo que ya les dimos, y dónde estáLo que nos devuelvenQué se detiene si no llega
R1El addendum COT-0003-A1 v2 redactado y el memo técnico que lo acompaña, ya entregadosEl PDF firmado por ambas partesTodo el camino de la escritura. Es la única pieza sin la cual no se escribe ni una fila, por contrato y por candado en el programa.
R2Las sentencias completas de creación del usuario y sus permisos, listas para ejecutar (abajo, en R2)Aviso de ejecutado, y la contraseña por la bóveda de secretos que les indicamos — nunca por correo ni por chatLa conexión de escritura. Todo lo demás sigue construyéndose y probándose sin esto.
R3El puerto, el sentido y la forma de la regla. La IP /32 no va en este documento: se entrega por canal privado, junto con la contraseña de R2Confirmación de que la regla está aplicadaLlegar al servidor. Se comprueba en un minuto con la consulta que dejamos en R3.
R4Las cuatro ALTER TABLE y los cuatro índices, por base, con la justificación de por qué es filtradoNada. Es opcional.Nada. La protección contra duplicados ya corre de nuestro lado con la marca en Observaciones. La columna es más limpia para ustedes; si no la agregan, seguimos igual.
R5Las tres convenciones ya decididas, con lo que medimos en su base y la consulta para comprobarloNada. No estamos esperando respuesta.Nada. Se escribe con lo decidido y queda registrado en cada alta. Cambiar un valor después cuesta un correo.
R6El logo en PNG y SVG, y el fragmento de HTML de la pestañaLa pestaña publicada, cuando puedanSólo la comodidad del clic. La gente entra igual con usuario y contraseña.
R7El script de la tabla, el código completo de la página en ASP clásico y en ASP.NET, la consulta de verificación de cada paso y una dirección de pruebas con informe en texto claroAviso de que la página está publicada, y en qué base escribe si no son las tresSólo la comodidad del clic. Nuestro lado queda construido y probado esperándola, sin fecha.
R8La equivalencia ya llena: los 10 cargos con empleados activos, con su rol y cuánta gente hay en cada unoNada, salvo que quieran objetar una filaNada. Se carga como está. Un cargo sin fila no entra: es deliberado, para que nadie reciba permisos por omisión.
Un solo interlocutor de cada lado. Seis de estas ocho las ejecutan equipos distintos —dirección, base de datos, redes y portal—, así que proponemos que TNI nombre a una persona que las reciba y las devuelva, para que nada quede esperando sin que se sepa en quién. Del lado de Catalizadora esa persona es Pablo Estrada.
↑ 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 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 dos nombres que no existen.
    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, sólo POST, declarada pública en apps/web/proxy.ts. Con el basePath de producción sirve /globalink/auth/exchange — una sola «l» en globalink, que es el prefijo real de la aplicación y no una errata.

    Qué hace, en el orden de la sección 8.4: valida la forma del campo credencial sin tocar la base; busca la fila en dbo.GlobalLinkAccesos de las tres bases productivas con el usuario de lectura user_globallink que ya existe, uniendo contra dbo.Empleados para traer nombre, correo, IdPais, IdOficina e IdCargo en la misma consulta; comprueba la vigencia con la hora del propio SQL Server de TNI y un tope propio de cinco minutos; marca el canje insertando en una tabla nueva app.sso_pases_vistos (credencial uuid PRIMARY KEY, visto_en), cuya llave única es la que garantiza el uso único —del lado nuestro, sin pedirle a TNI ningún permiso de escritura—; traduce el país y la oficina contra silver.oficinas y el cargo contra una tabla nueva app.tni_cargo_rol (id_cargo, role) que llenamos nosotros con la equivalencia de R8; busca o crea la cuenta en public.users por una columna nueva id_empleado_tni, que es la única llave de resolución (external_sub se escribe como valor derivado tni: + número, y no se usa para buscar); abre sesión con signSession y setSessionCookie de lib/auth/session.ts, registra en app.user_sessions y deja el intento —con su resultado y su motivo— en audit.events.

    Configuración: ninguna variable de entorno nueva. No hay emisor, ni llave, ni dirección remota que configurar: todo sale de la conexión de lectura que ya está en uso. Prueba de punta a punta insertando una fila a mano en la tabla de TNI y enviando esa credencial, sin necesidad de que su página exista todavía. Entrega a TNI: el logo en PNG y SVG, el fragmento HTML de la pestaña, la dirección de pruebas con el informe en texto claro, y la especificación versionada en specs/ACCESO-SSO.md.

    Migraciones: dos tablas nuevas (app.sso_pases_vistos, app.tni_cargo_rol), una columna nueva (public.users.id_empleado_tni, con índice único) y un motivo nuevo de revocación de sesión, employee_offboarded, que hoy no existe en app.user_sessions.

  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 tarea 4 de TNI, opcionalDatos mínimos, duplicados, números devueltos, bitácora completa al espejo; y la columna anti-duplicados si la agregan. Verificable en modo sombra.
no depende 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 con una credencial insertada a mano en su tabla, sin esperar su página.
tabla 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
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. Se lo informamos a TNI en R5; si prefieren el 2, se cambia con un correoIdTipoClienteint · obligatoriaarreglar hoy no se manda decidido
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 (la credencial resuelve el empleado, ver sección 8). Antes de eso, el cruce es por correo contra silver.empleados.IdVendedorint → 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; cuando no la tengamos escribimos el texto fijo «C/F», decidido en R5Identidadnvarchar(50)decidido
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 único · opcional, sólo si aplican R4crear 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 nueva · opcional (R4)crear 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 guarda una credencial de un solo uso en su propia base, junto al número de empleado de Carlos, y la manda a GlobalLink. Dura dos minutos y sirve una sola vez.

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

    GlobalLink busca la credencial en la base de TNI con el usuario de lectura que ya tiene, comprueba que siga vigente y que no se haya usado, lee de ahí mismo su oficina y su cargo, y abre su tablero de Guatemala, oficina 679, con permisos de vendedor. Como todo se lee de la base de TNI, GlobalLink ya sabe quién es Carlos: 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 y las once comprobaciones
  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 1 «Creative / venta activa», decidido en R5. 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 el acceso de red
  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

El acceso, para construir y para auditar

La especificación completa del método elegido en R7: la credencial de un solo uso. Dice qué pieza es de cada lado, qué viaja y qué no, las once comprobaciones en orden, y quién sostiene cada garantía de seguridad. La variante con token firmado, que fue la especificación acordada entre los equipos técnicos el 5 de mayo de 2026, queda como anexo al final de R7: no se descarta, pero no es lo que se construye.

En lenguaje llanoCómo se entra a GlobalLink desde el sistema de gestión sin escribir otra contraseña, por qué es seguro, y qué parte de esa seguridad sostiene cada lado.
👤 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 de quién es cada una

PiezaQué es y dónde viveDe quién es · quién la construye y la mantiene
dbo.GlobalLinkAccesosLa tabla donde se guarda cada credencial. En las tres bases productivas de TNI.TNI La crea su administrador de base de datos (R7, paso 1). No la tocamos: sólo la leemos.
globallink.aspo .aspxLa página que lee la sesión del portal, guarda la credencial y la envía. En /proposals/TG/; el nombre y la extensión los eligen ellos.TNI La escribe su equipo del portal con el código de R7. Es el único desarrollo nuevo del lado de TNI.
La pestaña del menúUna entrada más en la barra que ya tiene Home y Reports, apuntando a esa página.TNI R6. Nosotros entregamos el logo y el fragmento de HTML.
user_globallinkEl usuario de lectura con el que ya leemos su base cada noche.TNI Ya existe y ya tiene alcance a nivel de base: la tabla nueva queda cubierta sin ningún permiso adicional.
POST /globalink/auth/exchangeEl punto de entrada que recibe la credencial y abre la sesión. En app.trulynolen.tech.Catalizadora Lo construimos, lo operamos y respondemos por él. Público, sin sesión previa, declarado en apps/web/proxy.ts.
app.sso_pases_vistosDonde anotamos cada credencial ya canjeada, para que no sirva dos veces.Catalizadora Vive en nuestra base, no en la de TNI. Por eso el un-solo-uso no les cuesta ningún permiso de escritura.
app.tni_cargo_rolLa equivalencia entre cada cargo de TNI y el rol dentro de GlobalLink.Catalizadora La cargamos nosotros con la equivalencia informada en R8; TNI puede objetar cualquier fila. Su página no traduce nada.
Destino y página de error/globalink/ y /globalink/auth/exchange/error?motivo=…, siempre con enlace de vuelta a su sistema.Catalizadora

8.2 · La credencial: qué se guarda y qué viaja

La credencial es un identificador único de SQL Server (UNIQUEIDENTIFIER, 122 bits aleatorios) generado por el propio motor con NEWID(). No lleva datos dentro, no está firmada y no significa nada por sí sola: es una llave de guardarropa. Todo lo que importa está en la fila que la acompaña, dentro de la base de TNI.

ColumnaEjemploQuién la escribeRegla que hacemos cumplir al recibirla
Token3f2504e0-4f89-41d3-9a0c-0305e82c3301motor SQLEs lo único que viaja. Tiene que tener forma de identificador único; cualquier otra cosa se rechaza sin consultar la base.
IdEmpleado22059su páginaLa identidad estable. Si cambia el correo o el nombre, la cuenta sigue siendo la misma. Se resuelve contra dbo.Empleados.
Creado2026-09-03 21:14:07motor SQLEn UTC, con SYSUTCDATETIME(). Sirve para auditar y para la limpieza.
Vence2026-09-03 21:16:07su páginaDos minutos después de crearse. Comparamos contra la hora del propio SQL Server de TNI, no contra la nuestra: así no hay problema de relojes desfasados.
Origen190.56.12.4su páginaOpcional. No decide nada; queda para auditar.

El resto de la identidad —país, oficina y cargo— no viaja en la credencial: lo leemos de dbo.Empleados en la misma consulta, crudo, con los identificadores de TNI. Eso es deliberado: nadie puede alterar por el camino la oficina o el cargo con que entra, porque nunca salen de su base.

8.3 · Cómo viaja

La página de TNI devuelve un documento HTML mínimo con un formulario que se envía solo. La credencial va en el cuerpo, no en la dirección: así no queda en el historial del navegador, ni en la cabecera 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="credencial" value="3f2504e0-4f89-41d3-9a0c-0305e82c3301">
  <noscript><button>Entrar a GlobalLink</button></noscript>
</form>
<script>document.getElementById("f").submit();</script>

Cabeceras de esa respuesta: Cache-Control: no-store y Referrer-Policy: no-referrer. La credencial nunca se escribe en un log del lado de TNI. Nada sale del servidor de TNI hacia el nuestro: es el navegador del empleado el que hace el envío, así que no hay que abrir ningún puerto ni permitir ninguna dirección.

8.4 · Qué hace GlobalLink al recibirla, en orden

Las once comprobaciones. Las cinco primeras son propias de este método; de la sexta en adelante son las mismas que tendría cualquier forma de entrar, y son las que hacen cumplir la equivalencia de cargos de R8.

#ComprobaciónSi fallaMotivo registrado
1El cuerpo trae credencial con forma de identificador único. Si no, ni siquiera se consulta la base de TNI.400credencial_malformada
2Existe la fila en dbo.GlobalLinkAccesos. Se busca en las tres bases productivas, con el usuario lector.401credencial_no_encontrada
3Vence > SYSUTCDATETIME(), evaluado por el propio SQL Server de TNI en la misma consulta.401credencial_vencida
4La credencial no fue canjeada antes. Se inserta en app.sso_pases_vistos dentro de la misma transacción; si la inserción choca con la llave única, es un reenvío o un intento de repetición.401credencial_ya_usada
5La antigüedad de la fila es menor a 5 minutos aunque su Vence diga otra cosa. Es un tope nuestro, por si alguna vez su página se despliega con una caducidad más larga.401credencial_demasiado_vieja
6El empleado existe en dbo.Empleados, con FechaBaja IS NULL.403empleado_inactivo
7Su IdPais se traduce a un país activo de la plataforma. El código de país de TNI no es ISO en 13 de sus 239 países —Guatemala es «GTB»—, por eso la traducción la hacemos nosotros con su propia tabla.403pais_no_habilitado
8Su IdOficina existe en silver.oficinas y pertenece a ese país.403oficina_no_habilitada
9Su IdCargo tiene fila con rol asignado en app.tni_cargo_rol, la tabla que carga la equivalencia de R8.403cargo_sin_equivalencia · cargo_sin_acceso
10Cuenta: se busca en public.users por id_empleado_tni. Si no existe, se crea con external_sub = 'tni:' + IdEmpleado, correo, nombre, rol, oficina y país leídos de su base. Si existe, se actualizan esos campos; si estaba desactivada, se rechaza.403cuenta_desactivada
11Sesión: cookie tn_session firmada por GlobalLink, 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 dispositivo, IP y ubicación aproximada. Es la misma sesión que abre el login normal. Respuesta: 303 See Other al tablero de su oficina.

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 y el número de empleado. La credencial misma nunca se registra. Límite: 30 intentos por minuto por IP; por encima, 429 sin detalle.

8.5 · Quién garantiza cada propiedad de seguridad

Esta es la lectura que va a querer un auditor: para cada garantía, qué lado la sostiene y con qué mecanismo. Ninguna depende de que el otro lado se acuerde de algo.

GarantíaCómo se sostiene del lado de TNICómo se sostiene del lado de Catalizadora
Sólo entra quien ya tiene sesión en TNILa página empieza comprobando la variable de sesión del portal; sin ella redirige al login y no genera nada.No existe otra manera de obtener una credencial válida: no la emitimos nosotros.
La credencial dura pocoSu página escribe Vence a dos minutos.Además rechazamos cualquier fila de más de cinco minutos, aunque su Vence diga otra cosa (comprobación 5).
Sirve una sola vezNada. No les cuesta permisos ni desarrollo.La anotamos en nuestra base al canjearla, con llave única. El segundo intento choca y se rechaza.
Nadie altera su oficina ni su cargoNo hace falta que los manden: viven en dbo.Empleados.Los leemos de su base en la misma consulta. Lo que viaja no contiene identidad.
Un empleado dado de baja no entraLa consulta de su página filtra por FechaBaja IS NULL y deja de emitirle credenciales.Lo volvemos a comprobar al canjear, y cada noche el espejo desactiva la cuenta y revoca sus sesiones abiertas.
Quién puede hacer qué dentro de GlobalLinkPueden objetar cualquier fila de la equivalencia de R8. Su página no decide permisos.Traducimos el cargo con esa tabla; un cargo sin fila no entra.
La tabla no crece sin controlSu página borra las filas vencidas de más de un día antes de insertar (paso 4 de R7). Sin trabajo programado ni mantenimiento: con unos 4.000 accesos diarios la tabla se estabiliza en ese orden de filas.
Queda registro de todo intentoOpcionalmente, Origen y Creado en su propia tabla.Una fila por intento en audit.events, consultable por el rol auditor.

8.6 · Qué ve la persona si algo falla

Una página en el idioma de la persona, o en español si no se conoce, 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 leídos de su tabla de empleados. No hay invitación ni contraseña de GlobalLink.
  • Cambio: nombre, correo, oficina y rol se releen de dbo.Empleados en cada acceso. TNI manda.
  • Baja: cuando TNI da de baja al empleado, su página deja de emitirle credenciales 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 este camino 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 la pestaña con el mismo correo.

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

TNI entrega · tres cosas, cuando puedan

  • La tabla dbo.GlobalLinkAccesos creada en sus bases productivas, con el script de R7 paso 1 ejecutado tal cual.
  • La página globallink.asp —o .aspx, según la tecnología que elijan— publicada con el código de R7 paso 4, y la pestaña de R6 apuntando a ella.
  • Nada más. La equivalencia de cargos (R8) la cargamos nosotros, con los 10 cargos con empleados activos ya resueltos, ocho con rol y dos sin acceso; si alguna fila no corresponde, se corrige con un correo y sin desplegar nada.

Nada más. Para esto no hay que abrir puertos, ni crear usuarios de base de datos, ni instalar librerías, ni administrar llaves, ni programar tareas de mantenimiento.

Catalizadora entrega · todo lo demás

  • El punto de entrada con las once comprobaciones, en producción y en una dirección de pruebas separada que habilitamos para su equipo antes de que toquen producción.
  • El informe de pruebas en texto claro (R7 paso 6): nueve líneas que dicen qué comprobación pasó y cuál falló, para que su equipo depure sin depender de nosotros.
  • El logo en PNG y SVG, y el fragmento de HTML de la pestaña.
  • La tabla de equivalencia cargada, el registro de auditoría consultable por el rol auditor, y esta especificación versionada en specs/ACCESO-SSO.md versión 2.
  • Un contacto técnico disponible durante la prueba conjunta, que dura menos de una hora.

Las ocho pruebas de aceptación. P1 la corremos juntos; P2 a P5 las corre su equipo solo, porque el resultado se ve en el informe de texto claro; P6 a P8 las corremos nosotros contra la dirección de pruebas.

#PruebaResultado esperadoQuién la corre
P1Un vendedor de Guatemala entra desde la pestañaLlega al tablero de la oficina 679 con rol de vendedor; fila en auditoría con resultado okLos dos, juntos
P2Se reenvía la misma credencial una segunda vez401 credencial_ya_usada. Volver a la pestaña genera una nueva y entra.TNI, solo
P3Se espera tres minutos antes de enviar401 credencial_vencidaTNI, solo
P4Se envía un identificador único inventado401 credencial_no_encontrada, sin filtrarse ninguna información sobre qué existe en su baseTNI, solo
P5Se abre la página sin sesión en el sistema de gestiónRedirige a su login. No se crea ninguna fila en la tabla.TNI, solo
P6Entra un empleado cuyo cargo no está en la equivalencia403 cargo_sin_equivalencia, con la página de error en su idioma y el enlace de vuelta al sistema de gestiónCatalizadora
P7Entra un empleado que nunca usó GlobalLinkSe crea la cuenta sola y entra; public.users.id_empleado_tni queda escritoCatalizadora
P8Entra un empleado con fecha de baja403 empleado_inactivo; sus sesiones anteriores quedan revocadasCatalizadora
Cuánto dura esto de verdad. Del lado de TNI son entre dos y tres horas de trabajo, una sola vez, hechas por una persona. La prueba conjunta es una llamada de menos de una hora. No hay mantenimiento posterior: la página no cambia cuando cambien los empleados, los cargos, las oficinas o los países, porque nada de eso está escrito dentro de ella.
↑ 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 completo 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 de la credencialid_empleado_tni
app.tni_cargo_rol · app.sso_pases_vistosLa equivalencia cargo → rol informada en R8, y las credenciales ya canjeadasnomigració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 esté el acceso
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 cuatro 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.

Sobre la última columna de cada sentencia. idempotency_key es la columna opcional de R4. El programa no la escribe a ciegas: al arrancar lee el catálogo de columnas de cada base y arma el INSERT con ella sólo si existe. Si TNI no aplica R4, las cuatro sentencias salen sin esa columna y sin ese parámetro, y la protección contra duplicados queda a cargo de la marca GLOBALLINK:{id_lead} en Observaciones, que se escribe siempre. Abajo se muestran con la columna puesta, que es la variante completa.

-- 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. La credencial se resuelve contra dbo.Empleados por IdEmpleado
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: es lo único de este documento que nos falta y que depende de TNI, y no bloquea nada. Lo único que quedó sin poder medirse: qué escribe TNI en Identidad cuando no conoce el número fiscal — la columna está llena en el 100 % de los casos, pero no se puede saber desde afuera cuál de esos valores es un relleno. Por eso lo decidimos nosotros («C/F», R5) en lugar de esperar la respuesta.