Saltar al contenido principal

Inmersys VRT VRL API — Documentación Técnica Completa

Tabla de Contenidos

  1. Descripción General
  2. Estructura del Proyecto
  3. Dependencias
  4. Variables de Entorno
  5. Instalación y Ejecución
  6. Arquitectura
  7. Base de Datos
  8. Modelos Mongoose
  9. Endpoints de la API
  10. Middlewares y Validaciones
  11. Helpers y Utilidades
  12. Autenticación y Seguridad
  13. Agregaciones MongoDB
  14. Integraciones Externas
  15. Soporte Multi-Base de Datos
  16. Testing
  17. CI/CD y Despliegue
  18. Notas Técnicas y Observaciones

1. Descripción General

inmersys-vrt-vrl-api es el backend REST API para la plataforma de capacitación y aprendizaje en realidad virtual de Inmersys. Gestiona dos modalidades:

  • VRT (Virtual Reality Training): entrenamiento industrial/corporativo
  • VRL (Virtual Reality Learning): aprendizaje académico/educativo

La API administra empresas, proyectos, grupos de aprendizaje, estudiantes y el registro de respuestas/resultados que los usuarios envían desde los headsets de realidad virtual (principalmente Meta Quest/Oculus). Soporta integraciones con la plataforma HBSA Engage y con una base de datos secundaria denominada Simi.

Stack tecnológico:

CapaTecnología
RuntimeNode.js 16.19.0
FrameworkExpress 4.18.1
Base de datosMongoDB (Mongoose 6.4.4)
AutenticaciónJWT (jsonwebtoken 8.5.1)
Validaciónexpress-validator 6.14.2
EmailNodemailer 6.8.0
HTTP ClientAxios 1.6.7
Proceso prod.PM2

2. Estructura del Proyecto

inmersys-vrt-vrl-api/
├── src/
│ ├── app.js # Punto de entrada
│ ├── server.js # Clase Server (Express)
│ ├── config/
│ │ ├── database.js # Conexión MongoDB principal
│ │ └── DatabaseClientSimi.js # Conexión MongoDB secundaria (Singleton)
│ ├── controllers/
│ │ ├── admin.js # CRUD y auth de administradores
│ │ ├── auth.js # Login, restauración de contraseña, tokens
│ │ ├── enterprise.js # CRUD empresas
│ │ ├── project.js # CRUD proyectos
│ │ ├── learningGroup.js # CRUD grupos de aprendizaje
│ │ ├── student.js # CRUD estudiantes, login Oculus
│ │ ├── learningAnswer.js # Registro respuestas VRL
│ │ ├── trainigAnswer.js # Registro respuestas VRT
│ │ ├── projectTraining.js # Datos completos de entrenamiento
│ │ └── thirdPartyControllers.js # Integración HBSA Engage
│ ├── routes/
│ │ ├── index.js # Exportación de todas las rutas
│ │ ├── auth.js
│ │ ├── admin.js
│ │ ├── enterprise.js
│ │ ├── project.js
│ │ ├── learningGroup.js
│ │ ├── student.js
│ │ ├── learningAnswer.js
│ │ ├── trainigAnswer.js
│ │ └── projectTraining.js
│ ├── models/
│ │ ├── index.js # Exportación de modelos
│ │ ├── Admin.js
│ │ ├── Enterprise.js
│ │ ├── Project.js
│ │ ├── LearningGroup.js
│ │ ├── Student.js
│ │ ├── LearningAnswer.js
│ │ ├── TrainigAnswer.js
│ │ └── Schemas/
│ │ ├── LearningAnswerSchema.js
│ │ ├── LearningGrupSchema.js
│ │ └── StudenSchema.js
│ ├── middlewares/
│ │ ├── index.js
│ │ ├── validateJWT.js # Validación de token admin
│ │ ├── validateDataExpress.js # Validación express-validator
│ │ ├── admin-validation.js
│ │ ├── enterprise-validation.js
│ │ ├── project-validation.js
│ │ ├── learningGroup-validation.js
│ │ ├── learningAnswer-validation.js
│ │ └── student-validations.js
│ ├── helpers/
│ │ ├── index.js
│ │ ├── router.js # Definición de rutas base
│ │ ├── response.js # Respuestas HTTP estándar
│ │ ├── bcrypt.js # Hash de contraseñas
│ │ ├── token.js # Generación/verificación JWT
│ │ ├── constanst.js # Constantes (claves JWT)
│ │ ├── db-validations.js # Transformaciones Mongoose
│ │ └── email.js # Envío de emails SMTP
│ ├── aggregates/
│ │ └── learningAnswer.js # Pipeline aggregation MongoDB
│ └── test/
│ ├── controllers/
│ │ ├── admin.test.js
│ │ ├── enterprise.test.js
│ │ └── utils.js
│ └── services/
│ └── enterprise.test.js
├── package.json
├── jest.config.js
├── .envexample
├── .gitignore
├── .gitlab-ci.yml
└── .vscode/
└── settings.json

