CEO Sim | Automata
secciones (10)
$ cd .. // volver a proyectos
automata@latam: ~/proyectos/ceo-sim
CASO DE USO · slug: ceo-sim ·2026 · 4 min read · 935 words

> CEO Sim

// Juego multiplayer de simulacion empresarial embebido en Moodle/Canvas/Blackboard via LTI 1.3 — auto-provisiona users, deep-link a rondas, sincroniza notas via AGS.

Laravel 10Angular 15MySQL 8Redis 7LTI 1.3JWTDocker Compose
CEO Sim
▸ rol
Backend · Integracion LTI 1.3
▸ status
ready
// section 01 · descubrimiento

$ cat ./descubrimiento.md

▸ descripcion

CEO Sim es una simulacion empresarial multiplayer donde estudiantes manejan empresas virtuales en rondas competitivas. El backend Laravel 10 orquesta rondas, finanzas e interacciones entre empresas; Angular 15 renderea dashboards, charts y decisiones; Redis maneja cache, rate-limits y colas. Lo distintivo es la integracion LTI 1.3: los docentes lanzan el juego desde Moodle, Canvas o Blackboard, los users se auto-provisionan con el rol correcto desde los claims del JWT, los docentes pueden deep-linkear a una ronda especifica, y las notas finales sincronizan al gradebook del LMS via AGS.

▸ problema

Las herramientas educativas viven o mueren por su integracion con el LMS. Sincronizar users a mano entre un LMS y una simulacion es inviable para una institucion con cientos de alumnos por semestre. LTI 1.3 es el estandar abierto, pero su handshake OIDC, rotacion de JWKS, mapeo de roles por LMS y grade passback no son triviales de implementar bien.

▸ audiencia

Escuelas de negocios, programas MBA y areas de capacitacion corporativa que usan Moodle/Canvas/Blackboard y quieren enseñar gestion empresarial via simulacion.

// section 03 · arquitectura

$ cat ./arquitectura.md

// section 03b · secuencias

$ cat ./secuencias.md

// flujos en runtime — quien habla con quien y en que orden

// Handshake OIDC, validacion del JWT contra el JWKS del LMS y provisioning idempotente del user.

Launch LTI 1.3 — del LMS al user auto-provisionado · flow-01
loading…
// section 04 · infraestructura

$ cat ./infraestructura.md

▸ servicios
provider: Laravel 10 + MySQL 8 + Redis 7 + Docker Compose
  • app (HTTP + cola Laravel)
  • db (MySQL 8)
  • redis (7 — cache, rate limit, cola)
  • front (Angular 15)
  • moodle (LMS de test, servicio docker compose para e2e local de flujos LTI)
// section 05 · implementacion

$ cat ./implementacion.md

▸ frontend
  • · Angular 15
  • · Angular Material
  • · HighCharts
  • · NgRx
  • · RxJS
  • · Socket.IO (updates en vivo)
▸ backend
  • · Laravel 10
  • · PHP 8.1
  • · JWT (tymon/jwt-auth 2)
  • · oat-sa LTI 1.3 (Core + Deep Linking + NRPS + AGS)
  • · OpenAI PHP SDK (coach IA)
  • · Maatwebsite Excel
  • · Google API client
▸ datos
  • · MySQL 8
  • · Redis 7 (cache + colas)
  • · Google Sheets API para reportes offline
▸ devops
  • · Docker Compose (con un container Moodle para e2e)
  • · Docs Swagger / OpenAPI
  • · Scripts de backup MySQL en local/backups/
// section 06 · desafios tecnicos

