{"openapi":"3.0.0","paths":{"/api/v1/auth/signup/creator":{"post":{"description":"Creates a User, a Tenant (with ownerId), and a CREATOR TenantMembership in a single transaction. The tenant slug is auto-generated from companyName. Returns an access + refresh token pair to log the creator in immediately.","operationId":"AuthController_signupCreator","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupCreatorDto"}}}},"responses":{"201":{"description":"Creator and tenant created, tokens returned"},"409":{"description":"Email or company slug already exists"}},"summary":"Sign up as Creator (new OF organisation)","tags":["Auth"]}},"/api/v1/auth/signup/learner":{"post":{"description":"Creates a User with no TenantMembership. The learner gains access to tenant courses via Enrollment only (invited or self-enrolled). The token's `activeTenantId` will be null until the learner is enrolled.","operationId":"AuthController_signupLearner","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupLearnerDto"}}}},"responses":{"201":{"description":"Learner created, tokens returned"},"409":{"description":"Email already registered"}},"summary":"Sign up as Learner (marketplace)","tags":["Auth"]}},"/api/v1/auth/login":{"post":{"description":"Verifies credentials and returns an access + refresh token pair. The token encodes all active memberships; call POST /auth/switch-tenant to set an active tenant context when the user belongs to multiple tenants.","operationId":"AuthController_login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"200":{"description":"Authentication successful, tokens returned"},"401":{"description":"Invalid credentials"}},"summary":"Authenticate with email/password","tags":["Auth"]}},"/api/v1/auth/refresh":{"post":{"description":"Exchanges a valid refresh token for a new access + refresh token pair. The refresh token is rotated on each use (one-time use).","operationId":"AuthController_refresh","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenDto"}}}},"responses":{"200":{"description":"New token pair issued"},"401":{"description":"Invalid or expired refresh token"}},"summary":"Refresh an expired access token","tags":["Auth"]}},"/api/v1/auth/switch-tenant":{"post":{"description":"Issues a new token pair with `activeTenantId` set to the requested tenant. The user must hold a TenantMembership for the target tenant; otherwise a 403 is returned.","operationId":"AuthController_switchTenant","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchTenantDto"}}}},"responses":{"200":{"description":"Tenant switched, new token pair issued"},"401":{"description":"Not authenticated"},"403":{"description":"User is not a member of the requested tenant"}},"security":[{"access-token":[]}],"summary":"Switch active tenant (multi-tenant N:N)","tags":["Auth"]}},"/api/v1/auth/accept-invite":{"post":{"description":"Validates the invitation JWT received by email and creates a TenantMembership. If the invitee does not yet have a QualiForma account, `password` is required to create one. Returns a token pair to log the user in immediately.","operationId":"AuthController_acceptInvite","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteDto"}}}},"responses":{"200":{"description":"Invite accepted, tokens returned"},"401":{"description":"Invalid or expired invitation token"},"409":{"description":"User is already a member of this tenant"}},"summary":"Accept a tenant membership invitation","tags":["Auth"]}},"/api/v1/auth/token-exchange":{"post":{"description":"Verifies the external JWT against the tenant JWKS endpoint, upserts the user, and returns QualiForma access + refresh tokens. The `isNewUser` flag signals the frontend to display onboarding.","operationId":"AuthController_tokenExchange","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenExchangeDto"}}}},"responses":{"200":{"description":"Token exchange successful"},"401":{"description":"External token verification failed"},"404":{"description":"Tenant not found or HEADLESS mode not enabled"}},"summary":"Exchange an external IdP token for LMS tokens (HEADLESS/OIDC)","tags":["Auth"]}},"/api/v1/auth/scim/users":{"post":{"description":"Creates or updates a user identified by (externalId + provider). Requires a valid X-API-Key header matching the tenant service API key. Returns `{ userId, isNewUser }` — idempotent (safe to call repeatedly).","operationId":"AuthController_provision","parameters":[{"name":"x-api-key","required":true,"in":"header","schema":{"type":"string"}},{"name":"X-API-Key","in":"header","description":"Tenant service API key","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisionDto"}}}},"responses":{"201":{"description":"User provisioned"},"401":{"description":"Missing or invalid X-API-Key"},"404":{"description":"Tenant not found or HEADLESS mode not enabled"}},"security":[{"X-API-Key":[]}],"summary":"Provision a user via API key (SCIM / HEADLESS)","tags":["Auth"]}},"/api/v1/auth/scim/users/{externalId}":{"delete":{"description":"Removes the external federation link for the user. The User record is preserved for Qualiopi compliance (audit trail). Requires X-API-Key header and `tenantSlug` query parameter.","operationId":"AuthController_deprovision","parameters":[{"name":"externalId","required":true,"in":"path","schema":{"type":"string"}},{"name":"tenantSlug","required":true,"in":"query","schema":{"type":"string"}},{"name":"x-api-key","required":true,"in":"header","schema":{"type":"string"}},{"name":"X-API-Key","in":"header","description":"Tenant service API key","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"User deprovisioned"},"401":{"description":"Missing or invalid X-API-Key"},"404":{"description":"User or tenant not found"}},"security":[{"X-API-Key":[]}],"summary":"Deprovision a user by externalId (SCIM / HEADLESS)","tags":["Auth"]}},"/api/v1/auth/forgot-password":{"post":{"description":"Sends a password reset link to the registered email. Always returns a success message to prevent email enumeration. The reset token is valid for 1 hour.","operationId":"AuthController_forgotPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordDto"}}}},"responses":{"200":{"description":"Reset email sent (if account exists)"}},"summary":"Request a password reset email","tags":["Auth"]}},"/api/v1/auth/reset-password":{"post":{"description":"Changes the user password using the token received by email. The token is valid for 1 hour. All existing sessions are invalidated after a successful password reset.","operationId":"AuthController_resetPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordDto"}}}},"responses":{"200":{"description":"Password reset successful"},"401":{"description":"Invalid or expired reset token"}},"summary":"Reset password with a valid reset token","tags":["Auth"]}},"/api/v1/auth/me":{"get":{"description":"Returns the full profile of the currently authenticated user including all tenant memberships and the active tenant context. Call this after login to populate the frontend session store.","operationId":"AuthController_getMe","parameters":[],"responses":{"200":{"description":"User profile with memberships array and active tenant context. Shape: { user, memberships, activeTenant, activeRole }"},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Get the authenticated user profile","tags":["Auth"]}},"/api/v1/tenants/me":{"get":{"description":"Retourne le tenant de l'utilisateur authentifié, résolu depuis le JWT (tenantId). Ne dépend pas du header X-Tenant-ID ni du TenantMiddleware — évite le paradoxe chicken-and-egg.","operationId":"TenantsController_getMe","parameters":[],"responses":{"200":{"description":"Profil tenant retourné"},"401":{"description":"Non authentifié"},"404":{"description":"Tenant introuvable (incohérence JWT ↔ DB)"}},"security":[{"access-token":[]}],"summary":"Profil public du tenant courant","tags":["Tenants"]}},"/api/v1/tenants/me/of-mode":{"patch":{"description":"true = active Qualiopi/BPF/PIF/émargements. false = LMS standalone (UI purgée). Les données historiques Qualiopi sont conservées en base.","operationId":"TenantsController_setOfMode","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleOfModeDto"}}}},"responses":{"200":{"description":"Mode mis à jour"},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Basculer le Mode OF du tenant courant","tags":["Tenants"]}},"/api/v1/tenants":{"post":{"description":"Creates a new isolated training organisation. Only platform administrators can create tenants. The slug is immutable after creation and drives subdomain routing (slug.qualiforma.com).","operationId":"TenantsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTenantDto"}}}},"responses":{"201":{"description":"Tenant created successfully"},"400":{"description":"Validation error (invalid slug, domain, etc.)"},"401":{"description":"Not authenticated"},"403":{"description":"Insufficient role (ADMIN required)"},"409":{"description":"Slug or domain already in use"}},"security":[{"access-token":[]}],"summary":"Create a new tenant organisation","tags":["Tenants"]}},"/api/v1/tenants/{slug}":{"get":{"description":"Returns the public tenant profile (name, branding, auth mode) identified by its slug. Used by login pages and SSO flows to configure the authentication UI. No auth required.","operationId":"TenantsController_findBySlug","parameters":[{"name":"slug","required":true,"in":"path","description":"Tenant slug (URL-safe identifier)","schema":{"example":"acme-formation","type":"string"}}],"responses":{"200":{"description":"Tenant profile returned"},"404":{"description":"Tenant not found"}},"summary":"Get tenant public profile by slug","tags":["Tenants"]}},"/api/v1/tenants/{id}":{"patch":{"description":"Partially updates a tenant by its CUID. The slug is immutable and ignored if present. Use this to configure integrations (LiveKit, Scellio), auth settings, or branding.","operationId":"TenantsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Tenant CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTenantDto"}}}},"responses":{"200":{"description":"Tenant updated successfully"},"400":{"description":"Validation error"},"401":{"description":"Not authenticated"},"403":{"description":"Insufficient role (ADMIN required)"},"404":{"description":"Tenant not found"},"409":{"description":"Domain already in use by another tenant"}},"security":[{"access-token":[]}],"summary":"Update a tenant configuration","tags":["Tenants"]}},"/api/v1/tenants/{id}/stats":{"get":{"description":"Returns aggregated statistics for a tenant: total users, courses, and payments. Designed for the platform admin dashboard to monitor usage across organisations.","operationId":"TenantsController_getStats","parameters":[{"name":"id","required":true,"in":"path","description":"Tenant CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Tenant statistics returned"},"401":{"description":"Not authenticated"},"403":{"description":"Insufficient role (ADMIN required)"},"404":{"description":"Tenant not found"}},"security":[{"access-token":[]}],"summary":"Get tenant usage statistics","tags":["Tenants"]}},"/api/v1/tenants/{id}/reseed-funders":{"post":{"description":"Idempotent. Crée les financeurs officiels (OPCO + CPF + dispositifs publics) manquants pour le tenant.","operationId":"TenantsController_reseedFunders","parameters":[{"name":"id","required":true,"in":"path","description":"Tenant CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Nombre de financeurs créés"},"401":{"description":"Not authenticated"},"403":{"description":"ADMIN required"},"404":{"description":"Tenant not found"}},"security":[{"access-token":[]}],"summary":"Rétro-alimente les financeurs officiels pour un tenant","tags":["Tenants"]}},"/api/v1/templates":{"get":{"operationId":"TemplateEditorController_list","parameters":[{"name":"category","required":true,"in":"query","schema":{"type":"string"}},{"name":"kind","required":true,"in":"query","schema":{"type":"string"}},{"name":"isActive","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste paginée des templates du tenant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Lister les templates du tenant (paginé)","tags":["Templates"]},"post":{"operationId":"TemplateEditorController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTemplateDto"}}}},"responses":{"201":{"description":"Template créé"},"400":{"description":"Corps de requête invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Créer un template CUSTOM","tags":["Templates"]}},"/api/v1/templates/variables/catalog":{"get":{"operationId":"TemplateEditorController_getVariablesCatalog","parameters":[{"name":"category","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des variables suggérées"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Catalogue de variables suggérées par catégorie","tags":["Templates"]}},"/api/v1/templates/export/{id}":{"get":{"operationId":"TemplateEditorController_export","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template à exporter","schema":{"type":"string"}}],"responses":{"200":{"description":"Fichier JSON du template en téléchargement"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Exporter un template en JSON","tags":["Templates"]}},"/api/v1/templates/{id}":{"get":{"operationId":"TemplateEditorController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"200":{"description":"Template avec champs, variables et dernière version"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'un template avec champs, variables et dernière version","tags":["Templates"]},"patch":{"operationId":"TemplateEditorController_update","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTemplateDto"}}}},"responses":{"200":{"description":"Template mis à jour"},"400":{"description":"Corps de requête invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Modifications interdites sur les templates SYSTEM"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Mettre à jour un template CUSTOM","tags":["Templates"]},"delete":{"operationId":"TemplateEditorController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"204":{"description":"Template supprimé"},"401":{"description":"Non authentifié"},"403":{"description":"Suppression interdite sur les templates SYSTEM"},"404":{"description":"Template introuvable"},"409":{"description":"Des documents ont été générés depuis ce template"}},"security":[{"access-token":[]}],"summary":"Supprimer un template CUSTOM (403 si SYSTEM, 409 si documents générés)","tags":["Templates"]}},"/api/v1/templates/{id}/duplicate":{"post":{"operationId":"TemplateEditorController_duplicate","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template source","schema":{"type":"string"}}],"responses":{"201":{"description":"Clone CUSTOM créé"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template source introuvable"}},"security":[{"access-token":[]}],"summary":"Dupliquer un template (clone CUSTOM)","tags":["Templates"]}},"/api/v1/templates/{id}/fields":{"put":{"operationId":"TemplateEditorController_upsertFields","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertFieldsDto"}}}},"responses":{"200":{"description":"Liste complète des champs remplacés"},"400":{"description":"Corps de requête invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Remplacer atomiquement tous les champs d'un template","tags":["Templates"]}},"/api/v1/templates/{id}/fields/{fieldId}":{"delete":{"operationId":"TemplateEditorController_deleteField","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}},{"name":"fieldId","required":true,"in":"path","description":"ID du champ à supprimer","schema":{"type":"string"}}],"responses":{"204":{"description":"Champ supprimé"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template ou champ introuvable"}},"security":[{"access-token":[]}],"summary":"Supprimer un champ individuel","tags":["Templates"]}},"/api/v1/templates/{id}/variables":{"get":{"operationId":"TemplateEditorController_getVariables","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des variables du template"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Variables définies pour ce template","tags":["Templates"]}},"/api/v1/templates/{id}/versions":{"post":{"operationId":"TemplateEditorController_createVersion","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVersionDto"}}}},"responses":{"201":{"description":"Snapshot de version créé"},"400":{"description":"Corps de requête invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Créer un snapshot de version","tags":["Templates"]},"get":{"operationId":"TemplateEditorController_getVersions","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des versions triées par date décroissante"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Lister les versions (desc)","tags":["Templates"]}},"/api/v1/templates/{id}/versions/{versionId}/restore":{"post":{"operationId":"TemplateEditorController_restoreVersion","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}},{"name":"versionId","required":true,"in":"path","description":"ID de la version à restaurer","schema":{"type":"string"}}],"responses":{"200":{"description":"Version restaurée (nouveau snapshot créé)"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template ou version introuvable"}},"security":[{"access-token":[]}],"summary":"Restaurer une version précédente (crée une nouvelle version)","tags":["Templates"]}},"/api/v1/templates/{id}/preview":{"post":{"operationId":"TemplateEditorController_previewPdf","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewTemplateDto"}}}},"responses":{"200":{"description":"PDF d'aperçu (application/pdf)"},"400":{"description":"Corps de requête invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Générer un aperçu PDF avec données fictives (binary stream)","tags":["Templates"]}},"/api/v1/templates/{id}/preview-html":{"post":{"operationId":"TemplateEditorController_previewHtml","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"200":{"description":"HTML de prévisualisation du template"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Générer l'HTML d'aperçu (sans PDF)","tags":["Templates"]}},"/api/v1/templates/{id}/render":{"post":{"operationId":"TemplateEditorController_render","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderTemplateDto"}}}},"responses":{"201":{"description":"QualiopiDocument créé avec le PDF généré"},"400":{"description":"Corps de requête invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Générer un document PDF et créer un QualiopiDocument","tags":["Templates"]}},"/api/v1/templates/{id}/activate":{"post":{"operationId":"TemplateEditorController_activate","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"200":{"description":"Template activé"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Activer un template (crée un snapshot de version si inactif→actif)","tags":["Templates"]}},"/api/v1/templates/{id}/set-default":{"post":{"operationId":"TemplateEditorController_setDefault","parameters":[{"name":"id","required":true,"in":"path","description":"ID du template","schema":{"type":"string"}}],"responses":{"200":{"description":"Template défini comme défaut pour sa catégorie"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Template introuvable"}},"security":[{"access-token":[]}],"summary":"Définir ce template comme défaut pour sa catégorie","tags":["Templates"]}},"/api/v1/templates/import":{"post":{"operationId":"TemplateEditorController_importTemplate","parameters":[],"responses":{"201":{"description":"Template importé"},"400":{"description":"Fichier JSON invalide ou manquant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Importer un template depuis un fichier JSON","tags":["Templates"]}},"/api/v1/qualiopi/documents/{id}/apply-canvas-signatures":{"post":{"operationId":"DocumentSigningController_applyCanvasSignatures","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyCanvasSignaturesDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Appliquer des signatures canvas sur un document PDF","tags":["Document Signing"]}},"/api/v1/qualiopi/documents/{id}/request-signature":{"post":{"operationId":"DocumentSigningController_requestSignature","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestSignatureDto"}}}},"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Initier une demande de signature (CANVAS ou eIDAS)","tags":["Document Signing"]}},"/api/v1/media/upload":{"post":{"description":"Uploads a file to MinIO and returns the storage path + a 1-hour presigned URL. Persist `storagePath` on your domain entity; call GET /media/:path to refresh the URL.","operationId":"MediaController_upload","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"File to upload. Accepted types: video (mp4/webm), audio (mp3/ogg), document (PDF/Word/Excel/PPT), image (jpeg/png/webp), SCORM (.zip). Max size: 500 MB."}}}}}},"responses":{"201":{"description":"File uploaded successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"f7e2c1d4-3a5b-4e8f-9c2d-1a6b7e8f9c3d"},"url":{"type":"string","example":"https://minio.qualiforma.fr/qualiforma/ten_abc/video/f7e2.mp4?X-Amz-Expires=3600&..."},"storagePath":{"type":"string","example":"ten_abc123/video/f7e2c1d4-3a5b-4e8f-9c2d-1a6b7e8f9c3d.mp4"},"type":{"type":"string","enum":["video","audio","document","image","scorm"]},"originalName":{"type":"string","example":"introduction-formation.mp4"},"size":{"type":"number","example":104857600},"mimeType":{"type":"string","example":"video/mp4"}}}}}},"400":{"description":"Unsupported file type or missing file."},"401":{"description":"JWT token missing or invalid."}},"security":[{"bearer":[]}],"summary":"Upload a media file","tags":["Media"]}},"/api/v1/media/upload-url":{"get":{"description":"Returns a presigned PUT URL valid for 1 hour. The client PUTs the file directly to MinIO — no proxy through NestJS. Ideal for videos and SCORM packages > 50 MB. After the PUT completes, persist `storagePath` on your entity.","operationId":"MediaController_getUploadUrl","parameters":[{"name":"filename","required":true,"in":"query","schema":{"example":"module-01.mp4","type":"string"}},{"name":"mimeType","required":true,"in":"query","schema":{"example":"video/mp4","type":"string"}}],"responses":{"200":{"description":"Presigned PUT URL.","content":{"application/json":{"schema":{"type":"object","properties":{"uploadUrl":{"type":"string","example":"https://minio.qualiforma.fr/qualiforma/ten_abc/video/uuid.mp4?X-Amz-Signature=..."},"storagePath":{"type":"string","example":"ten_abc123/video/uuid.mp4"}}}}}},"400":{"description":"Unsupported MIME type."}},"security":[{"bearer":[]}],"summary":"Get a presigned PUT URL for direct upload","tags":["Media"]}},"/api/v1/media/{path}":{"get":{"description":"Generates a fresh 1-hour presigned GET URL for the given storage path and redirects the client (302). Clients can follow the redirect to stream the file directly from MinIO without routing bytes through NestJS.","operationId":"MediaController_getFile","parameters":[{"name":"path","required":true,"in":"path","description":"Storage path (e.g. ten_abc123/video/uuid.mp4)","schema":{"example":"ten_abc123/video/f7e2c1d4-3a5b-4e8f-9c2d-1a6b7e8f9c3d.mp4","type":"string"}}],"responses":{"302":{"description":"Redirects to the presigned download URL."},"401":{"description":"JWT token missing or invalid."},"404":{"description":"File not found."}},"security":[{"bearer":[]}],"summary":"Redirect to a presigned download URL","tags":["Media"]},"delete":{"description":"Removes the object from MinIO. Does NOT cascade to domain entities — the caller is responsible for updating references.","operationId":"MediaController_deleteFile","parameters":[{"name":"path","required":true,"in":"path","description":"Storage path to delete","schema":{"example":"ten_abc123/video/f7e2c1d4-3a5b-4e8f-9c2d-1a6b7e8f9c3d.mp4","type":"string"}}],"responses":{"204":{"description":"File deleted successfully."},"401":{"description":"JWT token missing or invalid."},"403":{"description":"Insufficient role or cross-tenant access."}},"security":[{"bearer":[]}],"summary":"Delete a stored media file","tags":["Media"]}},"/api/v1/public/assets/illustrations/{tenantId}/{lessonId}/{filename}":{"get":{"description":"No-auth endpoint embedded in lesson HTML (<img src=\"…\">). Returns 302 to a short-lived (1h) presigned S3 URL. Whitelisted to illustrations only — other media types use the authenticated /media/* route.","operationId":"PublicAssetsController_getIllustration","parameters":[{"name":"tenantId","required":true,"in":"path","description":"Tenant cuid","schema":{"type":"string"}},{"name":"lessonId","required":true,"in":"path","description":"Lesson cuid","schema":{"type":"string"}},{"name":"filename","required":true,"in":"path","description":"UUID + extension (png/jpg/jpeg/webp)","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to presigned URL."},"404":{"description":"Invalid params or asset not found."}},"summary":"Public redirect to a presigned URL of a lesson illustration","tags":["Public Assets (no auth)"]}},"/api/v1/qualiopi/documents/generate":{"post":{"description":"Génère un PDF Qualiopi (convention, programme, émargement, attestation ou certificat) et le stocke dans MinIO. Temps de génération < 5 secondes.","operationId":"DocumentsController_generate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateDocumentDto"}}}},"responses":{"201":{"description":"Document généré avec succès","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"CUID du document"},"type":{"type":"string","enum":["CONVENTION","PROGRAM","EMARGEMENT_SHEET","ATTESTATION","CERTIFICATE","BPF","PIF","CONVOCATION","REGLEMENT_INTERIEUR","NEEDS_ANALYSIS"]},"courseId":{"type":"string"},"enrollmentId":{"type":"string","nullable":true},"pdfUrl":{"type":"string","description":"Chemin MinIO (pas une URL directe)"},"signatureStatus":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true},"generatedAt":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Paramètres invalides ou enrollmentId manquant"},"404":{"description":"Formation ou inscription introuvable"}},"security":[{"bearer":[]}],"summary":"Générer un document Qualiopi","tags":["Qualiopi"]}},"/api/v1/qualiopi/documents":{"get":{"description":"Retourne la liste des documents Qualiopi générés avec filtres optionnels par formation, inscription ou type.","operationId":"DocumentsController_list","parameters":[{"name":"courseId","required":false,"in":"query","description":"Filtrer par formation (Course CUID)","schema":{"example":"clxxxxxxxxxxxxxxxxxxxxxxxx","type":"string"}},{"name":"enrollmentId","required":false,"in":"query","description":"Filtrer par inscription (Enrollment CUID)","schema":{"example":"clyyyyyyyyyyyyyyyyyyyyyyyy","type":"string"}},{"name":"type","required":false,"in":"query","description":"Filtrer par type de document","schema":{"example":"CONVENTION","type":"string","enum":["CONVENTION","PROGRAM","EMARGEMENT_SHEET","ATTESTATION","CERTIFICATE","BPF","NEEDS_ANALYSIS","PIF"]}}],"responses":{"200":{"description":"Liste des documents","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"courseId":{"type":"string"},"enrollmentId":{"type":"string","nullable":true},"pdfUrl":{"type":"string"},"signatureStatus":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true},"generatedAt":{"type":"string","format":"date-time"}}}}}}}},"security":[{"bearer":[]}],"summary":"Lister les documents Qualiopi","tags":["Qualiopi"]}},"/api/v1/qualiopi/documents/audit-export":{"get":{"description":"Génère et télécharge un fichier ZIP contenant tous les PDFs Qualiopi (conventions, programmes, émargements, attestations, certificats) d'une période donnée, structuré par type pour faciliter les audits.","operationId":"DocumentsController_auditExport","parameters":[{"name":"startDate","required":true,"in":"query","description":"Date de début de la période (ISO 8601)","schema":{"example":"2024-01-01","type":"string"}},{"name":"endDate","required":true,"in":"query","description":"Date de fin de la période (ISO 8601)","schema":{"example":"2024-12-31","type":"string"}},{"name":"courseId","required":false,"in":"query","description":"Restreindre l'export à une formation spécifique","schema":{"example":"clxxxxxxxxxxxxxxxxxxxxxxxx","type":"string"}}],"responses":{"200":{"description":"Archive ZIP des documents Qualiopi","headers":{"Content-Disposition":{"description":"Nom du fichier ZIP à télécharger","schema":{"type":"string","example":"attachment; filename=\"qualiforma-audit-2024-01-01_2024-12-31.zip\""}}}},"404":{"description":"Aucun document trouvé pour la période"}},"security":[{"bearer":[]}],"summary":"Export ZIP des PDFs Qualiopi pour audit","tags":["Qualiopi"]}},"/api/v1/qualiopi/documents/{id}":{"get":{"description":"Retourne une URL présignée MinIO valide 1 heure pour télécharger le PDF du document spécifié.","operationId":"DocumentsController_getDownloadUrl","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du document Qualiopi","schema":{"type":"string"}}],"responses":{"200":{"description":"Métadonnées du document avec URL de téléchargement","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"courseId":{"type":"string"},"enrollmentId":{"type":"string","nullable":true},"pdfUrl":{"type":"string","description":"Chemin de stockage MinIO"},"downloadUrl":{"type":"string","description":"URL présignée GET valide 1 heure"},"signatureStatus":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true},"generatedAt":{"type":"string","format":"date-time"}}}}}},"404":{"description":"Document introuvable"}},"security":[{"bearer":[]}],"summary":"Obtenir l'URL de téléchargement d'un document","tags":["Qualiopi"]}},"/api/v1/qualiopi/documents/{id}/download":{"get":{"description":"Trace DOCUMENT_DOWNLOADED dans le journal Qualiopi puis streame le PDF depuis MinIO. Utilise la version signée eIDAS si disponible.","operationId":"DocumentsController_download","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du document Qualiopi","schema":{"type":"string"}}],"responses":{"200":{"description":"PDF streamé"}},"security":[{"bearer":[]}],"summary":"Télécharger un document (streaming + trace PAF)","tags":["Qualiopi"]}},"/api/v1/invoices":{"get":{"description":"Returns all payment records with invoice metadata for the authenticated user's tenant. Filterable by date range and invoice presence. Ordered by createdAt desc.","operationId":"BillingController_listInvoices","parameters":[{"name":"from","required":false,"in":"query","description":"ISO 8601 date — filter invoices created on or after this date","schema":{"example":"2025-01-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"ISO 8601 date — filter invoices created on or before this date","schema":{"example":"2025-12-31","type":"string"}},{"name":"hasInvoice","required":false,"in":"query","description":"When true, return only payments that already have an invoice URL","schema":{"type":"boolean"}},{"name":"skip","required":false,"in":"query","description":"Number of records to skip for pagination (default: 0)","schema":{"type":"number"}},{"name":"take","required":false,"in":"query","description":"Maximum number of records to return (default: 20, max: 100)","schema":{"type":"number"}}],"responses":{"200":{"description":"Paginated invoice list","content":{"application/json":{"schema":{"example":[{"paymentId":"clx1234567890abcdef","invoiceId":"inv_scellio_abc123","invoiceUrl":"https://minio.example.com/invoices/...","amount":490,"currency":"EUR","createdAt":"2025-06-01T10:00:00.000Z","enrollment":{"id":"cly0987654321fedcba","course":{"id":"clz000111222333444","title":"Devenir formateur Qualiopi"},"user":{"id":"clzuserid1234567890","firstName":"Marie","lastName":"Dupont","email":"marie.dupont@example.com"}}}]}}}},"401":{"description":"Unauthenticated"},"403":{"description":"Forbidden — insufficient role"}},"security":[{"access-token":[]}],"summary":"List invoices for the tenant","tags":["Billing"]}},"/api/v1/webhooks/scellio":{"post":{"description":"Processes Scellio signature lifecycle events. Authenticates via HMAC-SHA256 (X-Scellio-Signature header).","operationId":"BillingController_handleScellioWebhook","parameters":[{"name":"x-scellio-signature","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Scellio webhook event payload","content":{"application/json":{"schema":{"example":{"event":"signature.completed","data":{"request_id":"sig_req_abc123def456"}}}}}},"responses":{"200":{"description":"Event processed successfully"},"401":{"description":"Invalid webhook signature"}},"security":[{"access-token":[]}],"summary":"Scellio webhook receiver","tags":["Billing"]}},"/api/v1/email-templates":{"get":{"operationId":"EmailTemplatesController_list","parameters":[{"name":"category","required":false,"in":"query","description":"Filtrer par catégorie email","schema":{"type":"string"}},{"name":"kind","required":false,"in":"query","description":"Filtrer par kind (SYSTEM | CUSTOM)","schema":{"type":"string"}},{"name":"isActive","required":false,"in":"query","description":"Filtrer par statut actif (true | false)","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Recherche plein-texte sur le nom et le sujet","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","description":"Page courante (défaut : 1)","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Nombre d'éléments par page (défaut : 20, max : 100)","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste paginée des templates email"}},"security":[{"bearer":[]}],"summary":"Lister les templates email du tenant (paginé)","tags":["Email Templates"]},"post":{"operationId":"EmailTemplatesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmailTemplateDto"}}}},"responses":{"201":{"description":"Template email créé avec succès"},"400":{"description":"Corps de la requête invalide"}},"security":[{"bearer":[]}],"summary":"Créer un template email CUSTOM","tags":["Email Templates"]}},"/api/v1/email-templates/{id}":{"get":{"operationId":"EmailTemplatesController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail du template email"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Détail d'un template email avec variables et dernière version","tags":["Email Templates"]},"patch":{"operationId":"EmailTemplatesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmailTemplateDto"}}}},"responses":{"200":{"description":"Template email mis à jour"},"403":{"description":"Modification interdite sur les templates SYSTEM"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Mettre à jour un template email CUSTOM","tags":["Email Templates"]},"delete":{"operationId":"EmailTemplatesController_delete","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"responses":{"204":{"description":"Template supprimé avec succès"},"400":{"description":"Impossible de supprimer le template par défaut"},"403":{"description":"Suppression interdite sur les templates SYSTEM"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Supprimer un template email CUSTOM (403 si SYSTEM, 400 si template par défaut)","tags":["Email Templates"]}},"/api/v1/email-templates/{id}/duplicate":{"post":{"operationId":"EmailTemplatesController_duplicate","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email source","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateEmailTemplateDto"}}}},"responses":{"201":{"description":"Template email dupliqué avec succès"},"404":{"description":"Template source introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Dupliquer un template email (clone CUSTOM)","tags":["Email Templates"]}},"/api/v1/email-templates/{id}/activate":{"post":{"operationId":"EmailTemplatesController_activate","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"responses":{"200":{"description":"Statut d'activation mis à jour"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Activer ou désactiver un template email","tags":["Email Templates"]}},"/api/v1/email-templates/{id}/set-default":{"post":{"operationId":"EmailTemplatesController_setDefault","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"responses":{"200":{"description":"Template défini comme défaut (les autres templates de la même catégorie sont démarqués)"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Définir ce template comme défaut pour sa catégorie","tags":["Email Templates"]}},"/api/v1/email-templates/{id}/versions":{"get":{"operationId":"EmailTemplatesController_listVersions","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des versions du template"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Lister les versions du template (du plus récent au plus ancien)","tags":["Email Templates"]},"post":{"operationId":"EmailTemplatesController_createVersion","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}}],"responses":{"201":{"description":"Snapshot de version créé"},"404":{"description":"Template introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Créer un snapshot de version manuellement","tags":["Email Templates"]}},"/api/v1/email-templates/{id}/versions/{versionId}/restore":{"post":{"operationId":"EmailTemplatesController_restoreVersion","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du template email","schema":{"type":"string"}},{"name":"versionId","required":true,"in":"path","description":"Identifiant de la version à restaurer","schema":{"type":"string"}}],"responses":{"200":{"description":"Version restaurée avec succès"},"403":{"description":"Restauration interdite sur les templates SYSTEM"},"404":{"description":"Template ou version introuvable"}},"security":[{"bearer":[]}],"summary":"Restaurer une version précédente (crée un snapshot de l'état courant avant restauration)","tags":["Email Templates"]}},"/api/v1/funders":{"get":{"operationId":"FundersController_list","parameters":[{"name":"code","required":false,"in":"query","description":"Filtrer par code financeur","schema":{"enum":["OPCO_AKTO","OPCO_ATLAS","OPCO_AFDAS","OPCO_CONSTRUCTYS","OPCO_2I","OPCO_COMMERCE","OPCO_EP","OPCO_MOBILITES","OPCO_SANTE","UNIFORMATION","OCAPIAT","CPF","POLE_EMPLOI","REGION","AGEFIPH","EMPLOYEUR_DIRECT","FONDS_PROPRES","AUTRE"],"type":"string"}},{"name":"isOfficial","required":false,"in":"query","description":"Filtrer par statut officiel (true = seedés, false = custom)","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Liste des financeurs du tenant, triée officiel-first"}},"security":[{"bearer":[]}],"summary":"Lister les financeurs du tenant (avec filtres optionnels)","tags":["Funders"]},"post":{"operationId":"FundersController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFunderDto"}}}},"responses":{"201":{"description":"Financeur créé avec succès"},"400":{"description":"Corps de la requête invalide"}},"security":[{"bearer":[]}],"summary":"Créer un financeur CUSTOM (isOfficial=false)","tags":["Funders"]}},"/api/v1/funders/{id}":{"get":{"operationId":"FundersController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du financeur","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail du financeur"},"404":{"description":"Financeur introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Détail d'un financeur","tags":["Funders"]},"patch":{"operationId":"FundersController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du financeur","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFunderDto"}}}},"responses":{"200":{"description":"Financeur mis à jour"},"404":{"description":"Financeur introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Mettre à jour un financeur (officiel ou custom)","tags":["Funders"]},"delete":{"operationId":"FundersController_delete","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du financeur","schema":{"type":"string"}}],"responses":{"204":{"description":"Financeur supprimé avec succès"},"403":{"description":"Suppression interdite — financeur officiel non supprimable"},"404":{"description":"Financeur introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Supprimer un financeur CUSTOM (403 si officiel)","tags":["Funders"]}},"/api/v1/users/me":{"get":{"description":"Returns the full profile of the authenticated user including tenant info, trainer profile, and enrollment count.","operationId":"UsersController_getMe","parameters":[],"responses":{"200":{"description":"User profile returned"},"401":{"description":"Not authenticated"},"404":{"description":"User no longer exists"}},"security":[{"access-token":[]}],"summary":"Get current user profile","tags":["Users"]},"patch":{"description":"Update firstName, lastName, or email. Role changes are not allowed here — only admins can change roles.","operationId":"UsersController_updateMe","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"Profile updated"},"401":{"description":"Not authenticated"},"409":{"description":"Email already in use in this tenant"}},"security":[{"access-token":[]}],"summary":"Update current user profile","tags":["Users"]}},"/api/v1/users/me/stats":{"get":{"description":"Returns totalEnrolled, totalCompleted, totalMinutesSpent, averageQuizScore, certificatesEarned, and a 12-month rolling window of monthlyActivity. Response shape is strictly aligned with the frontend hook useLearnerStats().","operationId":"UsersController_getMyStats","parameters":[],"responses":{"200":{"description":"Learner statistics returned"},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Get aggregate learning statistics for the current user","tags":["Users"]}},"/api/v1/users/me/notification-preferences":{"get":{"description":"Returns { newEnrollment, questionnaireFilled, paymentReceived }. All fields default to true when no preference has been saved yet.","operationId":"UsersController_getNotificationPreferences","parameters":[],"responses":{"200":{"description":"Notification preferences returned"},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Get email notification preferences for the current user","tags":["Users"]},"patch":{"description":"Partial update — only supplied boolean fields are changed. Omitted fields keep their previous value.","operationId":"UsersController_updateNotificationPreferences","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPreferencesDto"}}}},"responses":{"200":{"description":"Updated notification preferences"},"401":{"description":"Not authenticated"},"422":{"description":"Validation error in request body"}},"security":[{"access-token":[]}],"summary":"Update email notification preferences for the current user","tags":["Users"]}},"/api/v1/users":{"get":{"description":"Returns a paginated list of users scoped to the admin tenant. Filter by role or search by name/email.","operationId":"UsersController_listUsers","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-indexed)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Items per page (max 100)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}},{"name":"role","required":false,"in":"query","description":"Filter by user role","schema":{"type":"string","enum":["ADMIN","CREATOR","TRAINER","LEARNER","FUNDER"]}},{"name":"search","required":false,"in":"query","description":"Search by first name, last name, or email (case-insensitive)","schema":{"example":"dupont","type":"string"}}],"responses":{"200":{"description":"Paginated user list with meta (total, page, limit, totalPages)"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN role required"}},"security":[{"access-token":[]}],"summary":"List all users in tenant (ADMIN only)","tags":["Users"]},"post":{"operationId":"UsersController_createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"User created"},"409":{"description":"Email already in use in this tenant"}},"security":[{"access-token":[]}],"summary":"Create a new user (ADMIN/CREATOR only)","tags":["Users"]}},"/api/v1/users/{id}":{"get":{"description":"Returns the full profile of any user including trainer profile and enrollment count.","operationId":"UsersController_getUserById","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"User detail returned"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN role required"},"404":{"description":"User not found"}},"security":[{"access-token":[]}],"summary":"Get user by ID (ADMIN only)","tags":["Users"]},"patch":{"description":"Update any user profile including role. All fields are optional.","operationId":"UsersController_updateUser","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"User updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN role required"},"404":{"description":"User not found"},"409":{"description":"Email already in use in this tenant"}},"security":[{"access-token":[]}],"summary":"Update user by ID (ADMIN only)","tags":["Users"]},"delete":{"operationId":"UsersController_deleteUserById","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"204":{"description":"User deleted"}},"security":[{"access-token":[]}],"summary":"Delete a user by ID (ADMIN only)","tags":["Users"]}},"/api/v1/users/{id}/trainer-profile":{"post":{"operationId":"UsersController_upsertTrainerProfile","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrainerProfileDto"}}}},"responses":{"200":{"description":"Trainer profile upserted"}},"security":[{"access-token":[]}],"summary":"Create or update a trainer profile (ADMIN/CREATOR)","tags":["Users"]},"get":{"operationId":"UsersController_getTrainerProfile","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Trainer profile returned"},"404":{"description":"User not found in this tenant"}},"security":[{"access-token":[]}],"summary":"Get a trainer profile with user info (ADMIN/CREATOR)","tags":["Users"]}},"/api/v1/users/{id}/trainer-profile/cv":{"post":{"operationId":"UsersController_uploadTrainerCv","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"CV stored and profile updated"},"400":{"description":"Invalid file type or missing file"}},"security":[{"access-token":[]}],"summary":"Upload trainer CV (PDF/DOCX) — ADMIN/CREATOR","tags":["Users"]}},"/api/v1/courses/generate-draft":{"post":{"description":"Upload PDF / DOCX / PPTX / TXT / MD files (plus optional text hints). Mistral analyses the content and returns a complete course structure (modules, lessons, objectives, pedagogical methods, quiz questions) ready to pre-fill the course creation wizard. No data is persisted.","operationId":"CoursesAiController_generateDraft","parameters":[],"requestBody":{"required":true,"description":"Multipart form with optional files and text hints","content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"string","format":"binary"},"description":"Files to analyse — PDF, DOCX, PPTX, TXT, MD"},"context":{"type":"string","description":"Free-text context"},"courseTitle":{"type":"string","description":"Suggested course title"},"targetAudience":{"type":"string","description":"Target audience description"},"format":{"type":"string","description":"Delivery format (ELEARNING, LIVE, BLENDED, PRESENTIAL)"},"targetDurationMinutes":{"type":"integer","description":"Target total duration in minutes"}}}}}},"responses":{"202":{"description":"Draft generation enqueued. A placeholder Course in DRAFT status is created immediately and its id is returned so the client can navigate to /createur/cours/:courseId/editer and poll GET /courses/drafts/:draftId/status for live progress."},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"}},"security":[{"access-token":[]}],"summary":"Generate a course draft from documents via Mistral AI (CREATOR / ADMIN)","tags":["Courses — AI"]}},"/api/v1/courses/drafts/{draftId}/status":{"get":{"operationId":"CoursesAiController_getDraftStatus","parameters":[{"name":"draftId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Current DraftJobState, enriched with per-lesson progress (Wave 4). `progress` is a percentage (0-100) once the generating-content stage starts. `lessons` is an array of DraftLessonState ordered by title."},"404":{"description":"Draft not found or expired (24h TTL)"}},"security":[{"access-token":[]}],"summary":"Status polling for background draft generation (CREATOR / ADMIN)","tags":["Courses — AI"]}},"/api/v1/courses/ai-suggest":{"post":{"operationId":"CoursesAiController_aiSuggest","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiSuggestDto"}}}},"responses":{"200":{"description":"Partial CourseSuggestResult"},"402":{"description":"Crédits insuffisants"}},"security":[{"access-token":[]}],"summary":"Short-form AI suggestion for wizard fields (CREATOR / ADMIN)","tags":["Courses — AI"]}},"/api/v1/courses/seo/fields":{"post":{"description":"Accepts the current course context (title, description, objectives, target audience) and returns a ready-to-use SEO payload: metaTitle (≤60 chars), metaDescription (≤158 chars), slug (kebab-case ASCII) and long-tail French B2B keywords. No course persistence.","operationId":"CoursesAiController_generateSeoFields","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateSeoFieldsDto"}}}},"responses":{"200":{"description":"SEO fields generated — returns SeoFields","content":{"application/json":{"schema":{"type":"object","properties":{"metaTitle":{"type":"string"},"metaDescription":{"type":"string"},"slug":{"type":"string"},"keywords":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Invalid payload"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"}},"security":[{"access-token":[]}],"summary":"Generate optimised SEO fields for a course draft (CREATOR / ADMIN)","tags":["Courses — AI"]}},"/api/v1/courses/enrich-draft":{"post":{"description":"Phase 2 de la génération IA. Prend en entrée la structure produite par /courses/generate-draft et ajoute : (1) un contenu markdown complet par leçon, (2) un QCM de validation par module. Débit crédits = lessons × 20 + modules × 15.","operationId":"CoursesAiController_enrichDraft","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichDraftDto"}}}},"responses":{"200":{"description":"Draft enrichi — { draft, lessonsGenerated, questionnairesGenerated, creditsConsumed }"},"402":{"description":"Crédits IA insuffisants"}},"security":[{"access-token":[]}],"summary":"Enrichit un brouillon IA avec contenu de leçons + questionnaires (CREATOR / ADMIN)","tags":["Courses — AI"]}},"/api/v1/courses/{courseId}/generate-all-content":{"post":{"description":"Enqueues one BullMQ job per empty lesson (or all lessons with onlyEmpty=false). Jobs run in the background — poll GET /courses/:courseId/modules to see content appear progressively. Credits are debited upfront for the full estimated cost. Idempotent by default: skips lessons that already have non-empty content.","operationId":"CoursesAiController_generateAllContent","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkGenerateDraftDto"}}}},"responses":{"202":{"description":"Jobs enqueued — { enqueued, skipped, totalCost }","content":{"application/json":{"schema":{"type":"object","properties":{"enqueued":{"type":"number"},"skipped":{"type":"number"},"totalCost":{"type":"number"}}}}}},"402":{"description":"Crédits IA insuffisants"},"404":{"description":"Course not found or does not belong to this tenant"}},"security":[{"access-token":[]}],"summary":"Backfill: enqueue AI content generation for empty lessons (ADMIN, CREATOR)","tags":["Courses — AI"]}},"/api/v1/courses/{courseId}/regenerate-from-brief":{"post":{"description":"Re-runs the entire draft pipeline: title, description, objectives, public, prerequisites, modules, lessons, lesson content, questionnaires and SEO. Existing modules and lessons are wiped and recreated. The structure cost (50 credits) is debited up-front; per-lesson costs are billed as each background job completes.","operationId":"CoursesAiController_regenerateFromBrief","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"202":{"description":"Regeneration enqueued — { courseId, draftId, status }"},"400":{"description":"Context manquant ou trop court"},"402":{"description":"Crédits IA insuffisants"},"404":{"description":"Course not found or does not belong to this tenant"}},"security":[{"access-token":[]}],"summary":"Full AI regeneration from a fresh brief (ADMIN, CREATOR)","tags":["Courses — AI"]}},"/api/v1/courses/{id}/images/banner/reference":{"patch":{"description":"Définit bannerMediaId sur le cours. mediaId=null délie. Le média doit appartenir au même tenant et être de type IMAGE.","operationId":"CourseImagesController_setBannerReference","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMediaReferenceDto"}}}},"responses":{"200":{"description":"Course mis à jour avec bannerMediaId"},"400":{"description":"Média non de type IMAGE"},"404":{"description":"Course ou média introuvable dans le tenant"}},"security":[{"access-token":[]}],"summary":"Lier/délier un média médiathèque sur le slot bannière (FK directe)","tags":["Courses"]}},"/api/v1/courses/{id}/images/featured/reference":{"patch":{"description":"Définit featuredMediaId sur le cours. mediaId=null délie. Le média doit appartenir au même tenant et être de type IMAGE.","operationId":"CourseImagesController_setFeaturedReference","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMediaReferenceDto"}}}},"responses":{"200":{"description":"Course mis à jour avec featuredMediaId"},"400":{"description":"Média non de type IMAGE"},"404":{"description":"Course ou média introuvable dans le tenant"}},"security":[{"access-token":[]}],"summary":"Lier/délier un média médiathèque sur le slot featured (FK directe)","tags":["Courses"]}},"/api/v1/courses":{"get":{"description":"Paginated course catalogue. Supports search, format, status, and price range filters. Defaults to PUBLISHED courses on the public endpoint.","operationId":"CoursesController_findAll","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Items per page (max 100)","schema":{"minimum":1,"maximum":100,"default":20,"example":20,"type":"number"}},{"name":"search","required":false,"in":"query","description":"Search in title and description","schema":{"example":"gestion de projet","type":"string"}},{"name":"format","required":false,"in":"query","description":"Filter by course delivery format","schema":{"enum":["ELEARNING","LIVE","BLENDED","PRESENTIAL"],"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by course status. Defaults to PUBLISHED on public endpoints.","schema":{"enum":["DRAFT","PUBLISHED","ARCHIVED"],"type":"string"}},{"name":"minPrice","required":false,"in":"query","description":"Minimum price filter (inclusive)","schema":{"example":"0.00","type":"string"}},{"name":"maxPrice","required":false,"in":"query","description":"Maximum price filter (inclusive)","schema":{"example":"999.00","type":"string"}}],"responses":{"200":{"description":"Paginated course list with metadata"}},"summary":"List published courses (public)","tags":["Courses"]},"post":{"description":"Creates a course in DRAFT status with an auto-generated slug. Use POST /:id/publish to publish after adding content and filling Qualiopi fields.","operationId":"CoursesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCourseDto"}}}},"responses":{"201":{"description":"Course created in DRAFT status"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR role required"},"422":{"description":"Validation error in request body"}},"security":[{"access-token":[]}],"summary":"Create a new course (CREATOR)","tags":["Courses"]}},"/api/v1/courses/my-courses":{"get":{"description":"Returns all courses created by the authenticated trainer across all statuses. Supports same filters as the public catalogue endpoint.","operationId":"CoursesController_findMyCourses","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-based)","schema":{"minimum":1,"default":1,"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Items per page (max 100)","schema":{"minimum":1,"maximum":100,"default":20,"example":20,"type":"number"}},{"name":"search","required":false,"in":"query","description":"Full-text search on title and description (case-insensitive)","schema":{"example":"gestion de projet","type":"string"}},{"name":"format","required":false,"in":"query","description":"Filter by course delivery format","schema":{"enum":["ELEARNING","LIVE","BLENDED","PRESENTIAL"],"type":"string"}},{"name":"status","required":false,"in":"query","description":"Filter by course status. Defaults to PUBLISHED on public endpoints.","schema":{"enum":["DRAFT","PUBLISHED","ARCHIVED"],"type":"string"}},{"name":"minPrice","required":false,"in":"query","description":"Minimum price filter (inclusive)","schema":{"example":"0.00","type":"string"}},{"name":"maxPrice","required":false,"in":"query","description":"Maximum price filter (inclusive)","schema":{"example":"999.00","type":"string"}}],"responses":{"200":{"description":"Creator's course list with metadata"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR role required"}},"security":[{"access-token":[]}],"summary":"List authenticated creator's own courses (CREATOR)","tags":["Courses"]}},"/api/v1/courses/my-trainer":{"get":{"description":"Returns all courses where the caller is assigned as CourseTrainer (PRIMARY or SECONDARY role). Does not require the caller to be the original creator.","operationId":"CoursesController_findMyTrainerCourses","parameters":[],"responses":{"200":{"description":"Trainer's animated courses list"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"}},"security":[{"access-token":[]}],"summary":"List courses where the authenticated user is a trainer (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/by-id/{id}":{"get":{"description":"Returns full course detail by CUID. Accessible to creators and admins. Includes DRAFT and ARCHIVED courses.","operationId":"CoursesController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Course detail"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Get course by ID including unpublished (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/{slug}":{"get":{"description":"Returns full course detail including modules list, lesson counts, and creator info. Returns 404 for DRAFT and ARCHIVED courses. Tenant resolved via X-Tenant-ID header (TenantMiddleware). User auth is optional — when present and the user is the course's creator (or ADMIN), DRAFT/ARCHIVED courses become visible for preview purposes.","operationId":"CoursesController_findBySlug","parameters":[{"name":"slug","required":true,"in":"path","description":"URL-safe course slug (unique within tenant)","schema":{"example":"introduction-gestion-projet-agile","type":"string"}}],"responses":{"200":{"description":"Course detail"},"404":{"description":"Course not found or not published"}},"summary":"Get course by slug (public, SSG-friendly)","tags":["Courses"]}},"/api/v1/courses/{slug}/view":{"post":{"operationId":"CoursesController_recordView","parameters":[{"name":"slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":"Consultation enregistrée"}},"security":[{"access-token":[]}],"summary":"Enregistre une consultation de cours (PAF, indicateur 7)","tags":["Courses"]}},"/api/v1/courses/bulk":{"post":{"description":"Creates a course with its modules, lessons, questionnaires and SEO metadata in one transactional request. Replaces the former multi-call sequence used by the course-creation wizard. Optionally publishes the course right after creation when `publish=true`.","operationId":"CoursesController_createBulk","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCourseBulkDto"}}}},"responses":{"201":{"description":"Course (+ optional publish) created"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR role required"},"422":{"description":"Validation or Qualiopi error"}},"security":[{"access-token":[]}],"summary":"Bulk-create a full course (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/{id}":{"patch":{"description":"Update course fields. All fields optional. Slug is immutable after creation. Status changes use dedicated endpoints.","operationId":"CoursesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCourseDto"}}}},"responses":{"200":{"description":"Course updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Update course (CREATOR, owner only)","tags":["Courses"]},"delete":{"description":"Soft-archives a course (status → ARCHIVED). Does not delete it. Enrolled learners keep access. The course is removed from the public catalogue.","operationId":"CoursesController_archive","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Course archived"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Archive course (CREATOR, owner only)","tags":["Courses"]}},"/api/v1/courses/{id}/publish":{"post":{"description":"Validates all Qualiopi mandatory fields before publishing. Returns 422 with structured errors list if any criterion is not met.","operationId":"CoursesController_publish","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"201":{"description":"Course published"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Course not found"},"422":{"description":"Qualiopi validation failed — returns { message, errors: string[] }"}},"security":[{"access-token":[]}],"summary":"Publish course with Qualiopi validation (CREATOR, owner only)","tags":["Courses"]}},"/api/v1/courses/{id}/qualiopi-status":{"get":{"description":"Returns a structured list of missing Qualiopi criteria. Use this to render a publish-readiness checklist in the course editor.","operationId":"CoursesController_getQualiopiStatus","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Qualiopi validation result: { valid: boolean, errors: string[] }"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Check Qualiopi readiness without publishing (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/{id}/generate-faq":{"post":{"description":"Uses Mistral Large to generate 5-8 SEO-optimised question-answer pairs from the course content. The FAQ is saved to course.seoMetadata.faq.","operationId":"CoursesController_generateFaq","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"201":{"description":"FAQ generated and saved — returns FaqItem[]"},"401":{"description":"Not authenticated"},"402":{"description":"Insufficient AI credits"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Generate SEO FAQ via Mistral AI (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/{id}/duplicate":{"post":{"description":"Creates a full copy of the course in DRAFT status. Modules, lessons (sans mediaUrl/scormPackageId), and questionnaires are duplicated. The new course is titled \"Copie de — {title}\".","operationId":"CoursesController_duplicate","parameters":[{"name":"id","required":true,"in":"path","description":"Source course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"201":{"description":"Duplicated course detail in DRAFT status"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Source course not found"}},"security":[{"access-token":[]}],"summary":"Duplicate a course (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/{id}/create-variant":{"post":{"description":"Duplicates the course then uses Mistral AI to adapt the content (vocabulary, examples, practical cases) for the specified target audience. Returns the variant in DRAFT status.","operationId":"CoursesController_createVariant","parameters":[{"name":"id","required":true,"in":"path","description":"Source course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVariantDto"}}}},"responses":{"201":{"description":"AI-adapted variant in DRAFT status"},"401":{"description":"Not authenticated"},"402":{"description":"Insufficient AI credits"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Source course not found"},"422":{"description":"Validation error in request body"}},"security":[{"access-token":[]}],"summary":"Create an AI-adapted variant for a new audience (CREATOR / ADMIN)","tags":["Courses"]}},"/api/v1/courses/{id}/qualiopi-info":{"get":{"description":"Returns NDA number, Qualiopi certificate number + validity, handicap referent contact. Resolves to CreatorBrandingConfig, with fallback on Tenant.","operationId":"CoursesController_getQualiopiInfo","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Qualiopi public info"}},"summary":"Get Qualiopi legal info for public course page","tags":["Courses"]}},"/api/v1/courses/{id}/public-stats":{"get":{"description":"Returns the dynamic success rate (COMPLETED / total enrollments) and the average satisfaction score (from POST_TRAINING_HOT questionnaires). Returns { available: false } when the sample is < 5 (PUBLIC_STATS_MIN_RESPONSES). When the formation is recent and has partial data (0 < totalResponses < 5), the field `preliminaryData: true` is set so the frontend can render an informative \"données préliminaires\" block instead of a generic placeholder. Qualiopi I3 + I28 partial bypass.","operationId":"CoursesController_getPublicStats","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Public stats"}},"summary":"Get course public stats (success + satisfaction)","tags":["Courses"]}},"/api/v1/courses/{id}/structured-data":{"get":{"description":"Returns a schema.org/Course JSON-LD object built with @qualiforma/seo. Inject as <script type=\"application/ld+json\"> in the page head.","operationId":"CoursesController_getStructuredData","parameters":[{"name":"id","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"schema.org/Course JSON-LD object"},"404":{"description":"Course not found"}},"summary":"Get schema.org/Course JSON-LD structured data (public)","tags":["Courses"]}},"/api/v1/courses/{courseId}/modules":{"post":{"description":"Creates a module appended at the end of the course. Use PUT /reorder to change module positions.","operationId":"ModulesController_create","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateModuleDto"}}}},"responses":{"201":{"description":"Module created"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Course not found"},"422":{"description":"Validation error"}},"security":[{"access-token":[]}],"summary":"Add a module to a course (CREATOR, owner only)","tags":["Modules"]},"get":{"description":"Returns all modules with their lesson counts, sorted by order asc.","operationId":"ModulesController_findByCourse","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Ordered module list"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR role required"}},"security":[{"access-token":[]}],"summary":"List modules for a course ordered by position (CREATOR, owner only)","tags":["Modules"]}},"/api/v1/courses/{courseId}/modules/reorder":{"put":{"description":"Two payload shapes supported (mutually exclusive) :\n  - `tree` (RECOMMANDÉ) : array {id, parentModuleId, order} pour le drag-and-drop multi-niveau cross-parent. Permet de déplacer un module entre parents (root ↔ sous-module) en une transaction.\n  - `moduleIds` (legacy) : liste plate qui réordonne UNIQUEMENT les modules root du cours.","operationId":"ModulesController_reorder","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderModulesDto"}}}},"responses":{"200":{"description":"Modules reordered — returns new ordered list"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — not the creator or ID list mismatch"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Reorder modules in a course (CREATOR, owner only)","tags":["Modules"]}},"/api/v1/courses/{courseId}/modules/{moduleId}":{"patch":{"description":"Update module fields. All fields are optional (PATCH semantics). To change order, use PUT /reorder instead.","operationId":"ModulesController_update","parameters":[{"name":"moduleId","required":true,"in":"path","description":"Module CUID","schema":{"example":"clx8def456abc123","type":"string"}},{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateModuleDto"}}}},"responses":{"200":{"description":"Module updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Module or course not found"}},"security":[{"access-token":[]}],"summary":"Update a module (CREATOR, owner only)","tags":["Modules"]},"delete":{"description":"Deletes the module and its lessons (cascade). Subsequent modules are automatically renumbered to maintain a contiguous order.","operationId":"ModulesController_delete","parameters":[{"name":"moduleId","required":true,"in":"path","description":"Module CUID","schema":{"example":"clx8def456abc123","type":"string"}},{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456"}}],"responses":{"204":{"description":"Module deleted"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Module or course not found"}},"security":[{"access-token":[]}],"summary":"Delete a module and reorder siblings (CREATOR, owner only)","tags":["Modules"]}},"/api/v1/courses/{courseId}/modules/{moduleId}/lessons":{"post":{"description":"Creates a lesson appended at the end of the module. The lesson type determines which content fields are relevant.","operationId":"LessonsController_create","parameters":[{"name":"moduleId","required":true,"in":"path","description":"Module CUID","schema":{"example":"clx8def456abc123","type":"string"}},{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLessonDto"}}}},"responses":{"201":{"description":"Lesson created"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Module or course not found"},"422":{"description":"Validation error"}},"security":[{"access-token":[]}],"summary":"Add a lesson to a module (CREATOR, owner only)","tags":["Lessons"]}},"/api/v1/courses/{courseId}/modules/{moduleId}/lessons/reorder":{"put":{"description":"Fournir le tableau ordonné complet des lessonIds. Le tableau doit inclure toutes les leçons du module — pas de réordonnement partiel. Toutes les mises à jour d'ordre sont appliquées atomiquement.","operationId":"LessonsController_reorder","parameters":[{"name":"moduleId","required":true,"in":"path","description":"Module CUID","schema":{"example":"clx8def456abc123","type":"string"}},{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderLessonsDto"}}}},"responses":{"200":{"description":"Leçons réordonnées"},"401":{"description":"Non authentifié"},"403":{"description":"Interdit — pas le créateur ou liste d'IDs incorrecte"},"404":{"description":"Module introuvable"}},"security":[{"access-token":[]}],"summary":"Réordonner toutes les leçons d'un module (CREATOR, propriétaire uniquement)","tags":["Lessons"]}},"/api/v1/lessons/{id}/move":{"patch":{"description":"Déplace la leçon vers `targetModuleId` à la position `order`. Le module destination doit appartenir au même cours. Recompacte automatiquement l'ordre des leçons du module source ET du module destination en une transaction atomique.","operationId":"LessonsController_move","parameters":[{"name":"id","required":true,"in":"path","description":"Lesson CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveLessonDto"}}}},"responses":{"200":{"description":"Leçon déplacée — { id, moduleId, order }"},"401":{"description":"Non authentifié"},"403":{"description":"Interdit — pas le créateur ou cross-cours"},"404":{"description":"Leçon ou module destination introuvable"}},"security":[{"access-token":[]}],"summary":"Déplacer une leçon vers un autre module (CREATOR, propriétaire uniquement)","tags":["Lessons"]}},"/api/v1/lessons/{id}":{"get":{"description":"Returns all lesson fields including content and media URL.","operationId":"LessonsController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx8ghi789jkl012","type":"string"}}],"responses":{"200":{"description":"Lesson detail"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR role required"},"404":{"description":"Lesson not found"}},"security":[{"access-token":[]}],"summary":"Get lesson detail by ID (CREATOR, owner only)","tags":["Lessons"]},"patch":{"description":"Update lesson fields. All fields are optional (PATCH semantics). Ownership is validated through the parent module and course.","operationId":"LessonsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx8ghi789jkl012","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLessonDto"}}}},"responses":{"200":{"description":"Lesson updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Lesson not found"}},"security":[{"access-token":[]}],"summary":"Update a lesson (CREATOR, owner only)","tags":["Lessons"]},"delete":{"description":"Deletes the lesson and its progress records (cascade). Subsequent lessons in the same module are renumbered contiguously.","operationId":"LessonsController_delete","parameters":[{"name":"id","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx8ghi789jkl012","type":"string"}}],"responses":{"204":{"description":"Lesson deleted"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — must be the course creator"},"404":{"description":"Lesson not found"}},"security":[{"access-token":[]}],"summary":"Delete a lesson and reorder siblings (CREATOR, owner only)","tags":["Lessons"]}},"/api/v1/lessons/{id}/media/reference":{"patch":{"description":"Définit mediaId sur la leçon. mediaId=null délie sans supprimer mediaUrl legacy. Isolation tenant stricte : le média doit appartenir au même tenant que la leçon.","operationId":"LessonsController_setMediaReference","parameters":[{"name":"id","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx8ghi789jkl012","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMediaReferenceDto"}}}},"responses":{"200":{"description":"Leçon mise à jour avec mediaId"},"404":{"description":"Leçon ou média introuvable dans le tenant"}},"security":[{"access-token":[]}],"summary":"Lier/délier un média médiathèque sur une leçon (FK directe)","tags":["Lessons"]}},"/api/v1/lessons/{lessonId}/blocks":{"get":{"operationId":"LessonBlocksController_list","parameters":[{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Ordered list of blocks"}},"security":[{"access-token":[]}],"summary":"List ordered blocks of a lesson","tags":["Lesson Blocks"]},"post":{"operationId":"LessonBlocksController_create","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"security":[{"access-token":[]}],"summary":"Append a block to the lesson","tags":["Lesson Blocks"]}},"/api/v1/lessons/{lessonId}/blocks/reorder":{"put":{"operationId":"LessonBlocksController_reorder","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderBlocksDto"}}}},"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Reorder all blocks of the lesson","tags":["Lesson Blocks"]}},"/api/v1/lessons/{lessonId}/blocks/{blockId}":{"patch":{"operationId":"LessonBlocksController_update","parameters":[{"name":"blockId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Update a single block payload","tags":["Lesson Blocks"]},"delete":{"operationId":"LessonBlocksController_delete","parameters":[{"name":"blockId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"security":[{"access-token":[]}],"summary":"Delete a block and compact sibling ordering","tags":["Lesson Blocks"]}},"/api/v1/lessons/{lessonId}/blocks/{blockId}/regenerate":{"post":{"operationId":"LessonBlocksController_regenerate","parameters":[{"name":"blockId","required":true,"in":"path","description":"LessonBlock CUID","schema":{"type":"string"}},{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID","schema":{}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegenerateBlockDto"}}}},"responses":{"200":{"description":"Bloc mis à jour avec le contenu régénéré"},"400":{"description":"Type de bloc non supporté pour régénération IA"},"402":{"description":"Crédits IA insuffisants"},"404":{"description":"Bloc ou leçon introuvable"}},"security":[{"access-token":[]}],"summary":"Régénère le contenu du bloc avec Mistral (TEXT, ACTIVITY, QUIZ uniquement)","tags":["Lesson Blocks"]}},"/api/v1/courses/{slug}/circuit":{"get":{"operationId":"CircuitController_get","parameters":[{"name":"slug","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Retourne la timeline Qualiopi par apprenant (CREATOR | ADMIN)","tags":["Circuit"]}},"/api/v1/courses/{courseId}/documents":{"post":{"operationId":"CourseDocumentsController_upload","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","title"],"properties":{"file":{"type":"string","format":"binary","description":"Fichier à uploader (PDF, DOCX, TXT, MD, PNG, JPEG). Max 50 MB."},"title":{"type":"string","example":"Guide pédagogique — Module 1"},"description":{"type":"string","example":"Support de cours PDF."},"kind":{"type":"string","enum":["PEDAGOGICAL_RESOURCE","CONTEXT_DOCUMENT","WORKSHEET","REFERENCE","OTHER"],"default":"PEDAGOGICAL_RESOURCE"},"order":{"type":"integer","example":0}}}}}},"responses":{"201":{"description":"Document uploaded"},"400":{"description":"MIME invalide ou fichier trop volumineux"},"404":{"description":"Formation introuvable"}},"security":[{"access-token":[]}],"summary":"Upload a document to a course (ADMIN | CREATOR)","tags":["Course Documents"]},"get":{"operationId":"CourseDocumentsController_findAll","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Document list"},"404":{"description":"Formation introuvable"}},"security":[{"access-token":[]}],"summary":"List all documents for a course (ADMIN | CREATOR)","tags":["Course Documents"]}},"/api/v1/courses/{courseId}/documents/{id}":{"get":{"operationId":"CourseDocumentsController_findOne","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","description":"Document CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Document detail + presigned URL"},"404":{"description":"Document introuvable"}},"security":[{"access-token":[]}],"summary":"Get document detail with presigned download URL (24h) (ADMIN | CREATOR)","tags":["Course Documents"]},"patch":{"operationId":"CourseDocumentsController_update","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","description":"Document CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCourseDocumentDto"}}}},"responses":{"200":{"description":"Document mis à jour"},"404":{"description":"Document introuvable"}},"security":[{"access-token":[]}],"summary":"Update document metadata — no file replacement (ADMIN | CREATOR)","tags":["Course Documents"]},"delete":{"operationId":"CourseDocumentsController_delete","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","description":"Document CUID","schema":{"type":"string"}}],"responses":{"204":{"description":"Document supprimé"},"404":{"description":"Document introuvable"}},"security":[{"access-token":[]}],"summary":"Delete a document record + MinIO object (ADMIN | CREATOR)","tags":["Course Documents"]}},"/api/v1/courses/{courseId}/documents/{id}/reorder":{"post":{"operationId":"CourseDocumentsController_reorder","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","description":"Document CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderCourseDocumentDto"}}}},"responses":{"200":{"description":"Ordre mis à jour"},"404":{"description":"Document introuvable"}},"security":[{"access-token":[]}],"summary":"Change document display order (ADMIN | CREATOR)","tags":["Course Documents"]}},"/api/v1/lessons/{lessonId}/content/generate":{"post":{"description":"Dispatches to the appropriate AI generator based on the lesson type. Overwrites existing content. Credit cost depends on the lesson type. Returns the generated content and credits consumed.","operationId":"LessonGenerationController_generate","parameters":[{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx8ghi789jkl012","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateLessonContentDto"}}}},"responses":{"200":{"description":"Content generated and persisted","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","nullable":true},"mediaUrl":{"type":"string","nullable":true},"creditsUsed":{"type":"number"}}}}}},"400":{"description":"Insufficient AI credits or invalid body"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong role"},"404":{"description":"Lesson not found or wrong tenant"}},"security":[{"access-token":[]}],"summary":"Generate lesson content from scratch (ADMIN, CREATOR)","tags":["Lesson Content"]}},"/api/v1/lessons/{lessonId}/content/modify":{"post":{"description":"Sends the existing content and a user instruction to Mistral to produce a modified version. The target field (content, title, mediaUrl) must already be non-empty. Cost: 10 credits regardless of lesson type.","operationId":"LessonGenerationController_modify","parameters":[{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx8ghi789jkl012","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModifyLessonContentDto"}}}},"responses":{"200":{"description":"Content modified and persisted","content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"},"creditsUsed":{"type":"number"}}}}}},"400":{"description":"Nothing to modify, insufficient credits, or invalid body"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong role"},"404":{"description":"Lesson not found or wrong tenant"}},"security":[{"access-token":[]}],"summary":"Modify existing lesson content via AI prompt (ADMIN, CREATOR)","tags":["Lesson Content"]}},"/api/v1/notifications":{"get":{"operationId":"NotificationsController_list","parameters":[{"name":"unreadOnly","required":false,"in":"query","schema":{"example":false,"type":"boolean"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}}],"responses":{"200":{"description":"{ data, total, page, limit }"}},"security":[{"access-token":[]}],"summary":"Notifications de l'utilisateur courant (paginées)","tags":["Notifications"]}},"/api/v1/notifications/unread-count":{"get":{"operationId":"NotificationsController_countUnread","parameters":[],"responses":{"200":{"description":"{ count: number }"}},"security":[{"access-token":[]}],"summary":"Nombre de notifications non-lues","tags":["Notifications"]}},"/api/v1/notifications/{id}/read":{"patch":{"operationId":"NotificationsController_markRead","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de la notification","schema":{"type":"string"}}],"responses":{"200":{"description":"Notification mise à jour"},"404":{"description":"Notification introuvable"}},"security":[{"access-token":[]}],"summary":"Marquer une notification comme lue","tags":["Notifications"]}},"/api/v1/notifications/read-all":{"post":{"operationId":"NotificationsController_markAllRead","parameters":[],"responses":{"201":{"description":"{ updated: number }"}},"security":[{"access-token":[]}],"summary":"Marquer toutes les notifications comme lues","tags":["Notifications"]}},"/api/v1/notifications/expo-token":{"post":{"operationId":"NotificationsController_registerExpoToken","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterExpoTokenDto"}}}},"responses":{"201":{"description":"{ id: string }"}},"security":[{"access-token":[]}],"summary":"Enregistrer un token Expo push (mobile)","tags":["Notifications"]}},"/api/v1/notifications/expo-token/{token}":{"patch":{"operationId":"NotificationsController_updateExpoPreferences","parameters":[{"name":"token","required":true,"in":"path","description":"Token Expo push (URL-encoded)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExpoPreferencesDto"}}}},"responses":{"200":{"description":"Préférences mises à jour"}},"security":[{"access-token":[]}],"summary":"Mettre à jour les préférences d’un token Expo","tags":["Notifications"]},"delete":{"operationId":"NotificationsController_unregisterExpoToken","parameters":[{"name":"token","required":true,"in":"path","description":"Token Expo push (URL-encoded)","schema":{"type":"string"}}],"responses":{"204":{"description":"Token supprimé"}},"security":[{"access-token":[]}],"summary":"Désinscrire un token Expo push","tags":["Notifications"]}},"/api/v1/ai-credits/balance":{"get":{"operationId":"AiCreditsController_getBalance","parameters":[],"responses":{"200":{"description":"{ tenantId, balance, currency, lastTopUpAt }"},"401":{"description":"Non authentifié"},"403":{"description":"Rôle insuffisant"}},"security":[{"access-token":[]}],"summary":"Solde de crédits IA du tenant (CREATOR / ADMIN)","tags":["AI Credits"]}},"/api/v1/ai-credits/transactions":{"get":{"operationId":"AiCreditsController_listTransactions","parameters":[{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}}],"responses":{"200":{"description":"{ data, total, page, limit }"}},"security":[{"access-token":[]}],"summary":"Transactions de crédits IA paginées (CREATOR / ADMIN)","tags":["AI Credits"]}},"/api/v1/ai-credits/estimate":{"post":{"description":"Retourne le nombre de crédits estimés pour une action sans les consommer","operationId":"AiCreditsEstimateController_estimateCost","parameters":[],"responses":{"200":{"description":"Estimation de coût"}},"security":[{"bearer":[]}],"summary":"Estimer le coût d'une génération IA","tags":["AI Credits"]}},"/api/v1/ai-credits/top-up":{"post":{"operationId":"AiCreditsController_topUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopUpDto"}}}},"responses":{"201":{"description":"Solde mis à jour"},"403":{"description":"Rôle ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Recharge manuelle du solde de crédits IA (ADMIN)","tags":["AI Credits"]}},"/api/v1/lessons/{lessonId}/generate/audio":{"post":{"description":"Crée un job BullMQ pour la génération asynchrone d'un MP3 via Mistral TTS. Coût dynamique : AI_COSTS.GENERATE_AUDIO_PER_MINUTE × estimatedMinutes. La vérification et le débit sont faits ici (pas de @AiCreditsCost car coût dynamique).","operationId":"ContentGenerationController_generateAudio","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"202":{"description":"Job lancé, traitement en arrière-plan"},"402":{"description":"Crédits IA insuffisants"}},"security":[{"bearer":[]}],"summary":"Lancer la génération audio (TTS) d'une leçon","tags":["Content Generation"]}},"/api/v1/lessons/{lessonId}/generate/slides":{"post":{"description":"Crée un job BullMQ pour générer un descriptor JSON slides via Mistral, sauvegardé en metadata","operationId":"ContentGenerationController_generateSlides","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"202":{"description":"Job lancé, descriptor slides généré"}},"security":[{"bearer":[]}],"summary":"Lancer la génération de slides d'une leçon","tags":["Content Generation"]}},"/api/v1/lessons/{lessonId}/generate":{"get":{"description":"Retourne tous les GeneratedMedia (TEXT, AUDIO, SLIDES) pour lessonId","operationId":"ContentGenerationController_listGeneratedMedia","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Lister les médias générés pour une leçon","tags":["Content Generation"]}},"/api/v1/lessons/{lessonId}/generate/{mediaId}/status":{"get":{"description":"Retourne le statut, la progression et l'URL du média si prêt","operationId":"ContentGenerationController_getMediaStatus","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}},{"name":"mediaId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Obtenir le statut d'un média généré","tags":["Content Generation"]}},"/api/v1/lessons/{lessonId}/jobs":{"post":{"description":"Pousse les actions IA cochées (metadata/content/enrich) dans la queue lesson-jobs. La progression est émise via WebSocket sur la room `lesson-job:<jobId>`. Renvoie immédiatement 202 + jobId.","operationId":"LessonJobController_start","parameters":[{"name":"lessonId","required":true,"in":"path","description":"CUID de la leçon cible","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Actions à exécuter (au moins une) + brief facultatif.","content":{"application/json":{"schema":{"type":"object","required":["actions"],"properties":{"actions":{"type":"array","items":{"type":"string","enum":["metadata","content","enrich"]},"minItems":1},"brief":{"type":"string","nullable":true}}}}}},"responses":{"202":{"description":"Job enqueued — { jobId, status: \"PROCESSING\" }"},"400":{"description":"Actions vides ou invalides"},"404":{"description":"Lesson not found or wrong tenant"}},"security":[{"access-token":[]}],"summary":"Lancer un job async de régénération de leçon","tags":["Lesson Jobs (async)"]}},"/api/v1/lessons/{lessonId}/jobs/{jobId}":{"get":{"description":"Endpoint consultatif. Pour le suivi temps réel préférer le canal WebSocket `lesson-job:<jobId>`.","operationId":"LessonJobController_getState","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}},{"name":"jobId","required":true,"in":"path","description":"UUID du job retourné par POST","schema":{"type":"string"}}],"responses":{"200":{"description":"LessonJobState"},"404":{"description":"Job introuvable ou expiré"}},"security":[{"access-token":[]}],"summary":"Etat courant d'un job de régénération","tags":["Lesson Jobs (async)"]}},"/api/v1/notifications/vapid-key":{"get":{"description":"Endpoint public, aucune authentification requise.","operationId":"RealtimeController_getVapidKey","parameters":[],"responses":{"200":{"description":"Clé VAPID publique","content":{"application/json":{"schema":{"properties":{"vapidPublicKey":{"type":"string"}}}}}}},"summary":"Récupérer la clé VAPID publique pour Web Push","tags":["Realtime"]}},"/api/v1/notifications/subscribe-push":{"post":{"description":"Associe un endpoint Web Push à l'utilisateur authentifié.","operationId":"RealtimeController_subscribePush","parameters":[],"responses":{"201":{"description":"Subscription enregistrée avec succès","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"401":{"description":"Non authentifié"}},"security":[{"bearer":[]}],"summary":"Enregistrer une subscription Web Push","tags":["Realtime"]}},"/api/v1/notifications/unsubscribe-push":{"delete":{"description":"Supprime l'endpoint Web Push identifié par son URL.","operationId":"RealtimeController_unsubscribePush","parameters":[],"responses":{"200":{"description":"Subscription supprimée","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"},"deleted":{"type":"number"}}}}}},"401":{"description":"Non authentifié"}},"security":[{"bearer":[]}],"summary":"Supprimer une subscription Web Push","tags":["Realtime"]}},"/api/v1/lessons/{lessonId}/enrich":{"post":{"description":"Mistral generates a plan of typed blocks (TEXT, IMAGE, QUIZ, ACTIVITY), produces each artefact, validates images via Pixtral, and stores LessonBlock rows. Existing blocks are wiped first.","operationId":"LessonEnrichmentController_enrich","parameters":[{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichLessonDto"}}}},"responses":{"202":{"description":"Job enqueued — { jobId, estimatedCost }"},"402":{"description":"Crédits IA insuffisants"}},"security":[{"access-token":[]}],"summary":"Enqueue full multi-format AI enrichment of a lesson","tags":["Lesson Enrichment"]}},"/api/v1/courses/{courseId}/enrich-all-lessons":{"post":{"description":"Iterates over every lesson, debits the total credit cost in one go, and queues an enrichment job per lesson. Existing blocks are wiped per lesson by the processor.","operationId":"CourseEnrichmentController_enrichAll","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichCourseDto"}}}},"responses":{"202":{"description":"Jobs enqueued — { jobIds: string[], lessonIds: string[], estimatedCost: number, totalLessons: number }"},"402":{"description":"Crédits IA insuffisants pour le total"},"403":{"description":"Le cours ne contient aucune leçon à enrichir"},"404":{"description":"Cours introuvable"}},"security":[{"access-token":[]}],"summary":"Enqueue multi-format AI enrichment for every lesson of a course","tags":["Course Enrichment"]}},"/api/v1/lesson-blocks/{blockId}/responses":{"put":{"description":"Sauvegarde la réponse actuelle de l'apprenant sans la soumettre. Idempotent : on UPSERT sur (lessonBlockId, enrollmentId).","operationId":"ExerciseResponsesController_autosave","parameters":[{"name":"blockId","required":true,"in":"path","description":"CUID du LessonBlock","schema":{"type":"string"}}],"responses":{"200":{"description":"{ id, updatedAt }"}},"security":[{"access-token":[]}],"summary":"Autosave de la réponse en cours (UPSERT)","tags":["Exercise Responses"]}},"/api/v1/lesson-blocks/{blockId}/responses/submit":{"post":{"description":"Marque la réponse comme soumise (pose `submittedAt`). Le score et le feedback sont calculés indépendamment (auto-judge IA ou correction humaine).","operationId":"ExerciseResponsesController_submit","parameters":[{"name":"blockId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"{ id, submittedAt }"}},"security":[{"access-token":[]}],"summary":"Soumission finale de la réponse","tags":["Exercise Responses"]}},"/api/v1/lesson-blocks/{blockId}/responses/me":{"get":{"operationId":"ExerciseResponsesController_findMine","parameters":[{"name":"blockId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Response object or null"}},"security":[{"access-token":[]}],"summary":"Récupère la dernière réponse de l'utilisateur courant","tags":["Exercise Responses"]}},"/api/v1/enrollments/admin-enroll":{"post":{"description":"Inscrit un ou plusieurs apprenants à un cours avec statut ACTIVE. Vérifie l'ownership du cours pour le CREATOR. Idempotent.","operationId":"EnrollmentsController_adminEnroll","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminEnrollDto"}}}},"responses":{"201":{"description":"Résultat de l'inscription batch"},"403":{"description":"Cours non possédé par le créateur"},"404":{"description":"Cours introuvable"}},"security":[{"access-token":[]}],"summary":"Inscription batch d'apprenants (CREATOR/ADMIN)","tags":["Enrollments"]}},"/api/v1/enrollments/invite":{"post":{"description":"Pour chaque email : inscrit directement si l'utilisateur existe, sinon crée un compte LEARNER en attente d'activation et envoie une invitation.","operationId":"EnrollmentsController_inviteLearners","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteLearnersDto"}}}},"responses":{"201":{"description":"Résultat des invitations"},"403":{"description":"Cours non possédé par le créateur"},"404":{"description":"Cours introuvable"}},"security":[{"access-token":[]}],"summary":"Inviter des apprenants par email (CREATOR/ADMIN)","tags":["Enrollments"]}},"/api/v1/enrollments/bulk-remind":{"post":{"description":"Sends reminder emails to multiple enrollments. Creator can only remind learners in their courses. Admin can remind any learner in the tenant.","operationId":"EnrollmentsController_bulkRemind","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRemindDto"}}}},"responses":{"200":{"description":"Bulk reminder sent"},"403":{"description":"Unauthorized for one or more enrollments"},"404":{"description":"Enrollment not found"}},"security":[{"access-token":[]}],"summary":"Send bulk reminder emails (CREATOR/ADMIN)","tags":["Enrollments"]}},"/api/v1/enrollments/managed":{"get":{"description":"Retourne la liste des inscriptions où l'utilisateur authentifié est le manager assigné, enrichie avec progression, dernière activité et niveau de rappel d'inactivité déjà envoyé.","operationId":"EnrollmentsController_listManaged","parameters":[],"responses":{"200":{"description":"Liste des inscriptions managées"}},"security":[{"access-token":[]}],"summary":"Liste des inscriptions managées (manager assigné = utilisateur courant)","tags":["Enrollments"]}},"/api/v1/enrollments/{id}/as-manager":{"get":{"description":"Retourne la structure complète du cours avec progression par leçon. L'utilisateur doit être le manager assigné de l'inscription.","operationId":"EnrollmentsController_getAsManager","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail enrichi pour manager"},"403":{"description":"Vous n'êtes pas le manager assigné"},"404":{"description":"Inscription introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'une inscription en tant que manager assigné","tags":["Enrollments"]}},"/api/v1/enrollments/course/{courseId}":{"get":{"description":"Retourne tous les apprenants inscrits avec leur progression calculée et leur dernière date d'activité.","operationId":"EnrollmentsController_listByCourse","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des inscrits avec progression"},"403":{"description":"Cours non possédé par le créateur"},"404":{"description":"Cours introuvable"}},"security":[{"access-token":[]}],"summary":"Liste des inscrits d'un cours (CREATOR/ADMIN)","tags":["Enrollments"]}},"/api/v1/enrollments/{id}/manager":{"post":{"description":"Assigne ou désassigne un manager à une inscription. Le manager doit avoir le rôle CREATOR ou ADMIN. Ometre managerId pour désassigner.","operationId":"EnrollmentsController_assignManager","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignManagerDto"}}}},"responses":{"200":{"description":"Manager assigné/désassigné"},"403":{"description":"Cours non possédé par le créateur"},"404":{"description":"Inscription ou manager introuvable"}},"security":[{"access-token":[]}],"summary":"Assigner/désassigner un manager à une inscription (CREATOR/ADMIN)","tags":["Enrollments"]}},"/api/v1/enrollments":{"post":{"description":"Creates a new enrollment for the authenticated learner. Status is PENDING_PAYMENT for paid courses, ACTIVE for free courses. Duplicate active enrollments are rejected with 409.","operationId":"EnrollmentsController_enroll","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnrollmentDto"}}}},"responses":{"201":{"description":"Enrollment created"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — LEARNER role required"},"404":{"description":"Course not found"},"409":{"description":"Duplicate enrollment — already enrolled in this course"}},"security":[{"access-token":[]}],"summary":"Enroll in a course (LEARNER)","tags":["Enrollments"]}},"/api/v1/enrollments/me":{"get":{"description":"Returns all enrollments for the authenticated user with course info and overall progress percentage. Ordered by most recent first. Available to all roles : un CREATOR ou ADMIN peut être inscrit comme apprenant à d'autres formations (qualification formateur, onboarding) et a besoin de voir sa propre progression.","operationId":"EnrollmentsController_getMyEnrollments","parameters":[],"responses":{"200":{"description":"List of enrollments with progress"},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Get my enrollments (any authenticated user)","tags":["Enrollments"]}},"/api/v1/enrollments/me/upcoming-sessions":{"get":{"description":"Retourne les créneaux (SessionDate + single-slot legacy) à venir pour les inscriptions ACTIVE/COMPLETED de l'utilisateur authentifié, triés chronologiquement. Utilisé par l'agenda du tableau de bord learner.","operationId":"EnrollmentsController_getMyUpcomingSessions","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des créneaux à venir"},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Liste des prochains créneaux de sessions de l'apprenant","tags":["Enrollments"]}},"/api/v1/enrollments/{id}":{"delete":{"description":"Soft-cancel : passe le statut de l'inscription à CANCELLED. Les données de progression sont conservées à des fins d'audit.","operationId":"EnrollmentsController_cancelEnrollment","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Inscription annulée"},"403":{"description":"Cours non possédé par le créateur"},"404":{"description":"Inscription introuvable"}},"security":[{"access-token":[]}],"summary":"Désinscrire un apprenant (CREATOR/ADMIN)","tags":["Enrollments"]},"get":{"description":"Returns full enrollment detail including per-lesson progress records. Lessons without a progress record have progress: null (implicitly NOT_STARTED).","operationId":"EnrollmentsController_getById","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Enrollment detail with full progress"},"401":{"description":"Not authenticated"},"404":{"description":"Enrollment not found"}},"security":[{"access-token":[]}],"summary":"Get enrollment detail with lesson-level progress","tags":["Enrollments"]}},"/api/v1/enrollments/{id}/progress/{lessonId}":{"post":{"description":"Upserts a LessonProgress record. If status=COMPLETED, triggers automatic module and course completion cascade. Enrollment must be ACTIVE.","operationId":"EnrollmentsController_updateProgress","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID","schema":{"example":"clx9xyz789ghi012","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProgressDto"}}}},"responses":{"201":{"description":"Lesson progress updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — LEARNER role required"},"404":{"description":"Enrollment or lesson not found"},"422":{"description":"Enrollment is not ACTIVE, or lesson does not belong to enrolled course"}},"security":[{"access-token":[]}],"summary":"Update lesson progress (LEARNER)","tags":["Enrollments"]}},"/api/v1/enrollments/{id}/progress":{"get":{"description":"Returns the progress summary (percentage, per-module breakdown) without loading the full course structure.","operationId":"EnrollmentsController_getProgress","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Progress summary"},"401":{"description":"Not authenticated"},"404":{"description":"Enrollment not found"}},"security":[{"access-token":[]}],"summary":"Get enrollment progress summary","tags":["Enrollments"]}},"/api/v1/enrollments/{id}/quiz-scores":{"get":{"description":"Agrège tous les scores QCM de l'apprenant avec moyenne, taux de réussite et statut pass/fail par QCM.","operationId":"EnrollmentsController_getQuizScores","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Scores QCM agrégés"},"404":{"description":"Enrollment not found"}},"security":[{"access-token":[]}],"summary":"Historique des scores QCM d'une inscription","tags":["Enrollments"]}},"/api/v1/enrollments/{id}/quiz-feedback/{lessonId}":{"get":{"description":"Retourne le détail per-question pour un QCM donné, incluant les réponses du learner, les réponses correctes, et le feedback/explication.","operationId":"EnrollmentsController_getQuizFeedback","parameters":[{"name":"id","required":true,"in":"path","description":"Enrollment CUID","schema":{"type":"string"}},{"name":"lessonId","required":true,"in":"path","description":"Lesson CUID (quiz)","schema":{"type":"string"}}],"responses":{"200":{"description":"Quiz feedback avec détails par question"},"404":{"description":"Enrollment or lesson not found"}},"security":[{"access-token":[]}],"summary":"Détail des réponses QCM avec feedback (LEARNER/ADMIN/CREATOR)","tags":["Enrollments"]}},"/api/v1/enrollments/course/{courseId}/export":{"get":{"description":"Retourne un fichier CSV avec les données de progression de tous les apprenants inscrits au cours, incluant les timestamps (inscriptionDate, completedAt, lastActivityAt).","operationId":"EnrollmentsController_exportProgressionCsv","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"CSV file with enrollments data"},"403":{"description":"Cours non possédé par le créateur"},"404":{"description":"Cours introuvable"}},"security":[{"access-token":[]}],"summary":"Exporter la progression des apprenants en CSV (CREATOR/ADMIN)","tags":["Enrollments"]}},"/api/v1/configuration/mail":{"get":{"description":"CREATOR returns their own config. ADMIN returns the tenant-level config (userId=null).","operationId":"MailConfigController_findMy","parameters":[],"responses":{"200":{"description":"Mail configuration or null"}},"security":[{"bearer":[]}],"summary":"Get my mail configuration","tags":["Mail Configuration"]},"post":{"description":"Upserts the mail config for the calling user (CREATOR) or tenant (ADMIN). Credentials are encrypted with AES-256-GCM before storage. Credentials are never returned in the response.","operationId":"MailConfigController_upsert","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMailConfigDto"}}}},"responses":{"201":{"description":"Config created / replaced (no credentials)"}},"security":[{"bearer":[]}],"summary":"Create or replace mail configuration","tags":["Mail Configuration"]}},"/api/v1/configuration/mail/{id}":{"patch":{"operationId":"MailConfigController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMailConfigDto"}}}},"responses":{"200":{"description":"Updated config (no credentials)"}},"security":[{"bearer":[]}],"summary":"Partially update a mail configuration","tags":["Mail Configuration"]},"delete":{"description":"Removes the config. Future sends fall back to tenant default → Resend → SMTP.","operationId":"MailConfigController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete a mail configuration","tags":["Mail Configuration"]}},"/api/v1/configuration/mail/test":{"post":{"description":"Resolves the transport for the calling user and sends a test email. Marks the configuration as verified on success.","operationId":"MailConfigController_testConnection","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestMailDto"}}}},"responses":{"200":{"description":"{ messageId: string }"}},"security":[{"bearer":[]}],"summary":"Send a test email using the resolved transport","tags":["Mail Configuration"]}},"/api/v1/configuration/mail/gmail/authorize":{"post":{"description":"Returns the Google consent URL. The frontend must redirect the user there.","operationId":"MailConfigController_gmailAuthorize","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestSendDto"}}}},"responses":{"200":{"description":"{ authorizeUrl: string }"}},"security":[{"bearer":[]}],"summary":"Initiate Google OAuth2 flow","tags":["Mail Configuration"]}},"/api/v1/configuration/mail/gmail/callback":{"get":{"description":"Called by Google after the user grants consent. Exchanges the code for tokens, persists the encrypted Gmail config, and redirects to the frontend.","operationId":"MailConfigController_gmailCallback","parameters":[{"name":"code","required":true,"in":"query","schema":{"type":"string"}},{"name":"state","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to frontend settings page"}},"security":[{"bearer":[]}],"summary":"Google OAuth2 callback (public)","tags":["Mail Configuration"]}},"/api/v1/courses/{slug}/activity-log":{"get":{"operationId":"ActivityLogController_byCourse","parameters":[{"name":"slug","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Journal PAF d'une formation (CREATOR propriétaire | ADMIN)","tags":["ActivityLog"]}},"/api/v1/enrollments/{id}/activity-log":{"get":{"operationId":"ActivityLogController_byEnrollment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Journal PAF d'un apprenant (CREATOR propriétaire | ADMIN)","tags":["ActivityLog"]}},"/api/v1/activity-logs":{"get":{"operationId":"ActivityLogController_admin","parameters":[{"name":"page","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"userId","required":true,"in":"query","schema":{"type":"string"}},{"name":"action","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateTo","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Journal PAF complet du tenant (ADMIN uniquement)","tags":["ActivityLog"]}},"/api/v1/qualiopi/emargement":{"post":{"description":"Records a Qualiopi-compliant attendance entry for a learner. The IP address and user-agent are captured automatically from the request. Duplicate emargements (same enrollment + session + method) are rejected with 422.","operationId":"EmargementController_recordEmargement","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEmargementDto"}}}},"responses":{"201":{"description":"Emargement recorded"},"401":{"description":"Not authenticated"},"404":{"description":"Enrollment or session not found"},"422":{"description":"Duplicate emargement or enrollment not active"}},"security":[{"access-token":[]}],"summary":"Record attendance (emargement)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/qr-generate/{sessionId}":{"post":{"description":"Generates a short-lived JWT (5 min) for the trainer to display as a QR code. Learners scan it and submit via POST /qr-validate to record attendance.","operationId":"EmargementController_generateQrToken","parameters":[{"name":"sessionId","required":true,"in":"path","description":"LiveSession CUID","schema":{"example":"clx9xyz789ghi012","type":"string"}}],"responses":{"201":{"description":"QR token generated with expiry timestamp"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Session not found"}},"security":[{"access-token":[]}],"summary":"Generate QR token for session attendance (ADMIN/CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/qr-validate":{"post":{"description":"Validates the scanned QR token and records a QR_CODE emargement. The token must match a token generated by POST /qr-generate/:sessionId and must not be expired (5 minute window).","operationId":"EmargementController_validateQrAndRecord","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateQrDto"}}}},"responses":{"201":{"description":"QR emargement recorded"},"400":{"description":"Invalid or expired QR token"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — LEARNER role required"},"422":{"description":"Duplicate emargement or enrollment not found"}},"security":[{"access-token":[]}],"summary":"Validate QR scan and record attendance (LEARNER)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/enrollment/{enrollmentId}":{"get":{"description":"Returns the complete attendance history for a given enrollment, ordered by signature time. Covers all sessions in the course.","operationId":"EmargementController_getByEnrollment","parameters":[{"name":"enrollmentId","required":true,"in":"path","description":"Enrollment CUID","schema":{"example":"clx9abc123def456","type":"string"}}],"responses":{"200":{"description":"Attendance history for the enrollment"},"401":{"description":"Not authenticated"},"404":{"description":"Enrollment not found"}},"security":[{"access-token":[]}],"summary":"List attendance history for an enrollment (LEARNER/ADMIN/CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/{sessionId}":{"get":{"description":"Returns all emargements for a live session including learner names, timestamps, method, and presence duration. Ordered by signature time.","operationId":"EmargementController_getBySession","parameters":[{"name":"sessionId","required":true,"in":"path","description":"LiveSession CUID","schema":{"example":"clx9xyz789ghi012","type":"string"}}],"responses":{"200":{"description":"Attendance list for the session"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Session not found"}},"security":[{"access-token":[]}],"summary":"List attendance for a session (ADMIN/CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/{sessionId}/sheet":{"get":{"description":"Returns structured session + attendance data for generating the Qualiopi attendance sheet PDF. Includes trainer info and all learner signatures.","operationId":"EmargementController_getEmargementSheet","parameters":[{"name":"sessionId","required":true,"in":"path","description":"LiveSession CUID","schema":{"example":"clx9xyz789ghi012","type":"string"}}],"responses":{"200":{"description":"Emargement sheet data ready for PDF generation"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Session not found"}},"security":[{"access-token":[]}],"summary":"Get emargement sheet data for PDF (ADMIN/CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/slots/generate":{"post":{"description":"Auto-generates emargement slots based on session schedule and frequency. Existing slots are deleted and recreated. Supports hourly, half-day, and full-session frequencies.","operationId":"EmargementController_generateSlots","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateSlotsDto"}}}},"responses":{"201":{"description":"Slots generated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Session not found"}},"security":[{"access-token":[]}],"summary":"Generate time slots for a session (ADMIN/CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/slots/{sessionId}":{"get":{"description":"Returns all emargement slots for a session, ordered by start time. Each slot includes the list of learner signatures.","operationId":"EmargementController_getSlotsBySession","parameters":[{"name":"sessionId","required":true,"in":"path","description":"LiveSession CUID","schema":{"example":"clx9xyz789ghi012","type":"string"}}],"responses":{"200":{"description":"Slot list with signatures"},"401":{"description":"Not authenticated"},"404":{"description":"Session not found"}},"security":[{"access-token":[]}],"summary":"List time slots with signatures for a session","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/slots/{slotId}/sign":{"post":{"description":"Records a digital signature for a specific emargement time slot. The slot must have already started. Duplicate signatures are rejected with 422.","operationId":"EmargementController_signSlot","parameters":[{"name":"slotId","required":true,"in":"path","description":"EmargementSlot CUID","schema":{"example":"clx9abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignSlotDto"}}}},"responses":{"201":{"description":"Signature recorded"},"400":{"description":"Slot has not started yet"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — LEARNER role required"},"404":{"description":"Slot or enrollment not found"},"422":{"description":"Slot already signed by this learner"}},"security":[{"access-token":[]}],"summary":"Learner signs a time slot (LEARNER)","tags":["Qualiopi"]}},"/api/v1/qualiopi/emargement/{id}/seal-certificate":{"get":{"description":"Returns the Scell.io eIDAS seal certificate URL for an attendance record. sealedAt and certificateUrl are null if the document has not been sealed yet. Qualiopi indicator I22 — legal evidentiary value of attendance tracking.","operationId":"EmargementController_getSealCertificate","parameters":[{"name":"id","required":true,"in":"path","description":"Emargement CUID","schema":{"example":"clx9abc123def456","type":"string"}}],"responses":{"200":{"description":"Seal certificate metadata (fields may be null if not yet sealed)"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Emargement not found"}},"security":[{"access-token":[]}],"summary":"Get eIDAS seal certificate for an emargement (ADMIN/CREATOR)","tags":["Qualiopi"]}},"/api/v1/search":{"get":{"description":"Full-text search over the published course catalogue. Results are scoped to the current tenant and always restricted to PUBLISHED courses. Supports free-text search, format filter, price range, and pagination. Returns highlighted snippets and facet counts for the sidebar filter UI.","operationId":"SearchController_search","parameters":[{"name":"q","required":false,"in":"query","description":"Full-text search query","schema":{"example":"gestion de projet","type":"string"}},{"name":"format","required":false,"in":"query","description":"Filter by format","schema":{"enum":["ELEARNING","LIVE","BLENDED","PRESENTIAL"],"type":"string"}},{"name":"priceMin","required":false,"in":"query","description":"Minimum price (inclusive, €)","schema":{"minimum":0,"example":0,"type":"number"}},{"name":"priceMax","required":false,"in":"query","description":"Maximum price (inclusive, €)","schema":{"minimum":0,"example":999,"type":"number"}},{"name":"page","required":false,"in":"query","description":"Page number (default 1)","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Results per page (default 20)","schema":{"minimum":1,"maximum":100,"default":20,"type":"number"}}],"responses":{"200":{"description":"Search results with highlighted hits, facets, and pagination metadata","content":{"application/json":{"schema":{"example":{"hits":[{"id":"clx8abc123def456","slug":"introduction-gestion-projet","title":"Introduction à la gestion de projet","description":"Apprenez les fondamentaux…","format":"ELEARNING","price":299,"status":"PUBLISHED","creatorName":"Jean Dupont","publishedAt":"2024-03-01T10:00:00Z","_formatted":{"title":"Introduction à la <em>gestion de projet</em>"}}],"totalHits":42,"page":1,"limit":20,"totalPages":3,"facets":{"format":{"ELEARNING":18,"LIVE":12,"BLENDED":8,"PRESENTIAL":4}}}}}}}},"summary":"Search published courses (public)","tags":["Search"]}},"/api/v1/questionnaires":{"post":{"description":"Creates a new Qualiopi questionnaire linked to a course. Use GET /questionnaires/templates to retrieve pre-configured templates. The sendDaysOffset field is required when sendAt=CUSTOM_DAYS.","operationId":"QuestionnairesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuestionnaireDto"}}}},"responses":{"201":{"description":"Questionnaire created"},"400":{"description":"Validation error — invalid questions or schedule config"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Create a questionnaire (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/{id}":{"patch":{"description":"Updates title, questions, schedule, or isActive. courseId and type are immutable after creation. Ownership is verified — only the owning tenant can update.","operationId":"QuestionnairesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuestionnaireDto"}}}},"responses":{"200":{"description":"Updated questionnaire"},"400":{"description":"Validation error"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong tenant or insufficient role"},"404":{"description":"Questionnaire not found"}},"security":[{"access-token":[]}],"summary":"Partially update a questionnaire (ADMIN | CREATOR)","tags":["Questionnaires"]},"get":{"description":"Returns full questionnaire definition and aggregated statistics across all responses. Includes NPS calculation for SCALE 0–10 questions.","operationId":"QuestionnairesController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Questionnaire detail with aggregated stats"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Questionnaire not found"}},"security":[{"access-token":[]}],"summary":"Get questionnaire detail + aggregated stats (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/{id}/move":{"patch":{"description":"Moves the questionnaire to a different course by updating courseId. All existing QuestionnaireResponse records are preserved. The target course must belong to the same tenant. Templates (isTemplate=true) cannot be moved.","operationId":"QuestionnairesController_moveQuestionnaire","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveQuestionnaireDto"}}}},"responses":{"200":{"description":"Questionnaire moved — returns updated item"},"400":{"description":"Bad request — questionnaire is a template"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong tenant or insufficient role"},"404":{"description":"Questionnaire or target course not found"}},"security":[{"access-token":[]}],"summary":"Move questionnaire to another course (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/tenant-templates":{"get":{"description":"Returns all reusable questionnaire templates for the current tenant. Templates are cloned into new courses on COURSE_CREATED, overriding hardcoded defaults.","operationId":"QuestionnairesController_listTenantTemplates","parameters":[],"responses":{"200":{"description":"Array of tenant templates"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"}},"security":[{"access-token":[]}],"summary":"List tenant questionnaire templates (ADMIN | CREATOR)","tags":["Questionnaires"]},"post":{"description":"Creates a reusable tenant-level questionnaire template. courseId is not required. On COURSE_CREATED, if a matching template exists for the tenant type, its questions are cloned into the new course questionnaire.","operationId":"QuestionnairesController_createTenantTemplate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTenantTemplateDto"}}}},"responses":{"201":{"description":"Template created"},"400":{"description":"Validation error"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN role required"}},"security":[{"access-token":[]}],"summary":"Create a tenant questionnaire template (ADMIN only)","tags":["Questionnaires"]}},"/api/v1/questionnaires/tenant-templates/{id}":{"patch":{"operationId":"QuestionnairesController_updateTenantTemplate","parameters":[{"name":"id","required":true,"in":"path","description":"Template CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuestionnaireDto"}}}},"responses":{"200":{"description":"Updated template"},"400":{"description":"Validation error"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong tenant or ADMIN role required"},"404":{"description":"Template not found"}},"security":[{"access-token":[]}],"summary":"Update a tenant questionnaire template (ADMIN only)","tags":["Questionnaires"]},"delete":{"operationId":"QuestionnairesController_deleteTenantTemplate","parameters":[{"name":"id","required":true,"in":"path","description":"Template CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Template deleted — returns { id }"},"400":{"description":"Record is not a template"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong tenant or ADMIN role required"},"404":{"description":"Template not found"}},"security":[{"access-token":[]}],"summary":"Delete a tenant questionnaire template (ADMIN only)","tags":["Questionnaires"]}},"/api/v1/questionnaires/tenant-templates/{id}/duplicate":{"post":{"description":"Creates a copy of the tenant template with the same type, questions, and schedule. The new template title gets a \"(copie)\" suffix unless overridden via the `title` body field.","operationId":"QuestionnairesController_duplicateTenantTemplate","parameters":[{"name":"id","required":true,"in":"path","description":"Template CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"201":{"description":"Duplicated template"},"400":{"description":"Record is not a template"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — wrong tenant or insufficient role"},"404":{"description":"Template not found"}},"security":[{"access-token":[]}],"summary":"Duplicate a tenant questionnaire template (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/templates":{"get":{"description":"Returns pre-configured templates for the four Qualiopi-mandatory questionnaire types. Templates include structured questions aligned with indicators 7 and 13.","operationId":"QuestionnairesController_getTemplates","parameters":[],"responses":{"200":{"description":"Array of Qualiopi templates"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"}},"security":[{"access-token":[]}],"summary":"Get Qualiopi questionnaire templates (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/pending":{"get":{"operationId":"QuestionnairesController_pending","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]},{"access-token":[]}],"summary":"Liste des questionnaires à remplir pour l'apprenant courant","tags":["Questionnaires"]}},"/api/v1/questionnaires/course/{courseId}":{"get":{"description":"Returns all questionnaires linked to a course, ordered by type. Includes response count per questionnaire. Use GET /questionnaires/:id for detailed statistics.","operationId":"QuestionnairesController_findByCourse","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"List of questionnaires for the course"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"List questionnaires for a course (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/course/{courseId}/generate-all":{"post":{"description":"Génère les 5 questionnaires Qualiopi standards (PRE_TRAINING, POST_TRAINING_HOT, POST_TRAINING_COLD_30, POST_TRAINING_COLD_90, COMMANDITAIRE) via Mistral AI. Les types déjà présents sont ignorés. Coût : 15 crédits par questionnaire effectivement créé (max 75 crédits).","operationId":"QuestionnairesController_generateAllForCourse","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateAllQuestionnairesDto"}}}},"responses":{"201":{"description":"Liste des questionnaires créés (peut être vide si tous existaient déjà)"},"401":{"description":"Non authentifié"},"402":{"description":"Crédits IA insuffisants"},"403":{"description":"Forbidden — ADMIN ou CREATOR requis"},"404":{"description":"Course non trouvé ou inaccessible"}},"security":[{"access-token":[]}],"summary":"Génère en bulk les questionnaires Qualiopi pour un cours via IA (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/{id}/regenerate-with-ai":{"post":{"description":"Régénère le titre et les questions d'un questionnaire existant via Mistral AI. Le brief est optionnel — sans brief, le modèle utilise le contexte du cours. Le planning (sendAt, sendDaysOffset) et les réponses déjà soumises sont conservés. Coût : 20 crédits.","operationId":"QuestionnairesController_regenerateWithAi","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegenerateQuestionnaireDto"}}}},"responses":{"200":{"description":"Questionnaire régénéré"},"400":{"description":"Mistral n'a retourné aucune question valide"},"401":{"description":"Non authentifié"},"402":{"description":"Crédits IA insuffisants"},"403":{"description":"Forbidden — ADMIN ou CREATOR requis, ou mauvais tenant"},"404":{"description":"Questionnaire non trouvé"}},"security":[{"access-token":[]}],"summary":"Réécrit un questionnaire via IA à partir d'un brief (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/{id}/send":{"post":{"description":"Enqueues a BullMQ job to send questionnaire invitation emails to all active enrolled learners who have not yet responded. An optional delayMs body field defers dispatch by that many milliseconds. Returns the Bull job ID for tracking.","operationId":"QuestionnairesController_triggerSend","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleSendDto"}}}},"responses":{"202":{"description":"Job accepted — returns Bull job ID","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"oneOf":[{"type":"string"},{"type":"number"}]}}}}}},"400":{"description":"Validation error on delayMs"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Questionnaire not found"},"422":{"description":"Unprocessable — questionnaire is inactive"}},"security":[{"access-token":[]}],"summary":"Manually trigger questionnaire email dispatch (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/questionnaires/{id}/respond":{"post":{"description":"Submits a learner's answers to a questionnaire. Only one response per enrollment is allowed. All required questions must be answered. Answer types are validated against the question schema.","operationId":"QuestionnairesController_submitResponse","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitResponseDto"}}}},"responses":{"201":{"description":"Response submitted successfully"},"400":{"description":"Invalid answers — missing required answers or type mismatch"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — LEARNER role required"},"404":{"description":"Questionnaire or enrollment not found"},"409":{"description":"Conflict — response already submitted for this enrollment"},"422":{"description":"Unprocessable — questionnaire is inactive or enrollment mismatch"}},"security":[{"access-token":[]}],"summary":"Submit a questionnaire response (LEARNER)","tags":["Questionnaires"]}},"/api/v1/questionnaires/{id}/export":{"get":{"description":"Exports aggregated questionnaire results as CSV or PDF. CSV includes UTF-8 BOM for Excel compatibility. PDF is an HTML report suitable for browser print-to-PDF.","operationId":"QuestionnairesController_exportResults","parameters":[{"name":"id","required":true,"in":"path","description":"Questionnaire CUID","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"format","required":false,"in":"query","description":"Export format (csv or pdf). Defaults to csv.","schema":{"enum":["csv","pdf"],"type":"string"}}],"responses":{"200":{"description":"Export file stream"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — ADMIN or CREATOR role required"},"404":{"description":"Questionnaire not found"}},"security":[{"access-token":[]}],"summary":"Export questionnaire results (ADMIN | CREATOR)","tags":["Questionnaires"]}},"/api/v1/scorm/upload":{"post":{"description":"Extracts the ZIP, parses imsmanifest.xml, validates the launch file, uploads assets to MinIO, and creates a ScormPackage record.","operationId":"ScormController_uploadPackage","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","courseId"],"properties":{"file":{"type":"string","format":"binary","description":"SCORM ZIP package (max 500 MB)"},"courseId":{"type":"string","description":"Target course CUID","example":"clq1234abcdef"}}}}}},"responses":{"201":{"description":"Package uploaded successfully"},"400":{"description":"Invalid ZIP or malformed manifest"},"404":{"description":"Course not found"}},"security":[{"access-token":[]}],"summary":"Upload SCORM package","tags":["SCORM"]}},"/api/v1/scorm":{"get":{"operationId":"ScormController_listPackages","parameters":[{"name":"courseId","required":true,"in":"query","description":"Course CUID to filter packages","schema":{"example":"clq1234abcdef","type":"string"}}],"responses":{"200":{"description":"Package list returned"}},"security":[{"access-token":[]}],"summary":"List SCORM packages for a course","tags":["SCORM"]}},"/api/v1/scorm/{id}/launch":{"get":{"description":"Returns a presigned URL for the SCORM player iframe. Valid for 4 hours. Includes enrollmentId as a query parameter.","operationId":"ScormController_getLaunchUrl","parameters":[{"name":"id","required":true,"in":"path","description":"ScormPackage CUID","schema":{"type":"string"}},{"name":"enrollmentId","required":true,"in":"query","description":"Learner enrollment CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Presigned launch URL"},"404":{"description":"Package not found"}},"security":[{"access-token":[]}],"summary":"Get SCORM launch URL","tags":["SCORM"]}},"/api/v1/scorm/{id}/runtime":{"post":{"description":"Stores a CMI data model key-value pair. Common keys: cmi.core.lesson_status, cmi.core.score.raw, cmi.core.session_time.","operationId":"ScormController_setRuntimeData","parameters":[{"name":"id","required":true,"in":"path","description":"ScormPackage CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetRuntimeDataDto"}}}},"responses":{"204":{"description":"Value stored successfully"},"404":{"description":"Package or enrollment not found"}},"security":[{"access-token":[]}],"summary":"SCORM runtime — set value (LMSSetValue)","tags":["SCORM"]},"get":{"description":"Retrieves a CMI data model value. Returns \"\" (empty string) for unknown keys, per SCORM specification.","operationId":"ScormController_getRuntimeData","parameters":[{"name":"id","required":true,"in":"path","description":"ScormPackage CUID","schema":{"type":"string"}},{"name":"enrollmentId","required":true,"in":"query","description":"Learner enrollment CUID","schema":{"example":"clq9876zyxwvu","type":"string"}},{"name":"key","required":true,"in":"query","description":"CMI data model key","schema":{"example":"cmi.core.lesson_status","type":"string"}}],"responses":{"200":{"description":"CMI value returned"}},"security":[{"access-token":[]}],"summary":"SCORM runtime — get value (LMSGetValue)","tags":["SCORM"]}},"/api/v1/xapi/statements":{"post":{"description":"Validates and stores an xAPI statement. Assign an id in the body or one will be generated server-side. Provide context.registration (enrollment ID) to link to a learner session.","operationId":"XApiController_storeStatement","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/XApiStatementDto"}}}},"responses":{"201":{"description":"Statement stored. Returns the statement ID."},"400":{"description":"Invalid or incomplete statement"}},"security":[{"access-token":[]}],"summary":"Store an xAPI statement","tags":["xAPI"]},"get":{"description":"Returns stored xAPI statements matching the given filters. Results are sorted by timestamp (newest first), limited to 100 records.","operationId":"XApiController_getStatements","parameters":[{"name":"verb","required":false,"in":"query","description":"Filter by verb IRI","schema":{"example":"http://adlnet.gov/expapi/verbs/completed","type":"string"}},{"name":"activity","required":false,"in":"query","description":"Filter by activity IRI","schema":{"example":"https://lms.qualiforma.com/courses/intro-nestjs","type":"string"}},{"name":"enrollmentId","required":false,"in":"query","description":"Filter by enrollment ID (stored as context.registration)","schema":{"example":"clq9876zyxwvu","type":"string"}},{"name":"lessonId","required":false,"in":"query","description":"Filter by lesson ID (stored in context.extensions)","schema":{"example":"cls1234lesson","type":"string"}}],"responses":{"200":{"description":"Statements returned"}},"security":[{"access-token":[]}],"summary":"Query xAPI statements","tags":["xAPI"]}},"/api/v1/checkout":{"post":{"description":"Creates a Stripe marketplace payment order. Redirect the learner to `checkoutUrl` to complete payment.","operationId":"PaymentsController_createCheckout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCheckoutDto"}}}},"responses":{"201":{"description":"Checkout created — redirect learner to checkoutUrl","content":{"application/json":{"schema":{"example":{"checkoutUrl":"https://checkout.stripe.com/pay/cs_test_xxx","orderCode":"cs_test_xxx","paymentId":"clx1234567890abcdef","amount":99.99}}}}},"400":{"description":"Course is free — use direct enrollment"},"404":{"description":"Course not found"},"422":{"description":"Course not published or creator not onboarded"}},"security":[{"bearer":[]}],"summary":"Initiate course checkout","tags":["Payments"]}},"/api/v1/checkout/{orderCode}/status":{"get":{"description":"Returns the current status of a payment by its Stripe session id.","operationId":"PaymentsController_getCheckoutStatus","parameters":[{"name":"orderCode","required":true,"in":"path","description":"Stripe Checkout Session id returned by POST /checkout","schema":{"example":"cs_test_xxx","type":"string"}}],"responses":{"200":{"description":"Payment status","content":{"application/json":{"schema":{"example":{"orderCode":"123456789012","status":"COMPLETED","amount":99.99,"currency":"EUR","createdAt":"2024-06-01T10:30:00.000Z"}}}}},"404":{"description":"Order code not found"}},"security":[{"bearer":[]}],"summary":"Poll checkout payment status","tags":["Payments"]}},"/api/v1/refunds":{"post":{"description":"Issues a full or partial refund via the payment gateway. CREATORs can only refund their own courses; ADMINs can refund any payment.","operationId":"PaymentsController_initiateRefund","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateRefundDto"}}}},"responses":{"201":{"description":"Refund initiated","content":{"application/json":{"schema":{"example":{"paymentId":"clx1234567890abcdef","status":"REFUNDED","refundedAmount":99.99,"transactionId":"txn_abc123"}}}}},"404":{"description":"Payment not found"},"422":{"description":"Payment not in a refundable state"}},"security":[{"bearer":[]}],"summary":"Initiate a refund","tags":["Payments"]}},"/api/v1/payments":{"get":{"description":"Returns payments filtered by the authenticated user's role. Admins see all; creators see their sales; learners see their purchases.","operationId":"PaymentsController_getPayments","parameters":[{"name":"status","required":false,"in":"query","description":"Filter by payment status","schema":{"example":"COMPLETED","type":"string","enum":["PENDING","COMPLETED","REFUNDED","PARTIALLY_REFUNDED","FAILED"]}},{"name":"from","required":false,"in":"query","description":"Filter payments on or after this ISO 8601 date","schema":{"example":"2024-01-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"Filter payments on or before this ISO 8601 date","schema":{"example":"2024-12-31","type":"string"}},{"name":"courseId","required":false,"in":"query","description":"Filter by course CUID","schema":{"example":"clx1234567890abcdef","type":"string"}}],"responses":{"200":{"description":"Payment list"}},"security":[{"bearer":[]}],"summary":"List payments (role-scoped)","tags":["Payments"]}},"/api/v1/creators/dashboard":{"get":{"description":"Aggregated revenue KPIs: total CA, monthly CA, sales count, average basket, and enrollment conversion rate.","operationId":"PaymentsController_getCreatorDashboard","parameters":[],"responses":{"200":{"description":"Dashboard KPIs","content":{"application/json":{"schema":{"example":{"totalRevenue":4850,"monthlyRevenue":640,"totalSales":52,"averageOrderValue":93.27,"conversionRate":78.5,"currency":"EUR"}}}}}},"security":[{"bearer":[]}],"summary":"Creator revenue dashboard","tags":["Payments"]}},"/api/v1/creators/onboard":{"post":{"description":"Initiates the Stripe Connect onboarding flow. Returns a `redirectUrl` to send the creator to for KYC completion.","operationId":"CreatorsController_onboard","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatorOnboardDto"}}}},"responses":{"201":{"description":"Onboarding initiated — redirect the creator to redirectUrl","content":{"application/json":{"schema":{"example":{"redirectUrl":"https://connect.stripe.com/setup/e/acct_xxx/yyy","merchantId":"acct_xxx"}}}}},"404":{"description":"User or TrainerProfile not found"},"409":{"description":"Creator already has a connected account"}},"security":[{"bearer":[]}],"summary":"Start marketplace onboarding for a creator","tags":["Creators"]}},"/api/v1/creators/status":{"get":{"description":"Queries the payment gateway for live KYC status and syncs it to the creator profile.","operationId":"CreatorsController_getStatus","parameters":[],"responses":{"200":{"description":"Creator account status","content":{"application/json":{"schema":{"example":{"isVerified":false,"merchantId":"merch_abc123","verificationStatus":"PENDING"}}}}}},"security":[{"bearer":[]}],"summary":"Check creator's connected account verification status","tags":["Creators"]}},"/api/v1/creators/stripe/status":{"get":{"operationId":"CreatorsController_getStripeStatus","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Live Stripe Connect status for the authenticated creator","tags":["Creators"]}},"/api/v1/creators/stripe/connect":{"post":{"operationId":"CreatorsController_connectStripe","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Generate a Stripe Connect Express onboarding link","tags":["Creators"]}},"/api/v1/creators/stripe/dashboard":{"post":{"operationId":"CreatorsController_openStripeDashboard","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Single-use login link to the creator's Stripe dashboard","tags":["Creators"]}},"/api/v1/admin/stripe-config":{"get":{"description":"Returns platform account ID, commission rate, and a MASKED view of the stored secrets (last 4 chars only). Plain secrets are never returned.","operationId":"AdminStripeConfigController_get","parameters":[],"responses":{"200":{"description":"Current Stripe config (secrets masked)","content":{"application/json":{"schema":{"example":{"stripePlatformAccountId":"acct_1PAbc123","stripeSecretKeyMasked":"sk_****...1234","hasSecretKey":true,"hasWebhookSecret":true,"platformCommissionRate":0.05}}}}}},"security":[{"bearer":[]}],"summary":"Read the tenant's Stripe platform config","tags":["Admin — Stripe config"]},"patch":{"description":"Upserts the Stripe secret key, webhook secret, platform account ID and platform commission rate. Secrets are encrypted at rest (AES-256-GCM); passing a secret clears the previous value.","operationId":"AdminStripeConfigController_update","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStripeConfigDto"}}}},"responses":{"200":{"description":"Config updated"},"400":{"description":"Invalid secret format"},"403":{"description":"Requires ADMIN role"}},"security":[{"bearer":[]}],"summary":"Update the tenant's Stripe platform config","tags":["Admin — Stripe config"]}},"/api/v1/payments/stripe-connect/connect":{"get":{"description":"Generates a CSRF state, persists it (TTL 10 min), and issues a 302 redirect to the Stripe OAuth authorize URL. The creator must follow the redirect to grant access.","operationId":"StripeConnectController_connect","parameters":[],"responses":{"302":{"description":"Redirect to Stripe authorize URL"}},"security":[{"bearer":[]}],"summary":"Initiate Stripe Connect Standard OAuth — redirects to Stripe","tags":["Stripe Connect OAuth"]}},"/api/v1/payments/stripe-connect/status":{"get":{"operationId":"StripeConnectController_status","parameters":[],"responses":{"200":{"description":"Connection status","content":{"application/json":{"schema":{"example":{"connected":true,"stripeAccountId":"acct_1PAbc123","livemode":true,"scope":"read_write","connectedAt":"2026-04-21T10:00:00.000Z"}}}}}},"security":[{"bearer":[]}],"summary":"Stripe Connect Standard OAuth status for the authenticated creator","tags":["Stripe Connect OAuth"]}},"/api/v1/payments/stripe-connect/disconnect":{"delete":{"operationId":"StripeConnectController_disconnect","parameters":[],"responses":{"204":{"description":"Disconnected successfully"},"404":{"description":"No connected account found"}},"security":[{"bearer":[]}],"summary":"Disconnect Stripe Connect Standard OAuth — revokes access and clears DB","tags":["Stripe Connect OAuth"]}},"/api/v1/qualiopi/cross-tenant":{"get":{"description":"Agrège les scores de conformité Qualiopi pour tous les tenants de la plateforme. Évalue en parallèle les 32 indicateurs de chaque tenant et retourne un score global et un score par critère (1-7). Réservé aux administrateurs super-plateformistes.","operationId":"QualiopiController_getCrossTenantCompliance","parameters":[],"responses":{"200":{"description":"Rapport de conformité cross-tenant généré avec succès"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Conformité Qualiopi cross-tenant (ADMIN plateforme uniquement)","tags":["Qualiopi"]}},"/api/v1/qualiopi/dashboard":{"get":{"description":"Retourne le statut de conformité pour les 7 critères et les 32 indicateurs Qualiopi. Chaque indicateur est vérifié en temps réel contre les données du tenant. Les vérifications s'exécutent en parallèle pour minimiser la latence.","operationId":"QualiopiController_getDashboard","parameters":[],"responses":{"200":{"description":"Tableau de bord Qualiopi généré avec succès"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"}},"security":[{"access-token":[]}],"summary":"Tableau de bord Qualiopi complet (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/indicators/{number}":{"get":{"description":"Vérifie et retourne le statut de conformité d'un indicateur Qualiopi spécifique. La vérification est effectuée en temps réel contre les données du tenant. Numéros valides : 1 à 32.","operationId":"QualiopiController_getIndicator","parameters":[{"name":"number","required":true,"in":"path","description":"Numéro officiel de l'indicateur Qualiopi (1-32)","schema":{"example":6,"type":"number"}}],"responses":{"200":{"description":"Statut de l'indicateur retourné"},"400":{"description":"Numéro d'indicateur invalide (hors plage 1-32)"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"},"404":{"description":"Indicateur introuvable"}},"security":[{"access-token":[]}],"summary":"Statut d'un indicateur Qualiopi (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/indicators/{number}/proofs":{"get":{"description":"Retourne la liste des preuves documentaires disponibles pour justifier la conformité d'un indicateur Qualiopi lors d'un audit. Inclut documents, questionnaires, émargements et profils formateurs selon l'indicateur.","operationId":"QualiopiController_getIndicatorProofs","parameters":[{"name":"number","required":true,"in":"path","description":"Numéro officiel de l'indicateur Qualiopi (1-32)","schema":{"example":23,"type":"number"}}],"responses":{"200":{"description":"Liste des preuves retournée"},"400":{"description":"Numéro d'indicateur invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"},"404":{"description":"Indicateur introuvable"}},"security":[{"access-token":[]}],"summary":"Preuves documentaires d'un indicateur (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/alerts":{"get":{"description":"Retourne les indicateurs Qualiopi non-conformes ou en cours, avec des recommandations d'actions correctives. Les alertes sont triées par priorité (haute > moyenne > basse). Une liste vide indique une conformité totale sur les indicateurs vérifiables automatiquement.","operationId":"QualiopiController_getAlerts","parameters":[],"responses":{"200":{"description":"Liste des alertes Qualiopi retournée"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"}},"security":[{"access-token":[]}],"summary":"Alertes Qualiopi — indicateurs non conformes (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/audit-export":{"get":{"description":"Génère et télécharge un dossier d'audit complet au format ZIP contenant le tableau de bord, les alertes, les statistiques formations/questionnaires et le journal des documents Qualiopi. Paramètres `from` et `to` pour filtrer les logs par période.","operationId":"QualiopiController_exportAuditDossier","parameters":[{"name":"from","required":false,"in":"query","description":"Date de début de la période (ISO 8601 : YYYY-MM-DD)","schema":{"example":"2024-01-01","type":"string"}},{"name":"to","required":false,"in":"query","description":"Date de fin de la période (ISO 8601 : YYYY-MM-DD)","schema":{"example":"2024-12-31","type":"string"}},{"name":"year","required":true,"in":"query","schema":{"type":"string"}},{"name":"includeRawData","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Fichier ZIP du dossier d'audit retourné en téléchargement","headers":{"Content-Disposition":{"description":"attachment; filename=\"qualiopi-audit-{tenantId}-{date}.zip\"","schema":{"type":"string"}},"Content-Type":{"description":"application/zip","schema":{"type":"string"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"}},"security":[{"access-token":[]}],"summary":"Export dossier d'audit Qualiopi ZIP (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/check-publication":{"post":{"description":"Vérifie les critères Qualiopi bloquants avant publication sans déclencher la transition de statut. Retourne canPublish + la liste des blockers (indicator, field, message) pour alimenter une modale d'avertissement côté frontend.","operationId":"QualiopiController_checkPublication","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["courseId"],"properties":{"courseId":{"type":"string","description":"CUID du cours à vérifier","example":"clx8abc123def456"}}}}}},"responses":{"200":{"description":"Résultat de la vérification pré-publication","content":{"application/json":{"schema":{"type":"object","properties":{"canPublish":{"type":"boolean"},"blockers":{"type":"array","items":{"type":"object","properties":{"indicator":{"type":"number","example":2},"field":{"type":"string","example":"accessibilityInfo"},"message":{"type":"string"}}}}}}}}},"400":{"description":"courseId manquant ou invalide"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle CREATOR ou ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Pré-vérification publication Qualiopi (CREATOR | ADMIN)","tags":["Qualiopi"]}},"/api/v1/qualiopi/courses/{courseId}/audit-export/preview":{"get":{"description":"Retourne les compteurs agrégés du dossier preuve Qualiopi scope-cours (inscriptions, sessions, formateurs, questionnaires, émargements, documents, réclamations, plans d'amélioration) sans construire le ZIP.","operationId":"QualiopiController_previewCourseAuditExport","parameters":[{"name":"courseId","required":true,"in":"path","description":"CUID de la formation","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Compteurs du dossier preuve","content":{"application/json":{"schema":{"type":"object","properties":{"courseId":{"type":"string"},"courseTitle":{"type":"string"},"folders":{"type":"number","example":9},"files":{"type":"number"},"estimatedBytes":{"type":"number"},"indicators":{"type":"object","properties":{"enrollments":{"type":"number"},"sessions":{"type":"number"},"trainers":{"type":"number"},"questionnaires":{"type":"number"},"emargements":{"type":"number"},"documents":{"type":"number"},"complaints":{"type":"number"},"improvementPlans":{"type":"number"}}}}}}}},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"},"404":{"description":"Formation introuvable"}},"security":[{"access-token":[]}],"summary":"Aperçu du dossier preuve cours — compteurs (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/courses/{courseId}/audit-export":{"get":{"description":"Génère et télécharge le dossier preuve Qualiopi scope-cours au format ZIP. Contient 9 dossiers structurés (identification, programme, formateurs, inscriptions, sessions, questionnaires, résultats, amélioration continue, conformité) avec les données de la formation et les binaires PDF disponibles sur S3. Le MANIFEST.json garantit l'intégrité par empreinte SHA-256.","operationId":"QualiopiController_exportCourseAudit","parameters":[{"name":"courseId","required":true,"in":"path","description":"CUID de la formation","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"includeRawData","required":false,"in":"query","description":"Inclure les réponses brutes aux questionnaires (true | false)","schema":{"example":"false","type":"string"}}],"responses":{"200":{"description":"Fichier ZIP du dossier preuve retourné en téléchargement","headers":{"Content-Disposition":{"description":"attachment; filename=\"formation_{slug}_{date}.zip\"","schema":{"type":"string"}},"Content-Type":{"description":"application/zip","schema":{"type":"string"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"},"404":{"description":"Formation introuvable"}},"security":[{"access-token":[]}],"summary":"Dossier preuve Qualiopi d'une formation — ZIP (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/audit-dossier/preview":{"get":{"description":"Retourne les compteurs agrégés (documents, sessions, formateurs, questionnaires, émargements, réclamations, rapports veille) pour l'année demandée, sans générer le ZIP.","operationId":"QualiopiController_previewAuditDossier","parameters":[{"name":"year","required":false,"in":"query","description":"Année cible (filtre tous les modèles sur la plage [1er janv — 31 déc])","schema":{"example":2025,"type":"number"}}],"responses":{"200":{"description":"Objet de compteurs pour l'UI d'aperçu"}},"security":[{"access-token":[]}],"summary":"Aperçu du dossier d'audit (compteurs) (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/indicators/{number}/manual-proof":{"post":{"description":"Crée ou met à jour la vérification manuelle d'un indicateur Qualiopi. Utilisé pour les indicateurs sans données structurées (veille, réseaux, réclamations). Le statut est pris en compte dans le tableau de bord.","operationId":"QualiopiController_saveManualProof","parameters":[{"name":"number","required":true,"in":"path","description":"Numéro officiel de l'indicateur Qualiopi (1-32)","schema":{"example":24,"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status","proofDescription"],"properties":{"status":{"type":"string","enum":["conforme","non-conforme","en-cours"],"description":"Statut de conformité vérifié manuellement"},"proofDescription":{"type":"string","description":"Description de la vérification effectuée et des preuves disponibles"},"proofUrl":{"type":"string","nullable":true,"description":"Chemin MinIO vers le document justificatif (optionnel)"}}}}}},"responses":{"200":{"description":"Preuve manuelle enregistrée avec succès"},"400":{"description":"Paramètres invalides"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"},"404":{"description":"Indicateur introuvable"}},"security":[{"access-token":[]}],"summary":"Enregistrer une preuve manuelle pour un indicateur (ADMIN | CREATOR)","tags":["Qualiopi"]},"get":{"description":"Retourne la dernière vérification manuelle enregistrée pour l'indicateur spécifié. Retourne null si aucune preuve n'a été enregistrée.","operationId":"QualiopiController_getManualProof","parameters":[{"name":"number","required":true,"in":"path","description":"Numéro officiel de l'indicateur Qualiopi (1-32)","schema":{"example":24,"type":"number"}}],"responses":{"200":{"description":"Preuve manuelle retournée (null si absente)","content":{"application/json":{"schema":{"nullable":true,"type":"object","properties":{"indicatorNumber":{"type":"number"},"status":{"type":"string","enum":["conforme","non-conforme","en-cours"]},"description":{"type":"string"},"proofUrl":{"type":"string","nullable":true},"verifiedBy":{"type":"string","description":"ID de l'utilisateur ayant effectué la vérification"},"verifiedAt":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN ou CREATOR requis"}},"security":[{"access-token":[]}],"summary":"Récupérer la preuve manuelle d'un indicateur (ADMIN | CREATOR)","tags":["Qualiopi"]}},"/api/v1/qualiopi/legal-watch/entries":{"post":{"operationId":"LegalWatchController_createEntry","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLegalWatchEntryDto"}}}},"responses":{"201":{"description":"Entrée créée"},"400":{"description":"Données invalides"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"}},"security":[{"access-token":[]}],"summary":"Créer une entrée de veille légale (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]},"get":{"operationId":"LegalWatchController_listEntries","parameters":[{"name":"category","required":false,"in":"query","description":"Filtrer par catégorie","schema":{"type":"string"}},{"name":"isActive","required":false,"in":"query","description":"Filtrer par statut actif","schema":{"type":"boolean"}},{"name":"overdue","required":false,"in":"query","description":"Afficher uniquement les entrées en retard","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Liste des entrées"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"}},"security":[{"access-token":[]}],"summary":"Lister les entrées de veille légale (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]}},"/api/v1/qualiopi/legal-watch/entries/{id}":{"get":{"operationId":"LegalWatchController_getEntry","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"200":{"description":"Entrée avec ses rapports"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'une entrée de veille avec ses rapports (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]},"patch":{"operationId":"LegalWatchController_updateEntry","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLegalWatchEntryDto"}}}},"responses":{"200":{"description":"Entrée mise à jour"},"400":{"description":"Données invalides"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Mettre à jour une entrée de veille (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]},"delete":{"operationId":"LegalWatchController_deleteEntry","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"204":{"description":"Entrée supprimée"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN requis"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Supprimer une entrée de veille (ADMIN uniquement)","tags":["Qualiopi — Veille légale"]}},"/api/v1/qualiopi/legal-watch/entries/{id}/mark-checked":{"post":{"description":"Met à jour `lastCheckedAt` à now et recalcule `nextCheckDue` selon la fréquence : DAILY=+1j, WEEKLY=+7j, MONTHLY=+30j, QUARTERLY=+90j, YEARLY=+365j.","operationId":"LegalWatchController_markChecked","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"200":{"description":"Entrée mise à jour avec la nouvelle date de vérification"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Marquer une entrée comme vérifiée et recalculer nextCheckDue (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]}},"/api/v1/qualiopi/legal-watch/entries/{id}/reports":{"post":{"operationId":"LegalWatchController_createReport","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLegalWatchReportDto"}}}},"responses":{"201":{"description":"Rapport créé"},"400":{"description":"Données invalides"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Créer un rapport de veille sur une entrée (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]},"get":{"operationId":"LegalWatchController_listReports","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des rapports"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Lister les rapports d'une entrée de veille (ADMIN | CREATOR)","tags":["Qualiopi — Veille légale"]}},"/api/v1/qualiopi/legal-watch/export":{"get":{"description":"Génère un ZIP contenant `registre.csv` (toutes les entrées) et `reports/{entryId}-{reportId}.md` (chaque rapport en Markdown).","operationId":"LegalWatchController_exportZip","parameters":[],"responses":{"200":{"description":"Fichier ZIP retourné en téléchargement","headers":{"Content-Disposition":{"description":"attachment; filename=\"legal-watch-{tenantId}-{date}.zip\"","schema":{"type":"string"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé — rôle ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Export ZIP registre veille légale (ADMIN uniquement)","tags":["Qualiopi — Veille légale"]}},"/api/v1/qualiopi/legal-watch/auto/entries":{"get":{"description":"Retourne les entrées collectées automatiquement par le pipeline. Par défaut : non-dismissées.","operationId":"AutoLegalWatchController_listEntries","parameters":[{"name":"dismissed","required":false,"in":"query","description":"Filtrer par statut dismissed (true/false). Non fourni = toutes les entrées.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Liste des entrées"}},"security":[{"access-token":[]}],"summary":"Lister les entrées de veille légale auto","tags":["Qualiopi — Veille légale auto"]}},"/api/v1/qualiopi/legal-watch/auto/run":{"post":{"description":"Exécute le pipeline AutoLegalWatch pour le tenant courant. Utile pour tester ou rafraîchir à la demande.","operationId":"AutoLegalWatchController_runManual","parameters":[],"responses":{"200":{"description":"Pipeline exécuté"},"403":{"description":"Rôle ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Déclencher manuellement une collecte de veille légale (ADMIN)","tags":["Qualiopi — Veille légale auto"]}},"/api/v1/qualiopi/legal-watch/auto/entries/{id}/dismiss":{"patch":{"operationId":"AutoLegalWatchController_dismiss","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Entrée masquée"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Masquer une entrée (dismiss)","tags":["Qualiopi — Veille légale auto"]}},"/api/v1/qualiopi/legal-watch/auto/runs":{"get":{"operationId":"AutoLegalWatchController_listRuns","parameters":[{"name":"limit","required":false,"in":"query","description":"Nombre max de runs retournés (défaut 50, max 200)","schema":{"type":"number"}}],"responses":{"200":{"description":"Liste des runs, plus récents en premier"}},"security":[{"access-token":[]}],"summary":"Historique des exécutions du pipeline (audit trail Qualiopi ind. 21)","tags":["Qualiopi — Veille légale auto"]}},"/api/v1/qualiopi/partnerships":{"post":{"operationId":"PartnershipsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePartnershipDto"}}}},"responses":{"201":{"description":"Partenariat créé"}},"security":[{"bearer":[]}],"summary":"Créer un partenariat (Qualiopi indicateur 26)","tags":["Qualiopi — Partnerships"]},"get":{"operationId":"PartnershipsController_findAll","parameters":[],"responses":{"200":{"description":"Liste des partenariats"}},"security":[{"bearer":[]}],"summary":"Lister tous les partenariats du tenant","tags":["Qualiopi — Partnerships"]}},"/api/v1/qualiopi/partnerships/{id}":{"get":{"operationId":"PartnershipsController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du partenariat","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail du partenariat"},"404":{"description":"Partenariat introuvable"}},"security":[{"bearer":[]}],"summary":"Détail d'un partenariat (avec documents)","tags":["Qualiopi — Partnerships"]},"patch":{"operationId":"PartnershipsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du partenariat","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePartnershipDto"}}}},"responses":{"200":{"description":"Partenariat mis à jour"}},"security":[{"bearer":[]}],"summary":"Modifier un partenariat","tags":["Qualiopi — Partnerships"]},"delete":{"operationId":"PartnershipsController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du partenariat","schema":{"type":"string"}}],"responses":{"204":{"description":"Partenariat supprimé"}},"security":[{"bearer":[]}],"summary":"Supprimer un partenariat (ADMIN uniquement)","tags":["Qualiopi — Partnerships"]}},"/api/v1/qualiopi/partnerships/{id}/documents":{"post":{"operationId":"PartnershipsController_uploadDocument","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du partenariat","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","title"],"properties":{"file":{"type":"string","format":"binary","description":"Document à joindre (PDF, PNG, JPEG). Max 10 MB."},"title":{"type":"string","example":"Convention de partenariat 2024"}}}}}},"responses":{"201":{"description":"Document uploadé"},"400":{"description":"MIME invalide ou fichier trop volumineux"}},"security":[{"bearer":[]}],"summary":"Joindre un document à un partenariat","tags":["Qualiopi — Partnerships"]}},"/api/v1/qualiopi/partnerships/{id}/documents/{docId}":{"delete":{"operationId":"PartnershipsController_deleteDocument","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du partenariat","schema":{"type":"string"}},{"name":"docId","required":true,"in":"path","description":"CUID du document","schema":{"type":"string"}}],"responses":{"204":{"description":"Document supprimé"}},"security":[{"bearer":[]}],"summary":"Supprimer un document d'un partenariat (ADMIN uniquement)","tags":["Qualiopi — Partnerships"]}},"/api/v1/qualiopi/partnerships/{id}/reorder":{"patch":{"operationId":"PartnershipsController_reorder","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du partenariat","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderPartnershipDto"}}}},"responses":{"200":{"description":"Ordre mis à jour"}},"security":[{"bearer":[]}],"summary":"Modifier l'ordre d'affichage d'un partenariat (ADMIN uniquement)","tags":["Qualiopi — Partnerships"]}},"/api/v1/public/partnerships":{"get":{"description":"Retourne les partenariats où publishOnWebsite=true, triés par displayOrder. Requiert le header X-Tenant-ID pour résoudre le tenant.","operationId":"PublicPartnershipsController_listPublic","parameters":[{"name":"X-Tenant-ID","in":"header","description":"Slug du tenant (résolu par TenantMiddleware)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des partenariats publiés","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"website":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"displayOrder":{"type":"number"}}}}}}}},"summary":"Partenariats publiés sur le site (endpoint public)","tags":["Public — Partnerships"]}},"/api/v1/qualiopi/skills-watch/entries":{"post":{"operationId":"SkillsWatchController_createEntry","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSkillsWatchEntryDto"}}}},"responses":{"201":{"description":"Entrée de veille créée"},"400":{"description":"Validation échouée"},"409":{"description":"Code certification déjà suivi dans ce tenant"}},"security":[{"access-token":[]}],"summary":"Créer une entrée de veille certification (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]},"get":{"operationId":"SkillsWatchController_listEntries","parameters":[],"responses":{"200":{"description":"Liste des entrées retournée"}},"security":[{"access-token":[]}],"summary":"Lister les entrées de veille du tenant (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/entries/{id}":{"get":{"operationId":"SkillsWatchController_findEntry","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"200":{"description":"Entrée retournée"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'une entrée de veille avec ses alertes (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]},"patch":{"operationId":"SkillsWatchController_updateEntry","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSkillsWatchEntryDto"}}}},"responses":{"200":{"description":"Entrée mise à jour"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Mettre à jour une entrée de veille (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]},"delete":{"operationId":"SkillsWatchController_deleteEntry","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"204":{"description":"Entrée supprimée"},"403":{"description":"ADMIN requis"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Supprimer une entrée de veille (ADMIN uniquement)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/entries/{id}/link-course":{"post":{"operationId":"SkillsWatchController_linkCourse","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkCourseDto"}}}},"responses":{"201":{"description":"Cours lié à l'entrée"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Lier un cours à une entrée de veille (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/entries/{id}/link-course/{courseId}":{"delete":{"operationId":"SkillsWatchController_unlinkCourse","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}},{"name":"courseId","required":true,"in":"path","description":"CUID du cours à délier","schema":{"type":"string"}}],"responses":{"200":{"description":"Cours délié de l'entrée"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Délier un cours d'une entrée de veille (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/entries/{id}/check-now":{"post":{"description":"Interroge le FranceCompetencesClient pour cette entrée, compare le hash, crée une alerte REFERENCE_UPDATED si le référentiel a changé, met à jour nextCheckDue.","operationId":"SkillsWatchController_checkNow","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'entrée de veille","schema":{"type":"string"}}],"responses":{"200":{"description":"Vérification effectuée, entrée retournée"},"404":{"description":"Entrée introuvable"}},"security":[{"access-token":[]}],"summary":"Forcer une vérification immédiate via France Compétences (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/alerts":{"get":{"description":"Filtres disponibles : status (OPEN | ACKNOWLEDGED | RESOLVED), entryId.","operationId":"SkillsWatchController_listAlerts","parameters":[{"name":"status","required":false,"in":"query","description":"Statut de l'alerte","schema":{"type":"string","enum":["OPEN","ACKNOWLEDGED","RESOLVED"]}},{"name":"entryId","required":false,"in":"query","description":"Filtrer par entrée CUID","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Liste des alertes retournée"}},"security":[{"access-token":[]}],"summary":"Lister les alertes de veille du tenant (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/alerts/{id}/acknowledge":{"post":{"operationId":"SkillsWatchController_acknowledgeAlert","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'alerte","schema":{"type":"string"}}],"responses":{"200":{"description":"Alerte acquittée (statut → ACKNOWLEDGED)"},"404":{"description":"Alerte introuvable"},"409":{"description":"Alerte n'est pas OPEN"}},"security":[{"access-token":[]}],"summary":"Acquitter une alerte OPEN (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/skills-watch/alerts/{id}/resolve":{"post":{"operationId":"SkillsWatchController_resolveAlert","parameters":[{"name":"id","required":true,"in":"path","description":"CUID de l'alerte","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveAlertDto"}}}},"responses":{"200":{"description":"Alerte résolue (statut → RESOLVED)"},"404":{"description":"Alerte introuvable"},"409":{"description":"Alerte déjà résolue"}},"security":[{"access-token":[]}],"summary":"Résoudre une alerte (ADMIN | CREATOR)","tags":["Qualiopi — Skills Watch"]}},"/api/v1/qualiopi/improvement-plans":{"post":{"operationId":"ImprovementPlansController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateImprovementPlanDto"}}}},"responses":{"201":{"description":"Plan créé avec ses actions initiales"}},"security":[{"access-token":[]}],"summary":"Créer un plan d'amélioration (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]},"get":{"operationId":"ImprovementPlansController_findAll","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","IN_PROGRESS","DONE","ARCHIVED"]}},{"name":"trigger","required":false,"in":"query","schema":{"type":"string","enum":["COMPLAINT","SATISFACTION","AUDIT","INTERNAL","LEGAL_WATCH","SKILLS_WATCH"]}},{"name":"priority","required":false,"in":"query","schema":{"type":"string","enum":["LOW","MEDIUM","HIGH","CRITICAL"]}}],"responses":{"200":{"description":"Liste paginée des plans"}},"security":[{"access-token":[]}],"summary":"Lister les plans d'amélioration (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/qualiopi/improvement-plans/stats":{"get":{"operationId":"ImprovementPlansController_stats","parameters":[],"responses":{"200":{"description":"Agrégations par statut, déclencheur, délai moyen"}},"security":[{"access-token":[]}],"summary":"Statistiques des plans d'amélioration (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/qualiopi/improvement-plans/{id}":{"get":{"operationId":"ImprovementPlansController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}}],"responses":{"200":{"description":"Plan avec actions"},"404":{"description":"Plan introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'un plan + ses actions (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]},"patch":{"operationId":"ImprovementPlansController_update","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateImprovementPlanDto"}}}},"responses":{"200":{"description":"Plan mis à jour"}},"security":[{"access-token":[]}],"summary":"Mettre à jour un plan (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/qualiopi/improvement-plans/{id}/close":{"post":{"operationId":"ImprovementPlansController_close","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CloseImprovementPlanDto"}}}},"responses":{"200":{"description":"Plan clôturé"}},"security":[{"access-token":[]}],"summary":"Clôturer un plan — status=DONE (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/qualiopi/improvement-plans/{id}/archive":{"post":{"operationId":"ImprovementPlansController_archive","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}}],"responses":{"200":{"description":"Plan archivé"}},"security":[{"access-token":[]}],"summary":"Archiver un plan — status=ARCHIVED (ADMIN only)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/qualiopi/improvement-plans/{id}/actions":{"post":{"operationId":"ImprovementPlansController_addAction","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateImprovementActionDto"}}}},"responses":{"201":{"description":"Action créée"}},"security":[{"access-token":[]}],"summary":"Ajouter une action au plan (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/qualiopi/improvement-plans/{id}/actions/{actionId}":{"patch":{"operationId":"ImprovementPlansController_updateAction","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}},{"name":"actionId","required":true,"in":"path","description":"CUID de l'action","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateImprovementActionDto"}}}},"responses":{"200":{"description":"Action mise à jour"}},"security":[{"access-token":[]}],"summary":"Modifier une action du plan (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]},"delete":{"operationId":"ImprovementPlansController_deleteAction","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du plan","schema":{"type":"string"}},{"name":"actionId","required":true,"in":"path","description":"CUID de l'action","schema":{"type":"string"}}],"responses":{"204":{"description":"Action supprimée"}},"security":[{"access-token":[]}],"summary":"Supprimer une action du plan (ADMIN/CREATOR)","tags":["Qualiopi — Improvement Plans"]}},"/api/v1/sessions":{"post":{"description":"Creates a live session for a course. The LiveKit room name is generated automatically. Status starts as SCHEDULED until start() is called.","operationId":"SessionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSessionDto"}}}},"responses":{"201":{"description":"Session created successfully"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"},"403":{"description":"Requires CREATOR or ADMIN role"},"404":{"description":"Course not found"}},"security":[{"bearer":[]}],"summary":"Schedule a new live session (CREATOR)","tags":["Sessions"]}},"/api/v1/sessions/{id}":{"patch":{"description":"Met à jour titre, date, durée ou capacité. Impossible si ENDED ou CANCELLED.","operationId":"SessionsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSessionDto"}}}},"responses":{"200":{"description":"Session mise à jour"},"403":{"description":"Session non possédée par le créateur"},"404":{"description":"Session introuvable"},"422":{"description":"Session ENDED ou CANCELLED"}},"security":[{"bearer":[]}],"summary":"Modifier une session (CREATOR/ADMIN)","tags":["Sessions"]},"delete":{"description":"Passe la session en CANCELLED et notifie les apprenants inscrits.","operationId":"SessionsController_cancel","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Session annulée"},"403":{"description":"Session non possédée par le créateur"},"404":{"description":"Session introuvable"},"422":{"description":"Session non SCHEDULED"}},"security":[{"bearer":[]}],"summary":"Annuler une session (CREATOR/ADMIN)","tags":["Sessions"]},"get":{"description":"Returns session metadata including LiveKit room name and current status.","operationId":"SessionsController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Session detail"},"401":{"description":"Unauthorized"},"404":{"description":"Session not found"}},"security":[{"bearer":[]}],"summary":"Get session detail","tags":["Sessions"]}},"/api/v1/sessions/{id}/publish":{"post":{"description":"Active la session : inscrits reçoivent convocations, schedulers la prennent en compte. Valide les invariants (scheduledAt requis si SCHEDULED, absent si PERMANENT).","operationId":"SessionsController_publish","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Session publiée"},"403":{"description":"Session non possédée"},"404":{"description":"Session introuvable"},"422":{"description":"Invariant violé (date manquante ou en trop)"}},"security":[{"bearer":[]}],"summary":"Publier une session (DRAFT → PUBLISHED)","tags":["Sessions"]}},"/api/v1/sessions/{id}/archive":{"post":{"description":"Marque la session comme ARCHIVED. Terminal — ne plus prise en compte par les schedulers. Les inscrits conservent leurs données pour audit.","operationId":"SessionsController_archive","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Session archivée"},"403":{"description":"Session non possédée"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Archiver une session","tags":["Sessions"]}},"/api/v1/sessions/{id}/send-convocations":{"post":{"description":"Envoie un email de convocation à tous les inscrits ACTIVE. Met à jour convocationSentAt pour la traçabilité Qualiopi indicateur 11.","operationId":"SessionsController_sendConvocations","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Convocations envoyées"},"403":{"description":"Session non possédée par le créateur"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Envoyer les convocations manuellement (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/{id}/attendees":{"get":{"description":"Retourne les apprenants inscrits ACTIVE/COMPLETED du cours, avec leur statut de présence (émargement signé).","operationId":"SessionsController_getAttendees","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des participants avec statut présence"},"403":{"description":"Session non possédée par le créateur"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Liste des participants inscrits (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/{id}/stats":{"get":{"description":"Retourne enrolled, attended, attendanceRate (%), avgPresenceDuration (minutes).","operationId":"SessionsController_getStats","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Statistiques de participation"},"403":{"description":"Session non possédée par le créateur"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Statistiques de participation (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/{id}/captions.vtt":{"get":{"description":"Serves the AssemblyAI transcription of the session recording as a WebVTT document suitable for an HTML5 <track kind=\"captions\"> element.","operationId":"SessionsController_getCaptions","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"WebVTT document (text/vtt)"},"401":{"description":"Unauthorized"},"403":{"description":"Session belongs to another tenant"},"404":{"description":"Session, recording, or transcription not found"}},"security":[{"bearer":[]}],"summary":"Get session captions as WebVTT","tags":["Sessions"]}},"/api/v1/sessions/for-invite":{"get":{"description":"Retourne les sessions non-annulées du tenant avec courseTitle, startDate et endDate calculées (startDate + duration). Maximum 200 sessions, triées par date décroissante.","operationId":"SessionsController_listForInvite","parameters":[],"responses":{"200":{"description":"Liste des sessions pour le select d'invitation"},"401":{"description":"Unauthorized"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"bearer":[]}],"summary":"Lister les sessions pour le select d'invitation (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/overview":{"get":{"description":"Retourne les métriques agrégées du dashboard sessions : compteurs, taux de présence réel calculé depuis les émargements, heures totales.","operationId":"SessionsController_getOverview","parameters":[],"responses":{"200":{"description":"Métriques agrégées du dashboard sessions"},"401":{"description":"Unauthorized"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"bearer":[]}],"summary":"Vue d'ensemble des sessions (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/by-course/{courseId}":{"get":{"description":"Retourne toutes les sessions rattachées au cours, triées par scheduledAt.","operationId":"SessionsController_findByCourse","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des sessions du cours"}},"security":[{"bearer":[]}],"summary":"Lister les sessions d'un cours","tags":["Sessions"]}},"/api/v1/sessions/{id}/dates":{"get":{"description":"Retourne les SessionDate rattachés, triés par scheduledAt.","operationId":"SessionsController_listDates","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des dates"},"403":{"description":"Session non possédée par le créateur"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Lister les dates d'une session (CREATOR/ADMIN)","tags":["Sessions"]},"post":{"operationId":"SessionsController_addDate","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSessionDateDto"}}}},"responses":{"201":{"description":"Date ajoutée"}},"security":[{"bearer":[]}],"summary":"Ajouter une date à une session (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/{id}/dates/{dateId}":{"patch":{"operationId":"SessionsController_updateDate","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}},{"name":"dateId","required":true,"in":"path","description":"SessionDate CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSessionDateDto"}}}},"responses":{"200":{"description":"Date mise à jour"}},"security":[{"bearer":[]}],"summary":"Modifier une date de session (CREATOR/ADMIN)","tags":["Sessions"]},"delete":{"operationId":"SessionsController_deleteDate","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}},{"name":"dateId","required":true,"in":"path","description":"SessionDate CUID","schema":{"type":"string"}}],"responses":{"204":{"description":"Date supprimée"}},"security":[{"bearer":[]}],"summary":"Supprimer une date de session (CREATOR/ADMIN)","tags":["Sessions"]}},"/api/v1/sessions/{id}/join":{"post":{"description":"Verifies authorization and returns a LiveKit token to connect to the room. CREATORs/ADMINs join as trainers (can publish). LEARNERs must be enrolled and ACTIVE.","operationId":"SessionsController_join","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"LiveKit token + WebSocket URL","content":{"application/json":{"schema":{"example":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","wsUrl":"wss://livekit.qualiforma.fr","room":"session-abc12345-1x2y3z-4w5v6"}}}}},"401":{"description":"Unauthorized"},"403":{"description":"Not enrolled or enrollment not active"},"404":{"description":"Session not found"},"422":{"description":"Session cancelled or already ended"}},"security":[{"bearer":[]}],"summary":"Join a session — get LiveKit access token","tags":["Sessions"]}},"/api/v1/sessions/{id}/start":{"post":{"description":"Creates the LiveKit room, starts Egress recording to MinIO, and sets status to LIVE. Idempotent: calling on an already-LIVE session returns current state.","operationId":"SessionsController_start","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Session is now LIVE"},"401":{"description":"Unauthorized"},"403":{"description":"Requires CREATOR or ADMIN role"},"404":{"description":"Session not found"},"422":{"description":"Session is not in SCHEDULED status"}},"security":[{"bearer":[]}],"summary":"Start a session — transition to LIVE (CREATOR)","tags":["Sessions"]}},"/api/v1/sessions/{id}/end":{"post":{"description":"Stops the Egress recording, creates a Recording record in the DB, disconnects all participants, and queues transcription + AI insights processing.","operationId":"SessionsController_end","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Session ended, recording processing queued"},"401":{"description":"Unauthorized"},"403":{"description":"Requires CREATOR or ADMIN role"},"404":{"description":"Session not found"},"422":{"description":"Session is not in LIVE status"}},"security":[{"bearer":[]}],"summary":"End a session — transition to ENDED (CREATOR)","tags":["Sessions"]}},"/api/v1/sessions/{id}/recording":{"get":{"description":"Returns the recording media URL, transcription (JSON), AI summary, and insights. processingStatus indicates whether post-processing is PENDING, TRANSCRIBING, ANALYZING, COMPLETED or FAILED.","operationId":"SessionsController_getRecording","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Recording with transcription and insights"},"401":{"description":"Unauthorized"},"404":{"description":"Session or recording not found"}},"security":[{"bearer":[]}],"summary":"Get session recording with transcription and insights","tags":["Sessions"]}},"/api/v1/live-sessions":{"get":{"description":"Returns sessions scoped by role. Supports ?upcomingOnly=true to filter out sessions already ENDED or CANCELLED.","operationId":"LiveSessionsController_list","parameters":[{"name":"upcomingOnly","required":false,"in":"query","description":"When true, excludes sessions already ENDED or CANCELLED. Useful for the dashboard \"upcoming sessions\" widget.","schema":{"example":true,"type":"boolean"}}],"responses":{"200":{"description":"Live sessions list ordered by scheduledAt descending"},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"List live sessions accessible to the current user","tags":["Live sessions"]}},"/api/v1/public/courses/{slug}/sessions":{"get":{"description":"Retourne les sessions SCHEDULED/PUBLISHED non passées d'un cours. Requiert le header X-Tenant-ID (ou un sous-domaine tenant) pour résoudre le tenant. Les sessions DRAFT, ARCHIVED, CANCELLED et PERMANENT sont exclues.","operationId":"PublicSessionsController_listAvailableSessions","parameters":[{"name":"slug","required":true,"in":"path","description":"Slug du cours (unique par tenant)","schema":{"example":"initiation-qualiopi","type":"string"}},{"name":"X-Tenant-ID","in":"header","description":"Slug ou CUID du tenant (résolu par TenantMiddleware)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des sessions disponibles (potentiellement vide)","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"scheduledStart":{"type":"string","format":"date-time"},"scheduledEnd":{"type":"string","format":"date-time"},"duration":{"type":"number"},"location":{"type":"string","nullable":true},"isOnline":{"type":"boolean"},"capacity":{"type":"number","nullable":true},"enrolledCount":{"type":"number"},"priceAmount":{"type":"number","nullable":true},"priceCurrency":{"type":"string"}}}}}}}},"summary":"Sessions disponibles à l'inscription (endpoint public)","tags":["Public — Courses"]}},"/api/v1/sessions/{id}/documents":{"post":{"description":"Crée un enregistrement SessionDocument pointant vers un fichier préalablement uploadé sur MinIO/iDrive e2 (via POST /media/upload ou presigned PUT). Le `fileUrl` est l’object path retourné par ces endpoints.","operationId":"SessionDocumentsController_create","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSessionDocumentDto"}}}},"responses":{"201":{"description":"Document rattaché"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé (tenant ou propriétaire)"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Rattacher un document à une session (CREATOR/ADMIN)","tags":["Session Documents"]},"get":{"description":"Retourne les documents rattachés à la session, ordonnés par date d'upload décroissante. Chaque document inclut une `signedUrl` presignée valide 1h pour le téléchargement direct.","operationId":"SessionDocumentsController_list","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des documents"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Session introuvable"}},"security":[{"bearer":[]}],"summary":"Liste des documents d’une session (CREATOR/ADMIN)","tags":["Session Documents"]}},"/api/v1/sessions/{id}/documents/{docId}":{"delete":{"description":"Supprime l’enregistrement SessionDocument ET le fichier binaire associé sur MinIO/iDrive e2. Opération irréversible.","operationId":"SessionDocumentsController_delete","parameters":[{"name":"id","required":true,"in":"path","description":"Session CUID","schema":{"type":"string"}},{"name":"docId","required":true,"in":"path","description":"Document CUID","schema":{"type":"string"}}],"responses":{"204":{"description":"Document supprimé"},"401":{"description":"Non authentifié"},"403":{"description":"Accès refusé"},"404":{"description":"Document introuvable"}},"security":[{"bearer":[]}],"summary":"Supprimer un document (CREATOR/ADMIN)","tags":["Session Documents"]}},"/api/v1/health":{"get":{"description":"Returns 200 if the process is alive. No dependency checks.","operationId":"HealthController_healthCheck","parameters":[],"responses":{"200":{"description":"Process is alive"}},"summary":"Basic health check","tags":["Health"]}},"/api/v1/health/live":{"get":{"description":"Returns 200 if the Node.js process is responsive. Used by Kubernetes liveness probes.","operationId":"HealthController_livenessCheck","parameters":[],"responses":{"200":{"description":"Process is alive and responsive"}},"summary":"Liveness probe","tags":["Health"]}},"/api/v1/health/ready":{"get":{"description":"Checks PostgreSQL, Redis, and MinIO. Returns 200 when all dependencies are ready, 503 otherwise.","operationId":"HealthController_readinessCheck","parameters":[],"responses":{"200":{"description":"All dependencies are ready"},"503":{"description":"One or more dependencies are unavailable"}},"summary":"Readiness probe","tags":["Health"]}},"/api/v1/complaints":{"post":{"operationId":"ComplaintsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateComplaintDto"}}}},"responses":{"201":{"description":"Complaint created"}},"security":[{"access-token":[]}],"summary":"File a new complaint (any authenticated user)","tags":["Complaints"]},"get":{"operationId":"ComplaintsController_findAll","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"category","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Complaints list"}},"security":[{"access-token":[]}],"summary":"List all complaints in tenant (ADMIN/CREATOR)","tags":["Complaints"]}},"/api/v1/complaints/my":{"get":{"operationId":"ComplaintsController_findMy","parameters":[],"responses":{"200":{"description":"Own complaints list"}},"security":[{"access-token":[]}],"summary":"List authenticated user's own complaints","tags":["Complaints"]}},"/api/v1/complaints/stats":{"get":{"operationId":"ComplaintsController_getStats","parameters":[],"responses":{"200":{"description":"Complaint statistics"}},"security":[{"access-token":[]}],"summary":"Get complaint statistics for the tenant (ADMIN/CREATOR)","tags":["Complaints"]}},"/api/v1/complaints/{id}":{"get":{"operationId":"ComplaintsController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Complaint CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Complaint detail"},"404":{"description":"Not found"}},"security":[{"access-token":[]}],"summary":"Get complaint detail (ADMIN/CREATOR)","tags":["Complaints"]},"patch":{"operationId":"ComplaintsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Complaint CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateComplaintDto"}}}},"responses":{"200":{"description":"Updated complaint"},"403":{"description":"Forbidden"}},"security":[{"access-token":[]}],"summary":"Update complaint status or resolution","tags":["Complaints"]}},"/api/v1/complaints/{id}/assign":{"post":{"operationId":"ComplaintsController_assign","parameters":[{"name":"id","required":true,"in":"path","description":"Complaint CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Complaint assigned"}},"security":[{"access-token":[]}],"summary":"Assign complaint to a user (ADMIN/CREATOR)","tags":["Complaints"]}},"/api/v1/complaints/{id}/resolve":{"post":{"operationId":"ComplaintsController_resolve","parameters":[{"name":"id","required":true,"in":"path","description":"Complaint CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Complaint resolved"}},"security":[{"access-token":[]}],"summary":"Resolve a complaint and optionally auto-create an improvement action (ADMIN/CREATOR)","tags":["Complaints"]}},"/api/v1/complaints/{id}/escalate":{"post":{"operationId":"ComplaintsController_escalate","parameters":[{"name":"id","required":true,"in":"path","description":"Complaint CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Complaint escalated"}},"security":[{"access-token":[]}],"summary":"Escalate a complaint (ADMIN/CREATOR)","tags":["Complaints"]}},"/api/v1/improvements":{"post":{"operationId":"ImprovementsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateImprovementDto"}}}},"responses":{"201":{"description":"Improvement action created"}},"security":[{"access-token":[]}],"summary":"Create an improvement action (ADMIN/CREATOR)","tags":["Improvements"]},"get":{"operationId":"ImprovementsController_findAll","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"source","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Improvement actions list"}},"security":[{"access-token":[]}],"summary":"List all improvement actions (ADMIN/CREATOR)","tags":["Improvements"]}},"/api/v1/improvements/stats":{"get":{"operationId":"ImprovementsController_getStats","parameters":[],"responses":{"200":{"description":"Improvement statistics"}},"security":[{"access-token":[]}],"summary":"Get improvement action statistics (ADMIN/CREATOR)","tags":["Improvements"]}},"/api/v1/improvements/overdue":{"get":{"operationId":"ImprovementsController_findOverdue","parameters":[],"responses":{"200":{"description":"Overdue improvement actions"}},"security":[{"access-token":[]}],"summary":"List overdue improvement actions (ADMIN/CREATOR)","tags":["Improvements"]}},"/api/v1/improvements/{id}":{"get":{"operationId":"ImprovementsController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Improvement action CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Improvement action detail"},"404":{"description":"Not found"}},"security":[{"access-token":[]}],"summary":"Get improvement action detail (ADMIN/CREATOR)","tags":["Improvements"]}},"/api/v1/improvements/{id}/status":{"patch":{"operationId":"ImprovementsController_updateStatus","parameters":[{"name":"id","required":true,"in":"path","description":"Improvement action CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Updated improvement action"}},"security":[{"access-token":[]}],"summary":"Update status and outcome of an improvement action (ADMIN/CREATOR)","tags":["Improvements"]}},"/api/v1/webhooks/outbound":{"post":{"description":"Registers a new outbound webhook endpoint for the authenticated tenant. The secret is stored server-side and will not be returned after creation.","operationId":"WebhookOutboundController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookEndpointDto"}}}},"responses":{"201":{"description":"Endpoint created successfully."},"400":{"description":"Validation error."},"401":{"description":"Unauthorized."},"403":{"description":"Forbidden — ADMIN role required."}},"security":[{"bearer":[]}],"summary":"Create a webhook endpoint","tags":["Webhooks — Outbound"]},"get":{"description":"Returns all outbound webhook endpoints for the current tenant.","operationId":"WebhookOutboundController_findAll","parameters":[],"responses":{"200":{"description":"List of webhook endpoints."},"401":{"description":"Unauthorized."}},"security":[{"bearer":[]}],"summary":"List webhook endpoints","tags":["Webhooks — Outbound"]}},"/api/v1/webhooks/outbound/{id}":{"get":{"operationId":"WebhookOutboundController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Webhook endpoint CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Endpoint found."},"404":{"description":"Endpoint not found."}},"security":[{"bearer":[]}],"summary":"Get a webhook endpoint by ID","tags":["Webhooks — Outbound"]},"patch":{"description":"Updates the URL, events, or active status of an endpoint.","operationId":"WebhookOutboundController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Webhook endpoint CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookEndpointDto"}}}},"responses":{"200":{"description":"Endpoint updated."},"404":{"description":"Endpoint not found."}},"security":[{"bearer":[]}],"summary":"Update a webhook endpoint","tags":["Webhooks — Outbound"]},"delete":{"description":"Permanently deletes the endpoint and all its delivery records.","operationId":"WebhookOutboundController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"Webhook endpoint CUID","schema":{"type":"string"}}],"responses":{"204":{"description":"Endpoint deleted."},"404":{"description":"Endpoint not found."}},"security":[{"bearer":[]}],"summary":"Delete a webhook endpoint","tags":["Webhooks — Outbound"]}},"/api/v1/webhooks/outbound/{id}/deliveries":{"get":{"description":"Returns the last 100 delivery attempts with status codes and timestamps.","operationId":"WebhookOutboundController_findDeliveries","parameters":[{"name":"id","required":true,"in":"path","description":"Webhook endpoint CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Delivery list."},"404":{"description":"Endpoint not found."}},"security":[{"bearer":[]}],"summary":"List recent deliveries for a webhook endpoint","tags":["Webhooks — Outbound"]}},"/api/v1/adaptive-paths":{"post":{"operationId":"AdaptivePathsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdaptivePathDto"}}}},"responses":{"201":{"description":"AdaptivePath created"}},"security":[{"access-token":[]}],"summary":"Create adaptive branching rules for a lesson","tags":["Adaptive Paths (Qualiopi Criterion 3)"]},"get":{"operationId":"AdaptivePathsController_findAll","parameters":[{"name":"courseId","required":true,"in":"query","description":"CUID of the course","schema":{"type":"string"}}],"responses":{"200":{"description":"List of adaptive paths"}},"security":[{"access-token":[]}],"summary":"List all adaptive paths for a course","tags":["Adaptive Paths (Qualiopi Criterion 3)"]}},"/api/v1/adaptive-paths/evaluate/{enrollmentId}/{lessonId}":{"get":{"operationId":"AdaptivePathsController_evaluate","parameters":[{"name":"enrollmentId","required":true,"in":"path","description":"CUID of the enrollment","schema":{"type":"string"}},{"name":"lessonId","required":true,"in":"path","description":"CUID of the completed lesson","schema":{"type":"string"}}],"responses":{"200":{"description":"Next lesson ID or null for linear progression"}},"security":[{"access-token":[]}],"summary":"Evaluate next lesson for a learner (learning player)","tags":["Adaptive Paths (Qualiopi Criterion 3)"]}},"/api/v1/adaptive-paths/{id}":{"get":{"operationId":"AdaptivePathsController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the adaptive path","schema":{"type":"string"}}],"responses":{"200":{"description":"AdaptivePath detail"},"404":{"description":"AdaptivePath not found"}},"security":[{"access-token":[]}],"summary":"Get a specific adaptive path by ID","tags":["Adaptive Paths (Qualiopi Criterion 3)"]},"patch":{"operationId":"AdaptivePathsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the adaptive path","schema":{"type":"string"}}],"responses":{"200":{"description":"AdaptivePath updated"},"404":{"description":"AdaptivePath not found"}},"security":[{"access-token":[]}],"summary":"Update branching rules or default next lesson","tags":["Adaptive Paths (Qualiopi Criterion 3)"]},"delete":{"operationId":"AdaptivePathsController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the adaptive path","schema":{"type":"string"}}],"responses":{"200":{"description":"AdaptivePath deleted"},"404":{"description":"AdaptivePath not found"}},"security":[{"access-token":[]}],"summary":"Delete an adaptive path","tags":["Adaptive Paths (Qualiopi Criterion 3)"]}},"/api/v1/trainer-competencies/competencies":{"post":{"operationId":"TrainerCompetenciesController_createCompetency","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompetencyDto"}}}},"responses":{"201":{"description":"Competency created"},"409":{"description":"Duplicate (trainer + domain already exists)"}},"security":[{"access-token":[]}],"summary":"Declare a competency domain for a trainer","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainer-competencies/competencies/trainer/{trainerId}":{"get":{"operationId":"TrainerCompetenciesController_findCompetenciesByTrainer","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"Competency list"}},"security":[{"access-token":[]}],"summary":"List all competencies for a trainer","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainer-competencies/competencies/tenant/{tenantId}":{"get":{"operationId":"TrainerCompetenciesController_findCompetenciesByTenant","parameters":[{"name":"tenantId","required":true,"in":"path","description":"CUID of the tenant","schema":{"type":"string"}}],"responses":{"200":{"description":"Competency list"}},"security":[{"access-token":[]}],"summary":"List all trainer competencies within a tenant","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainer-competencies/competencies/{id}":{"get":{"operationId":"TrainerCompetenciesController_findOneCompetency","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the competency","schema":{"type":"string"}}],"responses":{"200":{"description":"Competency detail"},"404":{"description":"Competency not found"}},"security":[{"access-token":[]}],"summary":"Get a competency record by ID","tags":["Trainer Competencies (Qualiopi Criterion 5)"]},"patch":{"operationId":"TrainerCompetenciesController_updateCompetency","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the competency","schema":{"type":"string"}}],"responses":{"200":{"description":"Competency updated"},"404":{"description":"Competency not found"}},"security":[{"access-token":[]}],"summary":"Update competency level or certifications","tags":["Trainer Competencies (Qualiopi Criterion 5)"]},"delete":{"operationId":"TrainerCompetenciesController_removeCompetency","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the competency","schema":{"type":"string"}}],"responses":{"200":{"description":"Competency deleted"},"404":{"description":"Competency not found"}},"security":[{"access-token":[]}],"summary":"Delete a competency record","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainer-competencies/trainings":{"post":{"operationId":"TrainerCompetenciesController_createTraining","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrainingDto"}}}},"responses":{"201":{"description":"Training record created"}},"security":[{"access-token":[]}],"summary":"Add a continuous professional development record","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainer-competencies/trainings/trainer/{trainerId}":{"get":{"operationId":"TrainerCompetenciesController_findTrainingsByTrainer","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"Training list ordered by date desc"}},"security":[{"access-token":[]}],"summary":"List all CPD records for a trainer","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainer-competencies/trainings/{id}":{"get":{"operationId":"TrainerCompetenciesController_findOneTraining","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the training record","schema":{"type":"string"}}],"responses":{"200":{"description":"Training record detail"},"404":{"description":"Training record not found"}},"security":[{"access-token":[]}],"summary":"Get a CPD record by ID","tags":["Trainer Competencies (Qualiopi Criterion 5)"]},"patch":{"operationId":"TrainerCompetenciesController_updateTraining","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the training record","schema":{"type":"string"}}],"responses":{"200":{"description":"Training record updated"},"404":{"description":"Training record not found"}},"security":[{"access-token":[]}],"summary":"Update a CPD record","tags":["Trainer Competencies (Qualiopi Criterion 5)"]},"delete":{"operationId":"TrainerCompetenciesController_removeTraining","parameters":[{"name":"id","required":true,"in":"path","description":"CUID of the training record","schema":{"type":"string"}}],"responses":{"200":{"description":"Training record deleted"},"404":{"description":"Training record not found"}},"security":[{"access-token":[]}],"summary":"Delete a CPD record","tags":["Trainer Competencies (Qualiopi Criterion 5)"]}},"/api/v1/trainers/{trainerId}/continuous-trainings":{"post":{"operationId":"TrainerExtendedController_createContinuousTraining","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateContinuousTrainingDto"}}}},"responses":{"201":{"description":"Training record created"},"403":{"description":"CREATOR may only write their own records"},"404":{"description":"Trainer not found in this tenant"}},"security":[{"access-token":[]}],"summary":"Add a continuous training record with optional certificate PDF (Ind. 17)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]},"get":{"operationId":"TrainerExtendedController_listContinuousTrainings","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"Training list ordered by dateCompleted desc"}},"security":[{"access-token":[]}],"summary":"List continuous training records for a trainer (Ind. 17)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/{trainerId}/continuous-trainings/{id}":{"delete":{"operationId":"TrainerExtendedController_removeContinuousTraining","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}},{"name":"id","required":true,"in":"path","description":"CUID of the training record","schema":{"type":"string"}}],"responses":{"200":{"description":"Training record deleted"},"403":{"description":"CREATOR may only delete their own records"},"404":{"description":"Training record not found"}},"security":[{"access-token":[]}],"summary":"Delete a continuous training record (Ind. 17)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/{trainerId}/onboarding-checklist":{"get":{"operationId":"TrainerExtendedController_getOnboardingChecklist","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"Checklist (existing or newly created)"}},"security":[{"access-token":[]}],"summary":"Get onboarding checklist — auto-creates with defaults if absent (Ind. 18)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/{trainerId}/onboarding-checklist/items/{index}":{"patch":{"operationId":"TrainerExtendedController_updateOnboardingItem","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}},{"name":"index","required":true,"in":"path","description":"Zero-based index of the checklist item","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOnboardingItemDto"}}}},"responses":{"200":{"description":"Updated checklist with recalculated completionRate"},"404":{"description":"Trainer or item index not found"}},"security":[{"access-token":[]}],"summary":"Mark a checklist item completed/incomplete (Ind. 18)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/{trainerId}/onboarding-checklist/sign-procedures":{"post":{"operationId":"TrainerExtendedController_signProcedures","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"Checklist updated with signedProceduresId"}},"security":[{"access-token":[]}],"summary":"Record signed procedures document ID on the checklist (Ind. 18)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/{trainerId}/dossier/export":{"get":{"operationId":"TrainerExtendedController_exportDossier","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"ZIP file stream","content":{"application/zip":{}}},"404":{"description":"Trainer not found"}},"security":[{"access-token":[]}],"summary":"Stream a ZIP archive of the trainer dossier (CV, diplomas, certificates, checklist)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/{trainerId}/profile-extended":{"patch":{"operationId":"TrainerExtendedController_updateProfileExtended","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTrainerProfileExtendedDto"}}}},"responses":{"200":{"description":"Extended profile updated"},"404":{"description":"Trainer not found"}},"security":[{"access-token":[]}],"summary":"Update extended trainer profile (type, contract, Qualiopi, SIRET, hourly rate)","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]},"get":{"operationId":"TrainerExtendedController_getProfileExtended","parameters":[{"name":"trainerId","required":true,"in":"path","description":"CUID of the trainer","schema":{"type":"string"}}],"responses":{"200":{"description":"Extended profile retrieved"},"404":{"description":"Trainer not found"}},"security":[{"access-token":[]}],"summary":"Get extended trainer profile with Qualiopi warning","tags":["Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)"]}},"/api/v1/trainers/expiring-qualiopi":{"get":{"operationId":"TrainerComplianceController_getExpiringQualiopiTrainers","parameters":[],"responses":{"200":{"description":"List of trainers with expiring Qualiopi","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ExpiringQualiopiTrainerDto"}}}}}},"security":[{"access-token":[]}],"summary":"List trainers with Qualiopi expiring within 3 months","tags":["Trainers — Compliance (Wave 9)"]}},"/api/v1/trainer-competencies/derived/{trainerId}":{"get":{"description":"Retourne les compétences dérivées par le pipeline auto (NSF_CODE + PEDAGOGICAL + AI_INFERRED). Les compétences MANUAL (fallback admin) sont également incluses. Triées par poids décroissant.","operationId":"TrainerDerivedCompetenciesController_list","parameters":[{"name":"trainerId","required":true,"in":"path","description":"User.id du formateur","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des compétences dérivées"}},"security":[{"access-token":[]}],"summary":"Liste des compétences formateur dérivées automatiquement","tags":["Trainer Competencies — Derived (auto)"]}},"/api/v1/trainer-competencies/derived/{trainerId}/recompute":{"post":{"description":"Wipe les entrées auto (sources != MANUAL) et reconstruit depuis les cours assignés via CourseTrainer.","operationId":"TrainerDerivedCompetenciesController_recompute","parameters":[{"name":"trainerId","required":true,"in":"path","description":"User.id du formateur","schema":{"type":"string"}}],"responses":{"200":{"description":"Compétences dérivées recalculées"}},"security":[{"access-token":[]}],"summary":"Forcer le recalcul des compétences dérivées pour ce formateur","tags":["Trainer Competencies — Derived (auto)"]}},"/api/v1/trainer-competencies/derived/recompute-all":{"post":{"description":"Utile après un import massif de cours ou une mise à jour de la logique métier. Équivalent au cron nightly, mais à la demande.","operationId":"TrainerDerivedCompetenciesController_recomputeAll","parameters":[],"responses":{"200":{"description":"Recalcul terminé pour tous les formateurs du tenant"}},"security":[{"access-token":[]}],"summary":"Recalculer toutes les compétences dérivées du tenant (ADMIN)","tags":["Trainer Competencies — Derived (auto)"]}},"/api/v1/promo-codes":{"get":{"operationId":"PromoCodesController_findAll","parameters":[],"responses":{"200":{"description":"List of promo codes"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"}},"security":[{"access-token":[]}],"summary":"List all promo codes (CREATOR / ADMIN)","tags":["Promo Codes"]},"post":{"operationId":"PromoCodesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromoCodeDto"}}}},"responses":{"201":{"description":"Promo code created"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"409":{"description":"Code already exists for this tenant"},"422":{"description":"Validation error"}},"security":[{"access-token":[]}],"summary":"Create a promo code (CREATOR / ADMIN)","tags":["Promo Codes"]}},"/api/v1/promo-codes/validate":{"post":{"description":"Checks expiry, usage limits, course restrictions, and minimum order amount. Does NOT consume the code — call this before checkout.","operationId":"PromoCodesController_validate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidatePromoCodeDto"}}}},"responses":{"200":{"description":"Validation result with discount details"}},"summary":"Validate a promo code (public)","tags":["Promo Codes"]}},"/api/v1/promo-codes/{id}":{"get":{"operationId":"PromoCodesController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"PromoCode CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Promo code detail"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Promo code not found"}},"security":[{"access-token":[]}],"summary":"Get promo code by ID (CREATOR / ADMIN)","tags":["Promo Codes"]},"patch":{"operationId":"PromoCodesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"PromoCode CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Promo code updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Promo code not found"}},"security":[{"access-token":[]}],"summary":"Update a promo code (CREATOR / ADMIN)","tags":["Promo Codes"]},"delete":{"operationId":"PromoCodesController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"PromoCode CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Promo code deleted"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Promo code not found"}},"security":[{"access-token":[]}],"summary":"Delete a promo code (CREATOR / ADMIN)","tags":["Promo Codes"]}},"/api/v1/bundles/all":{"get":{"operationId":"BundlesController_findAll","parameters":[],"responses":{"200":{"description":"List of all bundles"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"}},"security":[{"access-token":[]}],"summary":"List all bundles including drafts (CREATOR / ADMIN)","tags":["Bundles"]}},"/api/v1/bundles/by-id/{id}":{"get":{"operationId":"BundlesController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Bundle CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Bundle detail"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Bundle not found"}},"security":[{"access-token":[]}],"summary":"Get bundle by ID (CREATOR / ADMIN)","tags":["Bundles"]}},"/api/v1/bundles":{"get":{"operationId":"BundlesController_findPublished","parameters":[],"responses":{"200":{"description":"List of published bundles"}},"summary":"List published bundles (public)","tags":["Bundles"]},"post":{"operationId":"BundlesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBundleDto"}}}},"responses":{"201":{"description":"Bundle created"},"400":{"description":"Bundle must contain at least 2 courses"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"422":{"description":"Validation error"}},"security":[{"access-token":[]}],"summary":"Create a bundle (CREATOR / ADMIN)","tags":["Bundles"]}},"/api/v1/bundles/{id}/purchase":{"post":{"description":"Creates enrollment records for all courses in the bundle that the user is not already enrolled in. Uses a transaction for atomicity.","operationId":"BundlesController_purchase","parameters":[{"name":"id","required":true,"in":"path","description":"Bundle CUID","schema":{"type":"string"}}],"responses":{"201":{"description":"Bundle purchased — enrollment list returned"},"400":{"description":"Bundle not published or courses unavailable"},"401":{"description":"Not authenticated"},"409":{"description":"Already enrolled in all courses of this bundle"}},"security":[{"access-token":[]}],"summary":"Purchase a bundle (authenticated learner)","tags":["Bundles"]}},"/api/v1/bundles/{slug}":{"get":{"description":"Returns bundle detail. Returns 404 for DRAFT and ARCHIVED bundles.","operationId":"BundlesController_findBySlug","parameters":[{"name":"slug","required":true,"in":"path","description":"URL-safe bundle slug (unique within tenant)","schema":{"type":"string"}}],"responses":{"200":{"description":"Bundle detail"},"404":{"description":"Bundle not found or not published"}},"summary":"Get published bundle by slug (public)","tags":["Bundles"]}},"/api/v1/bundles/{id}":{"patch":{"operationId":"BundlesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Bundle CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Bundle updated"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Bundle not found"}},"security":[{"access-token":[]}],"summary":"Update a bundle (CREATOR / ADMIN)","tags":["Bundles"]},"delete":{"operationId":"BundlesController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"Bundle CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Bundle deleted"},"401":{"description":"Not authenticated"},"403":{"description":"Forbidden — CREATOR or ADMIN role required"},"404":{"description":"Bundle not found"}},"security":[{"access-token":[]}],"summary":"Delete a bundle (CREATOR / ADMIN)","tags":["Bundles"]}},"/api/v1/companies":{"post":{"operationId":"CompaniesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompanyDto"}}}},"responses":{"201":{"description":""}},"security":[{"access-token":[]}],"summary":"Créer une entreprise/financeur (ADMIN | CREATOR)","tags":["Companies"]},"get":{"operationId":"CompaniesController_findAll","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Lister les entreprises du tenant (ADMIN | CREATOR)","tags":["Companies"]}},"/api/v1/companies/with-stats":{"get":{"operationId":"CompaniesController_findAllWithStats","parameters":[{"name":"search","required":false,"in":"query","description":"Recherche texte (nom, SIRET, email)","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Lister avec stats agrégées (ADMIN | CREATOR)","tags":["Companies"]}},"/api/v1/companies/{id}":{"get":{"operationId":"CompaniesController_findById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Détail d'une entreprise (ADMIN | CREATOR)","tags":["Companies"]},"patch":{"operationId":"CompaniesController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Mettre à jour une entreprise (ADMIN | CREATOR)","tags":["Companies"]},"delete":{"operationId":"CompaniesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Supprimer une entreprise (ADMIN | CREATOR)","tags":["Companies"]}},"/api/v1/needs-analysis":{"post":{"description":"Endpoint public — ne requiert pas d'authentification. Le X-Tenant-ID header doit être présent pour scoper la demande à un organisme de formation précis.","operationId":"NeedsAnalysisController_submit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitNeedsAnalysisDto"}}}},"responses":{"201":{"description":""}},"summary":"Soumettre une étude du besoin (public)","tags":["NeedsAnalysis"]},"get":{"operationId":"NeedsAnalysisController_findAll","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Lister les demandes du tenant (ADMIN | CREATOR)","tags":["NeedsAnalysis"]}},"/api/v1/needs-analysis/{id}":{"get":{"operationId":"NeedsAnalysisController_findById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"tags":["NeedsAnalysis"]}},"/api/v1/needs-analysis/{id}/assign":{"patch":{"operationId":"NeedsAnalysisController_assign","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"S'attribuer la demande (passe en IN_REVIEW)","tags":["NeedsAnalysis"]}},"/api/v1/needs-analysis/{id}/decide":{"patch":{"operationId":"NeedsAnalysisController_decide","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Accepter ou refuser la demande","tags":["NeedsAnalysis"]}},"/api/v1/funder/dashboard":{"get":{"operationId":"FunderController_dashboard","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Résumé agrégé pour le compte financeur (FUNDER)","tags":["Funder"]}},"/api/v1/funder/enrollments":{"get":{"operationId":"FunderController_learners","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Liste des apprenants financés (FUNDER)","tags":["Funder"]}},"/api/v1/funder/documents":{"get":{"operationId":"FunderController_documents","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Documents Qualiopi des inscriptions financées (FUNDER)","tags":["Funder"]}},"/api/v1/financier/tokens":{"post":{"description":"Génère un token opaque, l'envoie par email au destinataire et retourne le token brut UNE SEULE FOIS. Le token brut n'est jamais persisté — seul son hash SHA-256 est stocké.","operationId":"FinancierTokensController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFinancierAccessTokenDto"}}}},"responses":{"201":{"description":"Token créé. Le champ `token` n'est accessible qu'à cet instant."}},"security":[{"access-token":[]}],"summary":"Émet un token d'accès financeur (magic link)","tags":["Financier — Tokens"]},"get":{"operationId":"FinancierTokensController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Liste les tokens financeur du tenant (sans tokenHash)","tags":["Financier — Tokens"]}},"/api/v1/financier/tokens/{id}/revoke":{"post":{"operationId":"FinancierTokensController_revoke","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"security":[{"access-token":[]}],"summary":"Révoque immédiatement un token financeur","tags":["Financier — Tokens"]}},"/api/v1/financier/tokens/{id}/resend":{"post":{"description":"Le token brut n'est jamais stocké : il est impossible de le renvoyer. Créez un nouveau token via POST /financier/tokens.","operationId":"FinancierTokensController_resend","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"422":{"description":"Token is hashed — cannot resend."}},"security":[{"access-token":[]}],"summary":"Renvoi refusé — le token est hashé","tags":["Financier — Tokens"]}},"/api/v1/financier/portal/session":{"get":{"description":"Retourne companyName, scopes et expiresAt sans autre vérification d'accès.","operationId":"FinancierPortalController_getSession","parameters":[],"responses":{"200":{"description":""}},"security":[{"financier-token":["X-Financier-Token"]}],"summary":"Informations de session financeur (ping / init portail)","tags":["Financier — Portail"]}},"/api/v1/financier/portal/learners":{"get":{"operationId":"FinancierPortalController_listLearners","parameters":[],"responses":{"200":{"description":""},"403":{"description":"Scope manquant"}},"security":[{"financier-token":["X-Financier-Token"]}],"summary":"Apprenants financés (scope: learners)","tags":["Financier — Portail"]}},"/api/v1/financier/portal/documents":{"get":{"operationId":"FinancierPortalController_listDocuments","parameters":[],"responses":{"200":{"description":""},"403":{"description":"Scope manquant"}},"security":[{"financier-token":["X-Financier-Token"]}],"summary":"Documents Qualiopi des apprenants financés (scope: documents)","tags":["Financier — Portail"]}},"/api/v1/financier/portal/indicators":{"get":{"operationId":"FinancierPortalController_getIndicators","parameters":[],"responses":{"200":{"description":""},"403":{"description":"Scope manquant"}},"security":[{"financier-token":["X-Financier-Token"]}],"summary":"Indicateurs agrégés de formation (scope: indicators)","tags":["Financier — Portail"]}},"/api/v1/financier/portal/bpf/{year}":{"get":{"operationId":"FinancierPortalController_getBpf","parameters":[{"name":"year","required":true,"in":"path","schema":{"example":2024,"type":"number"}}],"responses":{"200":{"description":""},"403":{"description":"Scope manquant"}},"security":[{"financier-token":["X-Financier-Token"]}],"summary":"Rapport BPF pour une année (scope: bpf)","tags":["Financier — Portail"]}},"/api/v1/financier/portal/bpf/{year}/export":{"get":{"operationId":"FinancierPortalController_exportBpf","parameters":[{"name":"year","required":true,"in":"path","schema":{"example":2024,"type":"number"}}],"responses":{"200":{"description":"CSV BPF en content-disposition attachment"},"403":{"description":"Scope manquant"}},"security":[{"financier-token":["X-Financier-Token"]}],"summary":"Export CSV du BPF (scope: bpf, structure Cerfa 10443*17)","tags":["Financier — Portail"]}},"/api/v1/qualiopi/audit-bundle/{courseId}":{"get":{"description":"Produit une archive ZIP contenant tous les PDFs Qualiopi du cours, le journal PAF, le manifest audit et les stats questionnaires.","operationId":"AuditBundleController_download","parameters":[{"name":"courseId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Export ZIP complet pour audit (CREATOR | ADMIN)","tags":["Qualiopi"]}},"/api/v1/creators/learners":{"get":{"description":"CREATOR: returns learners enrolled in courses owned by the caller. ADMIN: returns all LEARNER users in the tenant enrolled in any course. Supports pagination, search (firstName/lastName/email), and courseId filter.","operationId":"CreatorsManagementController_listLearners","parameters":[{"name":"page","required":false,"in":"query","description":"Page number (1-indexed)","schema":{"minimum":1,"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Page size (max 100)","schema":{"minimum":1,"maximum":100,"example":20,"type":"number"}},{"name":"search","required":false,"in":"query","description":"Substring match on firstName, lastName or email","schema":{"maxLength":100,"example":"dupont","type":"string"}},{"name":"courseId","required":false,"in":"query","description":"Filter by course CUID — returns only learners enrolled in this course","schema":{"example":"clx8abc123def456","type":"string"}}],"responses":{"200":{"description":"Paginated learner list with meta"},"401":{"description":"Not authenticated"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"access-token":[]}],"summary":"List learners enrolled in the creator's courses","tags":["Creators management"]}},"/api/v1/creators/learners/invite":{"post":{"description":"Creates a LearnerInvitation (SHA-256 hashed token, 72h expiry) and pre-enrolls the invitee to the given courses. If the email already maps to a User in the tenant, enrollment is created directly and an 'enrolled' email is sent (no activation link). The plain token is never returned — only the generated invitationId + email.","operationId":"CreatorsManagementController_inviteLearner","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteLearnerDto"}}}},"responses":{"201":{"description":"Invitation created / existing user enrolled","content":{"application/json":{"schema":{"example":{"invitationId":"clx8invite123","email":"apprenant@example.com","expiresAt":"2026-04-18T12:00:00.000Z","status":"PENDING","enrolledExisting":false}}}}},"400":{"description":"Validation error"},"401":{"description":"Not authenticated"},"403":{"description":"One or more courses are not owned by the caller"},"404":{"description":"Course(s) not found"}},"security":[{"access-token":[]}],"summary":"Invite a learner to one or more courses","tags":["Creators management"]}},"/api/v1/creators/profile":{"patch":{"description":"All fields are optional. `competencies` maps to TrainerProfile.specialties. If the user has no TrainerProfile yet, it is created on demand.","operationId":"CreatorsManagementController_updateProfile","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCreatorProfileDto"}}}},"responses":{"200":{"description":"Profile updated"},"401":{"description":"Not authenticated"},"403":{"description":"Requires CREATOR or ADMIN role"},"404":{"description":"User not found"}},"security":[{"access-token":[]}],"summary":"Update the creator's profile (User + TrainerProfile)","tags":["Creators management"]}},"/api/v1/creators/branding":{"get":{"description":"Returns branding fields used on generated documents (conventions, invoices, certificates). All fields nullable — default config is all-null if none exists.","operationId":"CreatorsManagementController_getBranding","parameters":[],"responses":{"200":{"description":"Branding config returned"},"401":{"description":"Not authenticated"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"access-token":[]}],"summary":"Read the creator's branding config","tags":["Creators management"]},"patch":{"description":"Upserts the CreatorBrandingConfig row. Color fields are validated with @IsHexColor (#RRGGBB).","operationId":"CreatorsManagementController_updateBranding","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBrandingDto"}}}},"responses":{"200":{"description":"Branding config updated"},"400":{"description":"Validation error (e.g. invalid color)"},"401":{"description":"Not authenticated"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"access-token":[]}],"summary":"Update the creator's branding config (upsert)","tags":["Creators management"]}},"/api/v1/creators/branding/qualiopi-certificate":{"post":{"description":"Le PDF doit être uploadé au préalable via POST /media/upload. Inclus automatiquement dans le dossier d'audit Qualiopi exporté via GET /qualiopi/audit-dossier.","operationId":"CreatorsManagementController_setQualiopiCertificate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPdfUrlDto"}}}},"responses":{"200":{"description":"Certificate PDF enregistré"},"400":{"description":"pdfUrl manquant ou invalide"}},"security":[{"access-token":[]}],"summary":"Enregistre l'URL du PDF de certification Qualiopi","tags":["Creators management"]},"delete":{"operationId":"CreatorsManagementController_deleteQualiopiCertificate","parameters":[],"responses":{"200":{"description":"Certificate PDF retiré"}},"security":[{"access-token":[]}],"summary":"Retire le PDF de certification Qualiopi","tags":["Creators management"]}},"/api/v1/creators/branding/reglement-interieur":{"get":{"operationId":"CreatorsManagementController_getReglementInterieur","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"URL du règlement intérieur courant","tags":["Creators management"]},"post":{"description":"Joint automatiquement à chaque convocation envoyée aux apprenants.","operationId":"CreatorsManagementController_setReglementInterieur","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPdfUrlDto"}}}},"responses":{"200":{"description":"Règlement intérieur enregistré"},"400":{"description":"pdfUrl manquant ou invalide"}},"security":[{"access-token":[]}],"summary":"Enregistre l'URL du règlement intérieur (PDF)","tags":["Creators management"]},"delete":{"operationId":"CreatorsManagementController_deleteReglementInterieur","parameters":[],"responses":{"200":{"description":"Règlement intérieur retiré"}},"security":[{"access-token":[]}],"summary":"Retire le règlement intérieur","tags":["Creators management"]}},"/api/v1/creators/branding/factur-x-readiness":{"get":{"description":"Retourne { ready, missingFields[], vatNumberWaived }. Utilisé par la UI pour afficher un badge \"Prêt Factur-X\".","operationId":"CreatorsManagementController_getFacturXReadiness","parameters":[],"responses":{"200":{"description":"Résultat de l'évaluation"}},"security":[{"access-token":[]}],"summary":"Évalue si la fiche est prête pour Factur-X","tags":["Creators management"]}},"/api/v1/creators/payment-config":{"get":{"description":"Returns the Stripe Connect account ID, account status, and payment mode.","operationId":"CreatorsManagementController_getPaymentConfig","parameters":[],"responses":{"200":{"description":"Payment config returned"},"401":{"description":"Not authenticated"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"access-token":[]}],"summary":"Read the creator's Stripe Connect payment config","tags":["Creators management"]},"patch":{"description":"Upserts the CreatorPaymentConfig row. Only the payment mode can be changed here. Stripe Connect account fields are managed via the Stripe Connect Express onboarding flow (POST /creators/stripe/connect).","operationId":"CreatorsManagementController_updatePaymentConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePaymentConfigDto"}}}},"responses":{"200":{"description":"Payment config updated"},"400":{"description":"Validation error"},"401":{"description":"Not authenticated"},"403":{"description":"Requires CREATOR or ADMIN role"}},"security":[{"access-token":[]}],"summary":"Update the creator's payment config (upsert)","tags":["Creators management"]}},"/api/v1/users/me/branding":{"get":{"description":"Returns presigned GET URLs (1h TTL) for stamp and signature images. Both fields are null when no file has been uploaded yet. Callers should not cache the URL beyond 1 hour.","operationId":"BrandingController_getBranding","parameters":[],"responses":{"200":{"description":"Branding URLs returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandingResponseDto"}}}},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Get current user's stamp and signature presigned URLs","tags":["Users branding"]}},"/api/v1/users/me/branding/stamp":{"post":{"description":"Stores the stamp in MinIO at `branding/{userId}/stamp-{timestamp}.{ext}` and upserts `CreatorBrandingConfig.stampUrl`. The previous stamp is deleted (best-effort). Returns presigned GET URLs for both stamp and signature.","operationId":"BrandingController_uploadStamp","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Stamp image (PNG or JPEG). Max 2 MB."}}}}}},"responses":{"200":{"description":"Stamp uploaded — returns updated branding URLs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandingResponseDto"}}}},"400":{"description":"No file provided or file too large"},"401":{"description":"Not authenticated"},"415":{"description":"Unsupported media type — only PNG and JPEG are accepted"}},"security":[{"access-token":[]}],"summary":"Upload / replace the caller's stamp image","tags":["Users branding"]},"delete":{"description":"Sets `CreatorBrandingConfig.stampUrl` to null and removes the MinIO object (best-effort — a storage failure is logged but does not cause a 500). No-op if no stamp exists.","operationId":"BrandingController_deleteStamp","parameters":[],"responses":{"200":{"description":"Stamp deleted — returns updated branding URLs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandingResponseDto"}}}},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Delete the caller's stamp image","tags":["Users branding"]}},"/api/v1/users/me/branding/signature":{"post":{"description":"Stores the signature in MinIO at `branding/{userId}/signature-{timestamp}.{ext}` and upserts `CreatorBrandingConfig.signatureUrl`. The previous signature is deleted (best-effort). Returns presigned GET URLs for both stamp and signature.","operationId":"BrandingController_uploadSignature","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Signature image (PNG or JPEG). Max 2 MB."}}}}}},"responses":{"200":{"description":"Signature uploaded — returns updated branding URLs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandingResponseDto"}}}},"400":{"description":"No file provided or file too large"},"401":{"description":"Not authenticated"},"415":{"description":"Unsupported media type — only PNG and JPEG are accepted"}},"security":[{"access-token":[]}],"summary":"Upload / replace the caller's signature image","tags":["Users branding"]},"delete":{"description":"Sets `CreatorBrandingConfig.signatureUrl` to null and removes the MinIO object (best-effort). No-op if no signature exists.","operationId":"BrandingController_deleteSignature","parameters":[],"responses":{"200":{"description":"Signature deleted — returns updated branding URLs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandingResponseDto"}}}},"401":{"description":"Not authenticated"}},"security":[{"access-token":[]}],"summary":"Delete the caller's signature image","tags":["Users branding"]}},"/api/v1/users/me/branding/{slot}/reference":{"patch":{"description":"Définit logoMediaId, stampMediaId ou signatureMediaId selon le slot. mediaId=null délie sans supprimer les URL MinIO legacy. Le média doit appartenir au même tenant et être de type IMAGE.","operationId":"BrandingController_setMediaReference","parameters":[{"name":"slot","required":true,"in":"path","description":"Slot branding cible","schema":{"enum":["logo","stamp","signature"],"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMediaReferenceDto"}}}},"responses":{"200":{"description":"FK mise à jour — retourne les URLs branding courantes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandingResponseDto"}}}},"400":{"description":"Média non de type IMAGE ou slot invalide"},"401":{"description":"Non authentifié"},"404":{"description":"Média introuvable dans le tenant"}},"security":[{"access-token":[]}],"summary":"Lier/délier un média médiathèque sur un slot branding (FK directe)","tags":["Users branding"]}},"/api/v1/contact":{"post":{"description":"Endpoint public — pas d'authentification requise. Limite stricte de 3 envois par minute et par IP. Un email est envoyé à contact@qrcommunication.com et un accusé de réception au demandeur.","operationId":"ContactController_submit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitContactDto"}}}},"responses":{"204":{"description":"Message accepté et envoyé."},"422":{"description":"Validation du payload échouée."},"429":{"description":"Trop de soumissions — réessayez dans 1 minute."}},"summary":"Soumettre un message de contact (public)","tags":["Contact"]}},"/api/v1/configuration/google-calendar/connect":{"post":{"description":"Returns the Google consent URL. The frontend must redirect the user there.","operationId":"GoogleCalendarController_connect","parameters":[],"responses":{"200":{"description":"{ authorizeUrl: string }"}},"security":[{"bearer":[]}],"summary":"Initiate Google Calendar OAuth2 flow","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/callback":{"get":{"description":"Called by Google after the user grants consent. Exchanges the code for tokens, persists encrypted credentials, and redirects to frontend.","operationId":"GoogleCalendarController_callback","parameters":[{"name":"code","required":true,"in":"query","schema":{"type":"string"}},{"name":"state","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to frontend"}},"security":[{"bearer":[]}],"summary":"Google OAuth2 callback (public)","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/status":{"get":{"description":"Returns connection status without exposing tokens.","operationId":"GoogleCalendarController_getStatus","parameters":[],"responses":{"200":{"description":"GoogleCalendarStatusDto"}},"security":[{"bearer":[]}],"summary":"Get Google Calendar connection status","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/disconnect":{"delete":{"description":"Revokes OAuth2 and deletes the connection.","operationId":"GoogleCalendarController_disconnect","parameters":[],"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Disconnect Google Calendar","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/settings":{"patch":{"description":"Update syncEnabled or calendarId.","operationId":"GoogleCalendarController_updateSettings","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGoogleCalendarSettingsDto"}}}},"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Update Google Calendar settings","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/calendars":{"get":{"description":"Fetch all calendars from the connected Google account.","operationId":"GoogleCalendarController_listCalendars","parameters":[],"responses":{"200":{"description":"GoogleCalendarListDto"}},"security":[{"bearer":[]}],"summary":"List Google calendars","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/sync-now":{"post":{"description":"Register/renew watch channel for push notifications.","operationId":"GoogleCalendarController_syncNow","parameters":[],"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Force manual sync","tags":["Google Calendar Configuration"]}},"/api/v1/configuration/google-calendar/webhooks/google-calendar":{"post":{"description":"Receives push notifications from Google Calendar. Validates X-Goog-Channel-ID header.","operationId":"GoogleCalendarController_handleWebhook","parameters":[{"name":"x-goog-channel-id","required":true,"in":"header","schema":{"type":"string"}},{"name":"x-goog-resource-id","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookNotificationDto"}}}},"responses":{"204":{"description":""}},"security":[{"bearer":[]}],"summary":"Google Calendar push notification webhook (public)","tags":["Google Calendar Configuration"]}},"/api/v1/creators/google-config":{"get":{"description":"Retourne la config masquée (jamais les secrets). null si aucune config.","operationId":"CreatorGoogleConfigController_getConfig","parameters":[],"responses":{"200":{"description":"Config masquée ou null."},"401":{"description":"Non authentifié."},"403":{"description":"CREATOR ou ADMIN requis."}},"security":[{"access-token":[]}],"summary":"Lire la configuration Google OAuth du tenant actif","tags":["Creators — Google OAuth Config"]},"patch":{"description":"Mise à jour partielle. clientSecret suit le pattern 3-état : omis = inchangé, null = effacé, string non-vide = chiffré.","operationId":"CreatorGoogleConfigController_updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCreatorGoogleConfigBodyDto"}}}},"responses":{"200":{"description":"Config mise à jour (masquée)."},"400":{"description":"Données invalides ou tenant manquant."},"401":{"description":"Non authentifié."},"403":{"description":"CREATOR ou ADMIN requis."}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration Google OAuth du tenant actif","tags":["Creators — Google OAuth Config"]},"delete":{"description":"Désactive la config et efface les secrets. La row est conservée pour audit trail.","operationId":"CreatorGoogleConfigController_deleteConfig","parameters":[],"responses":{"204":{"description":"Config désactivée et secrets effacés."},"401":{"description":"Non authentifié."},"403":{"description":"CREATOR ou ADMIN requis."}},"security":[{"access-token":[]}],"summary":"Supprimer la configuration Google OAuth du tenant actif","tags":["Creators — Google OAuth Config"]}},"/api/v1/courses/{courseId}/trainers":{"get":{"description":"Returns all trainers assigned to the course with their roles and details.","operationId":"CourseTrainersController_list","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"List of trainers","content":{"application/json":{"schema":{"example":[{"userId":"user-123","role":"PRIMARY","user":{"firstName":"John","lastName":"Doe","email":"john@example.com","role":"TRAINER"}}]}}}},"401":{"description":"Unauthorized"},"403":{"description":"Access denied"},"404":{"description":"Course not found"}},"security":[{"bearer":[]}],"summary":"List trainers for a course","tags":["Course Trainers"]},"post":{"description":"Assigns an existing TRAINER, CREATOR, or ADMIN user as a trainer for this course.","operationId":"CourseTrainersController_assign","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignTrainerDto"}}}},"responses":{"201":{"description":"Trainer assigned"},"400":{"description":"Invalid user role"},"401":{"description":"Unauthorized"},"403":{"description":"Access denied"},"404":{"description":"Course or user not found"}},"security":[{"bearer":[]}],"summary":"Assign a trainer to a course","tags":["Course Trainers"]}},"/api/v1/courses/{courseId}/trainers/{userId}":{"delete":{"description":"Removes the trainer assignment. The user account is NOT deleted.","operationId":"CourseTrainersController_remove","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"204":{"description":"Trainer removed"},"401":{"description":"Unauthorized"},"403":{"description":"Access denied"},"404":{"description":"Course or trainer not found"}},"security":[{"bearer":[]}],"summary":"Remove a trainer from a course","tags":["Course Trainers"]}},"/api/v1/courses/{courseId}/trainers/invite":{"post":{"description":"Sends an invite email. If the email does not exist, creates a new TRAINER user. If the email exists, verifies the user has TRAINER/CREATOR/ADMIN role.","operationId":"CourseTrainersController_invite","parameters":[{"name":"courseId","required":true,"in":"path","description":"Course CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteTrainerDto"}}}},"responses":{"201":{"description":"Trainer invited"},"400":{"description":"Invalid input or user role"},"401":{"description":"Unauthorized"},"403":{"description":"Access denied"},"404":{"description":"Course not found"}},"security":[{"bearer":[]}],"summary":"Invite a trainer to a course","tags":["Course Trainers"]}},"/api/v1/trainer/my-sessions":{"get":{"description":"Returns sessions where I am the trainer or part of an assigned course.","operationId":"TrainerController_getMySessionsSession","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}},{"name":"take","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"List of sessions","content":{"application/json":{"schema":{"example":[{"id":"session-123","title":"Live Q&A","status":"SCHEDULED","scheduledAt":"2026-04-20T14:00:00Z","duration":60,"course":{"id":"course-123","title":"Advanced React"}}]}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Get my assigned sessions","tags":["Trainer Dashboard"]}},"/api/v1/trainer/my-courses":{"get":{"description":"Returns all courses where I am assigned as a trainer, with enrollment counts.","operationId":"TrainerController_getMyCourses","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}},{"name":"take","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"List of courses","content":{"application/json":{"schema":{"example":[{"id":"course-123","title":"Advanced React","description":"Deep dive into React patterns","status":"PUBLISHED","_count":{"enrollments":25}}]}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Get my assigned courses","tags":["Trainer Dashboard"]}},"/api/v1/trainer/my-stats":{"get":{"description":"Returns total hours taught, learner count, and attendance rate.","operationId":"TrainerController_getMyStats","parameters":[],"responses":{"200":{"description":"Trainer statistics","content":{"application/json":{"schema":{"example":{"totalHours":42.5,"totalLearners":87,"attendanceRate":92}}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Get my trainer statistics","tags":["Trainer Dashboard"]}},"/api/v1/trainer/my-emargements":{"get":{"description":"Returns attendance records (emargements) for all my sessions. Includes presence status and signature details.","operationId":"TrainerController_getMyEmargements","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}},{"name":"take","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"List of emargements","content":{"application/json":{"schema":{"example":[{"id":"emarg-123","userId":"user-456","present":true,"sessionId":"session-789","signatures":[{"id":"sig-123","method":"EMAIL_CLICK","signedAt":"2026-04-20T14:30:00Z"}]}]}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Get my emargements","tags":["Trainer Dashboard"]}},"/api/v1/trainer/my-qcm-results":{"get":{"description":"Returns questionnaires (PRE/POST) for my courses with response rates.","operationId":"TrainerController_getMyQcmResults","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}},{"name":"take","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"List of questionnaires with response rates","content":{"application/json":{"schema":{"example":[{"id":"qcm-123","courseId":"course-123","type":"PRE","title":"Pre-Course Assessment","responseCount":18,"totalEnrolled":25}]}}}},"401":{"description":"Unauthorized"}},"security":[{"bearer":[]}],"summary":"Get my QCM results","tags":["Trainer Dashboard"]}},"/api/v1/ai-copilot/suggest":{"post":{"description":"Retourne 3 à 5 suggestions basées sur le chemin courant. Aucun crédit consommé.","operationId":"AiCopilotController_suggest","parameters":[],"responses":{"200":{"description":"Liste de suggestions IA","content":{"application/json":{"schema":{"type":"array"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Obtenir des suggestions IA contextuelles","tags":["AI Copilot"]}},"/api/v1/ai-copilot/execute":{"post":{"description":"Exécute l'action IA demandée (COMPLETE_SECTION, IMPROVE_TEXT, etc.) et débite les crédits après succès.","operationId":"AiCopilotController_execute","parameters":[],"responses":{"200":{"description":"Action exécutée avec succès","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"},"result":{"type":"string"},"creditsCost":{"type":"number"}}}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes ou crédits insuffisants"}},"security":[{"bearer":[]}],"summary":"Exécuter une action IA et consommer des crédits","tags":["AI Copilot"]}},"/api/v1/learner-conversations":{"post":{"description":"LEARNER : initie vers un formateur (courseId ou trainerId requis). CREATOR/ADMIN : initie vers un apprenant du meme tenant (learnerId requis). Idempotent : retourne la conversation existante si le duo learner/trainer existe deja.","operationId":"LearnerConversationsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLearnerConversationDto"}}}},"responses":{"201":{"description":"Conversation creee ou existante retournee"},"400":{"description":"Parametres manquants selon le role"},"404":{"description":"Formateur ou apprenant introuvable dans ce tenant"}},"security":[{"access-token":[]}],"summary":"Creer une nouvelle conversation","tags":["Learner Conversations"]},"get":{"operationId":"LearnerConversationsController_list","parameters":[],"responses":{"200":{"description":"Liste des conversations"}},"security":[{"access-token":[]}],"summary":"Lister mes conversations (apprenant ou formateur)","tags":["Learner Conversations"]}},"/api/v1/learner-conversations/{id}":{"get":{"operationId":"LearnerConversationsController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Conversation CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Conversation detaillee"},"404":{"description":"Introuvable ou acces refuse"}},"security":[{"access-token":[]}],"summary":"Detail d'une conversation + messages","tags":["Learner Conversations"]}},"/api/v1/learner-conversations/{id}/messages":{"post":{"operationId":"LearnerConversationsController_sendMessage","parameters":[{"name":"id","required":true,"in":"path","description":"Conversation CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendLearnerConversationMessageDto"}}}},"responses":{"201":{"description":"Message envoye"}},"security":[{"access-token":[]}],"summary":"Poster un nouveau message dans la conversation","tags":["Learner Conversations"]}},"/api/v1/paf/enrollments/{enrollmentId}/timeline":{"get":{"description":"Récupère la timeline exhaustive des activités d'un apprenant pour audit Qualiopi.","operationId":"PafController_getEnrollmentPafTimeline","parameters":[{"name":"enrollmentId","required":true,"in":"path","description":"CUID de l'inscription","schema":{"type":"string"}}],"responses":{"200":{"description":"Timeline des activités de l'apprenant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes ou tenant non résolu"},"404":{"description":"Inscription introuvable"}},"security":[{"access-token":[]}],"summary":"Timeline PAF apprenant","tags":["PAF - Preuve d'Activité de Formation"]}},"/api/v1/paf/sessions/{sessionId}/timeline":{"get":{"description":"Récupère la timeline d'une session live (convocations, présences, signatures)","operationId":"PafController_getSessionPafTimeline","parameters":[{"name":"sessionId","required":true,"in":"path","description":"CUID de la session live","schema":{"type":"string"}}],"responses":{"200":{"description":"Timeline de la session (convocations, présences, signatures)"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes ou tenant non résolu"},"404":{"description":"Session introuvable"}},"security":[{"access-token":[]}],"summary":"Timeline PAF session","tags":["PAF - Preuve d'Activité de Formation"]}},"/api/v1/paf/courses/{courseId}/summary":{"get":{"description":"Récupère les métriques agrégées PAF d'une formation pour tableau de bord Qualiopi.","operationId":"PafController_getCoursePafSummary","parameters":[{"name":"courseId","required":true,"in":"path","description":"CUID du cours","schema":{"type":"string"}}],"responses":{"200":{"description":"Métriques agrégées PAF (complétion, assiduité, satisfaction)"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes ou tenant non résolu"},"404":{"description":"Cours introuvable"}},"security":[{"access-token":[]}],"summary":"Résumé PAF formation","tags":["PAF - Preuve d'Activité de Formation"]}},"/api/v1/conversations":{"get":{"operationId":"EmailInboxController_listThreads","parameters":[{"name":"courseId","required":true,"in":"query","schema":{"type":"string"}},{"name":"sessionId","required":true,"in":"query","schema":{"type":"string"}},{"name":"unreadOnly","required":true,"in":"query","schema":{"type":"string"}},{"name":"page","required":true,"in":"query","schema":{"type":"string"}},{"name":"pageSize","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List conversation threads","tags":["Email Inbox"]}},"/api/v1/conversations/sync":{"post":{"operationId":"EmailInboxController_syncInbox","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Force manual inbox sync (IMAP or Gmail)","tags":["Email Inbox"]}},"/api/v1/conversations/gmail/status":{"get":{"operationId":"EmailInboxController_getGmailStatus","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get Gmail connection status for current user","tags":["Email Inbox"]}},"/api/v1/conversations/gmail/connect":{"get":{"operationId":"EmailInboxController_connectGmail","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Initiate Gmail OAuth2 authorization","tags":["Email Inbox"]}},"/api/v1/conversations/gmail/callback":{"get":{"operationId":"EmailInboxController_gmailCallback","parameters":[{"name":"code","required":true,"in":"query","schema":{"type":"string"}},{"name":"state","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Handle Gmail OAuth2 callback (called by Google)","tags":["Email Inbox"]}},"/api/v1/conversations/gmail/disconnect":{"delete":{"operationId":"EmailInboxController_disconnectGmail","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Disconnect Gmail account","tags":["Email Inbox"]}},"/api/v1/conversations/gmail/sync":{"post":{"operationId":"EmailInboxController_triggerGmailSync","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Trigger incremental Gmail sync","tags":["Email Inbox"]}},"/api/v1/conversations/sequences":{"get":{"operationId":"EmailInboxController_listSequences","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List email sequences","tags":["Email Inbox"]},"post":{"operationId":"EmailInboxController_createSequence","parameters":[],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Create new email sequence","tags":["Email Inbox"]}},"/api/v1/conversations/sequences/available-templates":{"get":{"operationId":"EmailInboxController_getAvailableTemplates","parameters":[],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"List email templates available for sequence steps","tags":["Email Inbox"]}},"/api/v1/conversations/sequences/{id}":{"get":{"operationId":"EmailInboxController_getSequence","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get sequence detail","tags":["Email Inbox"]},"patch":{"operationId":"EmailInboxController_updateSequence","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Update email sequence","tags":["Email Inbox"]},"delete":{"operationId":"EmailInboxController_deleteSequence","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Delete email sequence","tags":["Email Inbox"]}},"/api/v1/conversations/sequences/{id}/duplicate":{"post":{"operationId":"EmailInboxController_duplicateSequence","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Duplicate email sequence (creates an inactive copy with \"(copie)\" suffix)","tags":["Email Inbox"]}},"/api/v1/conversations/{id}":{"get":{"operationId":"EmailInboxController_getThread","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Get thread detail with messages","tags":["Email Inbox"]}},"/api/v1/conversations/{id}/reply":{"post":{"operationId":"EmailInboxController_sendReply","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplyMessageDto"}}}},"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Send reply to conversation thread","tags":["Email Inbox"]}},"/api/v1/conversations/{msgId}/read":{"patch":{"operationId":"EmailInboxController_markAsRead","parameters":[{"name":"msgId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"bearer":[]}],"summary":"Mark message as read","tags":["Email Inbox"]}},"/api/v1/conversations/{msgId}/classify":{"post":{"operationId":"EmailInboxController_classifyMessage","parameters":[{"name":"msgId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Classify a message with AI","tags":["Email Inbox"]}},"/api/v1/conversations/{threadId}/classify-all":{"post":{"operationId":"EmailInboxController_classifyThread","parameters":[{"name":"threadId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"security":[{"bearer":[]}],"summary":"Classify all messages in a thread","tags":["Email Inbox"]}},"/api/v1/studio/trainers/{id}/avatar/generate":{"post":{"operationId":"VideoStudioController_generateAvatar","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du formateur","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateAvatarDto"}}}},"responses":{"201":{"description":"Avatar généré avec succès","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrainerAvatarResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Générer un avatar IA pour un formateur","tags":["Video Studio"]}},"/api/v1/studio/trainers/{id}/avatar":{"get":{"operationId":"VideoStudioController_getAvatar","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du formateur","schema":{"type":"string"}}],"responses":{"200":{"description":"Avatar du formateur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrainerAvatarResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Avatar non trouvé"}},"security":[{"bearer":[]}],"summary":"Récupérer l'avatar IA d'un formateur","tags":["Video Studio"]}},"/api/v1/studio/projects":{"post":{"operationId":"VideoStudioController_createProject","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStudioProjectDto"}}}},"responses":{"201":{"description":"Projet créé avec succès","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioProjectResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Créer un nouveau projet studio vidéo","tags":["Video Studio"]},"get":{"operationId":"VideoStudioController_listProjects","parameters":[{"name":"courseId","required":false,"in":"query","description":"Filtrer par cours","schema":{"type":"string"}},{"name":"take","required":false,"in":"query","description":"Nombre de résultats (défaut: 20)","schema":{"type":"number"}},{"name":"skip","required":false,"in":"query","description":"Offset de pagination (défaut: 0)","schema":{"type":"number"}}],"responses":{"200":{"description":"Liste paginée des projets studio","content":{"application/json":{"schema":{"properties":{"items":{"type":"array"},"total":{"type":"number"}}}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Lister les projets studio du tenant","tags":["Video Studio"]}},"/api/v1/studio/projects/{id}":{"patch":{"operationId":"VideoStudioController_updateProject","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du projet studio","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStudioProjectDto"}}}},"responses":{"200":{"description":"Projet mis à jour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioProjectResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Projet non trouvé"}},"security":[{"bearer":[]}],"summary":"Mettre à jour les pistes d'un projet studio","tags":["Video Studio"]},"get":{"operationId":"VideoStudioController_getProjectStatus","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du projet studio","schema":{"type":"string"}}],"responses":{"200":{"description":"Statut du projet studio","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioProjectResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Projet non trouvé"}},"security":[{"bearer":[]}],"summary":"Récupérer le statut d'un projet studio","tags":["Video Studio"]}},"/api/v1/studio/projects/{id}/render":{"post":{"operationId":"VideoStudioController_renderProject","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du projet studio","schema":{"type":"string"}}],"responses":{"202":{"description":"Rendu lancé (traitement asynchrone)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioProjectResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Projet non trouvé"}},"security":[{"bearer":[]}],"summary":"Lancer le rendu d'un projet studio vidéo","tags":["Video Studio"]}},"/api/v1/bpf/{year}":{"get":{"description":"Retourne le rapport Bilan Pédagogique et Financier avec toutes les sections auto-calculées.","operationId":"BpfController_getReport","parameters":[{"name":"year","required":true,"in":"path","description":"Année du rapport BPF (ex: 2024)","schema":{"type":"number"}}],"responses":{"200":{"description":"Rapport BPF calculé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BpfReportResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Récupérer le rapport BPF d'une année","tags":["BPF"]},"post":{"description":"Enregistre les charges saisies manuellement dans l'onglet B (personnel, locaux, matériels, autres).","operationId":"BpfController_saveCharges","parameters":[{"name":"year","required":true,"in":"path","description":"Année du rapport BPF","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveBpfChargesDto"}}}},"responses":{"200":{"description":"Charges sauvegardées, rapport recalculé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BpfReportResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Sauvegarder les charges manuelles du BPF","tags":["BPF"]}},"/api/v1/bpf/{year}/validate":{"post":{"description":"Passe le rapport BPF au statut VALIDATED. Action irréversible.","operationId":"BpfController_validateReport","parameters":[{"name":"year","required":true,"in":"path","description":"Année du rapport BPF","schema":{"type":"number"}}],"responses":{"200":{"description":"Rapport validé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BpfReportResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Rapport non trouvé"}},"security":[{"access-token":[]}],"summary":"Valider le rapport BPF","tags":["BPF"]}},"/api/v1/bpf/{year}/export-pdf":{"get":{"description":"Génère et télécharge le rapport BPF au format PDF Cerfa officiel.","operationId":"BpfController_exportPdf","parameters":[{"name":"year","required":true,"in":"path","description":"Année du rapport BPF","schema":{"type":"number"}}],"responses":{"200":{"description":"Fichier PDF du BPF","content":{"application/json":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Rapport non trouvé"}},"security":[{"access-token":[]}],"summary":"Exporter le BPF en PDF (Cerfa 10443-17)","tags":["BPF"]}},"/api/v1/bpf/{year}/export-csv":{"get":{"description":"Génère et télécharge le rapport BPF au format CSV (UTF-8 BOM, séparateur ;).","operationId":"BpfController_exportCsv","parameters":[{"name":"year","required":true,"in":"path","description":"Année du rapport BPF","schema":{"type":"number"}}],"responses":{"200":{"description":"Fichier CSV du BPF","content":{"application/json":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Rapport non trouvé"}},"security":[{"access-token":[]}],"summary":"Exporter le BPF en CSV","tags":["BPF"]}},"/api/v1/credit-store/packs":{"get":{"operationId":"CreditStoreController_listPacks","parameters":[],"responses":{"200":{"description":""}},"summary":"Liste les packs de crédits actifs (public)","tags":["Credit Store"]}},"/api/v1/credit-store/checkout":{"post":{"operationId":"CreditStoreController_createCheckout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCreditCheckoutDto"}}}},"responses":{"201":{"description":""}},"security":[{"access-token":[]}],"summary":"Créer une Stripe Checkout Session (redirect flow)","tags":["Credit Store"]}},"/api/v1/credit-store/purchase-intent":{"post":{"description":"Retourne clientSecret + paymentIntentId pour compléter le paiement directement dans la page via Stripe Elements. Le webhook payment_intent.succeeded crédite automatiquement le compte.","operationId":"CreditStoreController_createPurchaseIntent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseIntentDto"}}}},"responses":{"201":{"description":""},"404":{"description":"Pack introuvable ou inactif"}},"security":[{"access-token":[]}],"summary":"Créer un Stripe PaymentIntent (Stripe Elements inline flow)","tags":["Credit Store"]}},"/api/v1/credit-store/webhooks/stripe-credits":{"post":{"operationId":"CreditStoreController_handleWebhook","parameters":[],"responses":{"201":{"description":""}},"summary":"Stripe webhook handler (checkout + payment_intent)","tags":["Credit Store"]}},"/api/v1/admin/analytics/overview":{"get":{"description":"Retourne les métriques agrégées cross-tenant : tenants, users, cours, inscriptions, revenus du mois courant et du mois précédent, sessions live.","operationId":"AdminAnalyticsController_getOverview","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminOverviewDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"KPIs globaux plateforme","tags":["Admin — Analytics"]}},"/api/v1/admin/analytics/growth":{"get":{"description":"Retourne le nombre d'inscriptions et de paiements par jour pour la plage spécifiée.","operationId":"AdminAnalyticsController_getGrowth","parameters":[{"name":"range","required":false,"in":"query","schema":{"default":"30d","type":"string","enum":["7d","30d","90d","365d"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GrowthDataPointDto"}}}}},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Timeseries inscriptions + paiements","tags":["Admin — Analytics"]}},"/api/v1/admin/settings":{"get":{"description":"Retourne le singleton de configuration globale. Secrets jamais exposés — uniquement les indicateurs (hasPassword, last4).","operationId":"AdminSettingsController_getSettings","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Lire les paramètres plateforme","tags":["Admin — Settings"]},"patch":{"description":"Mise à jour partielle. Secrets suivent le pattern 3-état : omis = inchangé, null = effacé, string non-vide = chiffré et stocké.","operationId":"AdminSettingsController_updateSettings","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePlatformSettingDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}},"400":{"description":"Validation error"},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Mettre à jour les paramètres généraux, mail et Stripe","tags":["Admin — Settings"]}},"/api/v1/admin/settings/livekit":{"patch":{"description":"url, apiKey (public), apiSecret (chiffré). Pattern 3-état pour apiSecret.","operationId":"AdminSettingsController_updateLiveKit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLiveKitSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration LiveKit","tags":["Admin — Settings"]}},"/api/v1/admin/settings/ai":{"patch":{"description":"mistralApiKey, mistralModel, assemblyaiApiKey — tous 3-état pour les secrets.","operationId":"AdminSettingsController_updateAi","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAiSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration IA (Mistral + AssemblyAI)","tags":["Admin — Settings"]}},"/api/v1/admin/settings/storage":{"patch":{"description":"endpoint, region, bucket, accessKey (public), secretKey (chiffré), forcePathStyle, useSsl.","operationId":"AdminSettingsController_updateStorage","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStorageSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration stockage S3/MinIO/iDrive e2","tags":["Admin — Settings"]}},"/api/v1/admin/settings/meilisearch":{"patch":{"description":"url (public), masterKey (chiffré 3-état).","operationId":"AdminSettingsController_updateMeilisearch","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMeilisearchSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration Meilisearch","tags":["Admin — Settings"]}},"/api/v1/admin/settings/vapid":{"patch":{"description":"publicKey, privateKey (chiffré), subject.","operationId":"AdminSettingsController_updateVapid","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateVapidSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration VAPID (push web)","tags":["Admin — Settings"]}},"/api/v1/admin/settings/scellio":{"patch":{"description":"apiKey et webhookSecret — tous deux chiffrés au repos (3-état).","operationId":"AdminSettingsController_updateScellio","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScellioSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration Scellio (eIDAS)","tags":["Admin — Settings"]}},"/api/v1/admin/settings/stripe-connect":{"patch":{"description":"webhookSecretCredits, webhookSecretConnect (chiffrés), connectClientId, connectRedirectUri.","operationId":"AdminSettingsController_updateStripeConnect","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStripeConnectSettingsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformSettingDto"}}}}},"security":[{"access-token":[]}],"summary":"Mettre à jour la configuration Stripe Connect","tags":["Admin — Settings"]}},"/api/v1/admin/settings/test-mail":{"post":{"description":"Envoie un email de test à l'adresse de l'admin authentifié avec le provider actif (smtp | resend).","operationId":"AdminSettingsController_testMail","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}},"security":[{"access-token":[]}],"summary":"Tester la configuration email","tags":["Admin — Settings"]}},"/api/v1/admin/settings/test-stripe":{"post":{"description":"Appelle stripe.accounts.list() avec la clé platform stockée (DB ou env). Valide la clé sans déclencher de charge.","operationId":"AdminSettingsController_testStripe","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}},"security":[{"access-token":[]}],"summary":"Tester la configuration Stripe","tags":["Admin — Settings"]}},"/api/v1/admin/settings/test-storage":{"post":{"description":"List les buckets avec les credentials stockés. Valide endpoint, accessKey et secretKey.","operationId":"AdminSettingsController_testStorage","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}},"security":[{"access-token":[]}],"summary":"Tester la connexion S3/MinIO/iDrive e2","tags":["Admin — Settings"]}},"/api/v1/admin/settings/test-livekit":{"post":{"description":"Appelle listRooms() sur l'API LiveKit avec les credentials stockés. Valide url, apiKey et apiSecret.","operationId":"AdminSettingsController_testLiveKit","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}},"security":[{"access-token":[]}],"summary":"Tester la connexion LiveKit","tags":["Admin — Settings"]}},"/api/v1/admin/settings/test-meilisearch":{"post":{"description":"Appelle GET /health sur l'URL Meilisearch configurée. Valide url et masterKey.","operationId":"AdminSettingsController_testMeilisearch","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}},"security":[{"access-token":[]}],"summary":"Tester la connexion Meilisearch","tags":["Admin — Settings"]}},"/api/v1/admin/settings/test-ai":{"post":{"description":"Envoie un prompt trivial à Mistral pour valider la clé API.","operationId":"AdminSettingsController_testAi","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}},"security":[{"access-token":[]}],"summary":"Tester la connexion IA (Mistral)","tags":["Admin — Settings"]}},"/api/v1/admin/tenants":{"get":{"description":"Cross-tenant. Filtre par status (active/suspended/deleted) et search.","operationId":"AdminTenantsController_findAll","parameters":[{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}},{"name":"status","required":false,"in":"query","schema":{"example":"active","type":"string","enum":["active","suspended","deleted"]}},{"name":"search","required":false,"in":"query","schema":{"example":"acme","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantListDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Liste paginée des tenants","tags":["Admin — Tenants"]}},"/api/v1/admin/tenants/{id}/suspend":{"post":{"operationId":"AdminTenantsController_suspend","parameters":[{"name":"id","required":true,"in":"path","description":"Tenant CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantDto"}}}},"404":{"description":"Tenant introuvable"}},"security":[{"access-token":[]}],"summary":"Suspendre un tenant","tags":["Admin — Tenants"]}},"/api/v1/admin/tenants/{id}/reactivate":{"post":{"operationId":"AdminTenantsController_reactivate","parameters":[{"name":"id","required":true,"in":"path","description":"Tenant CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTenantDto"}}}},"404":{"description":"Tenant introuvable"}},"security":[{"access-token":[]}],"summary":"Réactiver un tenant suspendu","tags":["Admin — Tenants"]}},"/api/v1/admin/tenants/{id}":{"delete":{"operationId":"AdminTenantsController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"Tenant CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}},"404":{"description":"Tenant introuvable"}},"security":[{"access-token":[]}],"summary":"Soft-delete un tenant","tags":["Admin — Tenants"]}},"/api/v1/admin/users":{"get":{"description":"Filtre par role, tenantId, search. Pagination cursor-free (page/limit).","operationId":"AdminUsersController_findAll","parameters":[{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}},{"name":"role","required":false,"in":"query","schema":{"type":"string","enum":["ADMIN","CREATOR","TRAINER","LEARNER","FUNDER"]}},{"name":"tenantId","required":false,"in":"query","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"search","required":false,"in":"query","schema":{"example":"alice","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserListDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Liste paginée des utilisateurs (cross-tenant)","tags":["Admin — Users"]}},"/api/v1/admin/users/{id}":{"get":{"operationId":"AdminUsersController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDto"}}}},"404":{"description":"Utilisateur introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'un utilisateur","tags":["Admin — Users"]},"patch":{"operationId":"AdminUsersController_update","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdminUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDto"}}}},"400":{"description":"Validation error"},"404":{"description":"Utilisateur introuvable"}},"security":[{"access-token":[]}],"summary":"Mettre à jour un utilisateur (rôle / activation / tenant)","tags":["Admin — Users"]},"delete":{"operationId":"AdminUsersController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}},"404":{"description":"Utilisateur introuvable"}},"security":[{"access-token":[]}],"summary":"Soft-delete d'un utilisateur","tags":["Admin — Users"]}},"/api/v1/admin/users/{id}/suspend":{"post":{"operationId":"AdminUsersController_suspend","parameters":[{"name":"id","required":true,"in":"path","description":"User CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDto"}}}},"404":{"description":"Utilisateur introuvable"}},"security":[{"access-token":[]}],"summary":"Suspendre un utilisateur (isActivated = false)","tags":["Admin — Users"]}},"/api/v1/admin/audit-logs":{"get":{"description":"Retourne les entrées AuditLog de tous les tenants avec filtres optionnels par userId, action, tenantId, résultat et plage de dates.","operationId":"AdminAuditLogsController_findAll","parameters":[{"name":"page","required":false,"in":"query","description":"Numéro de page (≥ 1)","schema":{"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","description":"Entrées par page (max 100)","schema":{"default":25,"type":"number"}},{"name":"userId","required":false,"in":"query","description":"Filtrer par userId","schema":{"type":"string"}},{"name":"action","required":false,"in":"query","description":"Filtrer par action (LOGIN_FAILED, ROLE_CHANGE, etc.)","schema":{"type":"string"}},{"name":"tenantId","required":false,"in":"query","description":"Filtrer par tenantId (cross-tenant)","schema":{"type":"string"}},{"name":"result","required":false,"in":"query","description":"Filtrer par résultat : SUCCESS | FAILURE","schema":{"type":"string"}},{"name":"dateFrom","required":false,"in":"query","description":"Date de début ISO-8601 (incluse)","schema":{"type":"string"}},{"name":"dateTo","required":false,"in":"query","description":"Date de fin ISO-8601 (incluse)","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Journal de sécurité cross-tenant paginé (ADMIN)","tags":["Admin / Audit Logs"]}},"/api/v1/admin/security/sessions/active":{"get":{"description":"Retourne la liste des utilisateurs ayant un token JWT actif. Le système utilise des JWT stateless — cette vue synthétise les utilisateurs connectés récemment (lastLoginAt < 24h).","operationId":"AdminSecurityController_activeSessions","parameters":[],"responses":{"200":{"description":"Liste des utilisateurs connectés récemment (lastLoginAt < 24h)"},"401":{"description":"Non authentifié"},"403":{"description":"Réservé aux ADMIN système"}},"security":[{"access-token":[]}],"summary":"Sessions utilisateurs actives (ADMIN)","tags":["Admin / Security"]}},"/api/v1/admin/security/failed-logins":{"get":{"description":"Agrège les AuditLog action=LOGIN_FAILED sur 7j ou 30j.","operationId":"AdminSecurityController_failedLogins","parameters":[{"name":"range","required":false,"in":"query","description":"Plage temporelle : 7d ou 30d","schema":{"default":"7d","type":"string","enum":["7d","30d"]}}],"responses":{"200":{"description":"Décompte des tentatives échouées par jour sur la période"},"401":{"description":"Non authentifié"},"403":{"description":"Réservé aux ADMIN système"}},"security":[{"access-token":[]}],"summary":"Tentatives de connexion échouées par période (ADMIN)","tags":["Admin / Security"]}},"/api/v1/admin/security/events":{"get":{"description":"HIGH   → LOGIN_FAILED, PERMISSION_DENIED, DATA_EXPORT, SESSION_REVOKE_ALL\nMEDIUM → ROLE_CHANGE, API_KEY_CREATED, PASSWORD_RESET\nLOW    → PASSWORD_CHANGE, PROFILE_UPDATE","operationId":"AdminSecurityController_securityEvents","parameters":[{"name":"severity","required":false,"in":"query","description":"Sévérité : LOW | MEDIUM | HIGH","schema":{"type":"string","enum":["LOW","MEDIUM","HIGH"]}},{"name":"page","required":false,"in":"query","schema":{"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":25,"type":"number"}}],"responses":{"200":{"description":"Page d'événements de sécurité filtrés par sévérité"},"401":{"description":"Non authentifié"},"403":{"description":"Réservé aux ADMIN système"}},"security":[{"access-token":[]}],"summary":"Événements de sécurité filtrés par sévérité (ADMIN)","tags":["Admin / Security"]}},"/api/v1/admin/security/sessions/{userId}/revoke-all":{"post":{"description":"Invalide le passwordHash en le re-hachant avec un nouveau sel, forçant l'expiration de tous les refresh tokens existants au prochain appel. Enregistre un AuditLog SESSION_REVOKE_ALL.","operationId":"AdminSecurityController_revokeAllSessions","parameters":[{"name":"userId","required":true,"in":"path","description":"ID de l'utilisateur cible","schema":{"type":"string"}}],"responses":{"200":{"description":"Toutes les sessions de l'utilisateur révoquées"},"401":{"description":"Non authentifié"},"403":{"description":"Réservé aux ADMIN système"},"404":{"description":"Utilisateur introuvable"}},"security":[{"access-token":[]}],"summary":"Révoquer toutes les sessions d'un utilisateur (ADMIN)","tags":["Admin / Security"]}},"/api/v1/admin/qualiopi/overview":{"get":{"description":"Agrège pour chaque tenant : statut Qualiopi, indicateurs globaux moyens et dossiers en retard (derniers documents > 6 mois).","operationId":"AdminQualiopiController_overview","parameters":[],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Vue agrégée Qualiopi de tous les tenants (ADMIN)","tags":["Admin / Qualiopi"]}},"/api/v1/admin/qualiopi/tenants/{slug}/summary":{"get":{"description":"Retourne le tableau de bord Qualiopi complet d'un tenant identifié par son slug, en réutilisant QualiopiService.getDashboard().","operationId":"AdminQualiopiController_tenantSummary","parameters":[{"name":"slug","required":true,"in":"path","description":"Slug du tenant","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"access-token":[]}],"summary":"Résumé Qualiopi d'un tenant (ADMIN)","tags":["Admin / Qualiopi"]}},"/api/v1/admin/payments/creators":{"get":{"description":"Cross-tenant. Inclut le statut Stripe (active/pending/restricted), les flags de capability (charges, payouts, onboarding) et le stripeAccountId masqué.","operationId":"AdminPaymentsController_listCreators","parameters":[{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}},{"name":"tenantId","required":false,"in":"query","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"status","required":false,"in":"query","schema":{"example":"active","type":"string","enum":["active","pending","restricted"]}},{"name":"search","required":false,"in":"query","schema":{"example":"alice","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminConnectedCreatorListDto"}}}},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Liste des créateurs connectés à Stripe","tags":["Admin — Payments"]}},"/api/v1/admin/payments/transactions":{"get":{"description":"Filtres : tenantId, status, startDate, endDate. Retourne les totaux agrégés (gross, fees, net to creators) calculés sur le set filtré (tout le scope, pas seulement la page en cours).","operationId":"AdminPaymentsController_listTransactions","parameters":[{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}},{"name":"tenantId","required":false,"in":"query","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["PENDING","SUCCEEDED","FAILED","REFUNDED","PARTIAL_REFUND"]}},{"name":"startDate","required":false,"in":"query","schema":{"example":"2026-01-01","type":"string"}},{"name":"endDate","required":false,"in":"query","schema":{"example":"2026-12-31","type":"string"}},{"name":"format","required":false,"in":"query","schema":{"default":"json","type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTransactionListDto"}}}},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Liste paginée des transactions cross-tenant","tags":["Admin — Payments"]}},"/api/v1/admin/payments/transactions/export":{"get":{"description":"Même filtres que /transactions. Retourne un text/csv (UTF-8) streamé, capped à 10 000 lignes pour éviter les OOM.","operationId":"AdminPaymentsController_exportTransactions","parameters":[{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}},{"name":"tenantId","required":false,"in":"query","schema":{"example":"clx8abc123def456","type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["PENDING","SUCCEEDED","FAILED","REFUNDED","PARTIAL_REFUND"]}},{"name":"startDate","required":false,"in":"query","schema":{"example":"2026-01-01","type":"string"}},{"name":"endDate","required":false,"in":"query","schema":{"example":"2026-12-31","type":"string"}},{"name":"format","required":false,"in":"query","schema":{"default":"json","type":"string","enum":["json","csv"]}}],"responses":{"200":{"content":{"text/csv":{"schema":{"type":"string"}}},"description":""}},"security":[{"access-token":[]}],"summary":"Export CSV des transactions filtrées","tags":["Admin — Payments"]}},"/api/v1/admin/email-inbox/gmail-connections":{"get":{"description":"Retourne toutes les connexions Gmail (tous tenants confondus). Jamais les tokens.","operationId":"AdminEmailInboxController_listGmailConnections","parameters":[],"responses":{"200":{"description":"Liste des connexions Gmail"},"401":{"description":"Non authentifié"},"403":{"description":"ADMIN requis"}},"security":[{"access-token":[]}],"summary":"Liste cross-tenant des connexions Gmail","tags":["Admin — Email Inbox"]}},"/api/v1/admin/email-inbox/stats":{"get":{"description":"Agrégats inbound/outbound/gmail-actives sur les 30 derniers jours.","operationId":"AdminEmailInboxController_getStats","parameters":[],"responses":{"200":{"description":"Statistiques email cross-tenant"}},"security":[{"access-token":[]}],"summary":"Statistiques email 30 jours","tags":["Admin — Email Inbox"]}},"/api/v1/admin/email-inbox/gmail-connections/{id}":{"delete":{"description":"Super-admin uniquement. Révoque les tokens chez Google et supprime la GmailConnection.","operationId":"AdminEmailInboxController_revokeConnection","parameters":[{"name":"id","required":true,"in":"path","description":"GmailConnection CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}},"404":{"description":"Connexion introuvable"}},"security":[{"access-token":[]}],"summary":"Forcer la révocation d'une connexion Gmail","tags":["Admin — Email Inbox"]}},"/api/v1/admin/credit-packs":{"get":{"operationId":"AdminCreditPacksController_findAll","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CreditPackResponseDto"}}}}}},"security":[{"access-token":[]}],"summary":"Liste tous les packs de crédits (actifs + inactifs)","tags":["Admin — Credit Packs"]},"post":{"description":"Crée le pack en DB puis le synchronise automatiquement vers Stripe (product + price).","operationId":"AdminCreditPacksController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCreditPackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditPackResponseDto"}}}}},"security":[{"access-token":[]}],"summary":"Créer un pack de crédits","tags":["Admin — Credit Packs"]}},"/api/v1/admin/credit-packs/{id}":{"get":{"operationId":"AdminCreditPacksController_findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditPackResponseDto"}}}},"404":{"description":"Pack introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'un pack de crédits","tags":["Admin — Credit Packs"]},"patch":{"description":"Met à jour le pack en DB. Si le prix change, archive l'ancien Stripe Price et crée un nouveau. Si active:false, désactive le Stripe Product.","operationId":"AdminCreditPacksController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCreditPackDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditPackResponseDto"}}}},"404":{"description":"Pack introuvable"}},"security":[{"access-token":[]}],"summary":"Mettre à jour un pack de crédits","tags":["Admin — Credit Packs"]},"delete":{"description":"Passe active:false en DB et désactive le Stripe Product correspondant.","operationId":"AdminCreditPacksController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true}}}}},"404":{"description":"Pack introuvable"}},"security":[{"access-token":[]}],"summary":"Désactiver un pack de crédits (soft delete)","tags":["Admin — Credit Packs"]}},"/api/v1/admin/credit-packs/{id}/resync":{"post":{"description":"Idempotent. Recrée le Stripe Product/Price si nécessaire et met à jour syncedAt.","operationId":"AdminCreditPacksController_resync","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditPackResponseDto"}}}},"404":{"description":"Pack introuvable"},"409":{"description":"Resync Stripe échoué"}},"security":[{"access-token":[]}],"summary":"Forcer la re-synchronisation DB → Stripe","tags":["Admin — Credit Packs"]}},"/api/v1/trainers/{trainerId}/thematic-watch":{"get":{"operationId":"ThematicWatchController_listBulletins","parameters":[{"name":"trainerId","required":true,"in":"path","description":"Identifiant du formateur","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","description":"Numéro de page (défaut: 1)","schema":{"type":"number"}},{"name":"pageSize","required":false,"in":"query","description":"Taille de page (défaut: 10)","schema":{"type":"number"}}],"responses":{"200":{"description":"Liste paginée des bulletins de veille","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulletinListResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Lister les bulletins de veille thématique d'un formateur","tags":["Thematic Watch"]}},"/api/v1/trainers/{trainerId}/thematic-watch/generate-now":{"post":{"operationId":"ThematicWatchController_generateBulletinNow","parameters":[{"name":"trainerId","required":true,"in":"path","description":"Identifiant du formateur","schema":{"type":"string"}}],"responses":{"201":{"description":"Bulletin généré avec succès","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThematicWatchBulletinDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Générer immédiatement un bulletin de veille thématique","tags":["Thematic Watch"]}},"/api/v1/trainers/{trainerId}/thematic-watch/config":{"patch":{"operationId":"ThematicWatchController_updateConfig","parameters":[{"name":"trainerId","required":true,"in":"path","description":"Identifiant du formateur","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWatchConfigDto"}}}},"responses":{"200":{"description":"Configuration mise à jour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchConfigResponseDto"}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"bearer":[]}],"summary":"Mettre à jour la configuration de veille thématique d'un formateur","tags":["Thematic Watch"]}},"/api/v1/analytics/overview":{"get":{"operationId":"AnalyticsController_getOverview","parameters":[],"responses":{"200":{"description":"Métriques globales agrégées tous cours confondus"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Vue d'ensemble des KPIs analytiques du tenant","tags":["Analytics"]}},"/api/v1/analytics/funnel":{"get":{"operationId":"AnalyticsController_getFunnel","parameters":[{"name":"courseId","required":true,"in":"query","description":"Identifiant du cours","schema":{"type":"string"}}],"responses":{"200":{"description":"Étapes de l'entonnoir de conversion"},"400":{"description":"courseId manquant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Entonnoir de conversion d'un cours","tags":["Analytics"]}},"/api/v1/analytics/cohort":{"get":{"description":"Rétention des apprenants par mois d'inscription.","operationId":"AnalyticsController_getCohort","parameters":[{"name":"courseId","required":true,"in":"query","description":"Identifiant du cours","schema":{"type":"string"}}],"responses":{"200":{"description":"Données de cohorte par mois d'inscription"},"400":{"description":"courseId manquant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Analyse de cohorte d'un cours","tags":["Analytics"]}},"/api/v1/analytics/heatmap":{"get":{"operationId":"AnalyticsController_getHeatmap","parameters":[{"name":"courseId","required":true,"in":"query","description":"Identifiant du cours","schema":{"type":"string"}}],"responses":{"200":{"description":"Temps moyen passé par module et leçon"},"400":{"description":"courseId manquant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Heatmap du temps moyen par leçon d'un cours","tags":["Analytics"]}},"/api/v1/analytics/top-flop":{"get":{"description":"Questions les plus échouées et leçons avec le plus fort taux d'abandon.","operationId":"AnalyticsController_getTopFlop","parameters":[{"name":"courseId","required":true,"in":"query","description":"Identifiant du cours","schema":{"type":"string"}}],"responses":{"200":{"description":"Top/flop des questions et leçons"},"400":{"description":"courseId manquant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Top/flop des questions QCM et leçons abandonnées","tags":["Analytics"]}},"/api/v1/analytics/churn-risk/{enrollmentId}":{"get":{"description":"Retourne un score de risque d'abandon basé sur l'activité de l'apprenant.","operationId":"AnalyticsController_getChurnRisk","parameters":[{"name":"enrollmentId","required":true,"in":"path","description":"Identifiant de l'inscription","schema":{"type":"string"}}],"responses":{"200":{"description":"Score de risque d'abandon et facteurs explicatifs"},"400":{"description":"enrollmentId manquant"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"},"404":{"description":"Inscription non trouvée"}},"security":[{"access-token":[]}],"summary":"Prédire le risque d'abandon pour une inscription","tags":["Analytics"]}},"/api/v1/analytics/at-risk":{"get":{"operationId":"AnalyticsController_getAtRiskEnrollments","parameters":[],"responses":{"200":{"description":"Liste des inscriptions à risque avec score et dernière connexion"},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Lister les inscriptions à risque élevé d'abandon","tags":["Analytics"]}},"/api/v1/analytics/export-config":{"post":{"description":"Définit le format, la fréquence et les destinataires de l'export périodique.","operationId":"AnalyticsController_configureExport","parameters":[],"responses":{"201":{"description":"Configuration enregistrée","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"}}}}}},"401":{"description":"Non authentifié"},"403":{"description":"Permissions insuffisantes"}},"security":[{"access-token":[]}],"summary":"Configurer l'export automatique des analytiques","tags":["Analytics"]}},"/api/v1/courses/{courseId}/translations":{"get":{"operationId":"TranslationsController_getAvailableLocales","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant du cours","schema":{"type":"string"}}],"responses":{"200":{"description":"Locales disponibles pour le cours","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseLocalesResponseDto"}}}},"401":{"description":"Non authentifié"},"404":{"description":"Cours non trouvé"}},"security":[{"bearer":[]}],"summary":"Lister les traductions disponibles pour un cours","tags":["Translations"]}},"/api/v1/courses/{courseId}/translations/{locale}":{"get":{"operationId":"TranslationsController_getTranslation","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant du cours","schema":{"type":"string"}},{"name":"locale","required":true,"in":"path","description":"Code locale (fr, en, es, de)","schema":{"type":"string"}}],"responses":{"200":{"description":"Traduction du cours pour la locale demandée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseTranslationResponseDto"}}}},"400":{"description":"Locale non supportée"},"401":{"description":"Non authentifié"},"404":{"description":"Traduction non trouvée"}},"security":[{"bearer":[]}],"summary":"Récupérer la traduction d'un cours pour une locale","tags":["Translations"]},"post":{"operationId":"TranslationsController_createOrUpdateTranslation","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant du cours","schema":{"type":"string"}},{"name":"locale","required":true,"in":"path","description":"Code locale (fr, en, es, de)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseTranslationDto"}}}},"responses":{"201":{"description":"Traduction créée ou mise à jour","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseTranslationResponseDto"}}}},"400":{"description":"Locale non supportée"},"401":{"description":"Non authentifié"}},"security":[{"bearer":[]}],"summary":"Créer ou mettre à jour manuellement une traduction de cours","tags":["Translations"]},"delete":{"operationId":"TranslationsController_deleteTranslation","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant du cours","schema":{"type":"string"}},{"name":"locale","required":true,"in":"path","description":"Code locale (fr, en, es, de)","schema":{"type":"string"}}],"responses":{"200":{"description":"Traduction supprimée","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"description":"Locale non supportée"},"401":{"description":"Non authentifié"},"404":{"description":"Traduction non trouvée"}},"security":[{"bearer":[]}],"summary":"Supprimer la traduction d'un cours pour une locale","tags":["Translations"]}},"/api/v1/courses/{courseId}/translations/translate":{"post":{"description":"Lance une traduction automatique du cours d'une locale source vers une locale cible.","operationId":"TranslationsController_translateCourse","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant du cours","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslateCourseDto"}}}},"responses":{"201":{"description":"Traduction IA générée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourseTranslationResponseDto"}}}},"400":{"description":"Locale non supportée"},"401":{"description":"Non authentifié"},"404":{"description":"Cours non trouvé"}},"security":[{"bearer":[]}],"summary":"Traduire le contenu d'un cours via IA","tags":["Translations"]}},"/api/v1/ocr/process/{lessonId}":{"post":{"description":"Met la leçon en QUEUED et enfile un job BullMQ. Retourne 202 Accepted.\nRetourne 409 Conflict si la leçon est déjà PROCESSING ou READY.","operationId":"OcrController_process","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"202":{"description":"Job enfilé"},"404":{"description":"Leçon introuvable"},"409":{"description":"Déjà en cours ou prêt"}},"security":[{"bearer":[]}],"summary":"Demander le traitement OCR d'une leçon PDF","tags":["OCR"]}},"/api/v1/ocr/status/{lessonId}":{"get":{"description":"Retourne { status, ocrHtmlUrl, ocrProcessedAt }. Utilisé par le front pour le polling.","operationId":"OcrController_status","parameters":[{"name":"lessonId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"Statut récupéré"},"404":{"description":"Leçon introuvable"}},"security":[{"bearer":[]}],"summary":"Statut courant du pipeline OCR pour une leçon","tags":["OCR"]}},"/api/v1/subcontractors":{"get":{"operationId":"SubcontractorsController_list","parameters":[{"name":"status","required":false,"in":"query","description":"Filtrer par statut","schema":{"enum":["PENDING","APPROVED","REJECTED","ARCHIVED"],"type":"string"}},{"name":"search","required":false,"in":"query","description":"Recherche par nom, SIRET ou numéro Qualiopi","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des sous-traitants du tenant"}},"security":[{"bearer":[]}],"summary":"Lister les sous-traitants du tenant (avec filtres optionnels)","tags":["Subcontractors"]},"post":{"operationId":"SubcontractorsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubcontractorDto"}}}},"responses":{"201":{"description":"Sous-traitant créé avec succès"},"400":{"description":"Corps de la requête invalide"},"409":{"description":"Conflit SIRET — sous-traitant déjà existant"}},"security":[{"bearer":[]}],"summary":"Créer un sous-traitant","tags":["Subcontractors"]}},"/api/v1/subcontractors/{id}":{"get":{"operationId":"SubcontractorsController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail du sous-traitant"},"404":{"description":"Sous-traitant introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Détail d'un sous-traitant","tags":["Subcontractors"]},"patch":{"operationId":"SubcontractorsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSubcontractorDto"}}}},"responses":{"200":{"description":"Sous-traitant mis à jour"},"404":{"description":"Sous-traitant introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Mettre à jour un sous-traitant","tags":["Subcontractors"]},"delete":{"operationId":"SubcontractorsController_delete","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"responses":{"204":{"description":"Sous-traitant supprimé avec succès"},"403":{"description":"Suppression interdite — sous-traitant lié à une formation"},"404":{"description":"Sous-traitant introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Supprimer un sous-traitant (403 si lié à une formation active)","tags":["Subcontractors"]}},"/api/v1/subcontractors/{id}/upload-cv":{"post":{"operationId":"SubcontractorsController_uploadCv","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Fichier CV du sous-traitant (PDF, DOC, DOCX — max 10 MB)","content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"CV uploadé — statut repassé à PENDING pour re-validation"},"400":{"description":"Type de fichier non autorisé ou fichier absent"},"404":{"description":"Sous-traitant introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Uploader le CV / portfolio du sous-traitant vers MinIO","tags":["Subcontractors"]}},"/api/v1/subcontractors/{id}/approve-cv":{"post":{"description":"Passe le statut en APPROVED, stocke cvApprovedAt et cvApprovedById pour la traçabilité Qualiopi.","operationId":"SubcontractorsController_approveCv","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"responses":{"200":{"description":"CV approuvé — statut passé à APPROVED"},"400":{"description":"Aucun CV uploadé pour ce sous-traitant"},"404":{"description":"Sous-traitant introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Approuver le CV du sous-traitant (Qualiopi indicateur 23)","tags":["Subcontractors"]}},"/api/v1/subcontractors/{id}/reject-cv":{"post":{"operationId":"SubcontractorsController_rejectCv","parameters":[{"name":"id","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"responses":{"200":{"description":"CV rejeté — statut passé à REJECTED"},"404":{"description":"Sous-traitant introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Rejeter le CV du sous-traitant","tags":["Subcontractors"]}},"/api/v1/courses/{courseId}/subcontractors":{"get":{"operationId":"SubcontractorsCoursesController_list","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant de la formation","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste des sous-traitants avec leurs métadonnées"},"404":{"description":"Formation introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Lister les sous-traitants associés à une formation","tags":["courses / subcontractors"]},"post":{"operationId":"SubcontractorsCoursesController_attach","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant de la formation","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachSubcontractorDto"}}}},"responses":{"201":{"description":"Association créée avec succès"},"404":{"description":"Formation ou sous-traitant introuvable pour ce tenant"},"409":{"description":"Ce sous-traitant est déjà associé à cette formation"}},"security":[{"bearer":[]}],"summary":"Associer un sous-traitant à une formation","tags":["courses / subcontractors"]}},"/api/v1/courses/{courseId}/subcontractors/{subcontractorId}":{"patch":{"operationId":"SubcontractorsCoursesController_update","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant de la formation","schema":{"type":"string"}},{"name":"subcontractorId","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCourseSubcontractorDto"}}}},"responses":{"200":{"description":"Liaison mise à jour"},"404":{"description":"Formation ou liaison introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Mettre à jour les métadonnées d'un sous-traitant sur une formation","tags":["courses / subcontractors"]},"delete":{"operationId":"SubcontractorsCoursesController_detach","parameters":[{"name":"courseId","required":true,"in":"path","description":"Identifiant de la formation","schema":{"type":"string"}},{"name":"subcontractorId","required":true,"in":"path","description":"Identifiant du sous-traitant","schema":{"type":"string"}}],"responses":{"204":{"description":"Association supprimée avec succès"},"404":{"description":"Formation ou liaison introuvable pour ce tenant"}},"security":[{"bearer":[]}],"summary":"Dissocier un sous-traitant d'une formation","tags":["courses / subcontractors"]}},"/api/v1/media-library":{"post":{"description":"Uploads a file to MinIO, creates a Media record (status: UPLOADING → PROCESSING), and enqueues async thumbnail/conversion jobs. Returns the media record immediately — status transitions to READY once async processing completes.","operationId":"MediaLibraryController_upload","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"File to upload. Max 500 MB. Accepted: images (JPEG/PNG/WebP/GIF/AVIF), videos (MP4/WebM/MOV), audio (MP3/WAV/OGG), PDF, Office documents (DOCX/XLSX/PPTX)."},"name":{"type":"string","description":"Optional display name override."},"tags":{"type":"array","items":{"type":"string"},"description":"Optional tags."},"courseId":{"type":"string","description":"Optional course attachment (CUID)."}}}}}},"responses":{"201":{"description":"Media uploaded successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponseDto"}}}},"400":{"description":"Invalid file or MIME mismatch."},"401":{"description":"JWT missing or invalid."},"403":{"description":"Insufficient role."}},"security":[{"bearer":[]}],"summary":"Upload a media file to the library","tags":["Media Library"]},"get":{"description":"Returns a paginated list of media belonging to the caller's tenant. Supports filtering by type, tags, text search, and courseId.","operationId":"MediaLibraryController_findMany","parameters":[{"name":"type","required":false,"in":"query","description":"Filter by media type.","schema":{"example":"VIDEO","type":"string","enum":["IMAGE","VIDEO","AUDIO","PDF","OFFICE","OTHER"]}},{"name":"tags","required":false,"in":"query","description":"Filter by one or more tags (AND logic).","schema":{"example":["module-1"],"type":"array","items":{"type":"string"}}},{"name":"search","required":false,"in":"query","description":"Full-text search on the media name field.","schema":{"example":"introduction","type":"string"}},{"name":"courseId","required":false,"in":"query","description":"Restrict to media attached to a specific course (CUID).","schema":{"example":"clxyz123456","type":"string"}},{"name":"page","required":false,"in":"query","description":"Page number (1-based).","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"pageSize","required":false,"in":"query","description":"Number of results per page.","schema":{"minimum":1,"maximum":100,"default":24,"type":"number"}}],"responses":{"200":{"description":"Paginated list of media.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedMediaResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"List media with filters and pagination","tags":["Media Library"]}},"/api/v1/media-library/{id}":{"get":{"operationId":"MediaLibraryController_findById","parameters":[{"name":"id","required":true,"in":"path","description":"Media CUID","schema":{"example":"clxyz123456","type":"string"}}],"responses":{"200":{"description":"Media detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponseDto"}}}},"404":{"description":"Media not found."}},"security":[{"bearer":[]}],"summary":"Get a single media by ID","tags":["Media Library"]},"patch":{"operationId":"MediaLibraryController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Media CUID","schema":{"example":"clxyz123456","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMediaDto"}}}},"responses":{"200":{"description":"Updated media.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaResponseDto"}}}}},"security":[{"bearer":[]}],"summary":"Update media metadata (name, tags, courseId)","tags":["Media Library"]},"delete":{"description":"Marks the media as deleted (deletedAt set). The MinIO object is not removed immediately — a background job handles cleanup.","operationId":"MediaLibraryController_softDelete","parameters":[{"name":"id","required":true,"in":"path","description":"Media CUID","schema":{"example":"clxyz123456","type":"string"}}],"responses":{"204":{"description":"Deleted."},"404":{"description":"Media not found."}},"security":[{"bearer":[]}],"summary":"Soft-delete a media","tags":["Media Library"]}},"/api/v1/media-library/{id}/download":{"get":{"description":"Generates a 300-second presigned GET URL for the original file and redirects the client. The client downloads directly from MinIO.","operationId":"MediaLibraryController_getDownloadUrl","parameters":[{"name":"id","required":true,"in":"path","description":"Media CUID","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirects to presigned download URL."}},"security":[{"bearer":[]}],"summary":"Get a presigned download URL (302 redirect)","tags":["Media Library"]}},"/api/v1/media-library/{id}/thumbnail":{"get":{"description":"Returns a 300-second presigned GET URL for the 320×180 WebP thumbnail. Returns 404 if the thumbnail is not yet generated.","operationId":"MediaLibraryController_getThumbnailUrl","parameters":[{"name":"id","required":true,"in":"path","description":"Media CUID","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirects to presigned thumbnail URL."},"404":{"description":"Thumbnail not available yet."}},"security":[{"bearer":[]}],"summary":"Get a presigned thumbnail URL (302 redirect)","tags":["Media Library"]}},"/api/v1/media-library/{id}/preview":{"get":{"description":"Returns a 300-second presigned URL for the LibreOffice-converted PDF. Only applicable for OFFICE media (DOCX/XLSX/PPTX). Returns 404 if the conversion is not complete.","operationId":"MediaLibraryController_getPreviewUrl","parameters":[{"name":"id","required":true,"in":"path","description":"Media CUID","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirects to presigned PDF preview URL."},"404":{"description":"PDF preview not available yet."}},"security":[{"bearer":[]}],"summary":"Get a presigned PDF preview URL (OFFICE type, 302 redirect)","tags":["Media Library"]}},"/api/v1/payouts/balance":{"get":{"description":"Returns the real-time available, pending, and paid-out balances for the authenticated creator. Recalculated live from revenue records.","operationId":"PayoutsController_getMyBalance","parameters":[],"responses":{"200":{"description":"Creator balance","content":{"application/json":{"schema":{"example":{"creatorId":"clx1234567890abcdef","availableCents":125000,"pendingCents":0,"paidOutCents":375000,"currency":"EUR","lastCalculatedAt":"2026-04-21T10:30:00.000Z"}}}}},"401":{"description":"Non authentifié"},"403":{"description":"CREATOR ou ADMIN requis"}},"security":[{"bearer":[]}],"summary":"Get creator balance","tags":["Payouts — Creator"]}},"/api/v1/payouts/request":{"post":{"description":"Submit a payout request. Minimum 50 €. Requires Stripe Connect payouts enabled (stripePayoutsEnabled = true). The request is created in PENDING status and reviewed by an admin.","operationId":"PayoutsController_requestPayout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestPayoutDto"}}}},"responses":{"201":{"description":"Payout request created","content":{"application/json":{"schema":{"example":{"id":"clx1234567890abcdef","creatorId":"clx0987654321abcdef","amountCents":10000,"currency":"EUR","status":"PENDING","creatorNotes":"Virement mensuel avril","requestedAt":"2026-04-21T10:30:00.000Z"}}}}},"400":{"description":"Montant < 50 €"},"403":{"description":"Stripe payouts non activés"},"422":{"description":"Solde insuffisant"}},"security":[{"bearer":[]}],"summary":"Request a payout","tags":["Payouts — Creator"]}},"/api/v1/payouts/mine":{"get":{"description":"Returns payout requests for the authenticated creator, ordered by requestedAt descending. Filter by status with `?status=PENDING`.","operationId":"PayoutsController_listMyPayouts","parameters":[{"name":"status","required":false,"in":"query","description":"Filter by payout status","schema":{"enum":["PENDING","APPROVED","REJECTED","PROCESSING","PAID","FAILED"],"type":"string"}}],"responses":{"200":{"description":"List of payout requests"}},"security":[{"bearer":[]}],"summary":"List own payout requests","tags":["Payouts — Creator"]}},"/api/v1/admin/payouts":{"get":{"description":"Returns all payout requests across all tenants, ordered by requestedAt descending. Supports status filtering and pagination.","operationId":"AdminPayoutsController_listAll","parameters":[{"name":"status","required":false,"in":"query","description":"Filtrer les demandes de virement par statut. Si absent, tous les statuts sont retournés","schema":{"$ref":"#/components/schemas/PayoutStatus"}},{"name":"page","required":false,"in":"query","description":"Numéro de page (base 1)","schema":{"minimum":1,"default":1,"example":1,"type":"number"}},{"name":"pageSize","required":false,"in":"query","description":"Nombre d'éléments par page (défaut 20, max 100)","schema":{"minimum":1,"maximum":100,"default":20,"example":20,"type":"number"}}],"responses":{"200":{"description":"Paginated list of payout requests","content":{"application/json":{"schema":{"example":{"items":[],"total":0}}}}}},"security":[{"bearer":[]}],"summary":"List all payout requests","tags":["Admin — Payouts"]}},"/api/v1/admin/payouts/{id}":{"get":{"description":"Returns the full detail of a payout request including the creator profile, approver info, and linked revenue rows.","operationId":"AdminPayoutsController_getDetail","parameters":[{"name":"id","required":true,"in":"path","description":"PayoutRequest CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Payout request detail"},"404":{"description":"Payout request introuvable"}},"security":[{"bearer":[]}],"summary":"Get payout request detail","tags":["Admin — Payouts"]}},"/api/v1/admin/payouts/{id}/approve":{"post":{"description":"Approves a PENDING payout and immediately executes the Stripe Transfer. Uses SAGA pattern: revenues are locked atomically before Stripe is called. On Stripe failure, revenues are unlocked and status transitions to FAILED.","operationId":"AdminPayoutsController_approve","parameters":[{"name":"id","required":true,"in":"path","description":"PayoutRequest CUID","schema":{"type":"string"}}],"responses":{"200":{"description":"Payout approved and transfer executed"},"404":{"description":"Payout request introuvable"},"409":{"description":"Payout not in PENDING status"},"422":{"description":"Solde insuffisant ou compte Stripe manquant"}},"security":[{"bearer":[]}],"summary":"Approve a payout request","tags":["Admin — Payouts"]}},"/api/v1/admin/payouts/{id}/reject":{"post":{"description":"Rejects a PENDING payout request. The rejection reason is mandatory (min 10 chars) for audit trail and creator communication. Revenue rows are NOT modified — they remain AVAILABLE.","operationId":"AdminPayoutsController_reject","parameters":[{"name":"id","required":true,"in":"path","description":"PayoutRequest CUID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectPayoutDto"}}}},"responses":{"200":{"description":"Payout rejected"},"400":{"description":"Payout not in PENDING status or reason too short"},"404":{"description":"Payout request introuvable"}},"security":[{"bearer":[]}],"summary":"Reject a payout request","tags":["Admin — Payouts"]}},"/api/v1/vae/stats":{"get":{"description":"Taux de validation totale, répartition par statut, durée moyenne accompagnement.","operationId":"VaeController_getStats","parameters":[],"responses":{"200":{"description":"Statistiques VAE"}},"security":[{"access-token":[]}],"summary":"Statistiques globales des dossiers VAE du tenant","tags":["VAE"]}},"/api/v1/vae":{"get":{"operationId":"VaeController_findAll","parameters":[{"name":"status","required":false,"in":"query","schema":{"enum":["DOSSIER_RECEVABILITE","DOSSIER_VAE","ENTRETIEN_JURY","RESULTAT_OBTENU","ABANDON"],"type":"string"}},{"name":"candidateId","required":false,"in":"query","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}}],"responses":{"200":{"description":"Liste des dossiers VAE"}},"security":[{"access-token":[]}],"summary":"Liste paginée des dossiers VAE du tenant","tags":["VAE"]},"post":{"description":"Initialise le dossier au statut DOSSIER_RECEVABILITE. Le candidat doit être un LEARNER du tenant.","operationId":"VaeController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVaeApplicationDto"}}}},"responses":{"201":{"description":"Dossier VAE créé"},"404":{"description":"Candidat ou accompagnateur introuvable"}},"security":[{"access-token":[]}],"summary":"Créer un nouveau dossier VAE pour un candidat","tags":["VAE"]}},"/api/v1/vae/{id}":{"get":{"operationId":"VaeController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail du dossier"},"403":{"description":"Accès non autorisé"},"404":{"description":"Dossier introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'un dossier VAE","tags":["VAE"]},"patch":{"operationId":"VaeController_update","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateVaeApplicationDto"}}}},"responses":{"200":{"description":"Dossier mis à jour"}},"security":[{"access-token":[]}],"summary":"Mise à jour des informations générales du dossier VAE","tags":["VAE"]}},"/api/v1/vae/{id}/recevabilite-decision":{"post":{"description":"Transition DOSSIER_RECEVABILITE -> DOSSIER_VAE. Enregistre la date de validation.","operationId":"VaeController_approveRecevabilite","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecevabiliteDecisionDto"}}}},"responses":{"200":{"description":"Recevabilité approuvée"},"400":{"description":"Transition de statut invalide"}},"security":[{"access-token":[]}],"summary":"Approuver la recevabilité administrative et passer en DOSSIER_VAE","tags":["VAE"]}},"/api/v1/vae/{id}/submit-dossier":{"post":{"description":"Transition DOSSIER_VAE -> ENTRETIEN_JURY.","operationId":"VaeController_submitDossier","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"responses":{"200":{"description":"Dossier soumis"}},"security":[{"access-token":[]}],"summary":"Enregistrer la soumission du dossier de compétences (Livret 2)","tags":["VAE"]}},"/api/v1/vae/{id}/schedule-jury":{"post":{"description":"Statut doit être ENTRETIEN_JURY.","operationId":"VaeController_scheduleJury","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleJuryDto"}}}},"responses":{"200":{"description":"Jury planifié"}},"security":[{"access-token":[]}],"summary":"Planifier ou mettre à jour la date de passage devant le jury","tags":["VAE"]}},"/api/v1/vae/{id}/jury-decision":{"post":{"description":"Transition ENTRETIEN_JURY -> RESULTAT_OBTENU.","operationId":"VaeController_recordJuryDecision","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JuryDecisionDto"}}}},"responses":{"200":{"description":"Décision jury enregistrée"},"400":{"description":"Données invalides (ex: PARTIEL sans modules)"}},"security":[{"access-token":[]}],"summary":"Saisir la décision rendue par le jury (TOTAL / PARTIEL / REFUS)","tags":["VAE"]}},"/api/v1/vae/{id}/abandon":{"post":{"description":"Transition vers ABANDON depuis tout statut sauf RESULTAT_OBTENU.","operationId":"VaeController_abandon","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du dossier VAE","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbandonVaeDto"}}}},"responses":{"200":{"description":"Dossier abandonné"}},"security":[{"access-token":[]}],"summary":"Clore le dossier VAE (abandon)","tags":["VAE"]}},"/api/v1/alternance/stats":{"get":{"description":"Répartition par statut et type, durée moyenne, taux de complétion et de rupture.","operationId":"AlternanceController_getStats","parameters":[],"responses":{"200":{"description":"Statistiques alternance"}},"security":[{"access-token":[]}],"summary":"Statistiques globales des contrats d'alternance du tenant","tags":["Alternance"]}},"/api/v1/alternance/contracts":{"get":{"operationId":"AlternanceController_findAll","parameters":[{"name":"status","required":false,"in":"query","schema":{"enum":["DRAFT","SIGNED","ACTIVE","COMPLETED","TERMINATED"],"type":"string"}},{"name":"contractType","required":false,"in":"query","schema":{"enum":["APPRENTISSAGE","PROFESSIONNALISATION"],"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"example":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"example":20,"type":"number"}}],"responses":{"200":{"description":"Liste des contrats"}},"security":[{"access-token":[]}],"summary":"Liste paginée des contrats d'alternance du tenant","tags":["Alternance"]},"post":{"description":"Crée le contrat au statut DRAFT. Calculul automatique de la durée en mois. Un seul contrat par inscription.","operationId":"AlternanceController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlternanceContractDto"}}}},"responses":{"201":{"description":"Contrat créé"},"409":{"description":"Un contrat existe déjà pour cette inscription"}},"security":[{"access-token":[]}],"summary":"Créer un nouveau contrat d'alternance","tags":["Alternance"]}},"/api/v1/alternance/contracts/{id}":{"get":{"operationId":"AlternanceController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du contrat","schema":{"type":"string"}}],"responses":{"200":{"description":"Détail du contrat"},"404":{"description":"Contrat introuvable"}},"security":[{"access-token":[]}],"summary":"Détail d'un contrat d'alternance","tags":["Alternance"]},"patch":{"operationId":"AlternanceController_update","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du contrat","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlternanceContractDto"}}}},"responses":{"200":{"description":"Contrat mis à jour"},"400":{"description":"Contrat terminé — modifications non autorisées"}},"security":[{"access-token":[]}],"summary":"Mettre à jour les informations du contrat","tags":["Alternance"]}},"/api/v1/alternance/contracts/{id}/sign-convention":{"post":{"description":"Passe le statut DRAFT → SIGNED et horodate la signature.","operationId":"AlternanceController_signConvention","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du contrat","schema":{"type":"string"}}],"responses":{"200":{"description":"Convention signée"},"400":{"description":"Convention déjà signée"}},"security":[{"access-token":[]}],"summary":"Marquer la convention tripartite comme signée","tags":["Alternance"]}},"/api/v1/alternance/contracts/{id}/log-visit":{"post":{"description":"Ajoute une entrée dans visitsLog. Passe automatiquement SIGNED → ACTIVE lors de la première visite.","operationId":"AlternanceController_logVisit","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du contrat","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogVisitDto"}}}},"responses":{"200":{"description":"Visite enregistrée"}},"security":[{"access-token":[]}],"summary":"Enregistrer une visite tripartite ou de suivi","tags":["Alternance"]}},"/api/v1/alternance/contracts/{id}/terminate":{"post":{"description":"Passe le statut à TERMINATED. Enregistre date et motif de rupture.","operationId":"AlternanceController_terminate","parameters":[{"name":"id","required":true,"in":"path","description":"CUID du contrat","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TerminateContractDto"}}}},"responses":{"200":{"description":"Rupture enregistrée"},"400":{"description":"Contrat déjà terminé"}},"security":[{"access-token":[]}],"summary":"Enregistrer une rupture de contrat","tags":["Alternance"]}},"/api/v1/onboarding/progress":{"get":{"operationId":"OnboardingController_getProgress","parameters":[{"name":"flowId","required":false,"in":"query","description":"Si présent, retourne uniquement ce flow. Sinon, retourne tous les flows démarrés.","schema":{"type":"string"}}],"responses":{"200":{"description":"OnboardingProgress | OnboardingProgress[] | null"}},"security":[{"access-token":[]}],"summary":"État d'un flow d'onboarding (ou tous les flows)","tags":["Onboarding"]},"post":{"operationId":"OnboardingController_upsertProgress","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertProgressDto"}}}},"responses":{"201":{"description":"OnboardingProgress upserted"}},"security":[{"access-token":[]}],"summary":"Créer ou mettre à jour la position dans un flow","tags":["Onboarding"]}},"/api/v1/onboarding/progress/{flowId}/complete":{"post":{"operationId":"OnboardingController_markCompleted","parameters":[{"name":"flowId","required":true,"in":"path","description":"Identifiant logique du flow.","schema":{"type":"string"}}],"responses":{"201":{"description":"OnboardingProgress avec completedAt"}},"security":[{"access-token":[]}],"summary":"Marquer un flow comme terminé","tags":["Onboarding"]}},"/api/v1/onboarding/progress/{flowId}/skip":{"post":{"operationId":"OnboardingController_markSkipped","parameters":[{"name":"flowId","required":true,"in":"path","description":"Identifiant logique du flow.","schema":{"type":"string"}}],"responses":{"201":{"description":"OnboardingProgress avec skippedAt"}},"security":[{"access-token":[]}],"summary":"Marquer un flow comme explicitement passé","tags":["Onboarding"]}},"/api/v1/onboarding/progress/{flowId}":{"delete":{"operationId":"OnboardingController_reset","parameters":[{"name":"flowId","required":true,"in":"path","description":"Identifiant logique du flow.","schema":{"type":"string"}}],"responses":{"200":{"description":"{ deleted: true }"},"404":{"description":"Flow jamais démarré"}},"security":[{"access-token":[]}],"summary":"Reset un flow (relance possible ensuite)","tags":["Onboarding"]}},"/api/v1/onboarding/analytics/event":{"post":{"operationId":"OnboardingController_trackEvent","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackEventDto"}}}},"responses":{"201":{"description":"OnboardingEvent créé"}},"security":[{"access-token":[]}],"summary":"Enregistrer un événement d'onboarding (analytics)","tags":["Onboarding"]}},"/api/v1/onboarding/analytics/funnel":{"get":{"description":"Retourne les totaux start/complete/skip et le funnel par étape. Sans `flowId`, agrège tous les flows. Sans dates, fenêtre par défaut J-30 → maintenant.","operationId":"OnboardingController_getFunnel","parameters":[{"name":"flowId","required":false,"in":"query","description":"Restreint au flow spécifié. Sinon tous flows agrégés.","schema":{"maxLength":64,"example":"creator-first-course","type":"string"}},{"name":"from","required":false,"in":"query","description":"Borne basse de la fenêtre (ISO 8601). Défaut : J-30.","schema":{"example":"2026-04-10T00:00:00Z","type":"string"}},{"name":"to","required":false,"in":"query","description":"Borne haute de la fenêtre (ISO 8601). Défaut : maintenant.","schema":{"example":"2026-05-10T23:59:59Z","type":"string"}}],"responses":{"200":{"description":"FunnelStats"}},"security":[{"access-token":[]}],"summary":"Stats funnel d'un flow (ADMIN)","tags":["Onboarding"]}},"/api/v1/onboarding/analytics/recent-users":{"get":{"operationId":"OnboardingController_getRecentUsers","parameters":[{"name":"flowId","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"RecentUserState[]"}},"security":[{"access-token":[]}],"summary":"Derniers utilisateurs ayant un événement d'onboarding (ADMIN)","tags":["Onboarding"]}},"/api/v1/onboarding/analytics/flows":{"get":{"operationId":"OnboardingController_listFlows","parameters":[],"responses":{"200":{"description":"string[]"}},"security":[{"access-token":[]}],"summary":"Liste des flowIds connus (pour selecteur ADMIN)","tags":["Onboarding"]}},"/api/v1/onboarding/admin/progress/{userId}/{flowId}":{"delete":{"operationId":"OnboardingController_resetForUser","parameters":[{"name":"userId","required":true,"in":"path","description":"Identifiant de l'utilisateur cible.","schema":{"type":"string"}},{"name":"flowId","required":true,"in":"path","description":"Identifiant logique du flow.","schema":{"type":"string"}}],"responses":{"200":{"description":"{ deleted: true }"},"404":{"description":"Flow jamais démarré"}},"security":[{"access-token":[]}],"summary":"Reset le flow d'un user spécifique (ADMIN)","tags":["Onboarding"]}}},"info":{"title":"QualiForma API","description":"API REST de la plateforme QualiForma — LMS Qualiopi multi-tenant. Gestion de formations, apprenants, sessions live, paiements et conformité Qualiopi.","version":"1.0.0","contact":{"name":"QR Communication","url":"https://qualiforma.site","email":"contact@rlconseil.net"},"license":{"name":"Propriétaire","url":"https://qualiforma.site/legal"}},"tags":[{"name":"Auth","description":"Authentification & sessions (login, refresh, OIDC, SCIM)"},{"name":"Users","description":"Utilisateurs & profils"},{"name":"Tenants","description":"Gestion des tenants (branding, configuration, OIDC)"},{"name":"Courses","description":"Catalogue formations"},{"name":"Modules","description":"Modules de cours"},{"name":"Lessons","description":"Leçons"},{"name":"Enrollments","description":"Inscriptions & progression"},{"name":"Sessions","description":"Sessions live LiveKit"},{"name":"Emargement","description":"Présence numérique & signatures"},{"name":"Payments","description":"Paiements Viva Wallet & Stripe"},{"name":"Billing","description":"Facturation Factur-X"},{"name":"Qualiopi","description":"Indicateurs Qualiopi (32 critères)"},{"name":"Questionnaires","description":"Questionnaires & évaluations"},{"name":"Media","description":"Upload & stockage S3/MinIO"},{"name":"Search","description":"Recherche Meilisearch"},{"name":"Health","description":"Health checks"},{"name":"AI Credits","description":"Gestion des crédits IA par tenant"},{"name":"Credit Store","description":"Boutique de crédits IA"},{"name":"Content Generation","description":"Génération de contenu IA (programme, description, FAQ)"},{"name":"Lesson Enrichment","description":"Enrichissement multi-format IA de leçons"},{"name":"Lesson Blocks","description":"Blocs structurés de leçons"},{"name":"Course Enrichment","description":"Enrichissement IA de cours (toutes leçons)"},{"name":"Courses — AI","description":"Endpoints IA liés aux cours (génération, analyse)"},{"name":"Lesson Jobs (async)","description":"Jobs asynchrones de génération de leçons"},{"name":"AI Copilot","description":"Copilote IA (suggestions, analyse)"},{"name":"OCR","description":"Reconnaissance optique de caractères"},{"name":"BPF","description":"Bilan Pédagogique et Financier (Cerfa 10443-17)"},{"name":"PAF - Preuve d'Action Formation","description":"Plan d'action formation — preuves"},{"name":"Complaints","description":"Réclamations (Critère 6 Qualiopi)"},{"name":"Improvements","description":"Plans d'amélioration (Critère 7 Qualiopi)"},{"name":"Qualiopi — Improvement Plans","description":"Plans d'amélioration Qualiopi"},{"name":"Adaptive Paths (Qualiopi Criterion 3)","description":"Parcours adaptatifs — Critère 3"},{"name":"Trainer Competencies (Qualiopi Criterion 5)","description":"Compétences formateurs — Critère 5"},{"name":"Trainer Competencies — Derived (auto)","description":"Compétences dérivées automatiques"},{"name":"Trainers — Compliance (Wave 9)","description":"Conformité formateurs Qualiopi"},{"name":"Trainers — Formation continue & Onboarding (Qualiopi Ind. 17-18)","description":"Formation continue formateurs"},{"name":"Trainer Dashboard","description":"Tableau de bord formateur"},{"name":"Qualiopi — Veille légale","description":"Veille légale manuelle (Indicateurs 21-22)"},{"name":"Qualiopi — Veille légale auto","description":"Veille légale automatique IA"},{"name":"Qualiopi — Skills Watch","description":"Veille métiers/compétences (Indicateur 21)"},{"name":"Thematic Watch","description":"Veille thématique par formateur (Indicateur 21)"},{"name":"Qualiopi — Partnerships","description":"Partenariats Qualiopi"},{"name":"Admin — Tenants","description":"Administration cross-tenant des tenants"},{"name":"Admin — Users","description":"Administration cross-tenant des utilisateurs"},{"name":"Admin — Payments","description":"Administration cross-tenant des paiements"},{"name":"Admin — Payouts","description":"Administration cross-tenant des virements"},{"name":"Admin — Analytics","description":"Administration cross-tenant des analytiques"},{"name":"Admin — Email Inbox","description":"Administration cross-tenant de l'inbox email"},{"name":"Admin — Credit Packs","description":"Administration cross-tenant des packs crédits"},{"name":"Admin — Stripe config","description":"Administration cross-tenant config Stripe"},{"name":"Admin — Settings","description":"Administration cross-tenant des paramètres"},{"name":"Admin / Audit Logs","description":"Logs d'audit cross-tenant"},{"name":"Admin / Qualiopi","description":"Qualiopi cross-tenant (dashboard super-admin)"},{"name":"Admin / Security","description":"Événements sécurité cross-tenant"},{"name":"Creators","description":"Gestion créateurs (formateurs)"},{"name":"Creators management","description":"Management des créateurs"},{"name":"Creators — Google OAuth Config","description":"Config Google OAuth par créateur"},{"name":"Course Trainers","description":"Affectation formateurs aux cours"},{"name":"Course Documents","description":"Documents liés aux formations"},{"name":"courses / subcontractors","description":"Sous-traitance par cours"},{"name":"Subcontractors","description":"Gestion sous-traitants"},{"name":"Financier — Portail","description":"Portail financeur (OPCO, CPF) — accès magic-link"},{"name":"Financier — Tokens","description":"Tokens magic-link portail financeur"},{"name":"Payouts — Creator","description":"Virements formateurs / créateurs"},{"name":"Stripe Connect OAuth","description":"OAuth Stripe Connect (marketplace)"},{"name":"Mail Configuration","description":"Configuration SMTP / Gmail par tenant"},{"name":"Email Inbox","description":"Boîte de réception email (conversations)"},{"name":"Email Templates","description":"Templates email transactionnels"},{"name":"Notifications","description":"Notifications in-app & push web"},{"name":"Realtime","description":"WebSocket / Web Push (Socket.IO)"},{"name":"Learner Conversations","description":"Messagerie apprenants ↔ formateurs"},{"name":"Webhooks — Outbound","description":"Webhooks sortants par tenant (HMAC-SHA256)"},{"name":"Video Studio","description":"Studio vidéo (enregistrement, avatar IA, avatars formateurs)"},{"name":"Media Library","description":"Bibliothèque médias du tenant"},{"name":"SCORM","description":"Adaptateur SCORM 1.2/2004 + xAPI"},{"name":"xAPI","description":"Déclarations xAPI (Learning Record Store)"},{"name":"Lesson Content","description":"Contenu de leçon (blocs)"},{"name":"Exercise Responses","description":"Réponses aux exercices interactifs"},{"name":"Templates","description":"Templates PDF (convention, attestation, programme)"},{"name":"Document Signing","description":"Signature électronique de documents (eIDAS)"},{"name":"Session Documents","description":"Documents générés par session"},{"name":"Live sessions","description":"Sessions live (LiveKit, émargement, convocations)"},{"name":"Analytics","description":"Statistiques & tableaux de bord créateur"},{"name":"Promo Codes","description":"Codes promotionnels"},{"name":"Bundles","description":"Lots de formations"},{"name":"Translations","description":"i18n dynamique par tenant"},{"name":"VAE","description":"Validation des Acquis de l'Expérience (Critère 3 Qualiopi I15)"},{"name":"Alternance","description":"Contrats apprentissage/professionnalisation (I15)"},{"name":"Mediators","description":"Médiateurs de la consommation (obligation légale)"},{"name":"Funders","description":"Financeurs (OPCO, CPF, Pôle Emploi)"},{"name":"Google Calendar Configuration","description":"Synchronisation Google Calendar"},{"name":"Public — Courses","description":"Catalogue public (sans authentification)"},{"name":"Public — Partnerships","description":"Partenariats publics (sans authentification)"},{"name":"Public Assets (no auth)","description":"Ressources publiques (images, médias)"},{"name":"Contact","description":"Formulaire de contact public"}],"servers":[{"url":"https://api.qualiforma.site/api/v1","description":"Production"},{"url":"http://localhost:3001/api/v1","description":"Local"}],"components":{"securitySchemes":{"access-token":{"scheme":"bearer","bearerFormat":"JWT","type":"http","name":"Authorization","description":"Token JWT obtenu via POST /auth/login ou POST /auth/token-exchange. Format : Bearer <access_token>","in":"header"},"TenantId":{"type":"apiKey","in":"header","name":"X-Tenant-ID","description":"Identifiant du tenant (ex: qualiforma-demo). Requis pour toutes les requêtes."},"financier-token":{"type":"apiKey","in":"header","name":"X-Financier-Token","description":"Magic-link token pour portail financeur (OPCO, CPF, etc.)"}},"schemas":{"SignupCreatorDto":{"type":"object","properties":{"email":{"type":"string","example":"jean.dupont@acme.com"},"password":{"type":"string","example":"P@ssw0rd!2024","minLength":8},"firstName":{"type":"string","example":"Jean"},"lastName":{"type":"string","example":"Dupont"},"companyName":{"type":"string","example":"Acme Formation","description":"Nom de la société — sert à générer le slug du tenant"}},"required":["email","password","firstName","lastName","companyName"]},"SignupLearnerDto":{"type":"object","properties":{"email":{"type":"string","example":"marie.martin@example.com"},"password":{"type":"string","example":"P@ssw0rd!2024","minLength":8},"firstName":{"type":"string","example":"Marie"},"lastName":{"type":"string","example":"Martin"}},"required":["email","password","firstName","lastName"]},"LoginDto":{"type":"object","properties":{"email":{"type":"string","example":"jean.dupont@example.com"},"password":{"type":"string","example":"P@ssw0rd!2024"},"tenantSlug":{"type":"string","example":"acme-formation","description":"Optional — resolved from X-Tenant-ID header if omitted"}},"required":["email","password"]},"RefreshTokenDto":{"type":"object","properties":{"refreshToken":{"type":"string","description":"The refresh token issued during login or token-exchange","example":"eyJhbGciOiJIUzI1NiIs..."}},"required":["refreshToken"]},"SwitchTenantDto":{"type":"object","properties":{"tenantId":{"type":"string","description":"CUID of the tenant to switch to","example":"clh4k2q0r0000xxxxabcd1234"}},"required":["tenantId"]},"AcceptInviteDto":{"type":"object","properties":{"token":{"type":"string","description":"Invitation JWT received by email","example":"eyJhbGciOiJIUzI1NiIs..."},"password":{"type":"string","description":"Password — required only for new users without an existing account","minLength":12,"example":"S3cur3P@ssword!"}},"required":["token"]},"TokenExchangeDto":{"type":"object","properties":{"externalToken":{"type":"string","description":"JWT signed by the external identity provider","example":"eyJhbGciOiJSUzI1NiIs..."},"tenantSlug":{"type":"string","description":"Tenant slug identifying the target organisation","example":"acme-formation"}},"required":["externalToken","tenantSlug"]},"ProvisionDto":{"type":"object","properties":{"externalId":{"type":"string","description":"Stable unique identifier from the external IdP","example":"usr_abc123"},"provider":{"type":"string","description":"Identity provider name (e.g. saml, oidc, azure-ad)","example":"oidc"},"email":{"type":"string","example":"jean.dupont@acme.com"},"firstName":{"type":"string","example":"Jean"},"lastName":{"type":"string","example":"Dupont"},"tenantSlug":{"type":"string","description":"Tenant slug for provisioning scope","example":"acme-formation"},"role":{"type":"string","enum":["ADMIN","CREATOR","TRAINER","LEARNER","FUNDER"],"default":"LEARNER"}},"required":["externalId","provider","email","firstName","lastName","tenantSlug"]},"ForgotPasswordDto":{"type":"object","properties":{"email":{"type":"string","example":"jean.dupont@example.com"}},"required":["email"]},"ResetPasswordDto":{"type":"object","properties":{"token":{"type":"string","description":"Password reset token received by email","example":"abc123resettoken..."},"newPassword":{"type":"string","example":"NewP@ssw0rd!2024","minLength":8}},"required":["token","newPassword"]},"ToggleOfModeDto":{"type":"object","properties":{"ofMode":{"type":"boolean","description":"true = activer le Mode OF (Qualiopi/BPF), false = Mode Standalone","example":true}},"required":["ofMode"]},"CreateTenantDto":{"type":"object","properties":{"slug":{"type":"string","example":"acme-formation","description":"Unique slug used for subdomain routing and token identification. Immutable after creation.","minLength":3,"maxLength":63},"name":{"type":"string","example":"ACME Formation","description":"Display name of the organisation"},"domain":{"type":"string","example":"formation.acme.fr","description":"Custom domain for the tenant portal. Must be a valid hostname (no protocol, no path)."},"authMode":{"type":"string","enum":["STANDALONE","HEADLESS"],"default":"STANDALONE","description":"Authentication mode. STANDALONE = LMS manages auth. HEADLESS = external IdP via JWKS/API key."},"jwksUri":{"type":"string","example":"https://auth.acme.fr/.well-known/jwks.json","description":"JWKS endpoint URI for HEADLESS auth mode (external IdP public keys)"},"authIssuer":{"type":"string","example":"https://auth.acme.fr","description":"JWT issuer URL for HEADLESS auth mode"},"authAudience":{"type":"string","example":"qualiforma-lms","description":"Expected JWT audience for HEADLESS auth mode"},"commissionRate":{"type":"string","example":"0.20","description":"Platform commission rate as a decimal between 0 and 1 (default: 0.20 = 20%)"},"features":{"example":["qualiopi","livekit","scorm"],"description":"List of enabled feature flags for this tenant","type":"array","items":{"type":"string"}},"ofMode":{"type":"boolean","default":true,"description":"Mode Organisme de Formation. Quand `true`, active les fonctionnalités réglementaires FR : BPF, dashboard Qualiopi, veille légale, médiateurs, mentions OF sur pages publiques, PIF auto, financeurs OPCO. Quand `false` (Standalone), les outils pédagogiques (émargements, attestations, QCM, sessions, conventions, questionnaires) restent disponibles — seules les fonctionnalités strictement réglementaires FR sont masquées."},"scellioApiKey":{"type":"string","description":"Scellio API key for electronic signature (Qualiopi emargement)"},"livekitApiKey":{"type":"string","description":"LiveKit API key for video conferencing"},"livekitApiSecret":{"type":"string","description":"LiveKit API secret for video conferencing"}},"required":["slug","name"]},"UpdateTenantDto":{"type":"object","properties":{"slug":{"type":"string","example":"acme-formation","description":"Unique slug used for subdomain routing and token identification. Immutable after creation.","minLength":3,"maxLength":63},"name":{"type":"string","example":"ACME Formation","description":"Display name of the organisation"},"domain":{"type":"string","example":"formation.acme.fr","description":"Custom domain for the tenant portal. Must be a valid hostname (no protocol, no path)."},"authMode":{"type":"string","enum":["STANDALONE","HEADLESS"],"default":"STANDALONE","description":"Authentication mode. STANDALONE = LMS manages auth. HEADLESS = external IdP via JWKS/API key."},"jwksUri":{"type":"string","example":"https://auth.acme.fr/.well-known/jwks.json","description":"JWKS endpoint URI for HEADLESS auth mode (external IdP public keys)"},"authIssuer":{"type":"string","example":"https://auth.acme.fr","description":"JWT issuer URL for HEADLESS auth mode"},"authAudience":{"type":"string","example":"qualiforma-lms","description":"Expected JWT audience for HEADLESS auth mode"},"commissionRate":{"type":"string","example":"0.20","description":"Platform commission rate as a decimal between 0 and 1 (default: 0.20 = 20%)"},"features":{"example":["qualiopi","livekit","scorm"],"description":"List of enabled feature flags for this tenant","type":"array","items":{"type":"string"}},"ofMode":{"type":"boolean","default":true,"description":"Mode Organisme de Formation. Quand `true`, active les fonctionnalités réglementaires FR : BPF, dashboard Qualiopi, veille légale, médiateurs, mentions OF sur pages publiques, PIF auto, financeurs OPCO. Quand `false` (Standalone), les outils pédagogiques (émargements, attestations, QCM, sessions, conventions, questionnaires) restent disponibles — seules les fonctionnalités strictement réglementaires FR sont masquées."},"scellioApiKey":{"type":"string","description":"Scellio API key for electronic signature (Qualiopi emargement)"},"livekitApiKey":{"type":"string","description":"LiveKit API key for video conferencing"},"livekitApiSecret":{"type":"string","description":"LiveKit API secret for video conferencing"}}},"CreateTemplateDto":{"type":"object","properties":{"name":{"type":"string","description":"Nom du template (unique par tenant)","maxLength":255},"description":{"type":"string","description":"Description courte du template"},"category":{"type":"string","enum":["CONVENTION","CONVENTION_TRIPARTITE","PROGRAM","EMARGEMENT","ATTESTATION_SUIVI","ATTESTATION_INTERNE","CERTIFICATE","ACCUSE_LECTURE","REGLEMENT_INTERIEUR","LIVRET_SUIVI_ALTERNANCE","BPF","AUTRE"]},"content":{"type":"object","description":"Contenu ProseMirror JSON"},"bodyHtml":{"type":"string","description":"HTML brut Handlebars alternatif"},"pageWidthMm":{"type":"number","default":210},"pageHeightMm":{"type":"number","default":297},"marginTopMm":{"type":"number","default":15},"marginRightMm":{"type":"number","default":15},"marginBottomMm":{"type":"number","default":15},"marginLeftMm":{"type":"number","default":15},"defaultSignatureMode":{"type":"string","enum":["CANVAS","EIDAS","AUTO"],"default":"AUTO"},"isDefault":{"type":"boolean","default":false}},"required":["name","category"]},"UpdateTemplateDto":{"type":"object","properties":{"name":{"type":"string","description":"Nom du template (unique par tenant)","maxLength":255},"description":{"type":"string","description":"Description courte du template"},"category":{"type":"string","enum":["CONVENTION","CONVENTION_TRIPARTITE","PROGRAM","EMARGEMENT","ATTESTATION_SUIVI","ATTESTATION_INTERNE","CERTIFICATE","ACCUSE_LECTURE","REGLEMENT_INTERIEUR","LIVRET_SUIVI_ALTERNANCE","BPF","AUTRE"]},"content":{"type":"object","description":"Contenu ProseMirror JSON"},"bodyHtml":{"type":"string","description":"HTML brut Handlebars alternatif"},"pageWidthMm":{"type":"number","default":210},"pageHeightMm":{"type":"number","default":297},"marginTopMm":{"type":"number","default":15},"marginRightMm":{"type":"number","default":15},"marginBottomMm":{"type":"number","default":15},"marginLeftMm":{"type":"number","default":15},"defaultSignatureMode":{"type":"string","enum":["CANVAS","EIDAS","AUTO"],"default":"AUTO"},"isDefault":{"type":"boolean","default":false}}},"FieldDto":{"type":"object","properties":{"id":{"type":"string","description":"ID existant (omis pour création)"},"kind":{"type":"string","enum":["SIGNATURE","DATE","TEXT","CHECKBOX","INITIALS"]},"label":{"type":"string","description":"Libellé affiché dans l'éditeur","maxLength":255},"placeholder":{"type":"string","maxLength":255},"pageIndex":{"type":"number","default":0},"xMm":{"type":"number","description":"Position X en mm depuis le bord gauche"},"yMm":{"type":"number","description":"Position Y en mm depuis le bord haut"},"widthMm":{"type":"number","description":"Largeur en mm"},"heightMm":{"type":"number","description":"Hauteur en mm"},"required":{"type":"boolean","default":true},"signerRole":{"type":"string","enum":["LEARNER","TRAINER","ORGANIZATION","TUTOR","FINANCER","OTHER"]},"order":{"type":"number","default":0},"defaultValue":{"type":"string"}},"required":["kind","label","xMm","yMm","widthMm","heightMm"]},"UpsertFieldsDto":{"type":"object","properties":{"fields":{"description":"Remplacement atomique de tous les champs","type":"array","items":{"$ref":"#/components/schemas/FieldDto"}}},"required":["fields"]},"CreateVersionDto":{"type":"object","properties":{"comment":{"type":"string","description":"Commentaire optionnel décrivant les changements","maxLength":500}}},"PreviewTemplateDto":{"type":"object","properties":{"customVariables":{"type":"object","description":"Variables personnalisées à injecter dans l'aperçu (key → value)","example":{"custom.reference":"REF-2026-001"}}}},"CanvasSignatureDto":{"type":"object","properties":{"fieldId":{"type":"string","description":"ID du DocumentTemplateField associé"},"base64Png":{"type":"string","description":"Image PNG encodée en base64 (sans prefix data URI), max ~3.7MB"},"signedBy":{"type":"object","description":"Informations sur le signataire"}},"required":["fieldId","base64Png","signedBy"]},"RenderTemplateDto":{"type":"object","properties":{"courseId":{"type":"string","description":"ID du cours pour le contexte"},"enrollmentId":{"type":"string","description":"ID de l'enrollment pour le contexte"},"sessionId":{"type":"string","description":"ID de la session live pour le contexte"},"customVariables":{"type":"object","description":"Variables personnalisées (key → value)","example":{"custom.reference":"REF-2026-001"}},"canvasSignatures":{"description":"Signatures canvas à appliquer sur le PDF","type":"array","items":{"$ref":"#/components/schemas/CanvasSignatureDto"}}},"required":["courseId"]},"ApplyCanvasSignaturesDto":{"type":"object","properties":{"signatures":{"description":"Liste des signatures canvas à appliquer sur le document PDF","type":"array","items":{"$ref":"#/components/schemas/CanvasSignatureDto"}}},"required":["signatures"]},"SignerDto":{"type":"object","properties":{"name":{"type":"string","description":"Nom du signataire"},"email":{"type":"string","description":"Email du signataire"},"role":{"type":"string","description":"Rôle du signataire"}},"required":["name","email"]},"RequestSignatureDto":{"type":"object","properties":{"mode":{"type":"string","enum":["CANVAS","EIDAS"],"description":"Mode de signature"},"signers":{"description":"Liste des signataires (requis pour EIDAS)","type":"array","items":{"$ref":"#/components/schemas/SignerDto"}}},"required":["mode"]},"GenerateDocumentDto":{"type":"object","properties":{"type":{"type":"string","enum":["CONVENTION","PROGRAM","EMARGEMENT_SHEET","ATTESTATION","CERTIFICATE","BPF","NEEDS_ANALYSIS","PIF"],"description":"Type de document Qualiopi à générer","example":"CONVENTION"},"courseId":{"type":"string","description":"CUID de la formation (Course)","example":"clxxxxxxxxxxxxxxxxxxxxxxxx"},"enrollmentId":{"type":"string","description":"CUID de l'inscription (Enrollment) — requis pour CONVENTION, ATTESTATION, CERTIFICATE, EMARGEMENT_SHEET","example":"clyyyyyyyyyyyyyyyyyyyyyyyy"},"sessionId":{"type":"string","description":"CUID de la session live (LiveSession) — requis uniquement pour EMARGEMENT_SHEET si multiple sessions","example":"clzzzzzzzzzzzzzzzzzzzzzzzz"}},"required":["type","courseId"]},"EmailTemplateCategoryEnum":{"type":"string","enum":["CONVOCATION","LEARNER_INVITATION","CONVENTION_SENT","ATTESTATION_SENT","QUESTIONNAIRE_REMINDER","NEEDS_ANALYSIS_RECEIVED","ENROLLMENT_INACTIVITY_REMINDER","COMMANDITAIRE_SURVEY","AUTRE"],"description":"Catégorie métier du template (1 template par défaut par catégorie par tenant)"},"CreateEmailTemplateDto":{"type":"object","properties":{"category":{"description":"Catégorie métier du template (1 template par défaut par catégorie par tenant)","example":"CONVOCATION","allOf":[{"$ref":"#/components/schemas/EmailTemplateCategoryEnum"}]},"name":{"type":"string","description":"Nom unique du template pour ce tenant","example":"Convocation formation Excel","maxLength":150},"subject":{"type":"string","description":"Objet de l'email (Handlebars-compatible)","example":"Convocation — {{courseTitle}}","maxLength":250},"content":{"type":"object","description":"Représentation structurée ProseMirror (Tiptap JSON)","example":{"type":"doc","content":[]}},"bodyHtml":{"type":"string","description":"Rendu HTML Handlebars-compatible pour l'envoi email","example":"<p>Bonjour {{firstName}},</p>"},"bodyText":{"type":"string","description":"Version texte brut (fallback clients mail plain-text)","example":"Bonjour {{firstName}},"},"isActive":{"type":"boolean","description":"Activer le template immédiatement après création","default":true,"example":true},"isDefault":{"type":"boolean","description":"Définir ce template comme défaut pour sa catégorie (démarque les autres templates de la même catégorie)","default":false,"example":false},"attachedDocumentTemplateId":{"type":"string","description":"Identifiant d'un DocumentTemplate à attacher automatiquement lors de l'envoi (ex: convention, attestation)","example":"cldoctemplate789"}},"required":["category","name","subject","content","bodyHtml"]},"UpdateEmailTemplateDto":{"type":"object","properties":{"category":{"description":"Catégorie métier du template","example":"CONVOCATION","allOf":[{"$ref":"#/components/schemas/EmailTemplateCategoryEnum"}]},"name":{"type":"string","description":"Nom unique du template pour ce tenant","example":"Convocation formation Excel","maxLength":150},"subject":{"type":"string","description":"Objet de l'email (Handlebars-compatible)","example":"Convocation — {{courseTitle}}","maxLength":250},"content":{"type":"object","description":"Représentation structurée ProseMirror (Tiptap JSON)","example":{"type":"doc","content":[]}},"bodyHtml":{"type":"string","description":"Rendu HTML Handlebars-compatible pour l'envoi email","example":"<p>Bonjour {{firstName}},</p>"},"bodyText":{"type":"string","description":"Version texte brut (fallback clients mail plain-text)","example":"Bonjour {{firstName}},"},"isActive":{"type":"boolean","description":"Activer ou désactiver le template","example":true},"isDefault":{"type":"boolean","description":"Définir ce template comme défaut pour sa catégorie (démarque les autres)","example":false},"attachedDocumentTemplateId":{"type":"string","description":"Identifiant d'un DocumentTemplate à attacher lors de l'envoi. Passer null pour détacher","example":"cldoctemplate789"}}},"DuplicateEmailTemplateDto":{"type":"object","properties":{"name":{"type":"string","description":"Nom du template dupliqué. Si absent, le service génère automatiquement \"{nom source} (copie)\"","example":"Convocation formation Excel (copie)","maxLength":150},"category":{"description":"Catégorie du template dupliqué. Si absent, hérite de la catégorie du template source","example":"CONVOCATION","allOf":[{"$ref":"#/components/schemas/EmailTemplateCategoryEnum"}]}}},"CreateFunderDto":{"type":"object","properties":{"code":{"type":"string","enum":["OPCO_AKTO","OPCO_ATLAS","OPCO_AFDAS","OPCO_CONSTRUCTYS","OPCO_2I","OPCO_COMMERCE","OPCO_EP","OPCO_MOBILITES","OPCO_SANTE","UNIFORMATION","OCAPIAT","CPF","POLE_EMPLOI","REGION","AGEFIPH","EMPLOYEUR_DIRECT","FONDS_PROPRES","AUTRE"],"description":"Code normalié du financeur (ex : OPCO_AKTO, CPF, AUTRE…)","example":"AUTRE"},"name":{"type":"string","description":"Nom complet du financeur","example":"Mutuelle XYZ — Plan de financement 2026","minLength":2,"maxLength":200},"address":{"type":"string","description":"Adresse postale (rue, numéro)","example":"11 rue de l'Arrivée"},"postalCode":{"type":"string","description":"Code postal","example":"75015"},"city":{"type":"string","description":"Ville","example":"Paris"},"country":{"type":"string","description":"Code pays ISO 3166-1 alpha-2","example":"FR","default":"FR"},"contactEmail":{"type":"string","description":"Email destinataire pour l'envoi automatique des documents","example":"contact@akto.fr"},"contactName":{"type":"string","description":"Nom du contact ou du service","example":"Service Financement"},"contactPhone":{"type":"string","description":"Téléphone du contact","example":"01 44 38 28 28"},"siret":{"type":"string","description":"Numéro SIRET (14 chiffres)","example":"18004001900001"},"notes":{"type":"string","description":"Notes internes libres (visible uniquement par les admins)","example":"Contacter Sophie Dupont pour les conventions AFEST"}},"required":["code","name"]},"UpdateFunderDto":{"type":"object","properties":{"code":{"type":"string","enum":["OPCO_AKTO","OPCO_ATLAS","OPCO_AFDAS","OPCO_CONSTRUCTYS","OPCO_2I","OPCO_COMMERCE","OPCO_EP","OPCO_MOBILITES","OPCO_SANTE","UNIFORMATION","OCAPIAT","CPF","POLE_EMPLOI","REGION","AGEFIPH","EMPLOYEUR_DIRECT","FONDS_PROPRES","AUTRE"],"description":"Code normalisé du financeur","example":"AUTRE"},"name":{"type":"string","description":"Nom complet du financeur","example":"AKTO — OPCO des Services à Forte Intensité de Main-d'Œuvre"},"address":{"type":"string","description":"Adresse postale (rue, numéro)","example":"11 rue de l'Arrivée"},"postalCode":{"type":"string","description":"Code postal","example":"75015"},"city":{"type":"string","description":"Ville","example":"Paris"},"country":{"type":"string","description":"Code pays ISO 3166-1 alpha-2","example":"FR"},"contactEmail":{"type":"string","description":"Email destinataire pour l'envoi des documents"},"contactName":{"type":"string","description":"Nom du contact ou du service"},"contactPhone":{"type":"string","description":"Téléphone du contact"},"siret":{"type":"string","description":"Numéro SIRET (14 chiffres)"},"notes":{"type":"string","description":"Notes internes libres"}}},"UpdateUserDto":{"type":"object","properties":{"dateOfBirth":{"type":"string","description":"Date de naissance au format ISO (YYYY-MM-DD)","example":"1985-03-15"},"placeOfBirth":{"type":"string","example":"Paris","maxLength":120},"birthCountry":{"type":"string","description":"Pays de naissance au format ISO 3166-1 alpha-2","example":"FR"},"gender":{"type":"string","enum":["MALE","FEMALE","OTHER"]},"nationality":{"type":"string","description":"Nationalité au format ISO 3166-1 alpha-2","example":"FR"},"socialSecurityNumber":{"type":"string","description":"Numéro de sécurité sociale (NIR) — 15 chiffres","example":"185037512345678"},"phone":{"type":"string","description":"Téléphone (format libre — validation côté front)","example":"+33 6 12 34 56 78"},"streetAddress":{"type":"string","example":"10 rue des Lilas","maxLength":200},"addressComplement":{"type":"string","example":"Appartement 4B","maxLength":200},"postalCode":{"type":"string","example":"75011","maxLength":16},"city":{"type":"string","example":"Paris","maxLength":120},"country":{"type":"string","description":"Pays de résidence (ISO 3166-1 alpha-2)","example":"FR"},"educationLevel":{"type":"string","enum":["NV1","NV2","NV3","NV4","NV5","NV6","NV7","NV8"]},"professionalStatus":{"type":"string","enum":["SALARIED","JOB_SEEKER","SELF_EMPLOYED","PUBLIC_SERVANT","STUDENT","APPRENTICE","INTERN","INACTIVE","RETIRED","OTHER"]},"cspCategory":{"type":"string","description":"Catégorie socio-professionnelle (libre, ex: Cadre)","maxLength":120},"rqth":{"type":"boolean","description":"Reconnaissance Qualité Travailleur Handicapé (Qualiopi 26)"},"accommodations":{"type":"string","description":"Besoins d'adaptation / aménagements (texte libre)","maxLength":2000},"cpfIdentifier":{"type":"string","description":"Identifiant Compte Personnel de Formation (CPF)","maxLength":32},"franceTravailId":{"type":"string","description":"Identifiant France Travail (ex-Pôle Emploi)","maxLength":32},"firstName":{"type":"string","example":"Jean","maxLength":100},"lastName":{"type":"string","example":"Dupont","maxLength":100},"email":{"type":"string","example":"jean.dupont@example.com"},"role":{"type":"string","enum":["ADMIN","CREATOR","TRAINER","LEARNER","FUNDER"],"description":"ADMIN only — change the user role"}}},"UpdateNotificationPreferencesDto":{"type":"object","properties":{"newEnrollment":{"type":"boolean","description":"Receive an email when a learner enrolls in one of your courses.","example":true},"questionnaireFilled":{"type":"boolean","description":"Receive an email when a learner submits a questionnaire response.","example":true},"paymentReceived":{"type":"boolean","description":"Receive an email when a payment is confirmed for one of your courses.","example":true}}},"CreateUserDto":{"type":"object","properties":{"dateOfBirth":{"type":"string","description":"Date de naissance au format ISO (YYYY-MM-DD)","example":"1985-03-15"},"placeOfBirth":{"type":"string","example":"Paris","maxLength":120},"birthCountry":{"type":"string","description":"Pays de naissance au format ISO 3166-1 alpha-2","example":"FR"},"gender":{"type":"string","enum":["MALE","FEMALE","OTHER"]},"nationality":{"type":"string","description":"Nationalité au format ISO 3166-1 alpha-2","example":"FR"},"socialSecurityNumber":{"type":"string","description":"Numéro de sécurité sociale (NIR) — 15 chiffres","example":"185037512345678"},"phone":{"type":"string","description":"Téléphone (format libre — validation côté front)","example":"+33 6 12 34 56 78"},"streetAddress":{"type":"string","example":"10 rue des Lilas","maxLength":200},"addressComplement":{"type":"string","example":"Appartement 4B","maxLength":200},"postalCode":{"type":"string","example":"75011","maxLength":16},"city":{"type":"string","example":"Paris","maxLength":120},"country":{"type":"string","description":"Pays de résidence (ISO 3166-1 alpha-2)","example":"FR"},"educationLevel":{"type":"string","enum":["NV1","NV2","NV3","NV4","NV5","NV6","NV7","NV8"]},"professionalStatus":{"type":"string","enum":["SALARIED","JOB_SEEKER","SELF_EMPLOYED","PUBLIC_SERVANT","STUDENT","APPRENTICE","INTERN","INACTIVE","RETIRED","OTHER"]},"cspCategory":{"type":"string","description":"Catégorie socio-professionnelle (libre, ex: Cadre)","maxLength":120},"rqth":{"type":"boolean","description":"Reconnaissance Qualité Travailleur Handicapé (Qualiopi 26)"},"accommodations":{"type":"string","description":"Besoins d'adaptation / aménagements (texte libre)","maxLength":2000},"cpfIdentifier":{"type":"string","description":"Identifiant Compte Personnel de Formation (CPF)","maxLength":32},"franceTravailId":{"type":"string","description":"Identifiant France Travail (ex-Pôle Emploi)","maxLength":32},"firstName":{"type":"string","example":"Jean","maxLength":100},"lastName":{"type":"string","example":"Dupont","maxLength":100},"email":{"type":"string","example":"jean.dupont@example.com"},"role":{"type":"string","enum":["ADMIN","CREATOR","TRAINER","LEARNER","FUNDER"],"description":"Rôle affecté (défaut : LEARNER). ADMIN/CREATOR réservés à un admin."}},"required":["firstName","lastName","email"]},"QualificationItemDto":{"type":"object","properties":{"title":{"type":"string","example":"Master en Ingénierie de la Formation"},"institution":{"type":"string","example":"Université Paris-Saclay"},"year":{"type":"string","example":"2018"},"degreeLevel":{"type":"string","example":"Master (Bac+5)"}}},"ExperienceItemDto":{"type":"object","properties":{"company":{"type":"string","example":"QR Communication"},"role":{"type":"string","example":"Lead Developer"},"start":{"type":"string","example":"2018-01","description":"ISO date or YYYY-MM"},"end":{"type":"string","example":"2024-12","description":"ISO date, YYYY-MM, or \"present\""},"description":{"type":"string","example":"Direction technique d'une équipe de 6 développeurs."}}},"CreateTrainerProfileDto":{"type":"object","properties":{"bio":{"type":"string","example":"Formateur expert en développement web depuis 10 ans.","maxLength":2000},"qualifications":{"description":"Academic and professional qualifications (Qualiopi indicator 1)","type":"array","items":{"$ref":"#/components/schemas/QualificationItemDto"}},"specialties":{"example":["NestJS","TypeScript","Architecture logicielle"],"type":"array","items":{"type":"string"}},"siret":{"type":"string","example":"80212344200015","description":"SIRET (14 digits, no spaces)"},"experiences":{"description":"Professional experiences","type":"array","items":{"$ref":"#/components/schemas/ExperienceItemDto"}},"qualiopiNumber":{"type":"string","example":"QF-2024-001234"},"qualiopiExpiry":{"type":"string","example":"2027-06-30T00:00:00.000Z"},"trainerType":{"type":"string","enum":["INTERNAL","INDEPENDENT","SUBCONTRACTOR"],"example":"INTERNAL"},"contractType":{"type":"string","enum":["CDI","CDD","FREELANCE","SUBCONTRACT","VOLUNTEER"],"example":"CDI"},"hourlyRate":{"type":"number","example":65.5,"description":"Taux horaire brut EUR"},"watchEnabled":{"type":"boolean","example":true},"watchFrequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY"],"example":"WEEKLY"},"watchThemes":{"example":["NestJS","WCAG"],"type":"array","items":{"type":"string"}}}},"AiSuggestDto":{"type":"object","properties":{"type":{"type":"string","description":"Type de suggestion demandée.","enum":["info","objectives","audience-requirements","program-outline","lesson-details","questionnaire-questions","questionnaire-title","questionnaire-description","questionnaire-question-wording","questionnaire-full-template"]},"context":{"type":"string","description":"Contexte libre fourni par l'utilisateur (description, objectifs actuels, public visé…). Tronqué à 4 000 chars côté backend (suggest-service). La limite 16 000 sert uniquement de filet anti-DOS — collez un brief complet sans crainte.","example":"Formation sur l'IA générative pour les commerciaux immobiliers."},"courseTitle":{"type":"string","description":"Titre actuel du cours (hint pour la génération)."},"targetAudience":{"type":"string","description":"Public visé actuel (hint pour la génération)."},"lessonTitle":{"type":"string","description":"Titre de la leçon — requis pour le type lesson-details."},"moduleTitle":{"type":"string","description":"Titre du module parent — utilisé pour le type lesson-details."},"questionnaireType":{"type":"string","description":"Catégorie Qualiopi du questionnaire — requise pour questionnaire-questions, questionnaire-title, questionnaire-description, questionnaire-question-wording, questionnaire-full-template. Valeurs : PRE_TRAINING, POST_TRAINING_HOT, POST_TRAINING_COLD_30, POST_TRAINING_COLD_90, NEEDS_ANALYSIS, COMMANDITAIRE, SELF_POSITIONING, FINAL_ASSESSMENT, MID_COURSE, CUSTOM. Alias legacy acceptés : POST_HOT, POST_COLD, SATISFACTION, POSITIONING.","example":"POST_TRAINING_HOT"},"currentQuestionLabel":{"type":"string","description":"Formulation actuelle d'une question — utilisée uniquement par questionnaire-question-wording pour reformuler une question existante."}},"required":["type","context"]},"GenerateSeoFieldsDto":{"type":"object","properties":{"title":{"type":"string","description":"Course title that anchors the SEO context.","example":"Maîtriser Excel pour les RH","minLength":3,"maxLength":200},"description":{"type":"string","description":"Course description (max 4000 chars).","maxLength":4000},"objectives":{"description":"Pedagogical objectives (up to 20).","type":"array","items":{"type":"string"}},"targetAudience":{"type":"string","description":"Target audience description.","maxLength":1000},"fieldsToGenerate":{"type":"array","description":"If set, the backend will focus on regenerating these fields (others are still returned but may be less specific).","items":{"type":"string","enum":["metaTitle","metaDescription","slug","keywords"]}}},"required":["title"]},"EnrichDraftDto":{"type":"object","properties":{"draft":{"type":"object","description":"Structure de formation générée précédemment par POST /courses/generate-draft"},"draftId":{"type":"string","description":"Identifiant opaque retourné par /courses/generate-draft. Permet de récupérer les résumés des documents sources côté serveur pour ancrer la génération de contenu de leçons dans les supports uploadés plutôt que de les inventer."},"generateLessonContent":{"type":"boolean","description":"Active l'enrichissement du contenu des leçons (markdown complet).","default":true},"generateModuleQuestionnaires":{"type":"boolean","description":"Active la génération des questionnaires de validation par module.","default":true}},"required":["draft"]},"BulkGenerateDraftDto":{"type":"object","properties":{"onlyEmpty":{"type":"boolean","description":"Si true (défaut), ne régénère que les leçons sans contenu. Si false, régénère toutes les leçons.","default":true},"types":{"type":"array","description":"Filtrer par types de leçons à régénérer. Si absent, tous les types sont inclus.","example":["VIDEO","TEXT"],"items":{"type":"string","enum":["VIDEO","TEXT","SCORM","LIVE_SESSION","ACTIVITY","DOCUMENT","QUIZ"]}}}},"SetMediaReferenceDto":{"type":"object","properties":{"mediaId":{"type":"object","description":"CUID du média dans la médiathèque tenant. null pour délier.","example":"clx8abc123def456","nullable":true}}},"CreateCourseDto":{"type":"object","properties":{"title":{"type":"string","description":"Course title — used to auto-generate the URL slug","example":"Introduction à la gestion de projet Agile","minLength":3,"maxLength":255},"description":{"type":"string","description":"Rich-text course description. Minimum 3 characters. Qualiopi recommendation: at least 100 characters for a meaningful description. The frontend should display a soft warning when description < 100 characters.","example":"Découvrez les fondamentaux de la gestion de projet Agile...","minLength":3},"objectives":{"description":"List of learning objectives (Qualiopi indicateur 1)","example":["Maîtriser les cérémonies Scrum","Gérer un backlog produit"],"type":"array","items":{"type":"string"}},"prerequisites":{"type":"string","description":"Prerequisites for enrolling (Qualiopi indicateur 1)","example":"Connaissance de base en gestion de projet"},"targetAudience":{"type":"string","description":"Target audience description","example":"Chefs de projet, Product Owners, développeurs"},"evaluationMethods":{"description":"Evaluation methods used during the course (Qualiopi indicateur 6)","example":["Quiz en ligne","Étude de cas","Mise en situation"],"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"description":"Pedagogical methods and resources (Qualiopi indicateur 5)","example":["Cours magistral","Travaux pratiques","Simulation"],"type":"array","items":{"type":"string"}},"certificationInfo":{"type":"string","description":"Certification or accreditation information","example":"Certification PSM I — Professional Scrum Master"},"accessibilityInfo":{"type":"string","description":"Accessibility information for learners with disabilities","example":"Sous-titres disponibles, compatible lecteur d'écran"},"duration":{"type":"number","description":"Course duration expressed in the unit specified by `durationUnit` (default: MINUTES). The value is always persisted in MINUTES. Examples: 480 (MINUTES) = 8 h, 8 (HOURS) = 8 h, 1 (DAYS) = 8 h.","example":480,"minimum":1},"durationUnit":{"type":"string","description":"Unit of measurement for `duration`. MINUTES (default): value stored as-is. HOURS: multiplied by 60 before storage. DAYS: multiplied by 480 (8 h/day) before storage.","enum":["MINUTES","HOURS","DAYS"],"default":"MINUTES","example":"HOURS"},"format":{"type":"string","description":"Delivery format","enum":["ELEARNING","LIVE","BLENDED","PRESENTIAL"],"example":"BLENDED"},"price":{"type":"string","description":"Course price in the specified currency. Omit for free courses.","example":"499.00"},"currency":{"type":"string","description":"ISO 4217 currency code","example":"EUR","default":"EUR","maxLength":3},"maxParticipants":{"type":"number","description":"Maximum number of participants (null = unlimited)","example":20,"minimum":1,"maximum":9999},"nsfCode":{"type":"string","description":"NSF (Nomenclature des Spécialités de Formation) code for Qualiopi compliance","example":"326","maxLength":3},"nsfLabel":{"type":"string","description":"NSF label (human-readable name of the specialization)","example":"Informatique, traitement de l'information, réseaux de transmission"},"actionType":{"type":"string","description":"Type of training action (BPF — Plan de Financement)","enum":["STAGE","FORMATION_CONTINUE","PLAN_DEVELOPPEMENT","CPF","APPRENTISSAGE","PROFESSIONALISATION","ALTERNANCE","PRO_A","VAE","BILAN_COMPETENCES","OTHER"],"example":"PLAN_DEVELOPPEMENT"}},"required":["title","description","duration","format"]},"BulkLessonDto":{"type":"object","properties":{"title":{"type":"string","minLength":2,"maxLength":255},"type":{"type":"string","enum":["VIDEO","TEXT","SCORM","LIVE_SESSION","ACTIVITY","DOCUMENT","QUIZ"]},"content":{"type":"string"},"duration":{"type":"number","minimum":1,"maximum":9999},"isRequired":{"type":"boolean","default":true},"objectives":{"maxItems":20,"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"type":"array","maxItems":20,"items":{"type":"string","enum":["EXPOSITIVE","DEMONSTRATIVE","ACTIVE","INTERROGATIVE","DISCOVERY","EXPERIENTIAL","COLLABORATIVE","CASE_BASED","PROBLEM_BASED","FLIPPED_CLASSROOM"]}},"exerciseType":{"type":"string","enum":["QCM","QUIZ","DRILL","CASE_STUDY","SIMULATION","ROLE_PLAY","PROJECT","PRACTICAL","ORAL_DEFENSE","WRITTEN_EXAM","PEER_REVIEW","OBSERVATION","OTHER"]},"exerciseConfig":{"type":"object"},"estimatedMinMinutes":{"type":"number","minimum":1},"estimatedMaxMinutes":{"type":"number","minimum":1},"accessibilityNotes":{"type":"string","maxLength":2000},"keyPoints":{"maxItems":20,"type":"array","items":{"type":"string"}},"supportMaterials":{"maxItems":20,"type":"array","items":{"type":"string"}},"questions":{"type":"array","items":{"type":"string"}}},"required":["title","type"]},"BulkModuleDto":{"type":"object","properties":{"title":{"type":"string","minLength":2,"maxLength":255},"description":{"type":"string","maxLength":2000},"duration":{"type":"number","minimum":1,"maximum":100000},"isRequired":{"type":"boolean","default":true},"objectives":{"maxItems":30,"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"type":"array","maxItems":20,"items":{"type":"string","enum":["EXPOSITIVE","DEMONSTRATIVE","ACTIVE","INTERROGATIVE","DISCOVERY","EXPERIENTIAL","COLLABORATIVE","CASE_BASED","PROBLEM_BASED","FLIPPED_CLASSROOM"]}},"prerequisites":{"type":"string","maxLength":2000},"evaluationSummary":{"type":"string","maxLength":2000},"lessons":{"minItems":0,"maxItems":100,"type":"array","items":{"$ref":"#/components/schemas/BulkLessonDto"}}},"required":["title","lessons"]},"BulkQuestionnaireDto":{"type":"object","properties":{"type":{"type":"string","example":"PRE_TRAINING"},"title":{"type":"string","minLength":2,"maxLength":255},"sendAt":{"type":"string","example":"BEFORE_START"},"sendDaysOffset":{"type":"number","minimum":-365,"maximum":365},"isActive":{"type":"boolean","default":true},"questions":{"type":"array","items":{"type":"object"}}},"required":["type","title","sendAt","questions"]},"BulkSeoDto":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"slug":{"type":"string"},"featuredImage":{"type":"string"},"keywords":{"maxItems":20,"type":"array","items":{"type":"string"}}}},"CreateCourseBulkDto":{"type":"object","properties":{"title":{"type":"string","description":"Course title — used to auto-generate the URL slug","example":"Introduction à la gestion de projet Agile","minLength":3,"maxLength":255},"description":{"type":"string","description":"Rich-text course description. Minimum 3 characters. Qualiopi recommendation: at least 100 characters for a meaningful description. The frontend should display a soft warning when description < 100 characters.","example":"Découvrez les fondamentaux de la gestion de projet Agile...","minLength":3},"objectives":{"description":"List of learning objectives (Qualiopi indicateur 1)","example":["Maîtriser les cérémonies Scrum","Gérer un backlog produit"],"type":"array","items":{"type":"string"}},"prerequisites":{"type":"string","description":"Prerequisites for enrolling (Qualiopi indicateur 1)","example":"Connaissance de base en gestion de projet"},"targetAudience":{"type":"string","description":"Target audience description","example":"Chefs de projet, Product Owners, développeurs"},"evaluationMethods":{"description":"Evaluation methods used during the course (Qualiopi indicateur 6)","example":["Quiz en ligne","Étude de cas","Mise en situation"],"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"description":"Pedagogical methods and resources (Qualiopi indicateur 5)","example":["Cours magistral","Travaux pratiques","Simulation"],"type":"array","items":{"type":"string"}},"certificationInfo":{"type":"string","description":"Certification or accreditation information","example":"Certification PSM I — Professional Scrum Master"},"accessibilityInfo":{"type":"string","description":"Accessibility information for learners with disabilities","example":"Sous-titres disponibles, compatible lecteur d'écran"},"duration":{"type":"number","description":"Course duration expressed in the unit specified by `durationUnit` (default: MINUTES). The value is always persisted in MINUTES. Examples: 480 (MINUTES) = 8 h, 8 (HOURS) = 8 h, 1 (DAYS) = 8 h.","example":480,"minimum":1},"durationUnit":{"type":"string","description":"Unit of measurement for `duration`. MINUTES (default): value stored as-is. HOURS: multiplied by 60 before storage. DAYS: multiplied by 480 (8 h/day) before storage.","enum":["MINUTES","HOURS","DAYS"],"default":"MINUTES","example":"HOURS"},"format":{"type":"string","description":"Delivery format","enum":["ELEARNING","LIVE","BLENDED","PRESENTIAL"],"example":"BLENDED"},"price":{"type":"string","description":"Course price in the specified currency. Omit for free courses.","example":"499.00"},"currency":{"type":"string","description":"ISO 4217 currency code","example":"EUR","default":"EUR","maxLength":3},"maxParticipants":{"type":"number","description":"Maximum number of participants (null = unlimited)","example":20,"minimum":1,"maximum":9999},"nsfCode":{"type":"string","description":"NSF (Nomenclature des Spécialités de Formation) code for Qualiopi compliance","example":"326","maxLength":3},"nsfLabel":{"type":"string","description":"NSF label (human-readable name of the specialization)","example":"Informatique, traitement de l'information, réseaux de transmission"},"actionType":{"type":"string","description":"Type of training action (BPF — Plan de Financement)","enum":["STAGE","FORMATION_CONTINUE","PLAN_DEVELOPPEMENT","CPF","APPRENTISSAGE","PROFESSIONALISATION","ALTERNANCE","PRO_A","VAE","BILAN_COMPETENCES","OTHER"],"example":"PLAN_DEVELOPPEMENT"},"modules":{"maxItems":50,"type":"array","items":{"$ref":"#/components/schemas/BulkModuleDto"}},"questionnaires":{"maxItems":20,"type":"array","items":{"$ref":"#/components/schemas/BulkQuestionnaireDto"}},"seo":{"$ref":"#/components/schemas/BulkSeoDto"},"publish":{"type":"boolean","description":"If true, publish the course right after creation (applies the same Qualiopi validation as POST /:id/publish).","default":false}},"required":["title","description","duration","format"]},"UpdateCourseDto":{"type":"object","properties":{"title":{"type":"string","description":"Course title — used to auto-generate the URL slug","example":"Introduction à la gestion de projet Agile","minLength":3,"maxLength":255},"description":{"type":"string","description":"Rich-text course description. Minimum 3 characters. Qualiopi recommendation: at least 100 characters for a meaningful description. The frontend should display a soft warning when description < 100 characters.","example":"Découvrez les fondamentaux de la gestion de projet Agile...","minLength":3},"objectives":{"description":"List of learning objectives (Qualiopi indicateur 1)","example":["Maîtriser les cérémonies Scrum","Gérer un backlog produit"],"type":"array","items":{"type":"string"}},"prerequisites":{"type":"string","description":"Prerequisites for enrolling (Qualiopi indicateur 1)","example":"Connaissance de base en gestion de projet"},"targetAudience":{"type":"string","description":"Target audience description","example":"Chefs de projet, Product Owners, développeurs"},"evaluationMethods":{"description":"Evaluation methods used during the course (Qualiopi indicateur 6)","example":["Quiz en ligne","Étude de cas","Mise en situation"],"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"description":"Pedagogical methods and resources (Qualiopi indicateur 5)","example":["Cours magistral","Travaux pratiques","Simulation"],"type":"array","items":{"type":"string"}},"certificationInfo":{"type":"string","description":"Certification or accreditation information","example":"Certification PSM I — Professional Scrum Master"},"accessibilityInfo":{"type":"string","description":"Accessibility information for learners with disabilities","example":"Sous-titres disponibles, compatible lecteur d'écran"},"duration":{"type":"number","description":"Course duration expressed in the unit specified by `durationUnit` (default: MINUTES). The value is always persisted in MINUTES. Examples: 480 (MINUTES) = 8 h, 8 (HOURS) = 8 h, 1 (DAYS) = 8 h.","example":480,"minimum":1},"durationUnit":{"type":"string","description":"Unit of measurement for `duration`. MINUTES (default): value stored as-is. HOURS: multiplied by 60 before storage. DAYS: multiplied by 480 (8 h/day) before storage.","enum":["MINUTES","HOURS","DAYS"],"default":"MINUTES","example":"HOURS"},"format":{"type":"string","description":"Delivery format","enum":["ELEARNING","LIVE","BLENDED","PRESENTIAL"],"example":"BLENDED"},"price":{"type":"string","description":"Course price in the specified currency. Omit for free courses.","example":"499.00"},"currency":{"type":"string","description":"ISO 4217 currency code","example":"EUR","default":"EUR","maxLength":3},"maxParticipants":{"type":"number","description":"Maximum number of participants (null = unlimited)","example":20,"minimum":1,"maximum":9999},"nsfCode":{"type":"string","description":"NSF (Nomenclature des Spécialités de Formation) code for Qualiopi compliance","example":"326","maxLength":3},"nsfLabel":{"type":"string","description":"NSF label (human-readable name of the specialization)","example":"Informatique, traitement de l'information, réseaux de transmission"},"actionType":{"type":"string","description":"Type of training action (BPF — Plan de Financement)","enum":["STAGE","FORMATION_CONTINUE","PLAN_DEVELOPPEMENT","CPF","APPRENTISSAGE","PROFESSIONALISATION","ALTERNANCE","PRO_A","VAE","BILAN_COMPETENCES","OTHER"],"example":"PLAN_DEVELOPPEMENT"},"slug":{"type":"string","description":"Slug unique de l'URL publique (ex : \"formation-python-debutant\"). En cas de collision intra-tenant, un suffixe numérique est ajouté.","example":"formation-python-debutant","pattern":"^[a-z0-9-]+$"},"seoMetadata":{"type":"object","description":"Métadonnées SEO libres (description, keywords, ogImage, etc.). Surcharge les valeurs générées automatiquement à partir du titre, description et objectifs."},"featuredImage":{"type":"string","description":"URL de l'image en vedette (featured image). Stockée dans seoMetadata.featuredImage.","example":"https://cdn.qualiforma.fr/courses/abc/hero.jpg"}}},"CreateVariantDto":{"type":"object","properties":{"targetAudience":{"type":"string","description":"Description du nouveau public cible pour la variante","example":"Managers de PME sans expérience technique"},"context":{"type":"string","description":"Contexte supplémentaire pour guider l'adaptation IA (secteur, niveau, contraintes)","example":"Secteur industriel, niveau débutant, 0 prérequis informatique"}},"required":["targetAudience"]},"CreateModuleDto":{"type":"object","properties":{"title":{"type":"string","description":"Module title","example":"Introduction au Scrum","minLength":2,"maxLength":255},"description":{"type":"string","description":"Module description (optional)","example":"Ce module couvre les bases du framework Scrum."},"duration":{"type":"number","description":"Estimated duration of this module in minutes","example":90,"minimum":1,"maximum":9999},"isRequired":{"type":"boolean","description":"Whether completing this module is required to progress in the course","default":true},"objectives":{"description":"Learning objectives for this module (Qualiopi indicateur 2)","example":["Maîtriser les bases du framework Scrum","Animer une rétrospective"],"maxItems":15,"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"type":"array","description":"Pedagogical methods used in this module (Qualiopi indicateur 11)","maxItems":15,"items":{"type":"string","enum":["EXPOSITIVE","DEMONSTRATIVE","ACTIVE","INTERROGATIVE","DISCOVERY","EXPERIENTIAL","COLLABORATIVE","CASE_BASED","PROBLEM_BASED","FLIPPED_CLASSROOM"]}},"prerequisites":{"type":"string","description":"Prerequisites required before starting this module","example":"Avoir suivi le module Introduction aux méthodes agiles.","maxLength":2000},"evaluationSummary":{"type":"string","description":"Summary of evaluation methods used in this module","example":"QCM de validation + mise en situation pratique.","maxLength":2000},"parentModuleId":{"type":"string","description":"ID du module parent. Si fourni, ce module devient un sous-module dans l'arbre. Limité à 1 niveau de profondeur (rejeté si le parent a lui-même un parentModuleId). Le parent doit appartenir au même cours.","example":"clx8abc123def456"}},"required":["title","duration"]},"ModuleTreeMoveItem":{"type":"object","properties":{"id":{"type":"string","description":"CUID du module à déplacer."},"parentModuleId":{"type":"object","description":"Nouveau parent (CUID) ou null pour devenir module root. Si omis, la position du module est mise à jour mais son parent reste inchangé.","nullable":true},"order":{"type":"number","description":"Nouvel ordre dans le scope du parent (1-based, contigu)."}},"required":["id","order"]},"ReorderModulesDto":{"type":"object","properties":{"moduleIds":{"description":"Liste plate des CUIDs de modules root dans l'ordre voulu. Doit inclure exactement tous les modules root du cours.","example":["clx8abc123def456","clx8def456abc123"],"type":"array","items":{"type":"string"}},"tree":{"description":"Opérations cross-parent : pour chaque module à déplacer, son nouveau parentModuleId et son nouvel order. Mode RECOMMANDÉ pour le drag-and-drop multi-niveau (un module peut migrer entre parents).","type":"array","items":{"$ref":"#/components/schemas/ModuleTreeMoveItem"}}}},"UpdateModuleDto":{"type":"object","properties":{"title":{"type":"string","description":"Module title","example":"Introduction au Scrum","minLength":2,"maxLength":255},"description":{"type":"string","description":"Module description (optional)","example":"Ce module couvre les bases du framework Scrum."},"duration":{"type":"number","description":"Estimated duration of this module in minutes","example":90,"minimum":1,"maximum":9999},"isRequired":{"type":"boolean","description":"Whether completing this module is required to progress in the course","default":true},"objectives":{"description":"Learning objectives for this module (Qualiopi indicateur 2)","example":["Maîtriser les bases du framework Scrum","Animer une rétrospective"],"maxItems":15,"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"type":"array","description":"Pedagogical methods used in this module (Qualiopi indicateur 11)","maxItems":15,"items":{"type":"string","enum":["EXPOSITIVE","DEMONSTRATIVE","ACTIVE","INTERROGATIVE","DISCOVERY","EXPERIENTIAL","COLLABORATIVE","CASE_BASED","PROBLEM_BASED","FLIPPED_CLASSROOM"]}},"prerequisites":{"type":"string","description":"Prerequisites required before starting this module","example":"Avoir suivi le module Introduction aux méthodes agiles.","maxLength":2000},"evaluationSummary":{"type":"string","description":"Summary of evaluation methods used in this module","example":"QCM de validation + mise en situation pratique.","maxLength":2000},"parentModuleId":{"type":"string","description":"ID du module parent. Si fourni, ce module devient un sous-module dans l'arbre. Limité à 1 niveau de profondeur (rejeté si le parent a lui-même un parentModuleId). Le parent doit appartenir au même cours.","example":"clx8abc123def456"}}},"CreateLessonDto":{"type":"object","properties":{"title":{"type":"string","description":"Lesson title","example":"Les cérémonies Scrum","minLength":2,"maxLength":255},"type":{"type":"string","description":"Lesson type — determines content fields used","enum":["VIDEO","TEXT","SCORM","LIVE_SESSION","ACTIVITY","DOCUMENT","QUIZ"],"example":"VIDEO"},"content":{"type":"string","description":"Rich-text or HTML content (used for TEXT, ACTIVITY, QUIZ types)","example":"<p>Le Daily Scrum est une réunion quotidienne de 15 minutes...</p>"},"duration":{"type":"number","description":"Lesson duration in minutes","example":15,"minimum":1,"maximum":9999},"isRequired":{"type":"boolean","description":"Whether completing this lesson is required to complete the module","default":true},"scormPackageId":{"type":"string","description":"SCORM package ID to associate with this lesson (SCORM type only)","example":"clx8abc123def456"},"objectives":{"description":"Learning objectives for this lesson (Qualiopi indicateur 2)","example":["Comprendre les cérémonies Scrum","Identifier les rôles clés"],"maxItems":20,"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"type":"array","description":"Pedagogical methods used (Qualiopi indicateur 11)","maxItems":20,"items":{"type":"string","enum":["EXPOSITIVE","DEMONSTRATIVE","ACTIVE","INTERROGATIVE","DISCOVERY","EXPERIENTIAL","COLLABORATIVE","CASE_BASED","PROBLEM_BASED","FLIPPED_CLASSROOM"]}},"exerciseType":{"type":"string","description":"Exercise or evaluation type (Qualiopi indicateurs 7, 11, 12)","enum":["QCM","QUIZ","DRILL","CASE_STUDY","SIMULATION","ROLE_PLAY","PROJECT","PRACTICAL","ORAL_DEFENSE","WRITTEN_EXAM","PEER_REVIEW","OBSERVATION","OTHER"]},"exerciseConfig":{"type":"object","description":"Configuration for the exercise (schema depends on exerciseType)","example":{"questionCount":10,"passingScore":70}},"estimatedMinMinutes":{"type":"number","description":"Minimum estimated completion time in minutes","example":10,"minimum":1},"estimatedMaxMinutes":{"type":"number","description":"Maximum estimated completion time in minutes","example":20,"minimum":1},"accessibilityNotes":{"type":"string","description":"Accessibility notes for learners with disabilities","example":"Sous-titres disponibles. Navigation clavier complète.","maxLength":2000},"keyPoints":{"description":"Key points to retain from this lesson","example":["Daily Scrum = 15 min maximum","Le Scrum Master facilite, il ne dirige pas"],"maxItems":20,"type":"array","items":{"type":"string"}},"supportMaterials":{"description":"Support materials referenced in this lesson (titles, URLs, doc IDs)","example":["Guide pédagogique Scrum","https://scrumguides.org"],"maxItems":20,"type":"array","items":{"type":"string"}},"questions":{"description":"Quiz questions array for QUIZ-type lessons. Each question has a type, label, options, correct answer indices, and points.","example":[{"id":"q_1","type":"SINGLE_CHOICE","label":"Quel est le rôle du Scrum Master ?","required":true,"options":["Développer","Faciliter","Décider"],"correctIndex":1,"points":1}],"type":"array","items":{"type":"string"}}},"required":["title","type"]},"ReorderLessonsDto":{"type":"object","properties":{"lessonIds":{"description":"Liste ordonnée complète des lesson CUIDs. Doit inclure toutes les leçons du module. La position dans le tableau détermine le nouvel ordre (index 0 → order 1).","example":["clx8abc123def456","clx8def456abc123","clx8ghi789jkl012"],"type":"array","items":{"type":"string"}}},"required":["lessonIds"]},"MoveLessonDto":{"type":"object","properties":{"targetModuleId":{"type":"string","description":"CUID du module destination (peut être un sous-module).","example":"clx8abc123def456"},"order":{"type":"number","description":"Position 1-based dans la liste de leçons du module destination. 1 = en premier. Si > nb leçons existantes, append à la fin.","example":2,"minimum":1}},"required":["targetModuleId","order"]},"UpdateLessonDto":{"type":"object","properties":{"title":{"type":"string","description":"Lesson title","example":"Les cérémonies Scrum","minLength":2,"maxLength":255},"type":{"type":"string","description":"Lesson type — determines content fields used","enum":["VIDEO","TEXT","SCORM","LIVE_SESSION","ACTIVITY","DOCUMENT","QUIZ"],"example":"VIDEO"},"content":{"type":"string","description":"Rich-text or HTML content (used for TEXT, ACTIVITY, QUIZ types)","example":"<p>Le Daily Scrum est une réunion quotidienne de 15 minutes...</p>"},"duration":{"type":"number","description":"Lesson duration in minutes","example":15,"minimum":1,"maximum":9999},"isRequired":{"type":"boolean","description":"Whether completing this lesson is required to complete the module","default":true},"scormPackageId":{"type":"string","description":"SCORM package ID to associate with this lesson (SCORM type only)","example":"clx8abc123def456"},"objectives":{"description":"Learning objectives for this lesson (Qualiopi indicateur 2)","example":["Comprendre les cérémonies Scrum","Identifier les rôles clés"],"maxItems":20,"type":"array","items":{"type":"string"}},"pedagogicalMethods":{"type":"array","description":"Pedagogical methods used (Qualiopi indicateur 11)","maxItems":20,"items":{"type":"string","enum":["EXPOSITIVE","DEMONSTRATIVE","ACTIVE","INTERROGATIVE","DISCOVERY","EXPERIENTIAL","COLLABORATIVE","CASE_BASED","PROBLEM_BASED","FLIPPED_CLASSROOM"]}},"exerciseType":{"type":"string","description":"Exercise or evaluation type (Qualiopi indicateurs 7, 11, 12)","enum":["QCM","QUIZ","DRILL","CASE_STUDY","SIMULATION","ROLE_PLAY","PROJECT","PRACTICAL","ORAL_DEFENSE","WRITTEN_EXAM","PEER_REVIEW","OBSERVATION","OTHER"]},"exerciseConfig":{"type":"object","description":"Configuration for the exercise (schema depends on exerciseType)","example":{"questionCount":10,"passingScore":70}},"estimatedMinMinutes":{"type":"number","description":"Minimum estimated completion time in minutes","example":10,"minimum":1},"estimatedMaxMinutes":{"type":"number","description":"Maximum estimated completion time in minutes","example":20,"minimum":1},"accessibilityNotes":{"type":"string","description":"Accessibility notes for learners with disabilities","example":"Sous-titres disponibles. Navigation clavier complète.","maxLength":2000},"keyPoints":{"description":"Key points to retain from this lesson","example":["Daily Scrum = 15 min maximum","Le Scrum Master facilite, il ne dirige pas"],"maxItems":20,"type":"array","items":{"type":"string"}},"supportMaterials":{"description":"Support materials referenced in this lesson (titles, URLs, doc IDs)","example":["Guide pédagogique Scrum","https://scrumguides.org"],"maxItems":20,"type":"array","items":{"type":"string"}},"questions":{"description":"Quiz questions array for QUIZ-type lessons. Each question has a type, label, options, correct answer indices, and points.","example":[{"id":"q_1","type":"SINGLE_CHOICE","label":"Quel est le rôle du Scrum Master ?","required":true,"options":["Développer","Faciliter","Décider"],"correctIndex":1,"points":1}],"type":"array","items":{"type":"string"}}}},"ReorderBlocksDto":{"type":"object","properties":{"blockIds":{"description":"Liste complète des IDs de blocs dans le nouvel ordre souhaité. Tous les blocs de la leçon doivent être présents.","example":["block-cuid-1","block-cuid-3","block-cuid-2"],"type":"array","items":{"type":"string"}}},"required":["blockIds"]},"RegenerateBlockDto":{"type":"object","properties":{"brief":{"type":"string","description":"Instruction libre transmise à Mistral comme contexte supplémentaire (max 2 000 caractères).","maxLength":2000,"example":"Axe sur les cas pratiques et les erreurs courantes à éviter."}}},"UpdateCourseDocumentDto":{"type":"object","properties":{"title":{"type":"string","description":"Document title","example":"Guide pédagogique — Module 1 (v2)","minLength":2,"maxLength":255},"description":{"type":"string","description":"Document description","maxLength":1000},"kind":{"type":"string","description":"Document kind","enum":["PEDAGOGICAL_RESOURCE","CONTEXT_DOCUMENT","WORKSHEET","REFERENCE","OTHER"]},"order":{"type":"number","description":"Display order (lower = first)","example":2,"minimum":0}}},"ReorderCourseDocumentDto":{"type":"object","properties":{"order":{"type":"number","description":"New display order (0 = first)","example":0,"minimum":0}},"required":["order"]},"GenerateLessonContentDto":{"type":"object","properties":{"useSummaries":{"type":"boolean","description":"When true, injects document summaries attached to the parent course into the generation prompt. Produces richer, source-grounded content. Has no effect if no documents are attached to the course.","example":true,"default":false}}},"ModifyLessonContentDto":{"type":"object","properties":{"prompt":{"type":"string","description":"Natural-language instruction describing how to modify the existing content. Be specific: \"Simplify for beginners\", \"Add a real-world example in section 3\", \"Rewrite in French\", etc.","example":"Ajoute un exemple concret en section 2 et simplifie le vocabulaire technique.","maxLength":2000},"targetField":{"type":"string","description":"Which lesson field to modify. Defaults to `content`. Use `title` to rename the lesson.","enum":["content","title"],"default":"content","example":"content"}},"required":["prompt"]},"RegisterExpoTokenDto":{"type":"object","properties":{"token":{"type":"string","description":"Expo push token returned by getExpoPushTokenAsync().","example":"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"},"platform":{"type":"string","description":"Platform / channel — defaults to \"expo\".","example":"expo"},"preferences":{"type":"object","description":"Per-category opt-in map (boolean values).","example":{"session-reminder":true,"lesson-completed":true,"qr-attendance-success":true,"message-received":true}}},"required":["token"]},"UpdateExpoPreferencesDto":{"type":"object","properties":{"preferences":{"type":"object","description":"Per-category opt-in map (boolean values).","example":{"session-reminder":true,"lesson-completed":false}}},"required":["preferences"]},"EstimateCostDto":{"type":"object","properties":{"action":{"type":"string","description":"Clé de l'action IA (ex: GENERATE_COURSE_STRUCTURE)","example":"GENERATE_COURSE_STRUCTURE"},"params":{"type":"object","description":"Paramètres optionnels (ex: { minutes: 5 } pour GENERATE_AUDIO_PER_MINUTE)","example":{"minutes":5}}},"required":["action"]},"TopUpDto":{"type":"object","properties":{"amount":{"type":"number","description":"Nombre de crédits à ajouter","example":500,"minimum":1},"description":{"type":"string","description":"Description de la recharge","example":"Recharge manuelle par l'administrateur"}},"required":["amount"]},"EnrichLessonDto":{"type":"object","properties":{"brief":{"type":"string","description":"Brief créatif optionnel transmis à Mistral pour orienter la génération des blocs (ton, exemples métier, focus pédagogique). Limité à 2 000 caractères.","maxLength":2000,"example":"Cibler les chefs de projet agiles avec des exemples Scrum concrets."}}},"EnrichCourseDto":{"type":"object","properties":{"brief":{"type":"string","description":"Brief créatif optionnel transmis à Mistral pour chaque leçon du cours (ton, exemples métier, focus pédagogique). Limité à 2 000 caractères.","maxLength":2000,"example":"Adapter le contenu à un public de formateurs occasionnels en entreprise."}}},"AdminEnrollDto":{"type":"object","properties":{"sessionId":{"type":"string","description":"CUID de la session (LiveSession) cible. Obligatoire. Utiliser GET /sessions?courseId=...&includeMain=true pour résoudre la session principale d'un cours.","example":"clx8sess123def456"},"userIds":{"description":"Array of user CUIDs to enroll (batch)","example":["clx9xyz789ghi012","clxabc123def789"],"type":"array","items":{"type":"string"}}},"required":["sessionId","userIds"]},"InviteLearnersDto":{"type":"object","properties":{"sessionId":{"type":"string","description":"CUID de la session (LiveSession) cible. Obligatoire. Utiliser GET /sessions?courseId=...&includeMain=true pour résoudre la session principale d'un cours.","example":"clx8sess123def456"},"emails":{"description":"Email addresses to invite (existing or new users)","example":["alice@example.com","bob@example.com"],"type":"array","items":{"type":"string"}},"sendInvitation":{"type":"boolean","description":"Whether to send an invitation email to each learner","default":true}},"required":["sessionId","emails"]},"BulkRemindDto":{"type":"object","properties":{"enrollmentIds":{"description":"Liste des identifiants d'inscriptions pour lesquelles envoyer un rappel","example":["clenroll123","clenroll456"],"type":"array","items":{"type":"string"}},"message":{"type":"string","description":"Message personnalisé à inclure dans le rappel (optionnel)","example":"Merci de compléter votre formation avant la date limite."}},"required":["enrollmentIds"]},"AssignManagerDto":{"type":"object","properties":{"managerId":{"type":"string","description":"Identifiant du manager à assigner à cette inscription. Passer null pour désassigner","example":"clmanager123"}}},"CreateEnrollmentDto":{"type":"object","properties":{"sessionId":{"type":"string","description":"CUID de la session (LiveSession) cible. Obligatoire. Utiliser GET /sessions?courseId=...&includeMain=true pour résoudre la session principale d'un cours.","example":"clx8sess123def456"},"fundingSource":{"type":"string","description":"Funding source (BPF)","enum":["COMPANY_DIRECT","OPCO","PUBLIC_FUND","CPF","PERSONAL","OTHER"],"example":"COMPANY_DIRECT"},"professionalStatusAtEnrollment":{"type":"string","enum":["SALARIED","JOB_SEEKER","SELF_EMPLOYED","PUBLIC_SERVANT","STUDENT","APPRENTICE","INTERN","INACTIVE","RETIRED","OTHER"],"description":"Situation professionnelle au moment de l'inscription (enum typé, remplace learnerStatus)."},"companyNameAtEnrollment":{"type":"string","description":"Nom de l'employeur au moment de l'inscription (si SALARIED).","maxLength":200},"jobTitleAtEnrollment":{"type":"string","description":"Intitulé de poste / fonction au moment de l'inscription.","maxLength":200}},"required":["sessionId"]},"UpdateProgressDto":{"type":"object","properties":{"status":{"type":"string","description":"New progress status for the lesson","enum":["NOT_STARTED","IN_PROGRESS","COMPLETED","FAILED"],"example":"COMPLETED"},"score":{"type":"string","description":"Score obtained (0.00–100.00). Applies to QUIZ / ACTIVITY lessons.","example":"85.50"},"timeSpent":{"type":"number","description":"Time spent on the lesson in seconds (cumulative total)","example":3600,"minimum":0},"scormData":{"type":"object","description":"Raw SCORM runtime data (CMI object) for SCORM lessons"}},"required":["status"]},"CreateMailConfigDto":{"type":"object","properties":{"provider":{"type":"string","enum":["GMAIL","IMAP","RESEND","DEFAULT"],"description":"Email transport provider"},"fromName":{"type":"string","description":"Display name shown in the \"From\" header"},"fromEmail":{"type":"string","description":"Sender email address"},"credentials":{"type":"object","description":"Provider credentials. Structure depends on provider: GMAIL → GmailCredentialsDto, IMAP → ImapCredentialsDto, RESEND → ResendCredentialsDto, DEFAULT → empty object {}"},"isDefault":{"type":"boolean","description":"Set as the default configuration for the tenant"}},"required":["provider","fromName","fromEmail"]},"UpdateMailConfigDto":{"type":"object","properties":{"provider":{"type":"string","enum":["GMAIL","IMAP","RESEND","DEFAULT"],"description":"Email transport provider"},"fromName":{"type":"string","description":"Display name shown in the \"From\" header"},"fromEmail":{"type":"string","description":"Sender email address"},"credentials":{"type":"object","description":"Provider credentials. Structure depends on provider: GMAIL → GmailCredentialsDto, IMAP → ImapCredentialsDto, RESEND → ResendCredentialsDto, DEFAULT → empty object {}"},"isDefault":{"type":"boolean","description":"Set as the default configuration for the tenant"}}},"TestMailDto":{"type":"object","properties":{"to":{"type":"string","description":"Recipient email address for the test email"},"subject":{"type":"string","description":"Custom subject (defaults to \"QualiForma — Test email\")"}},"required":["to"]},"TestSendDto":{"type":"object","properties":{"fromName":{"type":"string","description":"Nom d'affichage de l'expéditeur","example":"QualiForma Formation"},"fromEmail":{"type":"string","description":"Adresse email de l'expéditeur","example":"formation@monorganisme.fr"}},"required":["fromName","fromEmail"]},"RecordEmargementDto":{"type":"object","properties":{"enrollmentId":{"type":"string","description":"Enrollment CUID of the learner","example":"clx8abc123def456"},"sessionId":{"type":"string","description":"LiveSession CUID (required for live sessions)","example":"clx9xyz789ghi012"},"method":{"type":"string","enum":["DIGITAL_SIGNATURE","LIVEKIT_PRESENCE","QR_CODE","MANUAL"],"description":"Method used to record attendance","example":"DIGITAL_SIGNATURE"},"signatureData":{"type":"string","description":"Base64-encoded signature image from canvas (DIGITAL_SIGNATURE method only)","example":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."},"durationSeconds":{"type":"number","description":"Effective presence duration in seconds (LIVEKIT_PRESENCE method, set by webhook)","example":3600}},"required":["enrollmentId","method"]},"ValidateQrDto":{"type":"object","properties":{"token":{"type":"string","description":"Short-lived JWT token encoded in the QR code","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"enrollmentId":{"type":"string","description":"Enrollment CUID of the learner scanning the QR code","example":"clx8abc123def456"}},"required":["token","enrollmentId"]},"GenerateSlotsDto":{"type":"object","properties":{"sessionId":{"type":"string","description":"LiveSession CUID","example":"clx9xyz789ghi012"},"frequency":{"type":"string","enum":["hourly","half-day","session"],"description":"Frequency of slot generation","example":"hourly"}},"required":["sessionId","frequency"]},"SignSlotDto":{"type":"object","properties":{"method":{"type":"string","enum":["DIGITAL_SIGNATURE","QR_CODE"],"description":"Method used to sign","example":"DIGITAL_SIGNATURE"},"signatureData":{"type":"string","description":"Base64-encoded signature image (DIGITAL_SIGNATURE only)","example":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."},"enrollmentId":{"type":"string","description":"Enrollment CUID (required if not derivable from context)","example":"clx8abc123def456"}},"required":["method"]},"QuestionDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier for the question (used to match answers)","example":"q1"},"type":{"type":"string","description":"Question type determining the UI control and validation","enum":["SINGLE_CHOICE","MULTIPLE_CHOICE","FREE_TEXT","RATING","BOOLEAN","SCALE"],"example":"SCALE"},"label":{"type":"string","description":"Human-readable question text","example":"Sur une échelle de 0 à 10, recommanderiez-vous cette formation ?"},"required":{"type":"boolean","description":"Whether an answer to this question is mandatory","default":true,"example":true},"options":{"description":"Available choices (required for SINGLE_CHOICE and MULTIPLE_CHOICE)","example":["Très satisfait","Satisfait","Neutre","Insatisfait"],"type":"array","items":{"type":"string"}},"min":{"type":"number","description":"Minimum value for SCALE questions","example":0},"max":{"type":"number","description":"Maximum value for SCALE questions (use 10 for NPS)","example":10},"step":{"type":"number","description":"Step increment for SCALE questions","example":1}},"required":["id","type","label"]},"CreateQuestionnaireDto":{"type":"object","properties":{"courseId":{"type":"string","description":"CUID of the course this questionnaire belongs to. Optional for tenant-level templates (isTemplate=true).","example":"clx8abc123def456"},"type":{"type":"string","description":"Questionnaire type (determines Qualiopi indicator mapping)","enum":["PRE_TRAINING","POST_TRAINING_HOT","POST_TRAINING_COLD_30","POST_TRAINING_COLD_90","COMMANDITAIRE","CUSTOM"],"example":"POST_TRAINING_HOT"},"title":{"type":"string","description":"Questionnaire title displayed to learners","example":"Évaluation de satisfaction à chaud"},"questions":{"description":"Ordered list of questions","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/QuestionDto"}},"sendAt":{"type":"string","description":"When the questionnaire is automatically sent to learners","enum":["BEFORE_START","AFTER_END","CUSTOM_DAYS"],"example":"AFTER_END"},"sendDaysOffset":{"type":"number","description":"Number of days offset for CUSTOM_DAYS schedule (e.g. 30 for J+30 cold evaluation)","example":30,"minimum":1},"isActive":{"type":"boolean","description":"Whether the questionnaire is active and will be sent","default":true,"example":true}},"required":["type","title","questions","sendAt"]},"UpdateQuestionnaireDto":{"type":"object","properties":{"title":{"type":"string","description":"Questionnaire title displayed to learners","example":"Évaluation de satisfaction — session printemps 2026","minLength":3,"maxLength":255},"questions":{"description":"Replacement question list (full replacement, not merge)","type":"array","items":{"$ref":"#/components/schemas/QuestionDto"}},"sendAt":{"type":"string","description":"When the questionnaire is automatically sent to learners","enum":["BEFORE_START","AFTER_END","CUSTOM_DAYS"]},"sendDaysOffset":{"type":"number","description":"Days offset for CUSTOM_DAYS schedule (min 1)","example":30,"minimum":1},"isActive":{"type":"boolean","description":"Activate or deactivate the questionnaire","example":true}}},"MoveQuestionnaireDto":{"type":"object","properties":{"courseId":{"type":"string","description":"ID of the target course (must belong to the same tenant)","example":"clx8abc123def456"}},"required":["courseId"]},"CreateTenantTemplateDto":{"type":"object","properties":{"type":{"type":"string","description":"Questionnaire type (determines Qualiopi indicator mapping)","enum":["PRE_TRAINING","POST_TRAINING_HOT","POST_TRAINING_COLD_30","POST_TRAINING_COLD_90","COMMANDITAIRE","CUSTOM"]},"title":{"type":"string","description":"Template title displayed to admins"},"questions":{"description":"Ordered list of questions. Optional at creation — admin can add them later.","type":"array","items":{"$ref":"#/components/schemas/QuestionDto"}},"sendAt":{"type":"string","description":"When the questionnaire is automatically sent. Defaults to AFTER_END.","enum":["BEFORE_START","AFTER_END","CUSTOM_DAYS"]},"sendDaysOffset":{"type":"number","description":"Number of days offset for CUSTOM_DAYS schedule","minimum":1},"isActive":{"type":"boolean","description":"Whether the template is active and used when a course is created","default":true}},"required":["type","title"]},"GenerateAllQuestionnairesDto":{"type":"object","properties":{"brief":{"type":"string","description":"Brief libre contextualisant la generation (themes a couvrir, public cible, particularites pedagogiques). Injecte dans chaque prompt.","example":"Formation certifiante pour des managers operationnels dans le secteur industriel. Insister sur l'impact terrain et les metriques de performance.","maxLength":2000},"types":{"type":"array","description":"Sous-ensemble de types a generer. Par defaut : PRE_TRAINING, POST_TRAINING_HOT, POST_TRAINING_COLD_30, POST_TRAINING_COLD_90, COMMANDITAIRE. Les types deja presents pour ce cours sont ignores.","example":["PRE_TRAINING","POST_TRAINING_HOT"],"items":{"type":"string","enum":["PRE_TRAINING","POST_TRAINING_HOT","POST_TRAINING_COLD_30","POST_TRAINING_COLD_90","COMMANDITAIRE","CUSTOM"]}}}},"RegenerateQuestionnaireDto":{"type":"object","properties":{"brief":{"type":"string","description":"Brief libre guidant la reecriture (themes a couvrir, ton souhaite, consignes particulieres). Si absent, le modele se base sur le contexte du cours.","example":"Mettre davantage l'accent sur le transfert de competences en milieu professionnel. Inclure une question sur l'impact sur la productivite.","maxLength":2000}}},"ScheduleSendDto":{"type":"object","properties":{"delayMs":{"type":"number","description":"Delay in milliseconds before the email dispatch job is processed. Defaults to 0 (immediate). Example: 3600000 = dispatch in 1 hour.","example":3600000,"minimum":0}}},"QuestionAnswerDto":{"type":"object","properties":{"questionId":{"type":"string","description":"ID of the question being answered (must match a question in the questionnaire)","example":"q1"},"value":{"type":"object","description":"Answer value. Type depends on the question type: string (SINGLE_CHOICE, FREE_TEXT), string[] (MULTIPLE_CHOICE), number (RATING, SCALE), boolean (BOOLEAN)","examples":{"singleChoice":{"value":"Très satisfait"},"multipleChoice":{"value":["Option A","Option B"]},"scale":{"value":9},"freeText":{"value":"Excellent formateur, contenu très pertinent."},"boolean":{"value":true}}}},"required":["questionId","value"]},"SubmitResponseDto":{"type":"object","properties":{"enrollmentId":{"type":"string","description":"Enrollment CUID of the learner submitting the response","example":"clx8abc123def456"},"answers":{"description":"Array of question answers","minItems":1,"type":"array","items":{"$ref":"#/components/schemas/QuestionAnswerDto"}}},"required":["enrollmentId","answers"]},"SetRuntimeDataDto":{"type":"object","properties":{"enrollmentId":{"type":"string","description":"Enrollment ID to scope the SCORM session","example":"clq9876zyxwvu"},"key":{"type":"string","description":"CMI data model key (e.g. cmi.core.lesson_status)","example":"cmi.core.lesson_status"},"value":{"type":"string","description":"Value to store for the given key","example":"completed"}},"required":["enrollmentId","key","value"]},"XApiAccountDto":{"type":"object","properties":{"homePage":{"type":"string","example":"https://lms.qualiforma.com"},"name":{"type":"string","example":"user-cuid-here"}},"required":["homePage","name"]},"XApiActorDto":{"type":"object","properties":{"objectType":{"type":"string","example":"Agent"},"name":{"type":"string","example":"Jean Dupont"},"mbox":{"type":"string","example":"mailto:jean@example.com"},"account":{"$ref":"#/components/schemas/XApiAccountDto"}},"required":["objectType"]},"XApiVerbDto":{"type":"object","properties":{"id":{"type":"string","example":"http://adlnet.gov/expapi/verbs/completed"},"display":{"type":"object","example":{"en-US":"completed"}}},"required":["id"]},"XApiActivityDefinitionDto":{"type":"object","properties":{"name":{"type":"object","example":{"en-US":"Introduction to NestJS"}},"description":{"type":"object","example":{"en-US":"This course covers NestJS basics."}},"type":{"type":"string","example":"http://adlnet.gov/expapi/activities/course"}}},"XApiObjectDto":{"type":"object","properties":{"id":{"type":"string","example":"https://lms.qualiforma.com/courses/intro-nestjs"},"objectType":{"type":"string","example":"Activity"},"definition":{"$ref":"#/components/schemas/XApiActivityDefinitionDto"}},"required":["id"]},"XApiScoreDto":{"type":"object","properties":{"raw":{"type":"number","example":85.5},"min":{"type":"number","example":0},"max":{"type":"number","example":100},"scaled":{"type":"number","example":0.855}}},"XApiResultDto":{"type":"object","properties":{"score":{"$ref":"#/components/schemas/XApiScoreDto"},"success":{"type":"boolean","example":true},"completion":{"type":"boolean","example":true},"duration":{"type":"string","example":"PT1H30M"}}},"XApiContextDto":{"type":"object","properties":{"registration":{"type":"string","description":"Registration UUID linking to enrollment"},"extensions":{"type":"object"}}},"XApiStatementDto":{"type":"object","properties":{"id":{"type":"string","description":"Statement UUID (generated server-side if omitted)","example":"6690e6c9-3ef0-4ed3-8b37-7f3964730bee"},"actor":{"$ref":"#/components/schemas/XApiActorDto"},"verb":{"$ref":"#/components/schemas/XApiVerbDto"},"object":{"$ref":"#/components/schemas/XApiObjectDto"},"result":{"$ref":"#/components/schemas/XApiResultDto"},"context":{"$ref":"#/components/schemas/XApiContextDto"},"timestamp":{"type":"string","description":"ISO 8601 timestamp (server-side if omitted)","example":"2024-06-15T14:30:00.000Z"}},"required":["actor","verb","object"]},"CreateCheckoutDto":{"type":"object","properties":{"courseId":{"type":"string","description":"CUID of the course to purchase","example":"clx1234567890abcdef"},"successUrl":{"type":"string","description":"URL to redirect the learner after a successful payment (overrides platform default)","example":"https://app.qualiforma.com/courses/my-course/success"},"failureUrl":{"type":"string","description":"URL to redirect the learner after a failed or cancelled payment","example":"https://app.qualiforma.com/courses/my-course/cancel"}},"required":["courseId"]},"InitiateRefundDto":{"type":"object","properties":{"paymentId":{"type":"string","description":"Payment CUID to refund","example":"clx1234567890abcdef"},"amount":{"type":"number","description":"Amount to refund in major currency units (e.g. 29.99 for €29.99). Omit for a full refund.","example":29.99},"reverseTransfers":{"type":"boolean","description":"Whether to reverse the seller transfer (clawback creator payout). Defaults to true.","default":true},"refundPlatformFee":{"type":"boolean","description":"Whether to also refund the platform commission. Defaults to false.","default":false}},"required":["paymentId"]},"CreatorOnboardDto":{"type":"object","properties":{"returnUrl":{"type":"string","description":"HTTPS URL the payment gateway redirects to after onboarding is complete","example":"https://app.qualiforma.com/creator/onboarding/complete"}},"required":["returnUrl"]},"UpdateStripeConfigDto":{"type":"object","properties":{"stripePlatformAccountId":{"type":"string","description":"Stripe platform account ID (acct_*)","example":"acct_1PAbc123","maxLength":64},"stripeSecretKey":{"type":"string","description":"Stripe secret API key (sk_live_* or sk_test_*). Encrypted at rest, never returned in clear.","example":"sk_test_4e...","minLength":20,"maxLength":256},"stripeWebhookSecret":{"type":"string","description":"Stripe webhook endpoint secret (whsec_*). Encrypted at rest, never returned in clear.","example":"whsec_...","minLength":20,"maxLength":256},"platformCommissionRate":{"type":"number","description":"Platform commission rate on marketplace sales, as a decimal in [0, 1). Example: 0.05 = 5%.","example":0.05,"minimum":0,"maximum":0.5}}},"CreateLegalWatchEntryDto":{"type":"object","properties":{"source":{"type":"string","description":"Source de la veille (ex: DARES, OPCO, Mon Compte Formation)"},"sourceUrl":{"type":"string","description":"URL de la source réglementaire"},"title":{"type":"string","description":"Titre de l'entrée de veille"},"description":{"type":"string","description":"Description détaillée"},"frequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY","QUARTERLY","YEARLY"],"description":"Fréquence de vérification"},"category":{"type":"string","enum":["LABOR_LAW","TRAINING_REGULATION","DATA_PROTECTION","SECTOR_SPECIFIC","CERTIFICATION","OTHER"],"description":"Catégorie réglementaire"},"responsibleUserId":{"type":"string","description":"CUID de l'utilisateur responsable"},"nextCheckDue":{"type":"string","description":"Date de prochaine vérification (ISO 8601)"},"isActive":{"type":"boolean","description":"Actif ou archivé","default":true}},"required":["source","title","frequency","category","responsibleUserId","nextCheckDue"]},"UpdateLegalWatchEntryDto":{"type":"object","properties":{"source":{"type":"string"},"sourceUrl":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"frequency":{"type":"string","enum":["DAILY","WEEKLY","MONTHLY","QUARTERLY","YEARLY"]},"category":{"type":"string","enum":["LABOR_LAW","TRAINING_REGULATION","DATA_PROTECTION","SECTOR_SPECIFIC","CERTIFICATION","OTHER"]},"responsibleUserId":{"type":"string"},"nextCheckDue":{"type":"string"},"isActive":{"type":"boolean"}}},"CreateLegalWatchReportDto":{"type":"object","properties":{"content":{"type":"string","description":"Contenu du rapport (analyse, synthèse)"},"impactedCourseIds":{"description":"CUIDs des formations impactées par cette évolution réglementaire","type":"array","items":{"type":"string"}},"actionTakenSummary":{"type":"string","description":"Résumé des actions correctives prises"}},"required":["content"]},"CreatePartnershipDto":{"type":"object","properties":{"name":{"type":"string","example":"OPCO Atlas","description":"Nom du partenaire"},"type":{"type":"string","enum":["NETWORK","UNION","ASSOCIATION","CONVENTION","BRANCH","PARTNER_COMPANY","OTHER"],"example":"NETWORK"},"description":{"type":"string","example":"OPCO du secteur tertiaire spécialisé"},"contactName":{"type":"string","example":"Marie Dupont"},"contactEmail":{"type":"string","example":"contact@opco-atlas.fr"},"contactPhone":{"type":"string","example":"+33 1 23 45 67 89"},"website":{"type":"string","example":"https://www.opco-atlas.fr"},"memberSince":{"type":"string","example":"2023-01-01T00:00:00.000Z","description":"Date d'adhésion (ISO 8601)"},"memberUntil":{"type":"string","example":"2025-12-31T23:59:59.000Z","description":"Date de fin d'adhésion (ISO 8601)"},"publishOnWebsite":{"type":"boolean","default":false,"description":"Afficher sur le site public"},"logoUrl":{"type":"string","example":"https://cdn.example.com/logo.png"},"displayOrder":{"type":"number","default":0,"description":"Ordre d'affichage sur le site"}},"required":["name","type","memberSince"]},"UpdatePartnershipDto":{"type":"object","properties":{"name":{"type":"string","example":"OPCO Atlas","description":"Nom du partenaire"},"type":{"type":"string","enum":["NETWORK","UNION","ASSOCIATION","CONVENTION","BRANCH","PARTNER_COMPANY","OTHER"],"example":"NETWORK"},"description":{"type":"string","example":"OPCO du secteur tertiaire spécialisé"},"contactName":{"type":"string","example":"Marie Dupont"},"contactEmail":{"type":"string","example":"contact@opco-atlas.fr"},"contactPhone":{"type":"string","example":"+33 1 23 45 67 89"},"website":{"type":"string","example":"https://www.opco-atlas.fr"},"memberSince":{"type":"string","example":"2023-01-01T00:00:00.000Z","description":"Date d'adhésion (ISO 8601)"},"memberUntil":{"type":"string","example":"2025-12-31T23:59:59.000Z","description":"Date de fin d'adhésion (ISO 8601)"},"publishOnWebsite":{"type":"boolean","default":false,"description":"Afficher sur le site public"},"logoUrl":{"type":"string","example":"https://cdn.example.com/logo.png"},"displayOrder":{"type":"number","default":0,"description":"Ordre d'affichage sur le site"}}},"ReorderPartnershipDto":{"type":"object","properties":{"displayOrder":{"type":"number","example":3,"description":"Nouvel ordre d'affichage"}},"required":["displayOrder"]},"CreateSkillsWatchEntryDto":{"type":"object","properties":{"certificationType":{"type":"string","enum":["RNCP","RS","CQP","OTHER"],"description":"Type de certification (RNCP, RS, CQP, OTHER)","example":"RNCP"},"certificationCode":{"type":"string","description":"Code France Compétences (RNCP12345, RS5215, CQP123). Pour OTHER : texte libre.","example":"RNCP35234"},"title":{"type":"string","description":"Intitulé de la certification","example":"Développeur web et web mobile"},"referenceUrl":{"type":"string","description":"URL de la fiche officielle France Compétences","example":"https://www.francecompetences.fr/recherche/rncp/35234/"},"courseIds":{"description":"Identifiants des cours QualiForma préparant à cette certification","type":"array","items":{"type":"string"}},"responsibleUserId":{"type":"string","description":"CUID de l'utilisateur responsable de la veille","example":"clx8abc123def456"},"nextCheckDue":{"type":"string","description":"Date de prochaine vérification (ISO 8601)","example":"2026-07-01T00:00:00.000Z"},"expirationDate":{"type":"string","description":"Date d'expiration de la certification (ISO 8601)","example":"2027-01-01T00:00:00.000Z"},"isActive":{"type":"boolean","description":"Entrée active (true par défaut)","default":true}},"required":["certificationType","certificationCode","title","responsibleUserId","nextCheckDue"]},"UpdateSkillsWatchEntryDto":{"type":"object","properties":{"certificationType":{"type":"string","enum":["RNCP","RS","CQP","OTHER"],"description":"Type de certification"},"certificationCode":{"type":"string","description":"Code France Compétences","example":"RNCP35234"},"title":{"type":"string","description":"Intitulé de la certification"},"referenceUrl":{"type":"string","description":"URL de la fiche officielle France Compétences"},"courseIds":{"description":"Identifiants des cours liés","type":"array","items":{"type":"string"}},"responsibleUserId":{"type":"string","description":"CUID de l'utilisateur responsable"},"nextCheckDue":{"type":"string","description":"Date de prochaine vérification (ISO 8601)"},"expirationDate":{"type":"string","description":"Date d'expiration de la certification (ISO 8601)"},"isActive":{"type":"boolean","description":"Entrée active"}}},"LinkCourseDto":{"type":"object","properties":{"courseId":{"type":"string","description":"CUID du cours à lier à cette entrée de veille","example":"clx8abc123def456"}},"required":["courseId"]},"ResolveAlertDto":{"type":"object","properties":{"note":{"type":"string","description":"Note de résolution (actions correctives prises, etc.)","example":"Référentiel mis à jour le 2026-04-15, titre et blocs de compétences vérifiés."}}},"CreateImprovementActionDto":{"type":"object","properties":{"title":{"type":"string","description":"Titre de l'action corrective","example":"Mettre à jour le contenu pédagogique du module 2"},"description":{"type":"string","description":"Description détaillée de l'action à mener","example":"Réviser les supports de cours pour intégrer les retours apprenants."},"assignedTo":{"type":"string","description":"CUID de l'utilisateur responsable de l'action","example":"clx8abc123def456"},"dueDate":{"type":"string","description":"Date d'échéance ISO 8601","example":"2026-09-30T00:00:00.000Z"}},"required":["title","description"]},"CreateImprovementPlanDto":{"type":"object","properties":{"title":{"type":"string","description":"Titre du plan d'amélioration","example":"Plan correctif accessibilité vidéos — Q3 2026"},"description":{"type":"string","description":"Description du problème et de l'approche correctrice","example":"Suite aux réclamations sur l'accessibilité, intégrer des sous-titres WCAG 2.1 AA."},"trigger":{"type":"string","description":"Déclencheur du plan","enum":["COMPLAINT","SATISFACTION","AUDIT","INTERNAL","LEGAL_WATCH","SKILLS_WATCH"],"example":"COMPLAINT"},"triggerSourceId":{"type":"string","description":"Identifiant fonctionnel de la source (complaintId, responseId…)","example":"clx8abc123def456"},"priority":{"type":"string","description":"Niveau de priorité","enum":["LOW","MEDIUM","HIGH","CRITICAL"],"default":"MEDIUM"},"responsibleUserId":{"type":"string","description":"CUID de l'utilisateur responsable du plan","example":"clx8abc123def456"},"dueDate":{"type":"string","description":"Date d'échéance ISO 8601","example":"2026-12-31T00:00:00.000Z"},"initialActions":{"description":"Actions initiales créées en même temps que le plan","type":"array","items":{"$ref":"#/components/schemas/CreateImprovementActionDto"}}},"required":["title","description","trigger","responsibleUserId"]},"UpdateImprovementPlanDto":{"type":"object","properties":{"title":{"type":"string","description":"Nouveau titre"},"description":{"type":"string","description":"Nouvelle description"},"status":{"type":"string","enum":["DRAFT","IN_PROGRESS","DONE","ARCHIVED"]},"priority":{"type":"string","enum":["LOW","MEDIUM","HIGH","CRITICAL"]},"responsibleUserId":{"type":"string","description":"CUID du responsable"},"dueDate":{"type":"object","description":"Date d'échéance ISO 8601 (null pour supprimer)"}}},"CloseImprovementPlanDto":{"type":"object","properties":{"closureNotes":{"type":"string","description":"Notes de clôture / bilan du plan","example":"Toutes les actions ont été menées. Taux de satisfaction remonté à 8.5/10."}}},"UpdateImprovementActionDto":{"type":"object","properties":{"title":{"type":"string","description":"Nouveau titre de l'action"},"description":{"type":"string","description":"Nouvelle description"},"status":{"type":"string","description":"Statut de l'action","enum":["PLANNED","IN_PROGRESS","COMPLETED","CANCELLED"]},"outcome":{"type":"string","description":"Résultat / bilan de l'action"},"completedAt":{"type":"string","description":"Date de complétion ISO 8601"},"assignedTo":{"type":"string","description":"CUID de l'utilisateur responsable"},"dueDate":{"type":"string","description":"Date d'échéance ISO 8601"}}},"CreateSessionDto":{"type":"object","properties":{"courseId":{"type":"string","description":"CUID of the course this session belongs to","example":"clx8abc123def456"},"title":{"type":"string","description":"Human-readable title shown to participants","example":"Introduction au droit du travail — Session 1"},"scheduledAt":{"type":"string","description":"ISO 8601 date-time when the session is scheduled to start. Obligatoire uniquement si `mode = SCHEDULED` + publication immédiate. Laissé vide pour une session DRAFT ou PERMANENT.","example":"2026-05-15T09:00:00.000Z"},"mode":{"type":"string","description":"Mode de session : SCHEDULED (date fixe) ou PERMANENT (e-learning asynchrone).","enum":["SCHEDULED","PERMANENT"],"default":"SCHEDULED"},"duration":{"type":"number","description":"Expected duration in minutes","example":90},"maxParticipants":{"type":"number","description":"Maximum number of concurrent participants (0 = unlimited)","example":30,"minimum":0,"maximum":500},"location":{"type":"string","description":"Physical or virtual location (room name, address, video link…)","example":"Salle A — 12 rue de la Paix, 75001 Paris"},"locationDetails":{"type":"string","description":"Additional logistical details: access instructions, parking, provided equipment, etc.","example":"Bâtiment C, 3e étage. Code d'accès: 1234B. Parking gratuit en sous-sol."},"requiredMaterials":{"description":"List of materials or prerequisites participants must bring","example":["Ordinateur portable","Câble réseau"],"type":"array","items":{"type":"string"}},"notes":{"type":"string","description":"Internal trainer notes (not visible in convocation documents)","example":"Vérifier la connexion LiveKit avant le démarrage. Salle réservée de 8h30."},"recurrence":{"type":"string","description":"Recurrence pattern: once (default) or weekly","example":"once","enum":["once","weekly"]},"recurrenceWeeks":{"type":"number","description":"Number of weeks to repeat for weekly recurrence (1-52)","example":4},"assistantTrainerIds":{"description":"CUIDs of assistant trainers to assign to this session","example":["trainer123","trainer456"],"type":"array","items":{"type":"string"}},"trainerId":{"type":"string","description":"CUID du formateur principal pour cette session (override du CourseTrainer PRIMARY). Si absent, on retombe sur le formateur principal du cours.","example":"trainer-cuid-001"},"sendConvocations":{"type":"boolean","description":"Envoyer les emails de convocation aux inscrits juste après la création de la session. Défaut : true (envoi automatique).","example":true,"default":true},"trainerCost":{"type":"number","description":"Coût formateur pour cette session en euros (net de charges). Agrégé annuellement dans le BPF — rubrique B.chargesPersonnel.","example":450,"minimum":0},"trainerCostCurrency":{"type":"string","description":"Devise du coût formateur (ISO 4217). EUR par défaut.","example":"EUR","maxLength":3},"alternateLocation":{"type":"string","description":"Lieu alternatif spécifique à cette session (salle louée, centre partenaire…). N'écrase pas `location` (logistique apprenant).","example":"Centre de conférences Parisien — 5 rue de Rivoli, 75004 Paris"},"alternateLocationCost":{"type":"number","description":"Coût de location du lieu alternatif pour cette session (EUR). Agrégé annuellement dans le BPF — rubrique B.chargesLocaux.","example":250,"minimum":0}},"required":["courseId","title","duration"]},"UpdateSessionDto":{"type":"object","properties":{"mode":{"type":"string","description":"Mode de session : SCHEDULED (date fixe) ou PERMANENT (e-learning). Changements restreints par le service (impossible PUBLISHED.SCHEDULED → PUBLISHED.PERMANENT).","enum":["SCHEDULED","PERMANENT"]},"title":{"type":"string","description":"Session title","example":"Introduction to React — Session 2"},"scheduledAt":{"type":"string","description":"New scheduled date/time (ISO 8601)","example":"2026-05-15T09:00:00.000Z"},"duration":{"type":"number","description":"Duration in minutes","example":120,"minimum":1},"maxParticipants":{"type":"number","description":"Maximum number of participants (null = unlimited)","example":30,"minimum":1},"location":{"type":"string","description":"Physical or virtual location (room name, address, video link…)","example":"Salle A — 12 rue de la Paix, 75001 Paris"},"locationDetails":{"type":"string","description":"Additional logistical details: access instructions, parking, provided equipment, etc.","example":"Bâtiment C, 3e étage. Code d'accès: 1234B."},"requiredMaterials":{"description":"List of materials or prerequisites participants must bring","example":["Ordinateur portable","Câble réseau"],"type":"array","items":{"type":"string"}},"notes":{"type":"string","description":"Internal trainer notes (not visible in convocation documents)","example":"Vérifier la connexion LiveKit avant le démarrage"},"trainerCost":{"type":"number","description":"Coût formateur pour cette session en euros (net de charges). Agrégé annuellement dans le BPF — rubrique B.chargesPersonnel.","example":450,"minimum":0},"trainerCostCurrency":{"type":"string","description":"Devise du coût formateur (ISO 4217). EUR par défaut.","example":"EUR","maxLength":3},"alternateLocation":{"type":"string","description":"Lieu alternatif spécifique à cette session (salle louée, centre partenaire…). N'écrase pas `location` (logistique apprenant).","example":"Centre de conférences Parisien — 5 rue de Rivoli, 75004 Paris"},"alternateLocationCost":{"type":"number","description":"Coût de location du lieu alternatif pour cette session (EUR). Agrégé annuellement dans le BPF — rubrique B.chargesLocaux.","example":250,"minimum":0}}},"CreateSessionDateDto":{"type":"object","properties":{"scheduledAt":{"type":"string","description":"Date et heure ISO 8601 du debut du slot","example":"2026-05-15T09:00:00.000Z"},"duration":{"type":"number","description":"Duree du slot en minutes","example":240,"minimum":15,"maximum":1440},"title":{"type":"string","description":"Titre du slot (ex : \"Jour 1 — Introduction\")","example":"Jour 1 — Introduction","maxLength":255},"isMainDate":{"type":"boolean","description":"Marque cette date comme date principale. Si true, les autres dates de la session basculent automatiquement a false.","default":false,"example":false}},"required":["scheduledAt","duration"]},"UpdateSessionDateDto":{"type":"object","properties":{"scheduledAt":{"type":"string","description":"Nouvelle date et heure ISO 8601 du slot","example":"2026-05-15T09:00:00.000Z"},"duration":{"type":"number","description":"Nouvelle duree du slot en minutes","example":180,"minimum":15,"maximum":1440},"title":{"type":"object","description":"Nouveau titre du slot (null pour le vider)","example":"Jour 2 — Approfondissement","maxLength":255,"nullable":true},"isMainDate":{"type":"boolean","description":"Marque cette date comme date principale. Si true, les autres dates de la session basculent automatiquement a false.","example":false}}},"CreateSessionDocumentDto":{"type":"object","properties":{"title":{"type":"string","description":"Titre affiché du document (ex : \"Facture formateur mars 2026\")","example":"Facture formateur — Session du 12 mars 2026","maxLength":255},"type":{"type":"string","description":"Type de document. Détermine les agrégations BPF : TRAINER_INVOICE → rubrique E (formateurs), VENUE_INVOICE → rubrique B (charges locaux), ATTENDANCE_SHEET/CONVENTION → dossier Qualiopi.","enum":["TRAINER_INVOICE","VENUE_INVOICE","ATTENDANCE_SHEET","CONVENTION","OTHER"],"example":"TRAINER_INVOICE"},"fileUrl":{"type":"string","description":"Object path MinIO/iDrive e2 obtenu via POST /media/upload ou presigned PUT. Ex : `{tenantId}/document/{uuid}.pdf`","example":"ten_abc123/document/f7e2c1d4-3a5b-4e8f-9c2d-1a6b7e8f9c3d.pdf","maxLength":1024},"fileSize":{"type":"number","description":"Taille du fichier en octets (pour affichage UI).","example":204800,"minimum":0},"mimeType":{"type":"string","description":"MIME type du fichier (ex : application/pdf).","example":"application/pdf","maxLength":128}},"required":["title","type","fileUrl"]},"CreateComplaintDto":{"type":"object","properties":{"subject":{"type":"string","description":"Subject / title of the complaint","example":"Video content is inaccessible"},"description":{"type":"string","description":"Detailed description of the complaint","example":"The video on module 2 has no subtitles and I cannot follow the content."},"category":{"type":"string","description":"Category of the complaint","enum":["CONTENT_QUALITY","TECHNICAL_ISSUE","TRAINER_BEHAVIOR","BILLING","ACCESSIBILITY","OTHER"],"example":"ACCESSIBILITY"},"priority":{"type":"string","description":"Priority level","enum":["LOW","MEDIUM","HIGH","URGENT"],"example":"MEDIUM"},"courseId":{"type":"string","description":"CUID of the course the complaint relates to (optional)","example":"clx8abc123def456"}},"required":["subject","description","category","priority"]},"UpdateComplaintDto":{"type":"object","properties":{"status":{"type":"string","description":"Updated status of the complaint","enum":["OPEN","IN_PROGRESS","RESOLVED","CLOSED","ESCALATED"],"example":"IN_PROGRESS"},"assignedTo":{"type":"string","description":"User ID of the person assigned to handle this complaint","example":"clx8abc123def456"},"resolution":{"type":"string","description":"Resolution description (required when status is RESOLVED or CLOSED)","example":"Subtitles have been added to the video in module 2."},"resolvedAt":{"type":"string","description":"ISO-8601 datetime when the complaint was resolved","example":"2026-04-12T10:00:00.000Z"}}},"CreateImprovementDto":{"type":"object","properties":{"title":{"type":"string","description":"Title of the improvement action","example":"Add subtitles to all video lessons"},"description":{"type":"string","description":"Detailed description of what needs to be done and why","example":"Following accessibility complaints, all video lessons must have French subtitles compliant with WCAG 2.1 AA by end of Q2."},"source":{"type":"string","description":"What triggered this improvement action","enum":["COMPLAINT","QUESTIONNAIRE_FEEDBACK","AUDIT_FINDING","TRAINER_FEEDBACK","REGULATORY_CHANGE","INTERNAL_REVIEW"],"example":"COMPLAINT"},"sourceId":{"type":"string","description":"ID of the source entity (complaint ID, questionnaire ID, etc.)","example":"clx8abc123def456"},"assignedTo":{"type":"string","description":"User ID of the person responsible for implementing this action","example":"clx8abc123def456"},"dueDate":{"type":"string","description":"ISO-8601 due date for completing the action","example":"2026-06-30T00:00:00.000Z"},"status":{"type":"string","description":"Initial status of the action","enum":["PLANNED","IN_PROGRESS","COMPLETED","CANCELLED"],"example":"PLANNED"}},"required":["title","description","source","status"]},"CreateWebhookEndpointDto":{"type":"object","properties":{"url":{"type":"string","description":"Target URL that will receive the webhook POST requests","example":"https://example.com/webhooks/lms"},"secret":{"type":"string","description":"HMAC-SHA256 secret used to sign webhook payloads (min 16 chars). Store this securely — it will not be returned after creation.","minLength":16,"example":"super-secret-at-least-16-chars"},"events":{"type":"array","description":"List of events to subscribe to","example":["enrollment.created","payment.completed"],"items":{"type":"string","enum":["enrollment.created","enrollment.completed","enrollment.cancelled","payment.completed","payment.refunded","course.published","course.archived","session.started","session.ended","document.signed"]}},"isActive":{"type":"boolean","description":"Whether the endpoint is active and should receive events","default":true}},"required":["url","secret","events"]},"UpdateWebhookEndpointDto":{"type":"object","properties":{"url":{"type":"string","description":"Target URL that will receive the webhook POST requests","example":"https://example.com/webhooks/lms-updated"},"events":{"type":"array","description":"List of events to subscribe to","example":["course.published","session.ended"],"items":{"type":"string","enum":["enrollment.created","enrollment.completed","enrollment.cancelled","payment.completed","payment.refunded","course.published","course.archived","session.started","session.ended","document.signed"]}},"isActive":{"type":"boolean","description":"Whether the endpoint is active and should receive events"}}},"AdaptiveRuleConditionDto":{"type":"object","properties":{"type":{"type":"string","description":"Type of learner data to evaluate","enum":["quiz_score","time_spent","scorm_status"]},"operator":{"type":"string","description":"Comparison operator","enum":["gte","lte","eq"]},"value":{"type":"number","description":"Threshold value to compare against","example":70}},"required":["type","operator","value"]},"AdaptiveRuleDto":{"type":"object","properties":{"condition":{"$ref":"#/components/schemas/AdaptiveRuleConditionDto"},"nextLessonId":{"type":"string","description":"Lesson ID to route to when condition is satisfied","example":"clx8abc123def456"}},"required":["condition","nextLessonId"]},"CreateAdaptivePathDto":{"type":"object","properties":{"courseId":{"type":"string","description":"CUID of the course this path belongs to","example":"clx8abc123def456"},"lessonId":{"type":"string","description":"CUID of the lesson that triggers the branching","example":"clx8abc123def457"},"rules":{"description":"Ordered list of branching rules","type":"array","items":{"$ref":"#/components/schemas/AdaptiveRuleDto"}},"defaultNextLessonId":{"type":"string","description":"Fallback lesson ID when no rule matches","example":"clx8abc123def458"}},"required":["courseId","lessonId","rules"]},"CertificationDto":{"type":"object","properties":{"name":{"type":"string","description":"Certificate name","example":"Certified Scrum Master"},"issuer":{"type":"string","description":"Issuing organization","example":"Scrum Alliance"},"dateObtained":{"type":"string","description":"ISO date of obtention","example":"2023-06-15"},"expiresAt":{"type":"string","description":"ISO date of expiry","example":"2025-06-15"}},"required":["name","issuer","dateObtained"]},"CreateCompetencyDto":{"type":"object","properties":{"tenantId":{"type":"string","description":"CUID of the tenant","example":"clx8abc123def456"},"trainerId":{"type":"string","description":"CUID of the trainer (user with CREATOR role)","example":"clx8abc123def457"},"domain":{"type":"string","description":"Competency domain name","example":"Développement Web"},"level":{"type":"string","enum":["BEGINNER","INTERMEDIATE","ADVANCED","EXPERT"],"description":"Proficiency level in this domain"},"certifications":{"description":"Supporting certifications","type":"array","items":{"$ref":"#/components/schemas/CertificationDto"}}},"required":["tenantId","trainerId","domain","level"]},"CreateTrainingDto":{"type":"object","properties":{"tenantId":{"type":"string","description":"CUID of the tenant","example":"clx8abc123def456"},"trainerId":{"type":"string","description":"CUID of the trainer (user with CREATOR role)","example":"clx8abc123def457"},"title":{"type":"string","description":"Title of the training","example":"Formation React 19 avancé"},"provider":{"type":"string","description":"Training provider name","example":"OpenClassrooms"},"durationHours":{"type":"number","description":"Duration in hours (replaces legacy `duration` field)","example":14},"category":{"type":"string","enum":["PEDAGOGY","DIGITAL_TOOLS","DOMAIN_EXPERTISE","ACCESSIBILITY","QUALITY_REGULATION","OTHER"],"description":"Training category for Qualiopi classification"},"dateCompleted":{"type":"string","description":"ISO date of completion","example":"2024-03-20"},"certificateUrl":{"type":"string","description":"MinIO path or URL to the completion certificate","example":"certificates/trainer-abc/react19.pdf"},"competencyDomains":{"description":"Competency domains this training reinforces","example":["Développement Web","Architecture Frontend"],"type":"array","items":{"type":"string"}}},"required":["tenantId","trainerId","title","durationHours","category","dateCompleted"]},"CreateContinuousTrainingDto":{"type":"object","properties":{"title":{"type":"string","description":"Training title","example":"Formation pédagogie inversée"},"provider":{"type":"string","description":"Training provider name","example":"GRETA"},"durationHours":{"type":"number","description":"Duration in hours","example":14},"category":{"type":"string","enum":["PEDAGOGY","DIGITAL_TOOLS","DOMAIN_EXPERTISE","ACCESSIBILITY","QUALITY_REGULATION","OTHER"],"description":"Training category (Qualiopi Ind. 17)"},"dateCompleted":{"type":"string","description":"ISO date of completion","example":"2024-03-20"},"competencyDomains":{"description":"Competency domains reinforced by this training","example":["Pédagogie","Accessibilité"],"type":"array","items":{"type":"string"}}},"required":["title","durationHours","category","dateCompleted"]},"UpdateOnboardingItemDto":{"type":"object","properties":{"completed":{"type":"boolean","description":"Whether the item is completed"},"signatureId":{"type":"string","description":"CUID of the QualiopiDocument that serves as signature proof"}},"required":["completed"]},"UpdateTrainerProfileExtendedDto":{"type":"object","properties":{"trainerType":{"type":"string","description":"Type de formateur (interne, indépendant, freelance)","example":"INDEPENDANT"},"qualiopiNumber":{"type":"string","description":"Numéro d'enregistrement Qualiopi du formateur","example":"11 75 12345 75"},"qualiopiExpiry":{"format":"date-time","type":"string","description":"Date d'expiration de la certification Qualiopi","example":"2026-12-31T00:00:00.000Z"},"siret":{"type":"string","description":"Numéro SIRET du formateur (si indépendant ou auto-entrepreneur)","example":"12345678901234"},"contractType":{"type":"string","description":"Type de contrat du formateur","example":"PRESTATION"},"hourlyRate":{"type":"number","description":"Taux horaire du formateur en euros","example":75}}},"ExpiringQualiopiTrainerDto":{"type":"object","properties":{"userId":{"type":"string","description":"Identifiant de l'utilisateur formateur","example":"cluser123"},"firstName":{"type":"string","description":"Prénom du formateur","example":"Marie"},"lastName":{"type":"string","description":"Nom de famille du formateur","example":"Martin"},"email":{"type":"string","description":"Adresse email du formateur","format":"email","example":"marie.martin@exemple.fr"},"qualiopiNumber":{"type":"string","description":"Numéro Qualiopi du formateur","example":"11 75 12345 75"},"qualiopiExpiry":{"format":"date-time","type":"string","description":"Date d'expiration de la certification Qualiopi","example":"2025-08-15T00:00:00.000Z"},"daysUntilExpiry":{"type":"number","description":"Nombre de jours restants avant expiration","example":42}},"required":["userId","firstName","lastName","email","qualiopiNumber","qualiopiExpiry","daysUntilExpiry"]},"ValidatePromoCodeDto":{"type":"object","properties":{"code":{"type":"string","description":"Promo code to validate","example":"SUMMER25","minLength":2},"courseId":{"type":"string","description":"Course ID to check course-specific restrictions","example":"clx8abc123def456"},"orderAmount":{"type":"string","description":"Order amount in centimes to check minimum order restrictions","example":"9900"}},"required":["code"]},"CreatePromoCodeDto":{"type":"object","properties":{"code":{"type":"string","description":"Unique promo code string within the tenant","example":"SUMMER25","minLength":2,"maxLength":50},"discountType":{"type":"string","description":"Discount type: PERCENTAGE (0-100) or FIXED_AMOUNT (centimes)","enum":["PERCENTAGE","FIXED_AMOUNT"],"example":"PERCENTAGE"},"discountValue":{"type":"string","description":"Discount value. For PERCENTAGE: 0–100. For FIXED_AMOUNT: amount in centimes.","example":"25.00"},"maxUses":{"type":"number","description":"Maximum number of uses. Omit for unlimited.","example":100,"minimum":1},"minOrderAmount":{"type":"string","description":"Minimum order amount in centimes required to use this code.","example":"5000"},"validFrom":{"type":"string","description":"ISO-8601 datetime from which the code is valid. Defaults to now.","example":"2026-06-01T00:00:00.000Z"},"validUntil":{"type":"string","description":"ISO-8601 datetime until which the code is valid. Omit for no expiry.","example":"2026-09-01T00:00:00.000Z"},"isActive":{"type":"boolean","description":"Whether the code is active. Defaults to true.","example":true,"default":true},"courseIds":{"description":"List of course IDs this code applies to. Empty array = applies to all courses.","example":["clx8abc123def456","clx8xyz789ghi012"],"type":"array","items":{"type":"string"}}},"required":["code","discountType","discountValue"]},"CreateBundleDto":{"type":"object","properties":{"title":{"type":"string","description":"Bundle title","example":"Pack Gestion de Projet Complète","minLength":3,"maxLength":255},"description":{"type":"string","description":"Bundle description","example":"Maîtrisez la gestion de projet avec 3 formations essentielles"},"courseIds":{"description":"List of course IDs included in this bundle (minimum 2)","example":["clx8abc123def456","clx8xyz789ghi012"],"type":"array","items":{"type":"string"}},"price":{"type":"string","description":"Bundle price in centimes (should be less than sum of individual course prices)","example":"79900"},"currency":{"type":"string","description":"ISO 4217 currency code","example":"EUR","default":"EUR","maxLength":3},"status":{"type":"string","description":"Publication status. Defaults to DRAFT.","enum":["DRAFT","PUBLISHED","ARCHIVED"],"example":"DRAFT"},"maxParticipants":{"type":"number","description":"Maximum number of participants (null = unlimited)","example":50,"minimum":1,"maximum":9999},"seoMetadata":{"type":"object","description":"SEO metadata for the bundle page","example":{"title":"Pack Agile | QualiForma","description":"Formation complète..."}}},"required":["title","courseIds","price"]},"CreateCompanyDto":{"type":"object","properties":{"name":{"type":"string","example":"Acme Formations","minLength":2,"maxLength":200},"type":{"type":"string","enum":["EMPLOYER","OPCO","PUBLIC_FUNDER","PRESCRIBER","OTHER"],"default":"EMPLOYER"},"siret":{"type":"string","example":"12345678900012"},"vatNumber":{"type":"string","example":"FR12345678900"},"address":{"type":"string","example":"12 rue de la Formation"},"postalCode":{"type":"string","example":"75010"},"city":{"type":"string","example":"Paris"},"country":{"type":"string","example":"FR"},"contactName":{"type":"string","example":"Marie Dubois"},"contactEmail":{"type":"string","example":"contact@acme-formations.fr"},"contactPhone":{"type":"string","example":"+33 1 23 45 67 89"},"notes":{"type":"string","description":"Notes internes visibles seulement par l'organisme"},"defaultFunderId":{"type":"object","description":"Identifiant du financeur (Funder) à utiliser par défaut pour les inscriptions liées à cette entreprise. Peut être surchargé au niveau de chaque Enrollment.","nullable":true}},"required":["name","contactName","contactEmail"]},"SubmitNeedsAnalysisDto":{"type":"object","properties":{"requesterName":{"type":"string","example":"Marie Dubois","minLength":2,"maxLength":120},"requesterEmail":{"type":"string","example":"marie.dubois@exemple.fr"},"requesterPhone":{"type":"string","example":"+33 6 12 34 56 78"},"requesterRole":{"type":"string","description":"Rôle du demandeur (DRH, gérant, responsable formation…)","example":"DRH"},"companyName":{"type":"string","description":"Nom de l'entreprise si connue — la fiche Company sera créée à l'acceptation","example":"Acme Industries"},"contextDescription":{"type":"string","description":"Contexte métier du besoin de formation","example":"Nous déployons un nouvel outil CRM pour 25 commerciaux et nous souhaitons les former d'ici 3 mois.","minLength":20},"targetAudience":{"type":"string","description":"Public cible","example":"Commerciaux terrain, niveau intermédiaire","minLength":5},"expectedOutcomes":{"type":"string","description":"Objectifs opérationnels attendus","example":"Maîtriser les fonctionnalités essentielles, gagner 15% de temps sur la saisie","minLength":10},"participantCount":{"type":"number","minimum":1,"maximum":1000},"preferredStartDate":{"type":"string","description":"Date de début souhaitée (ISO 8601)","example":"2026-05-15"},"budgetCents":{"type":"number","description":"Budget indicatif en centimes d'euros","example":500000},"fundingSource":{"type":"string","example":"OPCO Atlas"},"constraints":{"type":"string","description":"Contraintes logistiques (dates bloquées, accessibilité…)"},"courseId":{"type":"string","description":"CUID d'un cours pressenti (si la demande concerne une formation du catalogue)"}},"required":["requesterName","requesterEmail","contextDescription","targetAudience","expectedOutcomes"]},"CreateFinancierAccessTokenDto":{"type":"object","properties":{"companyId":{"type":"string","description":"Identifiant de l'entreprise financeur"},"recipientEmail":{"type":"string","description":"Email du destinataire du magic link"},"recipientName":{"type":"string","description":"Nom du destinataire (affiché dans l'email)"},"scopes":{"type":"array","description":"Périmètre d'accès","default":["learners","documents","indicators","bpf"],"items":{"type":"string","enum":["learners","documents","indicators","bpf"]}},"expiresInDays":{"type":"number","description":"Durée de validité en jours (défaut 30, max 180)","default":30,"minimum":1,"maximum":180}},"required":["companyId","recipientEmail"]},"InviteLearnerDto":{"type":"object","properties":{"email":{"type":"string","description":"Email de l'apprenant à inviter","example":"apprenant@example.com"},"firstName":{"type":"string","description":"Prénom (facultatif, pré-rempli à l'activation)","example":"Marie","maxLength":100},"lastName":{"type":"string","description":"Nom (facultatif, pré-rempli à l'activation)","example":"Dubois","maxLength":100},"sessionId":{"type":"string","description":"CUID de la session (LiveSession) dans laquelle pré-inscrire l'apprenant. Obligatoire. Utiliser GET /sessions?courseId=...&includeMain=true pour résoudre la session principale d'un cours.","example":"clx8session123"},"customMessage":{"type":"string","description":"Message personnalisé inclus dans l'email d'invitation. Les balises HTML sont retirées côté serveur. Limite : 500 caractères après sanitisation.","example":"Nous sommes ravis de vous accueillir dans notre prochaine session de formation.","maxLength":2000}},"required":["email","sessionId"]},"UpdateCreatorProfileDto":{"type":"object","properties":{"firstName":{"type":"string","example":"Jean","maxLength":100},"lastName":{"type":"string","example":"Dupont","maxLength":100},"bio":{"type":"string","description":"Bio affichée sur la page publique du formateur","example":"Formateur en ingénierie pédagogique depuis 15 ans.","maxLength":2000},"avatarUrl":{"type":"string","description":"URL HTTPS du logo/avatar — upload préalable via /storage","example":"https://s3.eu-west-4.idrivee2.com/qualiforma-prod/avatars/xyz.png","maxLength":500},"competencies":{"description":"Liste des domaines de compétence (aligné sur TrainerProfile.specialties). Les doublons sont rejetés, max 30 entrées.","example":["Pédagogie","Qualiopi","Ingénierie de formation"],"maxItems":30,"type":"array","items":{"type":"string"}}}},"UpdateBrandingDto":{"type":"object","properties":{"organizationName":{"type":"string","description":"Raison sociale du centre de formation","example":"QR Communication Formation","maxLength":255},"organizationLegalForm":{"type":"string","description":"Forme juridique (SAS, SARL, EURL, Auto-entrepreneur, etc.)","example":"SAS","maxLength":64},"organizationSiret":{"type":"string","description":"SIRET — 14 chiffres, sans espaces ni séparateurs","example":"12345678900012"},"organizationSiren":{"type":"string","description":"SIREN — 9 chiffres, sans espaces","example":"123456789"},"organizationRcs":{"type":"string","description":"Mention RCS (ville + numéro)","example":"Paris B 123 456 789","maxLength":128},"organizationCapital":{"type":"number","description":"Capital social en euros","example":10000,"minimum":0},"organizationApe":{"type":"string","description":"Code APE (4 chiffres + lettre optionnelle) — 8559A pour la formation","example":"8559A"},"vatNumber":{"type":"string","description":"Numéro de TVA intracommunautaire (FR + 11 chiffres). Optionnel si l'organisme est exonéré.","example":"FR12345678901"},"vatExempt":{"type":"boolean","description":"Organisme exonéré de TVA (CGI art. 261-4-4° a) — ajoute la mention légale sur les factures","example":true,"default":false},"streetAddress":{"type":"string","description":"Numéro et nom de rue","example":"12 rue des Écoles","maxLength":255},"addressComplement":{"type":"string","description":"Complément d'adresse (bâtiment, étage, BP...)","example":"Bâtiment A - 3ème étage","maxLength":255},"postalCode":{"type":"string","description":"Code postal (4 à 10 caractères)","example":"75005"},"city":{"type":"string","description":"Ville","example":"Paris","maxLength":128},"country":{"type":"string","description":"Code pays ISO 3166-1 alpha-2 (2 lettres majuscules)","example":"FR","default":"FR"},"contactEmail":{"type":"string","description":"E-mail de contact public (apparaît sur les documents)","example":"contact@qualiforma.fr"},"contactPhone":{"type":"string","description":"Téléphone de contact (format français — +33 ou 0X accepté)","example":"+33 1 23 45 67 89"},"websiteUrl":{"type":"string","description":"URL du site web (HTTPS)","example":"https://qualiforma.fr"},"legalRepresentativeName":{"type":"string","description":"Nom complet du représentant légal (signataire des conventions)","example":"Marie Dupont","maxLength":255},"legalRepresentativeRole":{"type":"string","description":"Qualité / rôle du représentant légal","example":"Présidente","maxLength":128},"declarationActiviteNumber":{"type":"string","description":"Numéro de déclaration d'activité DREETS","example":"11 75 12345 75","maxLength":32},"declarationActiviteRegion":{"type":"string","description":"Région délivrant la déclaration d'activité","example":"Île-de-France","maxLength":64},"qualiopiNumber":{"type":"string","description":"Numéro de certification Qualiopi","example":"FR-QUALIOPI-12345","maxLength":64},"qualiopiCertifiedAt":{"type":"string","description":"Date de certification Qualiopi (ISO 8601)","example":"2024-01-15"},"qualiopiExpiresAt":{"type":"string","description":"Date d'expiration de la certification Qualiopi (ISO 8601)","example":"2027-01-15"},"qualiopiCertifyingBody":{"type":"string","description":"Organisme certificateur Qualiopi","example":"AFNOR Certification","maxLength":128},"accessibilityContact":{"type":"string","description":"Coordonnées du référent handicap (e-mail, téléphone, formulaire)","example":"accessibilite@qualiforma.fr","maxLength":500},"complaintHandlingProcedure":{"type":"string","description":"Procédure de traitement des réclamations (texte libre)","example":"Toute réclamation peut être adressée par e-mail à reclamations@qualiforma.fr. Une réponse est apportée sous 10 jours ouvrés.","maxLength":2000},"bankName":{"type":"string","description":"Nom de la banque","example":"Crédit Agricole","maxLength":128},"bankIban":{"type":"string","description":"IBAN (format ISO 13616)","example":"FR7612345678901234567890123"},"bankBic":{"type":"string","description":"Code BIC / SWIFT","example":"AGRIFRPP"},"dataProtectionOfficer":{"type":"string","description":"Délégué à la protection des données (DPO) — nom ou e-mail","example":"dpo@qualiforma.fr","maxLength":255},"monthlyRent":{"type":"number","description":"Loyer mensuel des locaux du centre de formation (EUR). Agrégé × 12 dans le BPF — rubrique B.chargesLocaux.","example":1200,"minimum":0},"logoMediaId":{"type":"object","description":"CUID du média médiathèque à utiliser comme logo (type IMAGE). null pour délier.","example":"clx8abc123def456"},"primaryColor":{"type":"string","description":"Couleur primaire au format #RRGGBB","example":"#0F766E"},"secondaryColor":{"type":"string","description":"Couleur secondaire au format #RRGGBB","example":"#10B981"},"documentFooter":{"type":"string","description":"Texte libre en pied de page des documents","example":"SIRET 123456789 — Organisme de formation enregistré sous le n° 11 75 12345 75","maxLength":500},"pifHotlineEmail":{"type":"string","description":"Email hotline apprenant (support e-learning). Fallback sur contactEmail si absent.","example":"hotline@qualiforma.fr","maxLength":255},"pifHotlinePhone":{"type":"string","description":"Téléphone hotline apprenant (support e-learning).","example":"+33 1 23 45 67 89"},"pifHotlineHours":{"type":"string","description":"Plage horaire de disponibilité du support e-learning.","example":"Lun-Ven 9h-18h, hors jours fériés","maxLength":255},"pifResponseTimeHours":{"type":"number","description":"Délai maximal de réponse en heures ouvrées. Code du travail recommande 48h max.","example":48,"minimum":1},"pifTechnicalSupport":{"type":"string","description":"Description libre du support technique mis à disposition (LMS, tutoriels, canal chat/forum).","example":"Plateforme LMS QualiForma accessible 24/7 avec tutoriels vidéo, forum modéré et chat intégré.","maxLength":2000},"pifPedagogicalMeans":{"type":"string","description":"Moyens pédagogiques mis à disposition (LMS, ressources, forum, classe virtuelle).","example":"Accès LMS, ressources PDF téléchargeables, forum asynchrone, 2 classes virtuelles.","maxLength":2000},"pifEvaluationMethods":{"type":"string","description":"Modalités d'évaluation spécifiques e-learning (QCM, ePortfolio, oral visio).","example":"QCM final en ligne (seuil 70%), projet individuel évalué, oral visio de 30 min.","maxLength":2000}}},"SetPdfUrlDto":{"type":"object","properties":{}},"UpdatePaymentConfigDto":{"type":"object","properties":{"mode":{"type":"string","enum":["TEST","PRODUCTION"],"description":"Mode de paiement : TEST (sandbox) ou PRODUCTION","example":"TEST"}}},"BrandingResponseDto":{"type":"object","properties":{"stampUrl":{"type":"string","description":"MinIO presigned GET URL for the stamp image (1h TTL). Null if not uploaded.","example":"https://minio.example.com/bucket/branding/user123/stamp-1745000000000.png?...","nullable":true},"signatureUrl":{"type":"string","description":"MinIO presigned GET URL for the signature image (1h TTL). Null if not uploaded.","example":"https://minio.example.com/bucket/branding/user123/signature-1745000000000.png?...","nullable":true}},"required":["stampUrl","signatureUrl"]},"SubmitContactDto":{"type":"object","properties":{"name":{"type":"string","example":"Marie Dubois","description":"Nom complet du demandeur","minLength":2,"maxLength":100},"email":{"type":"string","example":"marie.dubois@exemple.fr","description":"Email du demandeur — utilisé en Reply-To pour la réponse"},"subject":{"type":"string","example":"Demande de devis pour formation React","description":"Sujet du message","minLength":3,"maxLength":200},"message":{"type":"string","example":"Bonjour, je souhaite organiser une formation React pour 12 développeurs. Pourriez-vous m'envoyer un devis et vos disponibilités sur le mois prochain ?","description":"Message libre","minLength":20,"maxLength":5000},"website":{"type":"string","description":"Champ honeypot anti-bot — DOIT rester vide. Si rempli, la requête est silencieusement rejetée."}},"required":["name","email","subject","message"]},"UpdateGoogleCalendarSettingsDto":{"type":"object","properties":{"syncEnabled":{"type":"boolean","description":"Activer ou désactiver la synchronisation Google Calendar","example":true},"calendarId":{"type":"string","description":"Identifiant du calendrier Google à synchroniser","example":"primary"}}},"WebhookNotificationDto":{"type":"object","properties":{"resourceId":{"type":"string","description":"Identifiant de la ressource Google Calendar ayant déclenché la notification","example":"calendars/primary"},"resourceUri":{"type":"string","description":"URI de la ressource notifiée","example":"https://www.googleapis.com/calendar/v3/calendars/primary/events"},"channelId":{"type":"string","description":"Identifiant du canal de notification Google","example":"chan-uuid-123"},"channelExpiration":{"type":"string","description":"Date d'expiration du canal de notification (RFC 2822)","example":"1715000000000"}},"required":["resourceId","resourceUri","channelId"]},"UpdateCreatorGoogleConfigBodyDto":{"type":"object","properties":{"clientId":{"type":"string","nullable":true,"example":"123456789-abc.apps.googleusercontent.com","description":"Client ID Google OAuth2 — public, non-secret."},"clientSecret":{"type":"string","nullable":true,"description":"Client secret Google OAuth2. null = effacer. Chiffré au repos."},"redirectUri":{"type":"string","nullable":true,"example":"https://api.qualiforma.site/api/v1/google-calendar/callback","description":"URI de redirection OAuth Google — doit être enregistrée dans la Google Console."},"scopes":{"example":["https://www.googleapis.com/auth/calendar.events"],"description":"Scopes Google OAuth demandés.","type":"array","items":{"type":"string"}},"isActive":{"type":"boolean","description":"Activer / désactiver cette configuration Google OAuth."}}},"CourseTrainerRole":{"type":"string","enum":["PRIMARY","ASSISTANT","OBSERVER"],"description":"Rôle du formateur pour ce cours (PRIMARY par défaut)"},"AssignTrainerDto":{"type":"object","properties":{"userId":{"type":"string","description":"Identifiant de l'utilisateur à assigner comme formateur","example":"clxyzuser789"},"role":{"description":"Rôle du formateur pour ce cours (PRIMARY par défaut)","example":"PRIMARY","allOf":[{"$ref":"#/components/schemas/CourseTrainerRole"}]}},"required":["userId"]},"InviteTrainerDto":{"type":"object","properties":{"email":{"type":"string","description":"Adresse email du formateur à inviter","format":"email","example":"jean.dupont@exemple.fr"},"firstName":{"type":"string","description":"Prénom du formateur","example":"Jean"},"lastName":{"type":"string","description":"Nom de famille du formateur","example":"Dupont"},"role":{"description":"Rôle du formateur pour ce cours (PRIMARY par défaut)","example":"PRIMARY","allOf":[{"$ref":"#/components/schemas/CourseTrainerRole"}]}},"required":["email","firstName","lastName"]},"CreateLearnerConversationDto":{"type":"object","properties":{"courseId":{"type":"string","description":"CUID du cours concerné. Pour LEARNER : le trainerId sera résolu automatiquement (PRIMARY course trainer ou creator). Pour CREATOR/ADMIN : rattachement informationnel uniquement.","example":"clx8course123"},"trainerId":{"type":"string","description":"CUID du formateur cible (LEARNER uniquement — obligatoire si courseId n'est pas fourni).","example":"clx8trainer456"},"learnerId":{"type":"string","description":"CUID de l'apprenant cible (CREATOR/ADMIN uniquement — obligatoire pour ce rôle).","example":"clx8learner789"},"subject":{"type":"string","description":"Sujet court affiché dans la liste des conversations.","example":"Question sur le module 3","maxLength":140},"initialMessage":{"type":"string","description":"Contenu du premier message (texte brut).","example":"Bonjour, pourriez-vous me préciser…","maxLength":10000}},"required":["subject"]},"SendLearnerConversationMessageDto":{"type":"object","properties":{"content":{"type":"string","description":"Contenu du message (texte brut).","example":"Merci pour votre retour !"}},"required":["content"]},"ReplyMessageDto":{"type":"object","properties":{"bodyHtml":{"type":"string","description":"Corps HTML de la réponse à envoyer","example":"<p>Bonjour, merci de votre message.</p>","minLength":1}},"required":["bodyHtml"]},"GenerateAvatarDto":{"type":"object","properties":{"photoUrl":{"type":"string","description":"URL publique de la photo du formateur à utiliser comme base de l'avatar IA","example":"https://storage.example.com/photos/trainer-123.jpg"}},"required":["photoUrl"]},"TrainerAvatarResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant unique de l'avatar formateur","example":"clavatar123"},"trainerId":{"type":"string","description":"Identifiant du formateur propriétaire de l'avatar","example":"cluser456"},"status":{"type":"string","description":"Statut de génération de l'avatar (PENDING, PROCESSING, READY, FAILED)","example":"READY"},"previewUrl":{"type":"object","description":"URL de prévisualisation de l'avatar généré","format":"uri","example":"https://cdn.qualiforma.site/avatars/avatar-123.mp4"},"createdAt":{"format":"date-time","type":"string","description":"Date de création","example":"2025-01-01T00:00:00.000Z"},"updatedAt":{"format":"date-time","type":"string","description":"Date de dernière modification","example":"2025-05-01T00:00:00.000Z"}},"required":["id","trainerId","status","createdAt","updatedAt"]},"CreateStudioProjectDto":{"type":"object","properties":{"courseId":{"type":"string","description":"Identifiant (cuid) du cours auquel rattacher ce projet studio","example":"clcourse456"},"title":{"type":"string","description":"Titre du projet studio vidéo","example":"Introduction à Excel — Module 1"},"description":{"type":"string","description":"Description du projet studio","example":"Vidéo d'introduction au module Excel avancé."}},"required":["courseId"]},"StudioProjectStatus":{"type":"string","enum":["DRAFT","RENDERING","READY","FAILED"],"description":"Statut actuel du projet studio"},"StudioProjectResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant unique du projet studio","example":"clproject123"},"courseId":{"type":"object","description":"Identifiant du cours associé","example":"clcourse456"},"title":{"type":"string","description":"Titre du projet studio","example":"Introduction à Excel — Module 1"},"description":{"type":"object","description":"Description du projet","example":"Vidéo d'introduction au module."},"status":{"description":"Statut actuel du projet studio","example":"DRAFT","allOf":[{"$ref":"#/components/schemas/StudioProjectStatus"}]},"tracks":{"type":"object","description":"Pistes audio/vidéo du projet (objet JSON structuré)","example":{}},"outputUrl":{"type":"object","description":"URL du fichier vidéo final rendu","format":"uri","example":"https://cdn.qualiforma.site/videos/project-123.mp4"},"duration":{"type":"object","description":"Durée de la vidéo finale en secondes","example":185},"lessonId":{"type":"object","description":"Identifiant de la leçon générée à partir de ce projet","example":"cllesson789"},"previewUrl":{"type":"object","description":"URL de prévisualisation du projet","format":"uri","example":"https://cdn.qualiforma.site/previews/project-123.jpg"},"createdAt":{"format":"date-time","type":"string","description":"Date de création","example":"2025-01-01T00:00:00.000Z"},"updatedAt":{"format":"date-time","type":"string","description":"Date de dernière modification","example":"2025-05-01T00:00:00.000Z"}},"required":["id","title","status","tracks","createdAt","updatedAt"]},"UpdateStudioProjectDto":{"type":"object","properties":{"title":{"type":"string","description":"Nouveau titre du projet studio vidéo","example":"Introduction à Excel — Module 1 (V2)"},"tracks":{"type":"object","description":"Pistes audio/vidéo mises à jour (objet JSON structuré)","example":{}}}},"BpfStatus":{"type":"string","enum":["DRAFT","VALIDATED","SUBMITTED"],"description":"Statut du rapport BPF"},"BpfFinancialDataDto":{"type":"object","properties":{"chargesPersonnel":{"type":"number","description":"Charges de personnel saisies manuellement (hors coûts formateur auto)","example":15000},"chargesLocaux":{"type":"number","description":"Charges de locaux saisies manuellement (hors loyer auto)","example":8000},"chargesMateriels":{"type":"number","description":"Charges de matériels (saisie libre — pas d'auto-agrégation)","example":3000},"chargesAutres":{"type":"number","description":"Autres charges (saisie libre)","example":1200}}},"BpfReportResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant unique du rapport BPF","example":"clxyz123"},"tenantId":{"type":"string","description":"Identifiant du tenant","example":"cltenant456"},"year":{"type":"number","description":"Année du bilan pédagogique et financier","example":2025},"status":{"description":"Statut du rapport BPF","example":"DRAFT","allOf":[{"$ref":"#/components/schemas/BpfStatus"}]},"createdAt":{"format":"date-time","type":"string","description":"Date de création","example":"2025-01-01T00:00:00.000Z"},"updatedAt":{"format":"date-time","type":"string","description":"Date de dernière modification","example":"2025-06-01T00:00:00.000Z"},"identification":{"type":"object","description":"Données d'identification du centre de formation"},"financial":{"type":"object","description":"Données financières (produits, charges, résultat)"},"pedagogical":{"type":"object","description":"Données pédagogiques (stagiaires, heures, taux)"},"subcontracting":{"type":"object","description":"Données de sous-traitance"},"staff":{"type":"object","description":"Données sur le personnel formateur"},"financialData":{"description":"Charges manuelles saisies pour l'année","allOf":[{"$ref":"#/components/schemas/BpfFinancialDataDto"}]}},"required":["id","tenantId","year","status","createdAt","updatedAt"]},"SaveBpfChargesDto":{"type":"object","properties":{"financialData":{"description":"Données financières manuelles à sauvegarder","allOf":[{"$ref":"#/components/schemas/BpfFinancialDataDto"}]}},"required":["financialData"]},"CreateCreditCheckoutDto":{"type":"object","properties":{"packId":{"type":"string","description":"Identifiant (UUID) du pack de crédits IA à acheter","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"}},"required":["packId"]},"PurchaseIntentDto":{"type":"object","properties":{"packId":{"type":"string","description":"Identifiant du CreditPack sélectionné"}},"required":["packId"]},"UsersByRoleDto":{"type":"object","properties":{"LEARNER":{"type":"number"},"CREATOR":{"type":"number"},"ADMIN":{"type":"number"},"TRAINER":{"type":"number"},"FUNDER":{"type":"number"}},"required":["LEARNER","CREATOR","ADMIN","TRAINER","FUNDER"]},"AdminOverviewDto":{"type":"object","properties":{"tenantsCount":{"type":"number"},"activeTenantsCount":{"type":"number"},"usersCount":{"type":"number"},"usersByRole":{"$ref":"#/components/schemas/UsersByRoleDto"},"coursesCount":{"type":"number"},"publishedCoursesCount":{"type":"number"},"enrollmentsCount":{"type":"number"},"activeEnrollmentsCount":{"type":"number"},"completedEnrollmentsCount":{"type":"number"},"revenueLastMonth":{"type":"string"},"revenueThisMonth":{"type":"string"},"sessionsCount":{"type":"number"},"upcomingSessionsCount":{"type":"number"}},"required":["tenantsCount","activeTenantsCount","usersCount","usersByRole","coursesCount","publishedCoursesCount","enrollmentsCount","activeEnrollmentsCount","completedEnrollmentsCount","revenueLastMonth","revenueThisMonth","sessionsCount","upcomingSessionsCount"]},"GrowthDataPointDto":{"type":"object","properties":{"date":{"type":"string"},"enrollments":{"type":"number"},"payments":{"type":"number"},"revenue":{"type":"string"}},"required":["date","enrollments","payments","revenue"]},"PlatformSettingDto":{"type":"object","properties":{"id":{"type":"string"},"maintenanceMode":{"type":"boolean"},"maintenanceMessage":{"type":"string","nullable":true},"signupOpen":{"type":"boolean"},"defaultLocale":{"type":"string"},"supportEmail":{"type":"string"},"termsUrl":{"type":"string","nullable":true},"privacyUrl":{"type":"string","nullable":true},"mailProvider":{"type":"string","enum":["smtp","resend"],"example":"smtp"},"mailFromAddress":{"type":"string"},"mailFromName":{"type":"string"},"smtpHost":{"type":"string","nullable":true},"smtpPort":{"type":"number","nullable":true},"smtpSecure":{"type":"boolean"},"smtpUser":{"type":"string","nullable":true},"smtpHasPassword":{"type":"boolean"},"smtpPasswordLast4":{"type":"string","nullable":true,"description":"4 derniers caractères du mot de passe SMTP."},"resendHasApiKey":{"type":"boolean"},"resendApiKeyLast4":{"type":"string","nullable":true,"description":"4 derniers caractères de la clé Resend."},"stripeHasSecretKey":{"type":"boolean"},"stripeSecretKeyLast4":{"type":"string","nullable":true,"description":"4 derniers caractères de la clé secrète Stripe."},"stripeHasWebhookSecret":{"type":"boolean"},"stripePublishableKey":{"type":"string","nullable":true},"stripePlatformAccountId":{"type":"string","nullable":true},"stripeHasWebhookSecretCredits":{"type":"boolean"},"stripeWebhookSecretCreditsLast4":{"type":"string","nullable":true},"stripeHasWebhookSecretConnect":{"type":"boolean"},"stripeWebhookSecretConnectLast4":{"type":"string","nullable":true},"stripeConnectClientId":{"type":"string","nullable":true},"stripeConnectRedirectUri":{"type":"string","nullable":true},"liveKitUrl":{"type":"string","nullable":true},"liveKitHasApiKey":{"type":"boolean"},"liveKitApiKeyLast4":{"type":"string","nullable":true,"description":"4 derniers caractères de la clé API LiveKit."},"liveKitHasApiSecret":{"type":"boolean"},"liveKitApiSecretLast4":{"type":"string","nullable":true},"mistralHasApiKey":{"type":"boolean"},"mistralApiKeyLast4":{"type":"string","nullable":true},"mistralModel":{"type":"string","nullable":true},"assemblyaiHasApiKey":{"type":"boolean"},"assemblyaiApiKeyLast4":{"type":"string","nullable":true},"s3Endpoint":{"type":"string","nullable":true},"s3Region":{"type":"string","nullable":true},"s3Bucket":{"type":"string","nullable":true},"s3HasAccessKey":{"type":"boolean"},"s3AccessKeyLast4":{"type":"string","nullable":true,"description":"4 derniers caractères de l'access key S3."},"s3HasSecretKey":{"type":"boolean"},"s3SecretKeyLast4":{"type":"string","nullable":true},"s3ForcePathStyle":{"type":"boolean"},"s3UseSsl":{"type":"boolean"},"meilisearchUrl":{"type":"string","nullable":true},"meilisearchHasMasterKey":{"type":"boolean"},"meilisearchMasterKeyLast4":{"type":"string","nullable":true},"vapidPublicKey":{"type":"string","nullable":true},"vapidHasPrivateKey":{"type":"boolean"},"vapidPrivateKeyLast4":{"type":"string","nullable":true},"vapidSubject":{"type":"string","nullable":true},"scellioHasApiKey":{"type":"boolean"},"scellioApiKeyLast4":{"type":"string","nullable":true},"scellioHasWebhookSecret":{"type":"boolean"},"scellioWebhookSecretLast4":{"type":"string","nullable":true},"updatedAt":{"format":"date-time","type":"string"},"updatedById":{"type":"string","nullable":true}},"required":["id","maintenanceMode","signupOpen","defaultLocale","supportEmail","mailProvider","mailFromAddress","mailFromName","smtpHasPassword","resendHasApiKey","stripeHasSecretKey","stripeHasWebhookSecret","stripeHasWebhookSecretCredits","stripeHasWebhookSecretConnect","liveKitHasApiKey","liveKitHasApiSecret","mistralHasApiKey","assemblyaiHasApiKey","s3HasAccessKey","s3HasSecretKey","s3ForcePathStyle","s3UseSsl","meilisearchHasMasterKey","vapidHasPrivateKey","scellioHasApiKey","scellioHasWebhookSecret","updatedAt"]},"UpdatePlatformSettingDto":{"type":"object","properties":{"maintenanceMode":{"type":"boolean"},"maintenanceMessage":{"type":"string","nullable":true},"signupOpen":{"type":"boolean"},"defaultLocale":{"type":"string","example":"fr"},"supportEmail":{"type":"string","example":"support@qualiforma.fr"},"termsUrl":{"type":"string","nullable":true},"privacyUrl":{"type":"string","nullable":true},"mailProvider":{"type":"string","enum":["smtp","resend"],"example":"smtp"},"mailFromAddress":{"type":"string","example":"noreply@qualiforma.fr"},"mailFromName":{"type":"string","example":"QualiForma"},"smtpHost":{"type":"object","example":"smtp.sendgrid.net"},"smtpPort":{"type":"object","example":587},"smtpSecure":{"type":"boolean"},"smtpUser":{"type":"object","example":"apikey"},"smtpPassword":{"type":"string","nullable":true,"description":"Plain SMTP password — encrypted at rest. null = clear."},"resendApiKey":{"type":"string","nullable":true,"description":"Plain Resend API key — encrypted at rest. null = clear."},"stripeSecretKey":{"type":"string","nullable":true,"description":"Plain Stripe secret key (sk_*) — encrypted at rest. null = clear."},"stripeWebhookSecret":{"type":"string","nullable":true,"description":"Plain Stripe webhook secret (whsec_*) — encrypted at rest. null = clear."},"stripePublishableKey":{"type":"string","nullable":true,"example":"pk_live_..."},"stripePlatformAccountId":{"type":"string","nullable":true,"example":"acct_..."}}},"UpdateLiveKitSettingsDto":{"type":"object","properties":{"liveKitUrl":{"type":"string","nullable":true,"example":"wss://livekit.qualiforma.site"},"liveKitApiKey":{"type":"string","nullable":true,"description":"Clé API LiveKit — chiffrée au repos (P1-4). null = effacer.","example":"APIxxxxxxxxxxxxxxx"},"liveKitApiSecret":{"type":"string","nullable":true,"description":"Secret API LiveKit — chiffré au repos. null = effacer."}}},"UpdateAiSettingsDto":{"type":"object","properties":{"mistralApiKey":{"type":"string","nullable":true,"description":"Clé API Mistral — chiffrée au repos. null = effacer."},"mistralModel":{"type":"string","nullable":true,"example":"mistral-large-latest"},"assemblyaiApiKey":{"type":"string","nullable":true,"description":"Clé API AssemblyAI — chiffrée au repos. null = effacer."}}},"UpdateStorageSettingsDto":{"type":"object","properties":{"s3Endpoint":{"type":"string","nullable":true,"example":"s3.eu-west-4.idrivee2.com"},"s3Region":{"type":"string","nullable":true,"example":"eu-west-4"},"s3Bucket":{"type":"string","nullable":true,"example":"qualiforma-prod"},"s3AccessKey":{"type":"string","nullable":true,"description":"Access key S3 — chiffrée au repos (P1-3). null = effacer."},"s3SecretKey":{"type":"string","nullable":true,"description":"Secret key S3 — chiffrée au repos. null = effacer."},"s3ForcePathStyle":{"type":"boolean"},"s3UseSsl":{"type":"boolean"}}},"UpdateMeilisearchSettingsDto":{"type":"object","properties":{"meilisearchUrl":{"type":"string","nullable":true,"example":"http://localhost:7700"},"meilisearchMasterKey":{"type":"string","nullable":true,"description":"Master key Meilisearch — chiffrée au repos. null = effacer."}}},"UpdateVapidSettingsDto":{"type":"object","properties":{"vapidPublicKey":{"type":"string","nullable":true,"description":"Clé publique VAPID (base64url)."},"vapidPrivateKey":{"type":"string","nullable":true,"description":"Clé privée VAPID — chiffrée au repos. null = effacer."},"vapidSubject":{"type":"string","nullable":true,"example":"mailto:admin@qualiforma.site"}}},"UpdateScellioSettingsDto":{"type":"object","properties":{"scellioApiKey":{"type":"string","nullable":true,"description":"Clé API Scellio (eIDAS) — chiffrée au repos. null = effacer."},"scellioWebhookSecret":{"type":"string","nullable":true,"description":"Secret webhook Scellio — chiffré au repos. null = effacer."}}},"UpdateStripeConnectSettingsDto":{"type":"object","properties":{"stripeWebhookSecretCredits":{"type":"string","nullable":true,"description":"Secret webhook Stripe pour les crédits IA (whsec_*) — chiffré au repos. null = effacer."},"stripeWebhookSecretConnect":{"type":"string","nullable":true,"description":"Secret webhook Stripe Connect (whsec_*) — chiffré au repos. null = effacer."},"stripeConnectClientId":{"type":"string","nullable":true,"example":"ca_xxxxxxxxxxxxxxxxxxxxxxxx"},"stripeConnectRedirectUri":{"type":"string","nullable":true,"example":"https://api.qualiforma.site/payments/stripe-connect/callback"}}},"AdminTenantDto":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"domain":{"type":"object"},"ofMode":{"type":"boolean"},"isSuspended":{"type":"boolean"},"deletedAt":{"type":"object"},"createdAt":{"format":"date-time","type":"string"},"usersCount":{"type":"number"},"coursesCount":{"type":"number"}},"required":["id","slug","name","ofMode","isSuspended","createdAt","usersCount","coursesCount"]},"AdminTenantListDto":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminTenantDto"}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"}},"required":["items","total","page","limit"]},"AdminUserDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"role":{"type":"string"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"isActivated":{"type":"boolean"},"lastLoginAt":{"type":"object"},"createdAt":{"format":"date-time","type":"string"}},"required":["id","email","firstName","lastName","role","tenantId","isActivated","createdAt"]},"AdminUserListDto":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminUserDto"}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"}},"required":["items","total","page","limit"]},"UpdateAdminUserDto":{"type":"object","properties":{"role":{"type":"string","enum":["ADMIN","CREATOR","TRAINER","LEARNER","FUNDER"]},"isActivated":{"type":"boolean"},"tenantId":{"type":"string","example":"clx8abc123def456"}}},"AdminConnectedCreatorDto":{"type":"object","properties":{"creatorId":{"type":"string"},"userId":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"tenantName":{"type":"string"},"stripeAccountIdMasked":{"type":"string","nullable":true},"stripeAccountStatus":{"type":"string","nullable":true},"stripeOnboardingCompleted":{"type":"boolean"},"stripeChargesEnabled":{"type":"boolean"},"stripePayoutsEnabled":{"type":"boolean"},"stripeConnectedAt":{"format":"date-time","type":"string","nullable":true},"stripeLivemode":{"type":"boolean","nullable":true},"transactionsCount":{"type":"number"},"createdAt":{"format":"date-time","type":"string"}},"required":["creatorId","userId","email","firstName","lastName","tenantId","tenantSlug","tenantName","stripeOnboardingCompleted","stripeChargesEnabled","stripePayoutsEnabled","transactionsCount","createdAt"]},"AdminConnectedCreatorListDto":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectedCreatorDto"}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"}},"required":["items","total","page","limit"]},"AdminTransactionDto":{"type":"object","properties":{"id":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"tenantId":{"type":"string"},"tenantSlug":{"type":"string"},"tenantName":{"type":"string"},"creatorId":{"type":"string","nullable":true},"creatorName":{"type":"string","nullable":true},"learnerId":{"type":"string","nullable":true},"learnerEmail":{"type":"string","nullable":true},"amount":{"type":"string"},"platformFee":{"type":"string"},"sellerAmount":{"type":"string"},"currency":{"type":"string"},"status":{"type":"string"},"stripePaymentIntentId":{"type":"string","nullable":true},"stripeChargeId":{"type":"string","nullable":true},"refundedAmount":{"type":"string","nullable":true}},"required":["id","createdAt","tenantId","tenantSlug","tenantName","amount","platformFee","sellerAmount","currency","status"]},"AdminTransactionListDto":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminTransactionDto"}},"total":{"type":"number"},"page":{"type":"number"},"limit":{"type":"number"},"totals":{"type":"object"}},"required":["items","total","page","limit","totals"]},"CreditPackResponseDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"object"},"credits":{"type":"number"},"priceEur":{"type":"string","example":"9.99"},"currency":{"type":"string","example":"EUR"},"stripeProductId":{"type":"object"},"stripePriceId":{"type":"object"},"active":{"type":"boolean"},"displayOrder":{"type":"number"},"status":{"type":"string"},"syncedAt":{"type":"object"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","credits","priceEur","currency","active","displayOrder","status","createdAt","updatedAt"]},"CreateCreditPackDto":{"type":"object","properties":{"name":{"type":"string","example":"Pack Starter","description":"Nom affiché du pack"},"description":{"type":"string","example":"Idéal pour démarrer avec les fonctionnalités IA"},"credits":{"type":"number","example":100,"description":"Nombre de crédits IA accordés"},"priceEur":{"type":"number","example":9.99,"description":"Prix en EUR (TTC)"},"currency":{"type":"string","example":"EUR","default":"EUR"},"displayOrder":{"type":"number","example":0,"description":"Ordre d'affichage (croissant)"}},"required":["name","credits","priceEur"]},"UpdateCreditPackDto":{"type":"object","properties":{"name":{"type":"string","example":"Pack Starter","description":"Nom affiché du pack"},"description":{"type":"string","example":"Idéal pour démarrer avec les fonctionnalités IA"},"credits":{"type":"number","example":100,"description":"Nombre de crédits IA accordés"},"priceEur":{"type":"number","example":9.99,"description":"Prix en EUR (TTC)"},"currency":{"type":"string","example":"EUR","default":"EUR"},"displayOrder":{"type":"number","example":0,"description":"Ordre d'affichage (croissant)"},"active":{"type":"boolean","example":true,"description":"Activer / désactiver le pack"}}},"ThematicWatchBulletinDto":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant unique du bulletin","example":"clx1234567890"},"trainerId":{"type":"string","description":"Identifiant du formateur","example":"clx0987654321"},"tenantId":{"type":"string","description":"Identifiant du tenant","example":"qualiforma-demo"},"themes":{"description":"Thèmes couverts par ce bulletin","example":["IA en formation","Qualiopi"],"type":"array","items":{"type":"string"}},"summary":{"type":"string","description":"Résumé du bulletin de veille","example":"Résumé des actualités de la semaine..."},"key_insights":{"description":"Points clés identifiés","example":["Nouvelle réglementation Qualiopi"],"type":"array","items":{"type":"string"}},"resources":{"description":"Ressources et liens recommandés","example":["https://example.com/article"],"type":"array","items":{"type":"string"}},"credits_deducted":{"type":"number","description":"Crédits IA consommés pour la génération","example":5},"created_at":{"format":"date-time","type":"string","description":"Date de création du bulletin","example":"2024-01-15T10:30:00.000Z"}},"required":["id","trainerId","tenantId","themes","summary","key_insights","resources","credits_deducted","created_at"]},"BulletinListResponseDto":{"type":"object","properties":{"items":{"description":"Liste des bulletins de veille","type":"array","items":{"$ref":"#/components/schemas/ThematicWatchBulletinDto"}},"total":{"type":"number","description":"Nombre total de bulletins","example":42},"page":{"type":"number","description":"Page courante","example":1},"pageSize":{"type":"number","description":"Taille de page","example":10}},"required":["items","total","page","pageSize"]},"UpdateWatchConfigDto":{"type":"object","properties":{"watchEnabled":{"type":"boolean","description":"Activer ou désactiver la veille thématique","example":true},"watchFrequency":{"type":"string","description":"Fréquence de génération des bulletins","example":"WEEKLY","enum":["WEEKLY","BIWEEKLY","MONTHLY"]},"watchThemes":{"description":"Thèmes suivis par le formateur","example":["IA en formation","Qualiopi","Digital learning"],"type":"array","items":{"type":"string"}}}},"WatchConfigResponseDto":{"type":"object","properties":{"trainerId":{"type":"string","description":"Identifiant du formateur","example":"clx0987654321"},"watchEnabled":{"type":"boolean","description":"Veille thématique activée","example":true},"watchFrequency":{"type":"string","description":"Fréquence de génération","example":"WEEKLY"},"watchThemes":{"description":"Thèmes suivis","example":["IA en formation","Qualiopi"],"type":"array","items":{"type":"string"}},"lastGeneratedAt":{"format":"date-time","type":"string","description":"Date du dernier bulletin généré","example":"2024-01-15T10:30:00.000Z"}},"required":["trainerId","watchEnabled","watchFrequency","watchThemes"]},"CourseLocalesResponseDto":{"type":"object","properties":{"courseId":{"type":"string","description":"Identifiant du cours","example":"clcourse456"},"availableLocales":{"type":"array","description":"Liste des langues disponibles pour ce cours","example":["fr","en"],"items":{"type":"string","enum":["fr","en","es","de"]}},"defaultLocale":{"type":"string","description":"Langue par défaut du cours","enum":["fr","en","es","de"],"example":"fr"}},"required":["courseId","availableLocales","defaultLocale"]},"CourseTranslationResponseDto":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant de la traduction","example":"cltranslation123"},"courseId":{"type":"string","description":"Identifiant du cours","example":"clcourse456"},"locale":{"type":"string","description":"Code de langue de la traduction","enum":["fr","en","es","de"],"example":"en"},"title":{"type":"string","description":"Titre traduit","example":"Advanced Excel for HR"},"description":{"type":"string","description":"Description traduite","example":"Master Excel in an HR context."},"objectives":{"type":"object","description":"Objectifs traduits","example":"Create pivot tables."},"prerequisites":{"type":"object","description":"Prérequis traduits","example":"Basic Excel knowledge."},"targetAudience":{"type":"object","description":"Public cible traduit","example":"HR managers and executive assistants."},"createdAt":{"type":"string","description":"Date de création (ISO 8601)","example":"2025-01-01T00:00:00.000Z"},"updatedAt":{"type":"string","description":"Date de dernière mise à jour (ISO 8601)","example":"2025-05-01T00:00:00.000Z"}},"required":["id","courseId","locale","title","description","createdAt","updatedAt"]},"CourseTranslationDto":{"type":"object","properties":{"title":{"type":"string","description":"Titre de la formation traduit","example":"Advanced Excel for HR"},"description":{"type":"string","description":"Description de la formation traduite","example":"Maîtrisez Excel en contexte RH."},"objectives":{"type":"string","description":"Objectifs pédagogiques traduits","example":"Créer des tableaux croisés dynamiques."},"prerequisites":{"type":"string","description":"Prérequis traduits","example":"Connaissance de base d'Excel."},"targetAudience":{"type":"string","description":"Public cible traduit","example":"Responsables RH et assistants de direction."}}},"TranslateCourseDto":{"type":"object","properties":{"fromLocale":{"type":"string","description":"Langue source de la traduction","enum":["fr","en","es","de"],"example":"fr"},"toLocale":{"type":"string","description":"Langue cible de la traduction","enum":["fr","en","es","de"],"example":"en"}},"required":["fromLocale","toLocale"]},"CreateSubcontractorDto":{"type":"object","properties":{"name":{"type":"string","description":"Raison sociale ou nom complet du sous-traitant","example":"Expertise Formation SAS","minLength":2,"maxLength":200},"siret":{"type":"string","description":"Numéro SIRET du sous-traitant (14 chiffres)","example":"80215213700010"},"qualiopiNumber":{"type":"string","description":"Numéro de certification Qualiopi du sous-traitant","example":"11-75-12345"},"qualiopiExpiry":{"type":"string","description":"Date d'expiration de la certification Qualiopi (ISO 8601)","example":"2026-12-31"},"contactEmail":{"type":"string","description":"Email de contact du sous-traitant","example":"contact@expertise-formation.fr"},"contactPhone":{"type":"string","description":"Numéro de téléphone du sous-traitant","example":"01 23 45 67 89"},"certifications":{"description":"Certifications annexes (ISO 9001, ICF, RNCP, sectoriels…)","example":["ISO 9001","ICF"],"type":"array","items":{"type":"string"}},"notes":{"type":"string","description":"Notes internes (visible uniquement par les admins)","example":"Spécialiste en management et leadership — Région PACA"}},"required":["name"]},"UpdateSubcontractorDto":{"type":"object","properties":{"name":{"type":"string","description":"Raison sociale ou nom complet du sous-traitant","example":"Expertise Formation SAS"},"siret":{"type":"string","description":"Numéro SIRET du sous-traitant (14 chiffres)","example":"80215213700010"},"qualiopiNumber":{"type":"string","description":"Numéro de certification Qualiopi du sous-traitant","example":"11-75-12345"},"qualiopiExpiry":{"type":"string","description":"Date d'expiration de la certification Qualiopi (ISO 8601)","example":"2026-12-31"},"contactEmail":{"type":"string","description":"Email de contact du sous-traitant","example":"contact@expertise-formation.fr"},"contactPhone":{"type":"string","description":"Numéro de téléphone du sous-traitant","example":"01 23 45 67 89"},"certifications":{"description":"Certifications annexes","type":"array","items":{"type":"string"}},"notes":{"type":"string","description":"Notes internes (visible uniquement par les admins)"}}},"AttachSubcontractorDto":{"type":"object","properties":{"subcontractorId":{"type":"string","description":"Identifiant du sous-traitant à associer","example":"clxxxxxxxxxxxxx"},"role":{"type":"string","description":"Rôle du sous-traitant sur cette formation","example":"trainer"},"hoursAllocated":{"type":"number","description":"Volume horaire alloué au sous-traitant sur cette action (en heures)","example":14},"invoiceAmount":{"type":"number","description":"Montant facturé par le sous-traitant pour cette action (EUR)","example":1800}},"required":["subcontractorId"]},"UpdateCourseSubcontractorDto":{"type":"object","properties":{"role":{"type":"string","description":"Rôle du sous-traitant sur cette formation","example":"expert"},"hoursAllocated":{"type":"number","description":"Volume horaire alloué (en heures)","example":21},"invoiceAmount":{"type":"number","description":"Montant facturé par le sous-traitant pour cette action (EUR)","example":2500}}},"MediaResponseDto":{"type":"object","properties":{"id":{"type":"string","example":"clxyz123456"},"tenantId":{"type":"string","example":"clxyz_tenant"},"uploadedById":{"type":"string","example":"clxyz_user"},"type":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","PDF","OFFICE","OTHER"],"example":"VIDEO"},"status":{"type":"string","enum":["UPLOADING","PROCESSING","READY","FAILED"],"example":"READY"},"name":{"type":"string","example":"introduction-formation.mp4"},"mimeType":{"type":"string","example":"video/mp4"},"size":{"type":"number","example":104857600},"duration":{"type":"object","example":3600000,"description":"Duration in milliseconds (video/audio only)."},"width":{"type":"object","example":1920},"height":{"type":"object","example":1080},"tags":{"example":["module-1","intro"],"type":"array","items":{"type":"string"}},"courseId":{"type":"object","example":"clxyz_course"},"metadata":{"type":"object","description":"Extra metadata (exif, codec, bitrate, etc.)."},"createdAt":{"format":"date-time","type":"string","example":"2026-04-21T10:00:00.000Z"},"updatedAt":{"format":"date-time","type":"string","example":"2026-04-21T10:01:00.000Z"},"hasThumbnail":{"type":"boolean","example":true},"hasPreviewPdf":{"type":"boolean","example":false}},"required":["id","tenantId","uploadedById","type","status","name","mimeType","size","createdAt","updatedAt","hasThumbnail","hasPreviewPdf"]},"PaginatedMediaResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MediaResponseDto"}},"pagination":{"type":"object","example":{"total":120,"page":1,"pageSize":24,"pageCount":5}}},"required":["data","pagination"]},"UpdateMediaDto":{"type":"object","properties":{"name":{"type":"string","description":"New display name for the media.","example":"Module 1 — Introduction v2.mp4"},"tags":{"description":"Replacement tag set (full replace, not merge).","example":["module-1","revised"],"type":"array","items":{"type":"string"}},"courseId":{"type":"object","description":"Attach (or detach when null) this media to a course (CUID).","example":"clxyz123456","nullable":true}}},"RequestPayoutDto":{"type":"object","properties":{"amountCents":{"type":"number","description":"Montant demandé en centimes (minimum 5000 = 50 €)","example":25000,"minimum":5000},"creatorNotes":{"type":"string","description":"Note du créateur accompagnant la demande (contexte, justification, période de facturation, etc.)","example":"Reversement pour les ventes de mai 2025."}},"required":["amountCents"]},"PayoutStatus":{"type":"string","enum":["PENDING","APPROVED","REJECTED","PROCESSING","PAID","FAILED"]},"RejectPayoutDto":{"type":"object","properties":{"reason":{"type":"string","description":"Motif de refus du virement (stocké sur PayoutRequest.rejectionReason et transmis au créateur). Minimum 10 caractères","example":"Informations bancaires incomplètes — veuillez mettre à jour votre RIB.","minLength":10}},"required":["reason"]},"CreateVaeApplicationDto":{"type":"object","properties":{"candidateId":{"type":"string","description":"Identifiant de l'apprenant candidat VAE","example":"clx8abc123def456"},"courseId":{"type":"string","description":"Identifiant de la formation / certification dans le catalogue LMS","example":"clx8course123"},"certificationLabel":{"type":"string","description":"Libellé complet de la certification visée (RNCP ou RS)","example":"Responsable en ingénierie des systèmes numériques","maxLength":500},"certificationCode":{"type":"string","description":"Code RNCP ou RS de la certification","example":"RNCP38945","maxLength":20},"accompagnateurId":{"type":"string","description":"Identifiant du formateur accompagnateur VAE désigné par l'OF","example":"clx8user789"},"totalHoursAccompaniment":{"type":"number","description":"Nombre total d'heures d'accompagnement planifiées","example":24,"minimum":0},"notes":{"type":"string","description":"Notes internes de l'OF sur ce dossier","example":"Candidat expérimenté — 12 ans de pratique en gestion de projets SI."}},"required":["candidateId","certificationLabel"]},"UpdateVaeApplicationDto":{"type":"object","properties":{"accompagnateurId":{"type":"string","description":"Identifiant du formateur accompagnateur VAE désigné par l'OF","example":"clx8user789"},"totalHoursAccompaniment":{"type":"number","description":"Total d'heures d'accompagnement réalisées","example":18,"minimum":0},"notes":{"type":"string","description":"Notes internes de l'OF","example":"Livret 2 déposé le 15/05. Jury prévu mi-juin."},"certificationLabel":{"type":"string","description":"Libellé certification mis à jour","example":"Responsable en ingénierie des systèmes numériques","maxLength":500},"certificationCode":{"type":"string","description":"Code RNCP ou RS mis à jour","example":"RNCP38945","maxLength":20},"partialModulesToComplete":{"description":"Modules complémentaires à compléter (validation partielle)","example":["Bloc 3 — Communication professionnelle"],"type":"array","items":{"type":"string"}}}},"RecevabiliteDecisionDto":{"type":"object","properties":{"notes":{"type":"string","description":"Notes sur la décision de recevabilité (motif de refus le cas échéant)"}}},"ScheduleJuryDto":{"type":"object","properties":{"juryDate":{"type":"string","description":"Date prévue du passage devant le jury (ISO 8601)","example":"2026-06-15T09:00:00.000Z"},"notes":{"type":"string","description":"Notes relatives à la planification du jury","example":"Jury présentiel à Lyon — salle 204."}}},"JuryDecisionDto":{"type":"object","properties":{"outcome":{"type":"string","description":"Résultat rendu par le jury","enum":["TOTAL","PARTIEL","REFUS"],"example":"TOTAL"},"juryNotes":{"type":"string","description":"Retours du jury (points forts, axes d'amélioration)","example":"Excellente maîtrise des blocs 1 et 2. Bloc 3 à renforcer."},"partialModulesToComplete":{"description":"Modules ou blocs de compétences à compléter (validation partielle uniquement)","example":["Bloc 3 — Communication professionnelle"],"type":"array","items":{"type":"string"}}},"required":["outcome"]},"AbandonVaeDto":{"type":"object","properties":{"reason":{"type":"string","description":"Motif de l'abandon","example":"Candidat a changé de projet professionnel."}}},"CreateAlternanceContractDto":{"type":"object","properties":{"enrollmentId":{"type":"string","description":"Identifiant de l'inscription Enrollment liée au contrat (1 contrat = 1 inscription)","example":"clx8enroll123"},"contractType":{"type":"string","description":"Type légal du contrat","enum":["APPRENTISSAGE","PROFESSIONNALISATION"],"example":"APPRENTISSAGE"},"apprenantId":{"type":"string","description":"Identifiant de l'apprenant en alternance (User LEARNER)","example":"clx8user123"},"tuteurEntrepriseName":{"type":"string","description":"Nom complet du tuteur / maître d'apprentissage côté entreprise","example":"Marie Dupont","maxLength":200},"tuteurEntrepriseEmail":{"type":"string","description":"Email du tuteur entreprise","example":"marie.dupont@acme.fr"},"tuteurEntreprisePhone":{"type":"string","description":"Téléphone du tuteur entreprise","example":"+33 6 12 34 56 78"},"maitreApprentissageId":{"type":"string","description":"Identifiant du formateur référent (maître d'apprentissage) côté OF","example":"clx8trainer456"},"companyId":{"type":"string","description":"Identifiant Company dans le CRM (optionnel — l'entreprise peut ne pas être référencée)","example":"clx8company789"},"companyName":{"type":"string","description":"Dénomination sociale de l'entreprise d'accueil","example":"ACME SAS","maxLength":300},"companySiret":{"type":"string","description":"SIRET de l'établissement d'accueil (14 chiffres)","example":"12345678901234","maxLength":14},"companyAddress":{"type":"string","description":"Adresse complète de l'établissement d'accueil","example":"12 rue de la Paix, 75001 Paris"},"startDate":{"type":"string","description":"Date de début du contrat (ISO 8601)","example":"2026-09-01T00:00:00.000Z"},"endDate":{"type":"string","description":"Date de fin du contrat (ISO 8601)","example":"2027-08-31T00:00:00.000Z"},"weeklyHoursCompany":{"type":"number","description":"Heures hebdomadaires moyennes passées en entreprise","example":28,"minimum":0},"weeklyHoursTraining":{"type":"number","description":"Heures hebdomadaires moyennes passées en formation","example":12,"minimum":0},"notes":{"type":"string","description":"Notes internes"}},"required":["enrollmentId","contractType","apprenantId","tuteurEntrepriseName","companyName","companySiret","companyAddress","startDate","endDate","weeklyHoursCompany","weeklyHoursTraining"]},"UpdateAlternanceContractDto":{"type":"object","properties":{"tuteurEntrepriseName":{"type":"string","description":"Nom complet du tuteur entreprise","maxLength":200},"tuteurEntrepriseEmail":{"type":"string","description":"Email du tuteur entreprise"},"tuteurEntreprisePhone":{"type":"string","description":"Téléphone du tuteur entreprise"},"maitreApprentissageId":{"type":"string","description":"Formateur référent côté OF"},"companyName":{"type":"string","description":"Dénomination sociale de l'entreprise","maxLength":300},"companySiret":{"type":"string","description":"SIRET établissement (14 chiffres)","maxLength":14},"companyAddress":{"type":"string","description":"Adresse de l'établissement d'accueil"},"startDate":{"type":"string","description":"Date de début du contrat (ISO 8601)"},"endDate":{"type":"string","description":"Date de fin du contrat (ISO 8601)"},"weeklyHoursCompany":{"type":"number","description":"Heures hebdo en entreprise","minimum":0},"weeklyHoursTraining":{"type":"number","description":"Heures hebdo en formation","minimum":0},"notes":{"type":"string","description":"Notes internes"}}},"LogVisitDto":{"type":"object","properties":{"date":{"type":"string","description":"Date de la visite (ISO 8601)","example":"2026-11-15T14:00:00.000Z"},"type":{"type":"string","description":"Type de visite","enum":["TRIPARTITE","OF_ONLY","ENTREPRISE_ONLY"],"example":"TRIPARTITE"},"notes":{"type":"string","description":"Notes de la visite (points abordés, actions décidées)","example":"Progression conforme. Tuteur signale un besoin de soutien supplémentaire en bloc 2."},"visiteurId":{"type":"string","description":"Identifiant de l'agent OF ayant effectué la visite","example":"clx8trainer456"}},"required":["date","type"]},"TerminateContractDto":{"type":"object","properties":{"terminationDate":{"type":"string","description":"Date effective de la rupture (ISO 8601) — par défaut aujourd'hui","example":"2026-12-01T00:00:00.000Z"},"terminationReason":{"type":"string","description":"Motif de rupture (libre)","example":"Commun accord — candidat a obtenu un CDI dans son domaine de formation."}}},"UpsertProgressDto":{"type":"object","properties":{"flowId":{"type":"string","description":"Identifiant logique du flow d'onboarding (slug stable).","example":"creator-first-course","maxLength":64},"stepIndex":{"type":"number","description":"Index de la dernière étape vue (0-based). Défaut 0.","example":2,"minimum":0,"default":0}},"required":["flowId"]},"TrackEventDto":{"type":"object","properties":{"flowId":{"type":"string","description":"Identifiant logique du flow (slug stable).","example":"creator-first-course","maxLength":64},"stepIndex":{"type":"number","description":"Index de l'étape concernée (0-based). null pour les events tour.* qui ne ciblent pas une étape précise.","example":2,"minimum":0},"eventName":{"type":"string","description":"Nom de l'événement (liste fermée — append-only).","enum":["step.viewed","step.completed","tour.started","tour.skipped","tour.completed","tour.restarted"],"example":"step.viewed"},"timeOnStepMs":{"type":"number","description":"Durée passée sur l'étape (ms). Mesurée côté client.","example":8423,"minimum":0},"metadata":{"type":"object","description":"Métadonnées libres (max ~1KB sérialisé). Pas de PII : pas d'email, pas de nom.","example":{"triggerSource":"auto-first-login"}}},"required":["flowId","eventName"]}}}}