3. Dependencias

Producción

PaqueteVersiónUso
express^4.18.1Framework web
mongoose^6.4.4ODM para MongoDB
jsonwebtoken^8.5.1Autenticación JWT
bcryptjs^2.4.3Encriptación de contraseñas
express-validator^6.14.2Validación declarativa de requests
cors^2.8.5CORS middleware
dotenv^16.0.1Variables de entorno
nodemailer^6.8.0Envío de emails SMTP
axios^1.6.7Cliente HTTP para integraciones externas
@sendgrid/mail^7.7.0Integración SendGrid (importado pero SMTP activo)

Desarrollo

PaqueteVersiónUso
jest^29.3.1Framework de testing
supertest^6.3.3Testing de endpoints HTTP
nodemon^2.0.20Hot-reload en desarrollo

Scripts disponibles

npm start       # Producción: node ./src/app
npm run dev # Desarrollo: nodemon ./src/app
npm test # Tests: jest --detectOpenHandles --watch

4. Variables de Entorno

Crear un archivo .env basado en .envexample:

VariableDescripciónRequerida
PORTPuerto del servidor (ej: 5000)
SECRETORPRIVATEKEYClave secreta para firma de JWT de admins
MONGO_DB_URLURI de conexión a MongoDB principal
DB_CLIENT_SIMIURI de conexión a MongoDB secundaria (Simi)
AUTH_OCULUS_KEYClave para firmar/verificar tokens de headsets Oculus
PROJECT_SIMI_VRObjectId del proyecto Simi VR en BD
PROJECT_SIMI_ARObjectId del proyecto Simi AR en BD
PROJECT_HBSAObjectId del proyecto HBSA para integración Engage
PROJECT_MERZObjectId del proyecto MERZ
MAIL_HOSTHost SMTP para envío de correos
MAIL_USERUsuario SMTP
MAIL_PASSWORDContraseña SMTP
RESPONSE_TOKENToken de respuesta genérico
TEST_TOKENToken para ambiente de pruebasSolo testing

5. Instalación y Ejecución

# 1. Clonar repositorio
git clone <repo-url>
cd inmersys-vrt-vrl-api

# 2. Instalar dependencias
npm install

# 3. Configurar variables de entorno
cp .envexample .env
# Editar .env con los valores reales

# 4. Ejecutar en desarrollo
npm run dev

# 5. Ejecutar en producción (directo)
npm start

# 6. Ejecutar en producción con PM2
pm2 start src/app.js --name "app"
pm2 save
pm2 startup

La API levanta en http://localhost:{PORT} y expone un health check en:

GET /api  →  { "message": "working good!", "version": "0.20.0" }

6. Arquitectura

Inicialización

app.js
└── new Server()
├── initDB() → Conecta MongoDB principal + MongoDB Simi
├── middlewares() → cors(), express.json(), static files
└── routes() → Monta todos los routers con sus middlewares