$ cat ./challenges/*.md

// 3 problemas tecnicos resueltos

01 / 03
challenge-01.md · lti · oidc · auth
▸ problema

Permitir que un docente lance CEO Sim desde cualquier LMS sin sync manual de users.

restriccion: El handshake OIDC requiere nonce + state guardados a traves de dos hops HTTP. El id_token JWT debe validarse contra el endpoint JWKS del LMS (distinto por plataforma). Los claims de rol difieren: Instructor en Canvas no es Instructor en Moodle con el mismo shape JSON. El deep linking permite al docente apuntar a una ronda especifica.

▸ enfoque

LtiSsoService maneja la iniciacion OIDC (nonce en Redis), el endpoint de launch valida el JWT id_token, y LtiUserMappingRepository provisiona o reusa un User idempotentemente. resolveRoleId() mapea URIs de rol del LMS a role_ids internos. LtiDeploymentRepository persiste contexto + URLs NRPS/AGS por deployment. Un middleware ValidateLtiLaunch intercepta cada launch.

app/Services/Lti/LtiSsoService.php php
// En el launch (id_token ya validado):
$platform = LtiPlatformRepository::findByIssuerAndClientId($issuer, $clientId);
$mapping  = LtiUserMappingRepository::findByPlatformAndLtiUserId(
    $platform->id, $claims['sub']
);

if (! $mapping) {
    $user = User::firstOrCreate(['email' => $claims['email']], [
        'name'     => $claims['name'],
        'password' => Hash::make(Str::random(16)),
    ]);
    $mapping = LtiUserMappingRepository::create([
        'lti_platform_id' => $platform->id,
        'lti_user_id'     => $claims['sub'],
        'user_id'         => $user->id,
        'lti_roles'       => $claims['roles'],
    ]);
}

$user->update(['role_id' => $this->resolveRoleId($claims['roles'])]);
return new JwtResponse($user, $this->getToolToken($user));
challenge-02.md · lti · ags · notas
▸ problema

Sincronizar las notas de las rondas de CEO Sim al gradebook del LMS sin crear line items duplicados.

restriccion: Cada deployment puede tener varios line items (Ronda 1, Ronda 2, Final). Las notas se POSTean via AGS con timestamp y un status de progreso. El LMS puede responder 404 si un docente borro un line item; la herramienta debe manejarlo sin tirar la ronda.

▸ enfoque

ags_lineitems_url se captura al momento del launch y se persiste en LtiDeployment. Cuando una ronda cierra, el grade service itera notas cerradas y POSTea a AGS con scoreGiven/scoreMaximum/timestamp/activityProgress/gradingProgress. Las entregas fallidas van a la cola con backoff; los 404 marcan el line item inactivo localmente.

app/Services/Lti/LtiGradeService.php php
foreach ($scores as $score) {
    $payload = [
        'userId'          => $score->user->lti_user_id,
        'scoreGiven'      => $score->final_score,
        'scoreMaximum'    => 100,
        'timestamp'       => $score->closed_at->toRfc3339String(),
        'activityProgress' => 'Completed',
        'gradingProgress'  => 'FullyGraded',
    ];
    $this->agsSender->send($deployment->ags_lineitems_url, $payload);
}
challenge-03.md · lti · tenant · config
▸ problema

Onboardear una instancia LMS nueva (cada una es su propio tenant con su keypair RSA).

restriccion: La herramienta necesita guardar issuer, client_id, JWKS URL, URLs de login/token, mas las propias claves RSA privadas/publicas de la herramienta. Los errores aca se convierten en 'Issuer not found' o 'Invalid JWT signature' — debuggeable pero lento si el dueño del LMS no ve el error correcto.

▸ enfoque

La tabla lti_platforms guarda el set completo de campos por LMS. plan-lti-technical.md documenta cada campo y el orden de operaciones. El container Moodle en docker-compose corre CEO Sim end-to-end asi el mismo flujo se ejercita en local antes de ir a un LMS real.

database/migrations/*_create_lti_platforms_table.php php
Schema::create('lti_platforms', function (Blueprint $table) {
    $table->id();
    $table->string('issuer', 500)->unique();
    $table->string('client_id', 255);
    $table->string('auth_login_url', 500);
    $table->string('auth_token_url', 500);
    $table->string('jwks_url', 500);
    $table->unsignedBigInteger('license_id'); // tenant
    $table->text('tool_private_key');
    $table->text('tool_public_key');
    $table->string('tool_key_id', 100);
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});
// section 07 · testing & ci

$ cat ./testing.md

▸ estrategia

Unit tests cubren el flujo OIDC, validacion JWKS, mapeo de roles e idempotencia del provisioning. Tests de integracion usan el servicio docker-compose de Moodle para ejercitar el loop launch → grade-passback contra un LMS real. Las firmas JWT del LMS se mockean con keys RSA de test.

▸ herramientas
PHPUnit 10Mockery 1.4Pest (via Laravel)Swagger / OpenAPI para contrato de API
// section 09 · resultados

$ cat ./resultados.md

01 /
1460
commits en el repo de backend
02 /
60
modelos Eloquent
03 /
283
migrations
04 /
126
controllers
05 /
44
componentes Angular
▸ outcomes

Integracion LTI 1.3 production-grade que autentica users, provisiona roles y sincroniza notas. El setup Moodle-en-Docker permite validar el handshake completo con el LMS antes de promover a una instancia real. La simulacion en si (rondas, finanzas, scoring) es la mitad mas vieja y madura del codebase.

// section 10 · lecciones

$ cat ./lessons.md

// si lo hiciera de nuevo

  • 01 /

    LTI 1.3 es directo SI testeas contra un LMS real

    El spec es preciso pero pierde detalle: un mapeo de claim mal, un nonce que falta, un JWKS que no calza — cada uno produce un error generico del lado del LMS. Levantar un container de Moodle y correr el flujo entero en local bajo el ciclo de debug de horas a minutos.

  • 02 /

    El grade passback es async, tratalo como envio de email

    La primera version POSTeaba notas inline al cierre de ronda y mostraba 502 al user. Mover el envio a AGS a la cola con backoff (y un dashboard de cola) hizo que el failure mode fuera operacional en vez de UX.

// relacionados

$ grep -l "tech" ./projects/*

// proyectos que comparten parte del stack

// siguiente paso

$ automata deploy --tu-operacion

// Conversemos sobre como adaptamos esto a tu caso.

./contactar.sh