Schema Prisma — Referencia Completa
Fuente: apps/api/prisma/schema.prisma
Identidad y Acceso
Section titled “Identidad y Acceso”Tabla: User | Propósito: Usuarios del sistema — todos los roles
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| String (unique) | No | Email del usuario | |
| passwordHash | String? | Sí | null si solo OAuth |
| fullName | String | No | Nombre completo |
| avatarUrl | String? | Sí | URL del avatar |
| avatar | Bytes? | Sí | Bytes del avatar (almacenamiento inline) |
| role | UserRole | No | FREE por defecto |
| professionalRole | String? | Sí | ”sales”, “developer”, “hr”, etc. |
| industry | String? | Sí | Sector profesional |
| organizationId | String? | Sí | FK a Organization (null = usuario individual) |
| departmentId | String? | Sí | FK a Department |
| stripeCustomerId | String (unique)? | Sí | Customer Stripe para pagos |
| stripeSubId | String (unique)? | Sí | Subscription activa en Stripe |
| subPeriodEnd | DateTime? | Sí | Vencimiento de la suscripción |
| fluidityScore | Float? | Sí | Score 0-100 calculado por NappAI |
| locale | String | No | ”es-ES” por defecto |
| isEmailVerified | Boolean | No | Verificación de email completada |
| isSuspended | Boolean | No | Flag de suspensión por SUPER_ADMIN |
| lastActiveAt | DateTime? | Sí | Última actividad registrada |
| jobTitle | String? | Sí | Cargo profesional |
| company | String? | Sí | Empresa actual |
| linkedinUrl | String? | Sí | URL del perfil LinkedIn |
| aiLevel | String? | Sí | ’explorer’ / ‘curious’ / ‘practitioner’ / ‘advanced’ |
| goals | String[] | No | Objetivos de aprendizaje del usuario |
| onboardingCompleted | Boolean | No | Indica si completó el onboarding |
| notificationPrefs | Json? | Sí | Preferencias de notificación por tipo |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: enrollments → Enrollment[], progress → Progress[], certificates → Certificate[], quizAttempts → QuizAttempt[], tutorMessages → TutorMessage[], bookmarks → Bookmark[], oauthAccounts → OAuthAccount[], refreshTokens → RefreshToken[], creatorProfile → CreatorProfile?, applications → InstructorApplication[], aiTasks → AITask[], notifications → Notification[], studentBadgeGrants → StudentBadgeGrant[], xpEvents → StudentXPEvent[], studentProfile → StudentProfile?
Índices: @@index([email]), @@index([organizationId]), @@index([stripeCustomerId])
OAuthAccount
Section titled “OAuthAccount”Tabla: OAuthAccount | Propósito: Cuentas OAuth vinculadas (Google, LinkedIn, GitHub)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User (cascade delete) |
| provider | String | No | ”google” / “linkedin” / “github” |
| providerId | String | No | ID del usuario en el proveedor externo |
| createdAt | DateTime | No | Timestamp de vinculación |
Relaciones: user → User
Índices: @@unique([provider, providerId]), @@index([userId])
RefreshToken
Section titled “RefreshToken”Tabla: RefreshToken | Propósito: Tokens de refresco JWT con metadatos de sesión
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User (cascade delete) |
| token | String (unique) | No | Valor del refresh token |
| deviceInfo | String? | Sí | User-Agent del dispositivo |
| expiresAt | DateTime | No | Fecha de expiración del token |
| ipAddress | String? | Sí | IP de la sesión |
| city | String? | Sí | Ciudad detectada por geolocalización |
| country | String? | Sí | País detectado por geolocalización |
| lastActiveAt | DateTime? | Sí | Última actividad con este token |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: user → User
Índices: @@index([userId]), @@index([token])
Contenido Educativo
Section titled “Contenido Educativo”Path (Ruta de aprendizaje)
Section titled “Path (Ruta de aprendizaje)”Tabla: Path | Propósito: Curso o ruta de aprendizaje completa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| title | String | No | Título del curso |
| slug | String (unique) | No | URL amigable |
| description | String | No | Descripción corta |
| longDescription | String? | Sí | Descripción larga para la página de detalle |
| thumbnailUrl | String? | Sí | Imagen de portada |
| accessLevel | AccessLevel | No | Nivel mínimo requerido para acceder |
| targetRoles | String[] | No | Público objetivo ([“sales”, “developer”]) |
| targetIndustries | String[] | No | Industrias objetivo |
| estimatedHours | Int? | Sí | Duración estimada en horas |
| isPublished | Boolean | No | Visible en el catálogo |
| isFeatured | Boolean | No | Destacado en portada |
| organizationId | String? | Sí | null = ruta global; no null = privada corporativa |
| difficultyLevel | String | No | ”Principiante” por defecto |
| accentColor | String? | Sí | Hex para display de card (ej. “#4f46e5”) |
| enrollmentCount | Int | No | Contador cacheado para display |
| isNew | Boolean | No | Muestra badge “NUEVO” en la card |
| order | Int | No | Orden en el catálogo (default 0) |
| creatorId | String? | Sí | FK a CreatorProfile |
| avgRating | Float | No | Rating denormalizado (actualizado por job nocturno) |
| reviewCount | Int | No | Número de reviews denormalizado |
| totalEnrollments | Int | No | Enrollments reales denormalizados |
| skills | String[] | No | Habilidades que desarrolla la ruta |
| status | PathStatus | No | DRAFT / PENDING_REVIEW / PUBLISHED / ARCHIVED |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: modules → Module[], enrollments → Enrollment[], orgPaths → OrgPath[], certificate → CertificateTemplate?, quiz → Quiz?, reviews → PathReview[], resourceAttachments → ResourceAttachment[], checklist → PathChecklist?
Índices: @@index([slug]), @@index([accessLevel]), @@index([organizationId]), @@index([creatorId]), @@index([status])
Module
Section titled “Module”Tabla: Module | Propósito: Módulo dentro de un Path
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| pathId | String | No | FK a Path (cascade delete) |
| title | String | No | Título del módulo |
| description | String? | Sí | Descripción opcional |
| accessLevel | AccessLevel | No | Hereda del Path si no se especifica explícitamente |
| order | Int | No | Posición dentro del Path |
| isPublished | Boolean | No | Visible para usuarios |
| estimatedMin | Int? | Sí | Duración estimada en minutos |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: path → Path, moduleLessons → ModuleLesson[], quiz → Quiz?, progress → Progress[], attachments → ResourceAttachment[]
Índices: @@index([pathId]), @@index([pathId, order])
ModuleLesson
Section titled “ModuleLesson”Tabla: ModuleLesson | Propósito: Tabla de unión muchos-a-muchos entre Module y Lesson (permite reutilizar lecciones)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| moduleId | String | No | FK a Module (cascade delete) |
| lessonId | String | No | FK a Lesson (cascade delete) |
| order | Int | No | Posición de la lección dentro del módulo (default 0) |
| addedAt | DateTime | No | Timestamp en que se vinculó la lección al módulo |
Relaciones: module → Module, lesson → Lesson
Índices: @@unique([moduleId, lessonId]), @@index([moduleId]), @@index([lessonId])
Lesson
Section titled “Lesson”Tabla: Lesson | Propósito: Unidad de contenido individual (reutilizable entre módulos)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| title | String | No | Título de la lección |
| contentType | LessonContentType | No | MARKDOWN / VIDEO / H5P / MIXED |
| contentMd | String? | Sí | Contenido Markdown (para MARKDOWN o MIXED) |
| videoUrl | String? | Sí | URL Bunny.net (para VIDEO o MIXED) |
| videoDurationSec | Int? | Sí | Duración del vídeo en segundos |
| accessLevel | AccessLevel | No | Puede ser más abierto que el módulo padre (para previews) |
| order | Int | No | Orden global para display en librería |
| durationMin | Int | No | Duración estimada en minutos (default 10) |
| isPublished | Boolean | No | Visible para usuarios |
| seoDescription | String? | Sí | Descripción SEO (para lecciones PUBLIC) |
| changeLogs | Json | No | Array de eventos de cambio: [{note, authorName, date}] |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: moduleLessons → ModuleLesson[], progress → Progress[], bookmarks → Bookmark[], attachments → ResourceAttachment[]
Índices: @@index([accessLevel])
Resource
Section titled “Resource”Tabla: Resource | Propósito: Recurso educativo reutilizable (PDF, vídeo, enlace, etc.) creado por un instructor
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| creatorId | String | No | FK a CreatorProfile |
| type | ResourceType | No | PDF / DOCX / PPTX / XLSX / IMAGE / YOUTUBE / H5P / LINK |
| title | String | No | Nombre del recurso |
| description | String? | Sí | Descripción del recurso |
| fileData | Bytes? | Sí | Contenido binario del archivo (≤10MB en PostgreSQL) |
| mimeType | String? | Sí | Tipo MIME del archivo |
| fileSize | Int? | Sí | Tamaño en bytes |
| fileName | String? | Sí | Nombre original del archivo |
| externalUrl | String? | Sí | URL externa (YouTube, enlace externo) |
| tags | String[] | No | Etiquetas para búsqueda y filtrado |
| aiTagged | Boolean | No | Si fue etiquetado automáticamente por IA |
| version | Int | No | Versión del recurso (default 1) |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: creator → CreatorProfile, attachments → ResourceAttachment[]
Índices: @@index([creatorId]), @@index([type]), @@index([creatorId, type])
ResourceAttachment
Section titled “ResourceAttachment”Tabla: ResourceAttachment | Propósito: Vinculación de un Resource a una Lesson, Module o Path (exactamente uno de los tres)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| resourceId | String | No | FK a Resource (cascade delete) |
| lessonId | String? | Sí | FK a Lesson (si se adjunta a lección) |
| moduleId | String? | Sí | FK a Module (si se adjunta a módulo) |
| pathId | String? | Sí | FK a Path (si se adjunta a ruta) |
| role | AttachmentRole | No | MAIN_CONTENT / SUPPLEMENTARY |
| order | Int | No | Posición en la lista de adjuntos (default 0) |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: resource → Resource, lesson → Lesson?, module → Module?, path → Path?
Índices: @@index([resourceId]), @@index([lessonId]), @@index([moduleId]), @@index([pathId])
PathChecklist
Section titled “PathChecklist”Tabla: PathChecklist | Propósito: Checklist de publicación de una ruta (flujo DRAFT → PUBLISHED)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| pathId | String (unique) | No | FK a Path (cascade delete) — 1:1 |
| checkedItems | String[] | No | Items marcados manualmente por el creador |
| autoResults | Json | No | Resultados de validaciones automáticas (default {}) |
| isComplete | Boolean | No | Indica si el checklist está completo |
| completedAt | DateTime? | Sí | Timestamp en que se completó |
| reviewRequestedAt | DateTime? | Sí | Timestamp en que se solicitó revisión |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: path → Path
Índices: @@index([pathId]), @@index([isComplete])
Bookmark
Section titled “Bookmark”Tabla: Bookmark | Propósito: Marcador de una lección guardada por un usuario
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User |
| lessonId | String | No | FK a Lesson |
| note | String? | Sí | Nota personal del usuario |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: user → User, lesson → Lesson
Índices: @@unique([userId, lessonId])
Organización Corporativa
Section titled “Organización Corporativa”Organization
Section titled “Organization”Tabla: Organization | Propósito: Multi-tenancy — cuenta corporativa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| name | String | No | Nombre de la organización |
| slug | String (unique) | No | Identificador URL |
| logoUrl | String? | Sí | URL del logo para co-branding |
| brandColor | String? | Sí | Hex para co-branding de certificados |
| plan | OrgPlan | No | TEAM / BUSINESS / ENTERPRISE |
| stripeCustomerId | String (unique)? | Sí | Customer de Stripe |
| stripeSubId | String (unique)? | Sí | Subscription de Stripe |
| maxSeats | Int | No | Límite de usuarios (default 5) |
| isActive | Boolean | No | Estado activo de la cuenta |
| status | OrgStatus | No | TRIAL / ACTIVE / SUSPENDED / CANCELLED / CHURNED |
| trialEndsAt | DateTime? | Sí | Fecha de fin del período de prueba |
| contractStartAt | DateTime? | Sí | Inicio del contrato |
| contractEndAt | DateTime? | Sí | Fin del contrato |
| accountManagerId | String? | Sí | ID del account manager asignado |
| salesNotes | String? | Sí | Notas internas del equipo de ventas |
| source | OrgSource | No | DEMO_REQUEST / DIRECT_SALE / PARTNER / EVENT |
| demoRequestId | String (unique)? | Sí | FK a DemoRequest de origen |
| customPricePerSeat | Decimal? | Sí | Precio por asiento negociado (10,2) |
| billingCycle | BillingCycle | No | MONTHLY / ANNUAL |
| discountPercent | Int? | Sí | Descuento en porcentaje (default 0) |
| industry | String? | Sí | Sector de la empresa |
| website | String? | Sí | Sitio web |
| country | String? | Sí | País |
| companySize | String? | Sí | Tamaño de la empresa |
| description | String? | Sí | Descripción de la organización |
| contactName | String? | Sí | Nombre del contacto principal |
| contactEmail | String? | Sí | Email del contacto principal |
| contactPhone | String? | Sí | Teléfono del contacto principal |
| brandPrimaryColor | String? | Sí | Color primario de marca (default “#7C3AED”) |
| brandSecondaryColor | String? | Sí | Color secundario de marca (default “#1E1E2D”) |
| loginMethod | String | No | Método de login permitido (default “email+google”) |
| passwordStrength | String | No | Nivel de fortaleza de contraseña (default “medium”) |
| require2FA | Boolean | No | Requiere autenticación de dos factores |
| restrictDomain | Boolean | No | Restringe login a dominio específico |
| allowedDomain | String? | Sí | Dominio permitido si restrictDomain es true |
| sessionTimeoutHours | Int? | Sí | Timeout de sesión en horas |
| autoDeactivateDays | Int? | Sí | Días de inactividad antes de desactivar usuario |
| seatAutoReclaim | Boolean | No | Reclama asientos de usuarios inactivos automáticamente |
| seatAlertAt90 | Boolean | No | Alerta cuando se alcanza el 90% de asientos |
| ssoEntityId | String? | Sí | Entity ID del SSO (Enterprise only) |
| ssoUrl | String? | Sí | URL del SSO |
| ssoX509Certificate | String? | Sí | Certificado X.509 para SSO |
| ssoDefaultRole | String? | Sí | Rol por defecto en auto-provisioning SSO |
| ssoAutoProvisioning | Boolean | No | Crea usuarios automáticamente vía SSO |
| notifPreferences | Json? | Sí | Preferencias de notificación de la org |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: members → User[], customPaths → Path[], orgPaths → OrgPath[], invitations → Invitation[], certificates → Certificate[], departments → Department[], webhooks → OrgWebhook[], apiKeys → OrgApiKey[], integrations → OrgIntegration[], demoRequest → DemoRequest?, auditLogs → OrgAuditLog[], globalAuditLogs → AuditLog[]
Índices: @@index([slug])
OrgPlanDefinition
Section titled “OrgPlanDefinition”Tabla: OrgPlanDefinition | Propósito: Configuración de planes corporativos disponibles con precios y características
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String | No | PK (slug del plan, ej. “TEAM”) |
| name | String | No | Nombre legible del plan |
| pricePerSeat | Decimal | No | Precio por asiento (10,2) |
| minSeats | Int | No | Mínimo de asientos requeridos |
| maxSeats | Int? | Sí | Máximo de asientos (null = ilimitado) |
| features | Json | No | Features incluidas en el plan |
| stripePriceIdMonthly | String? | Sí | Price ID de Stripe para facturación mensual |
| stripePriceIdAnnual | String? | Sí | Price ID de Stripe para facturación anual |
| isActive | Boolean | No | Si el plan está disponible para contratar |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Department
Section titled “Department”Tabla: Department | Propósito: Departamento dentro de una organización corporativa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String | No | FK a Organization (cascade delete) |
| name | String | No | Nombre del departamento |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: organization → Organization, members → User[]
Índices: @@unique([organizationId, name]), @@index([organizationId])
Invitation
Section titled “Invitation”Tabla: Invitation | Propósito: Invitación de email para incorporar miembros a una organización
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String | No | FK a Organization (cascade delete) |
| String | No | Email del invitado | |
| role | UserRole | No | Rol que se asignará al aceptar (default CORPORATE_USER) |
| department | String? | Sí | Departamento asignado al aceptar |
| status | InvitationStatus | No | PENDING / ACCEPTED / EXPIRED / REVOKED |
| token | String (unique) | No | Token UUID para el enlace de invitación |
| invitedById | String | No | FK a User (quien envió la invitación) |
| expiresAt | DateTime | No | Fecha de expiración del enlace |
| acceptedAt | DateTime? | Sí | Timestamp de aceptación |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: organization → Organization, invitedBy → User
Índices: @@index([organizationId]), @@index([email]), @@index([token])
OrgPath
Section titled “OrgPath”Tabla: OrgPath | Propósito: Asignación de una ruta de aprendizaje a una organización corporativa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String | No | FK a Organization (cascade delete) |
| pathId | String | No | FK a Path (cascade delete) |
| assignedAt | DateTime | No | Timestamp de asignación |
Relaciones: organization → Organization, path → Path
Índices: @@unique([organizationId, pathId]), @@index([organizationId])
OrgApiKey
Section titled “OrgApiKey”Tabla: OrgApiKey | Propósito: API key corporativa para integraciones externas (almacenada como hash)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String (unique) | No | FK a Organization (cascade delete) — una key por org |
| keyHash | String | No | Hash bcrypt de la API key |
| keyPrefix | String | No | Prefijo legible de la key (ej. “napp_“) |
| keySuffix | String | No | Últimos 4 caracteres de la key (display) |
| createdAt | DateTime | No | Timestamp de creación |
| lastUsedAt | DateTime? | Sí | Último uso de la key |
Relaciones: organization → Organization
Índices: @@unique([organizationId]), @@index([organizationId])
OrgAuditLog
Section titled “OrgAuditLog”Tabla: OrgAuditLog | Propósito: Registro de auditoría interno de acciones dentro de una organización corporativa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String | No | FK a Organization (cascade delete) |
| actorId | String? | Sí | ID del usuario que realizó la acción |
| actorRole | String | No | Rol del actor en el momento de la acción |
| action | String | No | Acción realizada |
| targetType | String? | Sí | Tipo de entidad afectada |
| targetId | String? | Sí | ID de la entidad afectada |
| oldValue | Json? | Sí | Valor anterior |
| newValue | Json? | Sí | Valor nuevo |
| ipAddress | String? | Sí | IP del actor |
| createdAt | DateTime | No | Timestamp del evento |
Relaciones: organization → Organization
Índices: @@index([organizationId, createdAt(sort: Desc)])
OrgIntegration
Section titled “OrgIntegration”Tabla: OrgIntegration | Propósito: Integración de terceros configurada para una organización
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String | No | FK a Organization (cascade delete) |
| provider | String | No | Nombre del proveedor de integración |
| status | String | No | Estado de la integración (default “connected”) |
| config | Json? | Sí | Configuración específica del proveedor |
| connectedAt | DateTime | No | Timestamp de conexión |
Relaciones: organization → Organization
Índices: @@unique([organizationId, provider]), @@index([organizationId])
OrgWebhook
Section titled “OrgWebhook”Tabla: OrgWebhook | Propósito: Webhook saliente configurado por una organización para recibir eventos
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| organizationId | String | No | FK a Organization (cascade delete) |
| url | String | No | URL destino del webhook |
| events | String[] | No | Lista de eventos suscritos |
| status | String | No | Estado del webhook (default “active”) |
| secret | String | No | Secret para verificar la firma HMAC |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: organization → Organization
Índices: @@index([organizationId])
Aprendizaje y Progreso
Section titled “Aprendizaje y Progreso”Enrollment
Section titled “Enrollment”Tabla: Enrollment | Propósito: Matrícula de un usuario en una ruta de aprendizaje
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User |
| pathId | String | No | FK a Path |
| enrolledAt | DateTime | No | Timestamp de matrícula |
| completedAt | DateTime? | Sí | Timestamp de finalización (null = en curso) |
Relaciones: user → User, path → Path
Índices: @@unique([userId, pathId]), @@index([userId]), @@index([pathId])
Progress
Section titled “Progress”Tabla: Progress | Propósito: Seguimiento del progreso de un usuario en lecciones, módulos y rutas
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User |
| lessonId | String? | Sí | FK a Lesson (si es progreso de lección) |
| moduleId | String? | Sí | FK a Module (si es progreso de módulo) |
| pathId | String? | Sí | FK a Path (si es progreso de ruta) — sin FK explícita |
| status | ProgressStatus | No | NOT_STARTED / IN_PROGRESS / COMPLETED |
| timeSpentSec | Int | No | Tiempo total dedicado en segundos |
| lastPosition | Int? | Sí | Posición en vídeo (segundos) para reanudar |
| startedAt | DateTime? | Sí | Timestamp de inicio |
| completedAt | DateTime? | Sí | Timestamp de finalización |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: user → User, lesson → Lesson?, module → Module?
Índices: @@unique([userId, lessonId]), @@unique([userId, moduleId]), @@unique([userId, pathId]), @@index([userId]), @@index([userId, pathId])
Tabla: Quiz | Propósito: Evaluación asociada a un módulo o a una ruta completa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| moduleId | String (unique)? | Sí | FK a Module (null = evaluación de ruta) |
| pathId | String (unique)? | Sí | FK a Path (para evaluaciones de ruta) |
| title | String | No | Título del quiz |
| passingScore | Int | No | Puntuación mínima para aprobar (default 70) |
| maxAttempts | Int | No | Intentos máximos permitidos (default 3) |
| timeLimitMin | Int? | Sí | Límite de tiempo en minutos (null = sin límite) |
| changeLogs | Json | No | Historial de cambios: [{note, authorName, date}] |
| isAiGenerated | Boolean | No | Si fue generado por IA |
| isPublished | Boolean | No | Visible para los estudiantes |
| publishedAt | DateTime? | Sí | Timestamp de publicación |
| quizType | String | No | ’EVALUATION’ / ‘CASE_STUDY’ (default ‘EVALUATION’) |
| instructions | String? | Sí | Instrucciones del quiz |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: module → Module?, path → Path?, questions → QuizQuestion[], attempts → QuizAttempt[]
QuizQuestion
Section titled “QuizQuestion”Tabla: QuizQuestion | Propósito: Pregunta individual dentro de un Quiz
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| quizId | String | No | FK a Quiz |
| type | QuizQuestionType | No | MULTIPLE_CHOICE / TRUE_FALSE / OPEN_ENDED |
| question | String | No | Texto de la pregunta |
| options | Json? | Sí | Opciones para MULTIPLE_CHOICE: [{id, text, isCorrect, order}] |
| correctAnswer | String? | Sí | Respuesta correcta para TRUE_FALSE: ‘true’ / ‘false’ |
| explanation | String? | Sí | Explicación de la respuesta correcta |
| order | Int | No | Posición en el quiz |
| points | Int | No | Puntos que vale la pregunta (default 1) |
| gradingCriteria | String? | Sí | Criterios de evaluación para OPEN_ENDED |
| gradingMode | String? | Sí | ’manual’ / ‘ai’ / ‘both’ — solo para OPEN_ENDED |
| aiGenerated | Boolean | No | Si fue generada por IA |
| aiTaskId | String? | Sí | ID del AITask que originó esta pregunta |
| difficulty | String? | Sí | ’easy’ / ‘medium’ / ‘hard’ |
| lessonRef | String? | Sí | Referencia a la lección de origen |
Relaciones: quiz → Quiz, answers → QuizAnswer[]
QuizAnswer
Section titled “QuizAnswer”Tabla: QuizAnswer | Propósito: Respuesta dada por un usuario a una pregunta en un intento de quiz
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| attemptId | String | No | FK a QuizAttempt |
| questionId | String | No | FK a QuizQuestion |
| answer | String | No | Respuesta dada por el usuario |
| isCorrect | Boolean? | Sí | Resultado de corrección (null = pendiente evaluación) |
| aiScore | Float? | Sí | Puntuación IA para respuestas abiertas (0-1) |
| aiFeedback | String? | Sí | Feedback textual de la IA |
Relaciones: attempt → QuizAttempt, question → QuizQuestion
QuizAttempt
Section titled “QuizAttempt”Tabla: QuizAttempt | Propósito: Intento de resolución de un quiz por un usuario
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User |
| quizId | String | No | FK a Quiz |
| score | Float | No | Puntuación obtenida |
| passed | Boolean | No | Si supera el passingScore del quiz |
| startedAt | DateTime | No | Timestamp de inicio |
| completedAt | DateTime? | Sí | Timestamp de finalización |
Relaciones: user → User, quiz → Quiz, answers → QuizAnswer[]
Índices: @@index([userId, quizId])
FluidityTestSession
Section titled “FluidityTestSession”Tabla: FluidityTestSession | Propósito: Sesión del AI Fluency Test (puede ser anónima antes del registro)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| sessionKey | String (unique) | No | Clave de sesión (permite usuarios anónimos) |
| userId | String? | Sí | FK a User (se asocia si el usuario se registra después) |
| answers | Json | No | Respuestas del test |
| profile | Json? | Sí | Resultado del AI Fluency Test con perfil calculado |
| score | Float? | Sí | Puntuación del test |
| completedAt | DateTime? | Sí | Timestamp de finalización |
| createdAt | DateTime | No | Timestamp de creación |
Índices: @@index([sessionKey])
Certificados
Section titled “Certificados”Certificate
Section titled “Certificate”Tabla: Certificate | Propósito: Diploma emitido y verificable públicamente
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK — también UUID público de verificación |
| userId | String | No | FK a User |
| pathId | String? | Sí | FK a Path (puede ser nulo para badges de módulo) |
| moduleId | String? | Sí | FK a Module (para MODULE_BADGE) |
| type | CertType | No | DIAGNOSTIC_BADGE / MODULE_BADGE / PATH_CERTIFICATE / SPECIALIZATION_CERTIFICATE / CORPORATE_CERTIFICATE |
| score | Float | No | Puntuación obtenida |
| verificationHash | String (unique) | No | SHA-256(userId+pathId+issuedAt+score) |
| issuedAt | DateTime | No | Fecha de emisión |
| expiresAt | DateTime? | Sí | null = no expira |
| linkedInUrl | String? | Sí | URL para compartir en LinkedIn |
| badgeMetadata | Json? | Sí | Open Badges 3.0 assertion JSON |
| blockchainTxHash | String? | Sí | Hash de transacción blockchain (Fase 3) |
| organizationId | String? | Sí | FK a Organization (para certificados corporativos) |
| pdfStorageKey | String? | Sí | Key en Cloudflare R2 |
Relaciones: user → User, organization → Organization?
Índices: @@index([userId]), @@index([verificationHash])
CertificateTemplate
Section titled “CertificateTemplate”Tabla: CertificateTemplate | Propósito: Plantilla de certificado asociada a una ruta (1:1 con Path)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| pathId | String (unique) | No | FK a Path — 1:1 |
| title | String | No | Título del certificado |
| description | String | No | Descripción del logro |
| issuerName | String | No | Nombre del emisor (default “NappAI Fluency”) |
| validityDays | Int? | Sí | Días de validez (null = no expira) |
| badgeImageUrl | String? | Sí | URL de la imagen del badge |
Relaciones: path → Path
Creadores
Section titled “Creadores”CreatorProfile
Section titled “CreatorProfile”Tabla: CreatorProfile | Propósito: Perfil público de un instructor certificado por NappAI
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String (unique) | No | FK a User — 1:1 |
| username | String (unique) | No | Slug para /creators/:username |
| displayName | String | No | Nombre público del creador |
| headline | String | No | Titular profesional |
| bio | String | No | Biografía extendida |
| avatarUrl | String? | Sí | URL del avatar |
| avatar | Bytes? | Sí | Bytes del avatar (almacenamiento inline) |
| bannerUrl | String? | Sí | URL del banner del perfil |
| linkedinUrl | String? | Sí | Perfil LinkedIn |
| websiteUrl | String? | Sí | Sitio web personal |
| twitterUrl | String? | Sí | Perfil Twitter/X |
| specialties | String[] | No | Especialidades del creador |
| yearsExperience | Int? | Sí | Años de experiencia |
| isPublic | Boolean | No | false hasta que NappAI certifique al creador |
| certifiedAt | DateTime? | Sí | Timestamp de certificación por NappAI |
| certifiedBy | String? | Sí | userId del staff de NappAI que aprobó |
| totalStudents | Int | No | Alumnos totales (denormalizado, job nocturno) |
| avgRating | Float | No | Rating medio (denormalizado) |
| completionRate | Float | No | Tasa de completado (denormalizado) |
| totalPaths | Int | No | Número de rutas publicadas (denormalizado) |
| suspendedAt | DateTime? | Sí | Timestamp de suspensión del creador |
| paymentDataNote | String? | Sí | Nota sobre datos de pago del creador |
| earningsTermsAcceptedAt | DateTime? | Sí | Timestamp de aceptación de los términos de ganancias |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: user → User, paths → Path[], resources → Resource[], badges → CreatorBadgeGrant[], reviews → CreatorReview[], monthlyEarnings → CreatorMonthlyEarning[], earningPreview → CreatorEarningPreview?
Índices: @@index([username]), @@index([isPublic])
CreatorReview
Section titled “CreatorReview”Tabla: CreatorReview | Propósito: Valoración de un usuario sobre un creador
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| creatorId | String | No | FK a CreatorProfile |
| userId | String | No | FK a User (quien valoró) |
| rating | Int | No | Puntuación 1-5 |
| comment | String? | Sí | Comentario textual |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: creator → CreatorProfile, user → User
Índices: @@unique([creatorId, userId]), @@index([creatorId])
InstructorApplication
Section titled “InstructorApplication”Tabla: InstructorApplication | Propósito: Solicitud para convertirse en instructor / creador de contenido
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| fullName | String | No | Nombre completo del solicitante |
| String | No | Email del solicitante | |
| linkedinUrl | String | No | Perfil LinkedIn |
| currentRole | String | No | Cargo actual |
| courseProposal | String | No | Propuesta de curso a crear |
| status | ApplicationStatus | No | PENDING / IN_REVIEW / APPROVED / REJECTED / WITHDRAWN |
| reviewNotes | String? | Sí | Notas internas de revisión |
| reviewedBy | String? | Sí | ID del admin que revisó |
| reviewedAt | DateTime? | Sí | Timestamp de revisión |
| statusHistory | Json | No | Historial de cambios de estado: ApplicationEvent[] |
| userId | String? | Sí | FK a User (si el solicitante ya tiene cuenta) |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: user → User?
Índices: @@index([status]), @@index([email])
Gamificación
Section titled “Gamificación”StudentProfile
Section titled “StudentProfile”Tabla: StudentProfile | Propósito: Perfil público del estudiante con configuración de privacidad
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String (unique) | No | FK a User (cascade delete) — 1:1 |
| username | String (unique) | No | Nombre de usuario único para el perfil público |
| isPublic | Boolean | No | Si el perfil es visible públicamente |
| bio | String? | Sí | Biografía del estudiante |
| location | String? | Sí | Ubicación |
| websiteUrl | String? | Sí | Sitio web personal |
| twitterUrl | String? | Sí | Perfil Twitter/X |
| githubUrl | String? | Sí | Perfil GitHub |
| showCertificates | Boolean | No | Muestra certificados en el perfil (default true) |
| showBadges | Boolean | No | Muestra badges en el perfil (default true) |
| showPaths | Boolean | No | Muestra rutas completadas (default true) |
| showTimeline | Boolean | No | Muestra timeline de actividad (default true) |
| showXP | Boolean | No | Muestra puntos XP (default true) |
| showScore | Boolean | No | Muestra fluency score (default true) |
| usernameChangedAt | DateTime? | Sí | Timestamp del último cambio de username |
| viewCount | Int | No | Número de visitas al perfil |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: user → User
Índices: @@index([username])
StudentBadgeDefinition
Section titled “StudentBadgeDefinition”Tabla: StudentBadgeDefinition | Propósito: Definición de un badge para estudiantes con criterios de concesión
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (cuid) | No | PK |
| slug | String (unique) | No | Identificador único del badge |
| name | String | No | Nombre del badge |
| description | String | No | Descripción del logro (default "") |
| category | StudentBadgeCategory | No | LEVEL / ROUTES / FLUENCY / STREAK / QUIZ / SPECIALIZATION / SPEED / HONOR |
| iconKey | String | No | Clave del icono para renderizado |
| tier | Int | No | Nivel del badge (1=base, 2=silver, 3=gold, 4=diamond) |
| criteria | Json | No | Criterios de concesión del badge |
| isActive | Boolean | No | Si el badge está activo |
| sortOrder | Int | No | Orden de visualización (default 0) |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: grants → StudentBadgeGrant[]
StudentBadgeGrant
Section titled “StudentBadgeGrant”Tabla: StudentBadgeGrant | Propósito: Concesión de un badge a un estudiante
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (cuid) | No | PK |
| studentId | String | No | FK a User |
| badgeId | String | No | FK a StudentBadgeDefinition |
| grantedBy | String? | Sí | ’system’ o userId del admin que concedió |
| grantedAt | DateTime | No | Timestamp de concesión |
| revokedAt | DateTime? | Sí | Timestamp de revocación (null = vigente) |
| supersededAt | DateTime? | Sí | Timestamp en que fue superado por un tier superior |
| metadata | Json? | Sí | Metadatos adicionales de la concesión |
| assertionId | String (unique)? | Sí | ID único Open Badges (cuid por defecto) |
| badgeUrl | String? | Sí | URL pública del badge assertion |
Relaciones: student → User, badge → StudentBadgeDefinition
Índices: @@index([studentId, badgeId])
StudentXPEvent
Section titled “StudentXPEvent”Tabla: StudentXPEvent | Propósito: Evento de puntos de experiencia (XP) ganado por un estudiante
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (cuid) | No | PK |
| userId | String | No | FK a User |
| eventType | String | No | Tipo de evento que generó el XP |
| xp | Int | No | Puntos XP otorgados |
| metadata | Json? | Sí | Contexto adicional del evento |
| createdAt | DateTime | No | Timestamp del evento |
Relaciones: user → User
Índices: @@index([userId])
CreatorBadgeDefinition
Section titled “CreatorBadgeDefinition”Tabla: CreatorBadgeDefinition | Propósito: Definición de un badge para creadores con criterios de concesión
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| slug | String (unique) | No | Identificador único del badge |
| name | String | No | Nombre del badge |
| description | String | No | Descripción del logro |
| iconEmoji | String | No | Emoji del badge (deprecated — usar iconKey) |
| iconKey | String | No | Clave del icono (default “rising-star”) |
| tier | Int | No | Nivel: 1=base, 2=silver, 3=gold, 4=diamond |
| criteria | Json | No | Criterios de concesión: {minStudents, minRating, ...} |
| isActive | Boolean | No | Si el badge está activo |
Relaciones: grants → CreatorBadgeGrant[]
CreatorBadgeGrant
Section titled “CreatorBadgeGrant”Tabla: CreatorBadgeGrant | Propósito: Concesión de un badge a un creador
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| creatorId | String | No | FK a CreatorProfile |
| badgeId | String | No | FK a CreatorBadgeDefinition |
| grantedAt | DateTime | No | Timestamp de concesión |
| revokedAt | DateTime? | Sí | Timestamp de revocación (null = vigente) |
| grantedBy | String? | Sí | ’system’ o userId del admin que concedió |
Relaciones: creator → CreatorProfile, badge → CreatorBadgeDefinition
Índices: @@unique([creatorId, badgeId]), @@index([creatorId])
Monetización
Section titled “Monetización”CreatorEarningLine
Section titled “CreatorEarningLine”Tabla: CreatorEarningLine | Propósito: Desglose por ruta de las ganancias mensuales de un creador
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| earningId | String | No | FK a CreatorMonthlyEarning (cascade delete) |
| pathId | String | No | ID de la ruta (referencia desnormalizada) |
| pathTitle | String | No | Título de la ruta (desnormalizado) |
| pathSlug | String | No | Slug de la ruta (desnormalizado) |
| completionsCount | Int | No | Completados de la ruta en el período |
| minutesConsumed | Int | No | Minutos consumidos en la ruta |
| avgRating | Float? | Sí | Rating medio de la ruta en el período |
| reviewsCount | Int | No | Número de reviews en el período |
| contributionPct | Float | No | Porcentaje de contribución al pool del creador |
| grossAmountEur | Float | No | Importe bruto en euros para esta ruta |
Relaciones: earning → CreatorMonthlyEarning
Índices: @@index([earningId]), @@index([pathId])
CreatorEarningPreview
Section titled “CreatorEarningPreview”Tabla: CreatorEarningPreview | Propósito: Vista previa en tiempo real de las ganancias estimadas del mes en curso (1:1 con CreatorProfile)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| creatorId | String (unique) | No | FK a CreatorProfile — 1:1 |
| periodMonth | String | No | Mes en formato YYYY-MM |
| completionsCount | Int | No | Completados acumulados en el mes |
| minutesConsumed | Int | No | Minutos consumidos acumulados |
| weightedRating | Float | No | Rating ponderado acumulado |
| reviewsCount | Int | No | Reviews acumuladas |
| estimatedGrossEur | Float | No | Estimación de ganancia bruta en euros |
| estimatedPoolAmount | Float | No | Estimación del pool total del mes |
| estimatedParticipationPct | Float | No | Estimación del porcentaje de participación |
| isEligibleSoFar | Boolean | No | Si cumple criterios de elegibilidad hasta ahora |
| ineligibilityReason | String? | Sí | Razón de inelegibilidad si aplica |
| lastCalculatedAt | DateTime | No | Timestamp del último cálculo |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: creator → CreatorProfile
Índices: @@index([creatorId]), @@index([periodMonth])
CreatorMonthlyEarning
Section titled “CreatorMonthlyEarning”Tabla: CreatorMonthlyEarning | Propósito: Liquidación mensual definitiva de ganancias de un creador
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| creatorId | String | No | FK a CreatorProfile |
| configId | String | No | FK a PlatformRevenueConfig |
| periodMonth | String | No | Mes en formato YYYY-MM |
| completionsCount | Int | No | Completados en el período |
| minutesConsumed | Int | No | Minutos consumidos en el período |
| weightedRating | Float | No | Rating ponderado del creador |
| reviewsCount | Int | No | Reviews recibidas en el período |
| rawScore | Float | No | Puntuación bruta calculada |
| participationPct | Float | No | Porcentaje de participación en el pool |
| grossAmountEur | Float | No | Importe bruto en euros |
| isEligible | Boolean | No | Si el creador es elegible para pago |
| ineligibilityReason | String? | Sí | Razón de inelegibilidad si aplica |
| paymentStatus | CreatorPaymentStatus | No | PENDING_REVIEW / IN_REVIEW / APPROVED / PAID / ON_HOLD / REJECTED |
| paymentNotes | String? | Sí | Notas del proceso de pago |
| paymentStatusUpdatedAt | DateTime? | Sí | Timestamp del último cambio de estado de pago |
| paymentStatusUpdatedBy | String? | Sí | ID del admin que cambió el estado de pago |
| exportedAt | DateTime? | Sí | Timestamp de exportación para pago |
| exportedBy | String? | Sí | ID del admin que exportó |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: creator → CreatorProfile, config → PlatformRevenueConfig, lines → CreatorEarningLine[]
Índices: @@unique([creatorId, periodMonth]), @@index([periodMonth]), @@index([creatorId]), @@index([paymentStatus])
PlatformRevenueConfig
Section titled “PlatformRevenueConfig”Tabla: PlatformRevenueConfig | Propósito: Configuración mensual del pool de ingresos para creadores (ADR-011)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| periodMonth | String (unique) | No | Mes en formato YYYY-MM |
| poolPercentage | Float | No | Porcentaje del ingreso neto para el pool (default 0.25) |
| minCompletionsForEligibility | Int | No | Completados mínimos para ser elegible (default 5) |
| minReviewsForRating | Int | No | Reviews mínimas para contar el rating (default 3) |
| grossRevenue | Float? | Sí | Ingreso bruto del mes |
| stripeFeesTotal | Float? | Sí | Total de comisiones de Stripe |
| refundsTotal | Float? | Sí | Total de reembolsos |
| netRevenue | Float? | Sí | Ingreso neto (grossRevenue - fees - refunds) |
| creatorPoolAmount | Float? | Sí | Importe total del pool para creadores |
| isLocked | Boolean | No | Si el período está cerrado para modificaciones |
| lockedAt | DateTime? | Sí | Timestamp de cierre del período |
| lockedBy | String? | Sí | ID del admin que cerró el período |
| adminNotes | String? | Sí | Notas internas del admin |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: earnings → CreatorMonthlyEarning[]
Índices: @@index([periodMonth])
Notificaciones y Comunicación
Section titled “Notificaciones y Comunicación”Notification
Section titled “Notification”Tabla: Notification | Propósito: Notificación in-app para un usuario (ADR-012)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (cuid) | No | PK |
| userId | String | No | FK a User (cascade delete) |
| type | NotificationType | No | Tipo de notificación (101 valores) |
| category | NotificationCategory | No | LEARNING / ACHIEVEMENT / CONTENT / AI_TASK / TEAM / CREATOR / SYSTEM |
| title | String | No | Título de la notificación |
| body | String | No | Cuerpo del mensaje |
| actionUrl | String | No | URL de acción principal |
| actionLabel | String? | Sí | Etiqueta del botón de acción |
| icon | String | No | Icono (default ”🔔“) |
| iconBg | String | No | Color de fondo del icono |
| severity | String | No | ’info’ / ‘warning’ / ‘critical’ (default ‘info’) |
| isRead | Boolean | No | Si ha sido leída por el usuario |
| readAt | DateTime? | Sí | Timestamp de lectura |
| channels | String[] | No | Canales de entrega (‘in_app’, ‘email’, ‘push’) |
| metadata | Json? | Sí | Metadatos adicionales para deduplicación y contexto |
| emailSentAt | DateTime? | Sí | Timestamp de envío por email |
| pushSentAt | DateTime? | Sí | Timestamp de envío por push |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: user → User
Índices: @@index([userId, isRead, createdAt]), @@index([userId, category, createdAt]), @@index([createdAt])
Infraestructura IA
Section titled “Infraestructura IA”AICredential
Section titled “AICredential”Tabla: AICredential | Propósito: API keys de proveedores LLM cifradas con AES-256-GCM
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| provider | String (unique) | No | ’anthropic’ / ‘openai’ / ‘gemini’ / ‘qdrant’ |
| label | String | No | Nombre amigable (ej. “Anthropic Claude”) |
| encryptedValue | String | No | API key cifrada: formato iv:tag:ciphertext (AES-256-GCM) |
| maskedPreview | String | No | Últimos 4 chars del valor real: ”…X4kp” |
| isActive | Boolean | No | false = no se usa aunque exista |
| lastTestedAt | DateTime? | Sí | Timestamp del último test de conectividad |
| lastTestStatus | String? | Sí | ’ok’ / ‘error’ / ‘timeout’ |
| updatedBy | String? | Sí | userId del SUPER_ADMIN que hizo el último cambio |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
AIRouteConfig
Section titled “AIRouteConfig”Tabla: AIRouteConfig | Propósito: Tabla de enrutamiento de casos de uso IA a proveedores LLM
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| useCase | String (unique) | No | Valor del enum AIUseCase |
| label | String | No | Nombre legible (ej. “Tutor conversacional”) |
| provider | String | No | Proveedor primario (AIProvider enum) |
| model | String | No | Modelo primario |
| fallbackProvider | String | No | Proveedor de fallback |
| fallbackModel | String | No | Modelo de fallback |
| temperature | Float | No | Temperatura del modelo (default 0.7) |
| maxTokens | Int | No | Máximo de tokens por respuesta (default 2048) |
| cacheTtlSeconds | Int | No | TTL de caché en segundos (0 = sin caché) |
| similarityThreshold | Float | No | Umbral de similitud para caché semántica (default 0.85) |
| isActive | Boolean | No | Si la ruta está habilitada |
| updatedBy | String? | Sí | userId del admin que realizó el último cambio |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
AIUsageMetric
Section titled “AIUsageMetric”Tabla: AIUsageMetric | Propósito: Registro de cada llamada al motor IA con coste estimado
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String? | Sí | FK a User (null = llamada de sistema) |
| useCase | String | No | Caso de uso ejecutado |
| provider | String | No | Proveedor que respondió |
| model | String | No | Modelo específico usado |
| latencyMs | Int | No | Latencia total en ms |
| tokensIn | Int | No | Tokens de entrada (default 0) |
| tokensOut | Int | No | Tokens de salida (default 0) |
| estimatedCostUsd | Float | No | Coste estimado en USD |
| fromCache | Boolean | No | Si la respuesta vino de caché |
| hadFallback | Boolean | No | Si se usó el proveedor de fallback |
| errorCode | String? | Sí | Código de error (null = éxito) |
| createdAt | DateTime | No | Timestamp de la llamada |
Índices: @@index([userId]), @@index([useCase]), @@index([provider]), @@index([createdAt]), @@index([hadFallback])
AIAlertConfig
Section titled “AIAlertConfig”Tabla: AIAlertConfig | Propósito: Configuración global de alertas IA (singleton — siempre id=‘singleton’)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String | No | PK — siempre “singleton” |
| dailyThresholdUsd | Float? | Sí | Umbral de coste diario en USD para alertas |
| monthlyThresholdUsd | Float? | Sí | Umbral de coste mensual en USD para alertas |
| notificationEmail | String? | Sí | Email para notificaciones de alertas |
| webhookUrl | String? | Sí | URL de webhook para notificaciones externas |
| updatedAt | DateTime | No | Timestamp de última modificación |
AIAlert
Section titled “AIAlert”Tabla: AIAlert | Propósito: Alerta disparada por el sistema de monitorización de IA
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| type | AIAlertType | No | PROVIDER_DOWN / HIGH_ERROR_RATE / COST_THRESHOLD_EXCEEDED / LATENCY_DEGRADED / QDRANT_UNAVAILABLE / CREDENTIAL_EXPIRING / FALLBACK_ACTIVATED |
| severity | String | No | ’info’ / ‘warning’ / ‘critical’ |
| message | String | No | Mensaje descriptivo de la alerta |
| metadata | Json? | Sí | Datos adicionales del contexto de la alerta |
| isResolved | Boolean | No | Si la alerta ha sido resuelta |
| resolvedBy | String? | Sí | userId del admin que resolvió la alerta |
| resolvedAt | DateTime? | Sí | Timestamp de resolución |
| createdAt | DateTime | No | Timestamp de creación |
Índices: @@index([isResolved]), @@index([createdAt])
AITask
Section titled “AITask”Tabla: AITask | Propósito: Tarea asíncrona de generación o evaluación IA con ciclo de revisión
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User |
| type | AITaskType | No | QUIZ_GENERATION / LESSON_GENERATION / SUMMARY_GENERATION / CASE_STUDY_GENERATION / OPEN_ANSWER_EVALUATION / FLUENCY_TEST_SCORING / QDRANT_REINDEX_FULL / QDRANT_REINDEX_LESSON / CHURN_ANALYSIS / ADAPTIVE_ENGINE / TAG_SUGGESTION |
| status | AITaskStatus | No | PENDING / RUNNING / COMPLETED / FAILED / CANCELLED |
| label | String | No | Etiqueta descriptiva para el usuario |
| contextId | String? | Sí | ID del módulo/lección/ruta asociado |
| contextType | String? | Sí | ’module’ / ‘lesson’ / ‘path’ |
| progressPct | Int | No | Progreso en porcentaje (default 0) |
| progressMsg | String? | Sí | Mensaje de progreso para el usuario |
| errorMsg | String? | Sí | Mensaje de error si falló |
| provider | String? | Sí | Proveedor IA que ejecutó la tarea |
| model | String? | Sí | Modelo IA que ejecutó la tarea |
| tokensUsed | Int | No | Tokens consumidos (default 0) |
| costUsd | Float | No | Coste estimado en USD (default 0) |
| bullJobId | String? | Sí | ID del job en BullMQ |
| seenByUser | Boolean | No | Si el usuario ha visto la notificación de resultado |
| dismissedAt | DateTime? | Sí | Timestamp en que el usuario descartó la tarea |
| startedAt | DateTime? | Sí | Timestamp de inicio de ejecución |
| completedAt | DateTime? | Sí | Timestamp de finalización |
| launchPayload | Json? | Sí | Payload de lanzamiento persistido |
| resultPayload | Json? | Sí | Output del modelo para el diálogo de revisión |
| reviewStatus | AITaskReviewStatus | No | PENDING_REVIEW / IN_REVIEW / ACCEPTED / REJECTED / REFINED |
| reviewedAt | DateTime? | Sí | Timestamp de revisión por el usuario |
| reviewedBy | String? | Sí | userId del usuario que revisó |
| parentTaskId | String? | Sí | FK a AITask padre (para refinamientos) |
| refinementCount | Int | No | Número de refinamientos realizados |
| refinementNotes | String? | Sí | Notas del refinamiento |
| importedEntityId | String? | Sí | ID de la entidad creada al importar el resultado |
| importedEntityType | String? | Sí | ’quiz’ / ‘lesson’ / ‘summary’ |
| importedAt | DateTime? | Sí | Timestamp de importación a BD |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: user → User, parentTask → AITask?, refinements → AITask[], quizGenerationResult → QuizGenerationResult?, lessonGenerationResult → LessonGenerationResult?, summaryGenerationResult → SummaryGenerationResult?
Índices: @@index([userId, status]), @@index([userId, type, status]), @@index([userId, createdAt]), @@index([parentTaskId]), @@index([contextId])
Generación IA
Section titled “Generación IA”LessonGenerationResult
Section titled “LessonGenerationResult”Tabla: LessonGenerationResult | Propósito: Resultado tipado de una tarea de generación de lección IA
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| taskId | String (unique) | No | FK a AITask — 1:1 |
| lessonId | String | No | ID de la lección de destino |
| moduleId | String | No | ID del módulo de contexto |
| pathId | String | No | ID de la ruta de contexto |
| contentMd | String | No | Contenido Markdown generado |
| suggestedTitle | String | No | Título sugerido por la IA (default "") |
| estimatedMin | Int | No | Duración estimada sugerida en minutos (default 10) |
| keyTopics | Json | No | Lista de temas clave identificados (default []) |
| resourcesReferenced | Json | No | Recursos referenciados en el contenido (default []) |
| insertedAt | DateTime? | Sí | Timestamp en que se insertó el contenido en la lección |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: task → AITask
Índices: @@index([lessonId]), @@index([moduleId])
QuizGenerationResult
Section titled “QuizGenerationResult”Tabla: QuizGenerationResult | Propósito: Resultado tipado de una tarea de generación de quiz IA
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| taskId | String (unique) | No | FK a AITask — 1:1 |
| moduleId | String | No | ID del módulo de contexto |
| pathId | String | No | ID de la ruta de contexto |
| questions | Json | No | Array de GeneratedQuestion[] serializado |
| totalGenerated | Int | No | Total de preguntas generadas |
| importedCount | Int | No | Preguntas importadas al quiz definitivo (default 0) |
| importedAt | DateTime? | Sí | Timestamp de importación |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: task → AITask
Índices: @@index([moduleId])
SummaryGenerationResult
Section titled “SummaryGenerationResult”Tabla: SummaryGenerationResult | Propósito: Resultado tipado de una tarea de generación de resumen IA
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| taskId | String (unique) | No | FK a AITask — 1:1 |
| lessonId | String | No | ID de la lección de destino |
| objectivesTitle | String? | Sí | Título de la sección de objetivos generada |
| objectivesBullets | Json? | Sí | Bullets de objetivos generados |
| summaryTitle | String? | Sí | Título de la sección de resumen generada |
| summaryBullets | Json? | Sí | Bullets del resumen generado |
| fromCache | Boolean | No | Si el resultado vino de caché |
| insertedSections | Json? | Sí | Secciones efectivamente insertadas en la lección |
| insertedAt | DateTime? | Sí | Timestamp de inserción en la lección |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: task → AITask
Guías Técnicas
Section titled “Guías Técnicas”GuideVersion
Section titled “GuideVersion”Tabla: GuideVersion | Propósito: Versión publicada de la Guía de Calidad del Creador (snapshot completo)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| version | String | No | Número de versión (ej. “2.1”) |
| publishedAt | DateTime | No | Timestamp de publicación |
| publishedBy | String | No | userId del admin que publicó |
| snapshot | Json | No | Snapshot completo del contenido de la guía |
| note | String? | Sí | Notas del cambio de versión |
GuideSection
Section titled “GuideSection”Tabla: GuideSection | Propósito: Sección de la Guía de Calidad del Creador
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| slug | String (unique) | No | Identificador URL de la sección |
| emoji | String | No | Emoji representativo de la sección |
| title | String | No | Título de la sección |
| eyebrow | String? | Sí | Texto supratítulo |
| subtitle | String? | Sí | Subtítulo descriptivo |
| order | Int | No | Posición en la guía |
| isActive | Boolean | No | Si la sección está visible |
| updatedBy | String? | Sí | userId del último editor |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: blocks → GuideBlock[]
Índices: @@index([order]), @@index([isActive])
GuideBlock
Section titled “GuideBlock”Tabla: GuideBlock | Propósito: Bloque de contenido dentro de una sección de la guía
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| sectionId | String | No | FK a GuideSection (cascade delete) |
| type | GuideBlockType | No | CALLOUT / RULE_PAIR / STEP / SPEC_TABLE / EXAMPLE_PAIR / CHECKLIST / TEXT |
| payload | Json | No | Contenido del bloque (estructura varía por tipo) |
| order | Int | No | Posición dentro de la sección |
| isActive | Boolean | No | Si el bloque está visible |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: section → GuideSection
Índices: @@index([sectionId, order])
Audit y Jobs
Section titled “Audit y Jobs”AuditLog
Section titled “AuditLog”Tabla: AuditLog | Propósito: Registro centralizado de trazabilidad de todas las acciones relevantes del sistema (ADR-013)
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (cuid) | No | PK |
| actorId | String? | Sí | ID del usuario que ejecutó la acción |
| actorRole | UserRole? | Sí | Rol del actor en el momento de la acción |
| actorEmail | String? | Sí | Email del actor |
| actorIp | String? | Sí | IP del actor |
| actorUserAgent | String? | Sí | User-Agent del actor |
| action | AuditAction | No | Acción realizada (97 valores en el enum) |
| category | AuditCategory | No | AUTH / USER_MANAGEMENT / CONTENT / ORGANIZATION / BILLING / AI_SYSTEM / SECURITY / STUDENT_PROGRESS / SYSTEM_CONFIG |
| severity | AuditSeverity | No | INFO / WARNING / CRITICAL (default INFO) |
| entityType | AuditEntityType? | Sí | Tipo de entidad afectada |
| entityId | String? | Sí | ID de la entidad afectada |
| entityLabel | String? | Sí | Label legible de la entidad afectada |
| before | Json? | Sí | Estado anterior de la entidad |
| after | Json? | Sí | Estado posterior de la entidad |
| metadata | Json? | Sí | Metadatos adicionales del contexto |
| success | Boolean | No | Si la acción fue exitosa (default true) |
| errorCode | String? | Sí | Código de error si falló |
| errorMessage | String? | Sí | Mensaje de error si falló |
| sessionId | String? | Sí | ID de la sesión del usuario |
| requestId | String? | Sí | ID de la request HTTP |
| orgId | String? | Sí | FK a Organization (si aplica) |
| createdAt | DateTime | No | Timestamp del evento |
Relaciones: organization → Organization?
Índices: @@index([actorId]), @@index([action]), @@index([category]), @@index([severity]), @@index([entityType, entityId]), @@index([orgId]), @@index([createdAt]), @@index([actorRole])
ScheduledJob
Section titled “ScheduledJob”Tabla: ScheduledJob | Propósito: Configuración y estado de los cron jobs del sistema, con panel de control para SUPER_ADMIN
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| slug | String (unique) | No | Identificador único del job (ej. “sync-creator-metrics”) |
| name | String | No | Nombre legible del job |
| description | String? | Sí | Descripción de qué hace el job |
| category | String | No | Categoría del job (ej. “earnings”, “metrics”) |
| icon | String? | Sí | Icono para el panel de control |
| cronExpr | String | No | Expresión cron activa |
| cronDefault | String | No | Expresión cron por defecto (para reset) |
| timezone | String | No | Timezone del job (default “UTC”) |
| isEnabled | Boolean | No | Si el job está habilitado |
| retentionDays | Int | No | Días de retención de ejecuciones históricas (default 30) |
| config | Json? | Sí | Configuración específica del job |
| notes | String? | Sí | Notas internas |
| lastRunAt | DateTime? | Sí | Timestamp de la última ejecución |
| lastStatus | JobStatus? | Sí | Estado de la última ejecución |
| lastDurationMs | Int? | Sí | Duración de la última ejecución en ms |
| lastItemsCount | Int? | Sí | Ítems procesados en la última ejecución |
| nextRunAt | DateTime? | Sí | Timestamp de la próxima ejecución prevista |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: executions → JobExecution[]
Índices: @@index([category]), @@index([isEnabled])
JobExecution
Section titled “JobExecution”Tabla: JobExecution | Propósito: Registro histórico de cada ejecución de un cron job
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| jobId | String | No | FK a ScheduledJob (cascade delete) |
| status | JobStatus | No | RUNNING / COMPLETED / FAILED / SKIPPED |
| startedAt | DateTime | No | Timestamp de inicio |
| finishedAt | DateTime? | Sí | Timestamp de finalización |
| durationMs | Int? | Sí | Duración en ms |
| itemsProcessed | Int? | Sí | Ítems procesados en esta ejecución |
| itemsSummary | String? | Sí | Resumen legible de ítems procesados |
| errorMessage | String? | Sí | Mensaje de error si falló |
| triggeredBy | String | No | ’cron’ / ‘manual’ / userId del admin que lanzó manualmente |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: job → ScheduledJob
Índices: @@index([jobId, startedAt(sort: Desc)]), @@index([status])
Tutor IA
Section titled “Tutor IA”TutorConversation
Section titled “TutorConversation”Tabla: TutorConversation | Propósito: Conversación del tutor IA — agrupa mensajes de un contexto específico
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| userId | String | No | FK a User |
| moduleId | String? | Sí | ID del módulo de contexto |
| lessonId | String? | Sí | ID de la lección de contexto |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: messages → TutorMessage[]
Índices: @@index([userId]), @@index([userId, lessonId])
TutorMessage
Section titled “TutorMessage”Tabla: TutorMessage | Propósito: Mensaje individual dentro de una conversación del tutor IA
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| conversationId | String | No | FK a TutorConversation |
| userId | String | No | FK a User |
| role | String | No | ”user” / “assistant” |
| content | String | No | Contenido del mensaje |
| rating | String? | Sí | Valoración del usuario: ‘up’ / ‘down’ / null |
| provider | String? | Sí | Proveedor IA (ej. ‘anthropic’) |
| model | String? | Sí | Modelo IA (ej. ‘claude-sonnet-4-5’) |
| latencyMs | Int? | Sí | Tiempo de respuesta en ms |
| createdAt | DateTime | No | Timestamp de creación |
Relaciones: conversation → TutorConversation, user → User
Índices: @@index([conversationId])
DemoRequest
Section titled “DemoRequest”Tabla: DemoRequest | Propósito: Lead B2B — solicitud de demo corporativa
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| firstName | String | No | Nombre del solicitante |
| lastName | String | No | Apellidos del solicitante |
| String | No | Email de contacto | |
| phone | String | No | Teléfono de contacto |
| company | String | No | Empresa |
| role | String | No | Cargo del solicitante |
| sector | String | No | Sector de la empresa |
| teamSize | String | No | Tamaño del equipo |
| message | String? | Sí | Mensaje adicional |
| source | String? | Sí | Fuente de captación |
| privacyAccepted | Boolean | No | Aceptación de política de privacidad |
| marketingConsent | Boolean | No | Consentimiento de marketing (default false) |
| utmSource | String? | Sí | UTM source del tráfico |
| utmMedium | String? | Sí | UTM medium del tráfico |
| utmCampaign | String? | Sí | UTM campaign del tráfico |
| referrer | String? | Sí | Referrer HTTP |
| originPage | String? | Sí | Página de origen del formulario |
| status | DemoRequestStatus | No | PENDING / CONTACTED / DEMO_SCHEDULED / DEMO_DONE / CONVERTED / DISCARDED |
| priority | DemoRequestPriority | No | NORMAL / HIGH_PRIORITY |
| internalNotes | String? | Sí | Notas internas del equipo de ventas |
| assignedTo | String? | Sí | ID del commercial asignado |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: organization → Organization? (si se convirtió en org activa)
Índices: @@index([email]), @@index([status])
PathReview
Section titled “PathReview”Tabla: PathReview | Propósito: Valoración de un estudiante sobre una ruta de aprendizaje
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
| id | String (uuid) | No | PK |
| pathId | String | No | FK a Path |
| userId | String | No | FK a User (quien valoró) |
| rating | Int | No | Puntuación 1-5 |
| comment | String? | Sí | Comentario textual |
| createdAt | DateTime | No | Timestamp de creación |
| updatedAt | DateTime | No | Timestamp de última modificación |
Relaciones: path → Path, user → User
Índices: @@unique([pathId, userId]), @@index([pathId])