Clase Server (src/server.js)

Orquesta toda la aplicación Express:

  • initDB() — conecta database.js (principal) y DatabaseClientSimi (secundaria) de forma asíncrona.
  • middlewares() — habilita CORS abierto, parseo JSON y archivos estáticos desde /public.
  • routes() — monta cada router; la mayoría de rutas tienen validateJWT como primer middleware (excepto /api/auth y /api/trainig-answer).
  • listen() — imprime hostname y puerto al iniciar.

Flujo de un request típico

Request HTTP
→ Express
→ CORS
→ express.json()
→ [validateJWT] ← verifica x-token, carga admin en req.body.requestingAdminInstance
→ [validaciones route] ← express-validator + custom validators
→ [validateData] ← corta si hay errores de validación
→ Controller function
→ getBadRequest / getGoodResponse
→ Response HTTP

Rutas base (src/helpers/router.js)

ConstanteRuta base
auth/api/auth
admin/api/admin
enterprise/api/enterprise
project/api/project
learningGroup/api/learning-group
student/api/student
learningAnswer/api/learning-answer
trainigAnswer/api/trainig-answer
projectTrainig/api/project-trainig

Nota: El typo trainig (sin segunda 'n') está en el código fuente en múltiples lugares. Mantener consistencia al agregar nuevas rutas.


7. Base de Datos

BD Principal (MONGO_DB_URL)

Contiene todas las colecciones del sistema. Se conecta via mongoose.connect().

BD Secundaria — Simi (DB_CLIENT_SIMI)

Base de datos separada para los proyectos del cliente "Simi" (Simi VR y Simi AR). Se conecta mediante mongoose.createConnection() usando el patrón Singleton en DatabaseClientSimi.

class DatabaseClientSimi {
constructor() {
if (DatabaseClientSimi.instance) {
return DatabaseClientSimi.instance; // Retorna instancia existente
}
DatabaseClientSimi.instance = this;
this.connectDB(this.schemas.bind(this));
}

schemas() {
// Registra los modelos en la conexión secundaria
this.learningAnswers = this.db.model("LearningAnswer", LearningAnswerSchema);
this.learningGroup = this.db.model("LearningGroup", LearningGroupSchema);
this.Student = this.db.model("Student", StudentSchema);
}
}

¿Cuándo se usa la BD Simi? Cuando el projectId del request coincide con process.env.PROJECT_SIMI_VR o process.env.PROJECT_SIMI_AR. Esta lógica está distribuida en los controladores de learningGroup, student y learningAnswer.


8. Modelos Mongoose

Admin

Colección: admins

{
name: String,
email: { type: String, required: true, unique: true },
password: { type: String, required: true }, // bcrypt hash
isActive: { type: Boolean, default: true, required: true },
enterpriseId: { type: ObjectId, ref: "Enterprise", required: true },
typeUser: { type: String, enum: ["VRT", "VRL", "INMERSYS"] },
createdAt: { type: Date, default: new Date(), required: true },
}

Tipos de usuario:

  • VRT — Admin de entrenamiento (Training)
  • VRL — Admin de aprendizaje (Learning)
  • INMERSYS — Super-admin con acceso cruzado a todas las empresas

Enterprise

Colección: enterprises

{
name: { type: String, required: true, unique: true },
imgUrl: String, // URL del logo
createdAt: { type: Date, required: true, default: new Date() },
isActive: { type: Boolean, required: true, default: true },
}

Project

Colección: projects

{
name: String,
enterpriseId: { type: ObjectId, ref: "Enterprise", required: true },
createdAt: { type: Date, default: new Date(), required: true },
}

LearningGroup

Colección: learninggroups

{
profesor: {
_id: { type: ObjectId, ref: "Admin", required: true },
name: { type: String, required: true },
},
name: String,
projectId: { type: ObjectId, ref: "Project", required: true },
isActive: { type: Boolean, required: true, default: true },
}
// Índice compuesto único: no puede haber dos grupos con el mismo nombre en el mismo proyecto
learningGroupSchema.index({ projectId: 1, name: 1 }, { unique: true });

Student

Colección: students

{
name: String,
userId: { type: String, required: true }, // ID/matrícula del usuario en el headset
createdAt: { type: Date, default: new Date(), required: true },
learningGroupId: { type: ObjectId, ref: "LearningGroup", required: true },
}

LearningAnswer

Colección: learninganswers

{
learningGroupId: { type: ObjectId, ref: "LearningGroup", required: true },
responses: Schema.Types.Mixed, // Objeto libre con respuestas a preguntas
studentId: { type: ObjectId, ref: "Student", required: true },
createdAt: { type: Date, default: new Date() },
updatedAt: Date,
durationInSeconds: Number,
}

TrainigAnswer

Colección: traiginanswers

{
matricula: { type: String, required: true }, // ID/matrícula del usuario
nameUser: String,
responses: Schema.Types.Mixed,
projectId: { type: ObjectId, ref: "Project", required: true },
durationInSeconds: Number,
createdAt: { type: Date, default: new Date() },
updatedAt: Date,
}

9. Endpoints de la API

Convenciones

  • Auth requerida: la mayoría de rutas requieren el header x-token: <JWT_admin>
  • Respuesta exitosa: { message: "...", status: 0, ...datos } HTTP 200
  • Respuesta error: { message: "...", status: 0|1, ...datos } HTTP 400/500

9.1 Autenticación — /api/auth

POST /api/auth/login

Login de administrador.

Body:

{
"email": "admin@empresa.com",
"password": "secret123"
}

Respuesta:

{
"message": "ok",
"token": "<JWT>",
"email": "admin@empresa.com"
}

POST /api/auth/email

Solicitar restauración de contraseña. Envía un email con enlace que contiene un token JWT.

Body:

{ "email": "admin@empresa.com" }

POST /api/auth/restore/:token

Restaurar contraseña usando el token recibido por email.

Params: token — JWT de restauración

Body:

{ "newPassword": "nuevaContraseña" }

POST /api/auth/oculus/key/

Login de estudiante desde headset Oculus/Meta Quest.

Headers: y-token: <oculus_jwt>

Body:

{ "userId": "matricula-del-estudiante" }

Respuesta:

{
"token": "<student_JWT>",
"projectId": "...",
"groupId": "..."
}

POST /api/auth/responses/addOne

Guardar respuesta de estudiante desde headset.

Headers: w-token: <student_JWT>

Body:

{
"responses": [...],
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:01:00Z"
}

9.2 Administradores — /api/admin (requiere JWT admin)

POST /api/admin

Crear nuevo administrador.

Body:

{
"name": "Juan Pérez",
"email": "juan@empresa.com",
"password": "contraseña",
"enterpriseId": "<ObjectId>",
"typeUser": "VRL"
}

Validaciones: email no existente, empresa existente, typeUser en enum ["VRT","VRL","INMERSYS"].


GET /api/admin

Obtener información del admin autenticado (desde el token). Respuesta incluye populate de empresa.


9.3 Empresas — /api/enterprise (requiere JWT admin)

POST /api/enterprise

Crear empresa.

Body:

{
"name": "Empresa S.A.",
"imgUrl": "https://..."
}

Validación: nombre único.


PUT /api/enterprise

Editar empresa.

Body:

{
"enterpriseId": "<ObjectId>",
"name": "Nuevo nombre",
"imgUrl": "https://..."
}

DELETE /api/enterprise/:id

Toggle isActive de una empresa (no elimina, solo desactiva/activa).

Params: id — ObjectId de la empresa.


GET /api/enterprise/all

Obtener todas las empresas activas.


9.4 Proyectos — /api/project (requiere JWT admin)

POST /api/project

Crear proyecto.

Body:

{
"name": "Proyecto Capacitación",
"enterpriseId": "<ObjectId>"
}

PUT /api/project

Editar nombre de proyecto.

Body:

{
"projectId": "<ObjectId>",
"name": "Nuevo nombre"
}

GET /api/project/all

Obtener todos los proyectos.


GET /api/project/enterprise/

Obtener proyectos de la empresa del admin autenticado.


GET /api/project/inmersys/enterprise/:enterpriseId

Obtener proyectos de una empresa específica. Solo para admins INMERSYS.

Params: enterpriseId


POST /api/project/completeData/:enterpriseId

Obtener proyectos de una empresa con estadísticas de uso (usuarios, grupos, respuestas).

Params: enterpriseId

Body (opcional):

{
"startDate": "2026-01-01",
"endDate": "2026-06-30"
}

9.5 Grupos de Aprendizaje — /api/learning-group (requiere JWT admin)

POST /api/learning-group

Crear grupo. El profesor del grupo es el admin autenticado.

Body:

{
"name": "Grupo A",
"projectId": "<ObjectId>"
}

Validación: nombre único dentro del mismo proyecto.


PUT /api/learning-group

Actualizar grupo.

Body:

{
"groupId": "<ObjectId>",
"name": "Nuevo nombre",
"projectId": "<ObjectId>"
}

GET /api/learning-group

Obtener todos los grupos del admin autenticado (filtrado por profesor._id).


GET /api/learning-group/project/:projectId

Obtener grupos de un proyecto específico.

Params: projectId


DELETE /api/learning-group/:groupId

Toggle isActive de un grupo.

Params: groupId


9.6 Estudiantes — /api/student (requiere JWT admin)

POST /api/student

Crear estudiante y asignarlo a un grupo.

Body:

{
"name": "María García",
"userId": "matricula-123",
"groupId": "<ObjectId>"
}

Validación: el userId no debe existir ya en el grupo.


PUT /api/student

Editar estudiante.

Body:

{
"studentId": "<ObjectId>",
"name": "Nuevo nombre",
"userId": "nueva-matricula",
"groupId": "<ObjectId>"
}

GET /api/student/all

Obtener todos los estudiantes.


GET /api/student/byGroup/:groupId

Obtener estudiantes de un grupo.

Params: groupId


PUT /api/student/group

Asignar múltiples estudiantes a un grupo.

Body:

{
"groupId": "<ObjectId>",
"userIds": ["<ObjectId>", "<ObjectId>"]
}

POST /api/student/many

Crear múltiples estudiantes en lote.

Body:

{
"students": [
{ "name": "Ana", "userId": "mat-001" },
{ "name": "Luis", "userId": "mat-002" }
],
"learningGroupId": "<ObjectId>"
}

POST /api/student/deleteMany

Eliminar múltiples estudiantes.

Body:

{
"users": ["<ObjectId>", "<ObjectId>"]
}

9.7 Respuestas de Aprendizaje — /api/learning-answer (requiere JWT admin)

POST /api/learning-answer/addMany

Registrar múltiples respuestas de sesiones de aprendizaje.

Body:

{
"responses": [
{
"userId": "matricula-123",
"createdAt": "2026-01-01T10:00:00Z",
"response": { "q1": "A", "q2": "B" },
"durationInSeconds": 300
}
]
}

GET /api/learning-answer

Obtener respuestas con datos de estudiante y grupo (via aggregation).

Query params (opcionales):

  • projectId — filtrar por proyecto
  • userId — filtrar por estudiante

DELETE /api/learning-answer

Eliminar múltiples respuestas.

Body:

{ "ids": ["<ObjectId>", "<ObjectId>"] }

9.8 Respuestas de Entrenamiento — /api/trainig-answer

Esta ruta no requiere JWT admin. Usa mecanismo propio del dispositivo.

POST /api/trainig-answer/addOne

Registrar una respuesta de entrenamiento.

Body:

{
"responses": [...],
"projectId": "<ObjectId>",
"createdAt": "2026-01-01T10:00:00Z"
}

Efecto secundario: si el proyecto es HBSA, envía los puntos a Engage automáticamente.


POST /api/trainig-answer/addMany

Registrar múltiples respuestas de entrenamiento en lote.

Body:

{
"responses": [...]
}

GET /api/trainig-answer

Obtener respuestas de entrenamiento.

Query params (opcionales):

  • projectId

9.9 Entrenamiento por Empresa — /api/project-trainig (requiere JWT admin)

POST /api/project-trainig/completeData/:enterpriseId

Obtener datos completos de entrenamiento de una empresa: proyectos, grupos, respuestas y progreso.

Params: enterpriseId


10. Middlewares y Validaciones

validateJWT (src/middlewares/validateJWT.js)

Verifica el header x-token. Decodifica el JWT usando SECRETORPRIVATEKEY, busca el admin en BD, verifica que esté activo e inyecta la instancia en req.body.requestingAdminInstance.

Header: x-token: <JWT>

verifyJWT(token, jwtSecretKey)

getActiveAdminById(uid)

req.body.requestingAdminInstance = admin

next()

validateDataExpress (src/middlewares/validateDataExpress.js)

Ejecuta validationResult(req) de express-validator. Si hay errores retorna HTTP 400. Siempre se coloca al final del array de middlewares en cada ruta.

Validaciones de dominio

ArchivoFunciones principales
admin-validation.jsEmail único, existencia por ID/email, token de restauración
enterprise-validation.jsNombre único, existencia por ID
project-validation.jsExistencia de proyecto por ID
learningGroup-validation.jsNombre único por proyecto, existencia por ID
learningAnswer-validation.jsValidaciones de respuestas
student-validations.jsuserId único en grupo, existencia por ID, validación masiva de IDs

11. Helpers y Utilidades

response.js — Respuestas HTTP estándar

// Error HTTP (default 400)
getBadRequest(res, status, message, payload, httpStatus = 400)
// → { message, status, ...payload }

// Éxito HTTP (default 200)
getGoodResponse(res, message, payload, httpStatus = 200)
// → { message, status: 0, ...payload }

bcrypt.js — Contraseñas

encryptPassword(password)          // Genera hash bcrypt con salt
verifyPassword(hash, password) // Compara texto plano con hash

token.js — JWT

generateJWT(payload, secret?)      // Crea token JWT (default: jwtSecretKey)
verifyToken(token) // Decodifica y verifica firma

constanst.js — Claves

const jwtSecretKey = process.env.SECRETORPRIVATEKEY  // Para admins
const jwtOculusKey = process.env.AUTH_OCULUS_KEY // Para headsets

email.js — SMTP

createTransporter()                          // Crea transporter nodemailer
verifyTransporter(transporter) // Verifica conexión SMTP
sendEmail(transporter, { toEmail, subject, html })

helpers/index.js

eliminateInvalidParams(bodyParams)
// Filtra keys con valor null o undefined
// Se usa antes de $set en MongoDB para evitar sobreescribir campos existentes

db-validations.js

Define jsonSchemaFunction que elimina __v del resultado al serializar documentos Mongoose.


12. Autenticación y Seguridad

Flujo Admin

1. POST /api/auth/login  { email, password }
2. Busca admin por email en BD
3. bcrypt.compareSync(password, admin.password)
4. generateJWT({ uid: admin._id }, SECRETORPRIVATEKEY)
5. Retorna token JWT
─────────────────────────────────────────────
6. Requests siguientes: Header x-token: <token>
7. validateJWT decodifica → carga admin en req.body.requestingAdminInstance

Flujo Estudiante (Oculus/Meta Quest)

1. POST /api/auth/oculus/key/  { userId }
Header: y-token: <oculus_token> (firmado con AUTH_OCULUS_KEY)
2. Verifica y-token (autenticación del dispositivo)
3. Busca estudiante por userId en BD principal Y BD Simi
4. generateJWT({ userId, groupId }, AUTH_OCULUS_KEY)
5. Retorna student token con datos del proyecto/grupo
─────────────────────────────────────────────────────
6. El headset envía: Header w-token: <student_token>
7. Controller verifica y usa datos del payload

Restauración de contraseña

1. POST /api/auth/email  { email }
2. Valida que el admin exista
3. generateJWT({ email }, SECRETORPRIVATEKEY)
4. Envía email con enlace que contiene el token
5. Usuario hace: POST /api/auth/restore/:token { newPassword }
6. Verifica token → hashea con bcrypt → actualiza en BD

Resumen de headers de autenticación

HeaderUsado porSecreto usado
x-tokenAdminsSECRETORPRIVATEKEY
y-tokenDispositivos Oculus autorizadosAUTH_OCULUS_KEY
w-tokenSesión de estudiante en headsetAUTH_OCULUS_KEY

13. Agregaciones MongoDB

src/aggregates/learningAnswer.js

Pipeline que enriquece las respuestas de aprendizaje con datos de estudiante y grupo:

[
// 1. Join con colección Student
{
$lookup: {
from: "Student",
localField: "studentId",
foreignField: "_id",
as: "student",
},
},
// 2. Join con colección LearningGroup
{
$lookup: {
from: "LearningGroup",
localField: "learningGroupId",
foreignField: "_id",
as: "group",
},
},
// 3. Desenvuelve array student (excluye docs sin estudiante)
{ $unwind: { path: "$student", preserveNullAndEmptyArrays: false } },
// 4. Desenvuelve array group (excluye docs sin grupo)
{ $unwind: { path: "$group", preserveNullAndEmptyArrays: false } },
// 5. Excluye campos innecesarios
{ $project: { "group.profesor": 0, "group.__v": 0 } },
]

Los filtros dinámicos (projectId, userId) se prependen como $match antes de este pipeline en el controlador.


14. Integraciones Externas

HBSA Engage (src/controllers/thirdPartyControllers.js)

Cuando se guarda una respuesta de entrenamiento y el projectId es PROJECT_HBSA, se ejecuta automáticamente el reporte a Engage:

1. pointsWon(payload)
→ Calcula puntos: (respuestas correctas / total respuestas) * 100

2. saveResponseEngage({ idUser, ...payload })
→ Obtiene token de autenticación de Engage API
→ POST https://my.engage.bz/api/v1/hbsa/answers
→ Body: { userId, competencia, puntuacion, ... }

Importante: el token de Engage se obtiene en cada llamada (sin cache). Si la API de Engage falla, el error se loguea pero no impide que la respuesta se guarde en BD local.


15. Soporte Multi-Base de Datos

Los controladores learningGroup, student y learningAnswer implementan selección de BD según el proyecto:

const isSimiProject = (projectId) =>
projectId.equals(process.env.PROJECT_SIMI_VR) ||
projectId.equals(process.env.PROJECT_SIMI_AR);

if (isSimiProject(projectId)) {
const simiDB = new DatabaseClientSimi();
// Opera con: simiDB.learningAnswers / simiDB.learningGroup / simiDB.Student
} else {
// Opera con los modelos Mongoose de la BD principal
}

Casos de uso clave:

  • Al buscar un estudiante para login Oculus, se busca en ambas BDs y se consolida el resultado para evitar duplicados.
  • Las respuestas de aprendizaje se almacenan en la BD que corresponde al proyecto.
  • Los grupos de Simi se crean y consultan en la BD secundaria.

16. Testing

Configuración (jest.config.js)

module.exports = {
collectCoverage: true,
coverageDirectory: "coverage",
coverageProvider: "v8",
};

Archivos de test

ArchivoQué prueba
test/controllers/admin.test.jsCreación de admin: validaciones nombre, email, contraseña
test/controllers/enterprise.test.jsEndpoints de empresa
test/services/enterprise.test.jsCapa de servicios de empresa
test/controllers/utils.jsHelper con instancia de supertest

Comandos

npm test                        # Modo watch con coverage
npx jest --no-coverage # Sin coverage
npx jest admin.test.js # Test específico

Los tests usan TEST_TOKEN del .env para autenticarse sin pasar por el flujo real de login.


17. CI/CD y Despliegue

Pipeline GitLab CI (.gitlab-ci.yml)

stages:
- deploy

deploy-production:
stage: deploy
environment: production
before_script:
- apt-get install openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY_PROD" | ssh-add -
- ssh-keyscan -t rsa $SERVER_HOST_PROD >> ~/.ssh/known_hosts
script:
- ssh $SERVER_USER_PROD@$SERVER_HOST_PROD "cd /projects/inmersys-vrt-vrl-api && git pull origin main"
- ssh $SERVER_USER_PROD@$SERVER_HOST_PROD "pm2 stop app && pm2 start app && pm2 status"
only:
- main

Variables requeridas en GitLab CI/CD Settings:

VariableDescripción
SSH_PRIVATE_KEY_PRODClave privada SSH para el servidor
SERVER_HOST_PRODIP o dominio del servidor de producción
SERVER_USER_PRODUsuario SSH del servidor

Flujo de deploy:

  1. Push a rama main → dispara el pipeline automáticamente
  2. SSH al servidor de producción
  3. git pull origin main
  4. Reinicio de la app con PM2

El deploy solo se activa en main. La rama develop no tiene pipeline de CI/CD configurado.

Gestión con PM2 en producción

pm2 start src/app.js --name "app"   # Primera vez
pm2 stop app # Detener
pm2 start app # Iniciar
pm2 restart app # Reiniciar
pm2 status # Ver estado
pm2 logs app # Ver logs en tiempo real
pm2 save # Guardar configuración
pm2 startup # Habilitar inicio automático con el OS

18. Notas Técnicas y Observaciones

Typos intencionales en el código

El nombre trainig (sin segunda 'n') aparece en toda la codebase: rutas, archivos, modelos y variables. Esto es el estado actual del código. No corregir sin migrar también los clientes (headsets, frontends) que consumen estas rutas, ya que rompería la compatibilidad.

default: new Date() en schemas (bug potencial)

Varios schemas usan default: new Date(), que se evalúa una sola vez al cargar el módulo Node, no en cada inserción. Todos los documentos creados en el mismo proceso tendrán la misma fecha. La corrección es usar default: Date.now (referencia a función, sin invocar).

responses como Schema.Types.Mixed

Tanto LearningAnswer como TrainigAnswer usan Mixed para el campo responses. Esto da flexibilidad total pero impide validaciones a nivel de schema. El formato real depende de cada módulo VR y proyecto.

Complejidad O(n²) en datos completos

formattProjectByEnterprise() en projectTraining.js tiene complejidad cuadrática al cruzar listas. Para volúmenes grandes se recomienda refactorizar usando Map o mover la lógica a una aggregation de MongoDB.

Error handling faltante en email

En el flujo de restauración de contraseña (auth.js), los errores de sendEmail() no están envueltos en try/catch. Si el SMTP falla, la API retorna un error 500 sin mensaje claro al cliente.

requestingAdminInstance en req.body

El middleware validateJWT inyecta el admin en req.body.requestingAdminInstance. Esto mezcla datos de autenticación con el body del request, lo cual podría causar conflictos si algún endpoint recibiera un campo con ese nombre. Se puede mejorar usando req.auth o req.user.

@sendgrid/mail instalado pero inactivo

El paquete está en dependencies pero el sistema usa Nodemailer SMTP directamente. Si se migra a SendGrid en el futuro, la dependencia ya está disponible.


Resumen General

MétricaValor
Endpoints totales~35
Modelos Mongoose7
Bases de datos2 (Principal + Simi)
Integraciones externas1 (HBSA Engage)
Proyectos especiales configurados4 (Simi VR, Simi AR, HBSA, MERZ)
Versión Node recomendada16.19.0
FrameworkExpress 4.18.1
ODMMongoose 6.4.4
DeployGitLab CI → SSH → PM2