# Cahier de recette utilisateur — Lot L1 « Socle tracking »

**Périmètre livré** : PR [#98](https://github.com/AAZTEKDEV/EFEKTIVACADEMIE-BACK/pull/98)
(EA-010 connexions) · [#99](https://github.com/AAZTEKDEV/EFEKTIVACADEMIE-BACK/pull/99)
(EA-011 ouvertures de supports) · [#101](https://github.com/AAZTEKDEV/EFEKTIVACADEMIE-BACK/pull/101)
(EA-012 durées et positions, empilée sur #99) ·
[#100](https://github.com/AAZTEKDEV/EFEKTIVACADEMIE-BACK/pull/100) (EA-013 tentatives de quiz).
**Reste à livrer dans L1** (hors recette) : EA-004/005 (téléchargements authentifiés),
EA-006 (restrictions upload), EA-016 (exposition API), EA-017 (RGPD).

**Nature du lot** : le tracking est un socle **invisible à l'écran** tant qu'EA-016 (exposition)
et le cockpit (L4) ne sont pas livrés. La recette se fait donc côté données : les scénarios
« technique » se vérifient en base staging ou via l'API, avec l'aide du dev si besoin.
**Convention** : ☐ → ✅ / ❌ + constat.

## Préparation

- Compte apprenant `learner@mail.com` (ou équivalent staging), inscrit à un cours avec
  vidéo + PDF + quiz.
- Accès lecture à la base staging (ou un dev qui exécute les requêtes de contrôle).

---

## S1 · Journal des connexions (EA-010, issue #19)

- ☐ **S1.1** Se connecter avec l'apprenant. → Une ligne apparaît dans `login_events`
  (`user_id`, `logged_at`, IP **tronquée** — jamais l'IP complète, `user_agent`).
- ☐ **S1.2** `users.last_login_at` de ce compte est mis à jour à l'instant de la connexion.
- ☐ **S1.3** Se déconnecter puis se reconnecter. → Une **nouvelle** ligne (l'historique ne
  dépend pas des tokens supprimés au logout).
- ☐ **S1.4** Vérifier qu'aucun écran n'expose encore ces données (l'exposition = EA-016).

## S2 · Ouvertures de supports (EA-011, issue #20)

- ☐ **S2.1** Ouvrir un support (vidéo). → Une ligne `lesson_views` avec `user_id`,
  `lesson_id`, `course_id` (dénormalisé), `viewed_at`.
- ☐ **S2.2** **Anti-bruit** : fermer puis rouvrir le même support 3 fois en moins de 30 s.
  → **Une seule** ligne au total (les rafales sont ignorées).
- ☐ **S2.3** Rouvrir le même support après plus de 30 s. → Une **deuxième** ligne (les
  consultations réellement distinctes sont bien historisées — insertion, pas d'écrasement).
- ☐ **S2.4** (Dev) La fenêtre anti-bruit est **configurable** (pas une constante en dur).

## S3 · Durée de consultation et reprise (EA-012, issue #21)

- ☐ **S3.1** Regarder une vidéo ~2 minutes puis quitter la page. → La ligne `lesson_views`
  correspondante porte une `duration_seconds` cohérente (~120 s, best effort).
- ☐ **S3.2** **Scénario piège (dédoublonnage ↔ durée)** : rouvrir 3 fois en 10 s, regarder,
  fermer. → **1 seule ligne, 1 durée cohérente** — ni ligne orpheline, ni durée écrasée.
- ☐ **S3.3** Deux consultations espacées avec des durées différentes. → 2 lignes, 2 durées
  distinctes.
- ☐ **S3.4** Quitter une vidéo à mi-lecture : la **position de lecture** est persistée par
  user×support (`media_progress`) — c'est le socle de la reprise à la seconde (EA-035, L3 :
  la reprise à l'écran n'est PAS attendue dans cette recette).

## S4 · Historisation des tentatives de quiz (EA-013, issue #22)

**Avant correctif : refaire un quiz effaçait toute trace de la tentative précédente.**

- ☐ **S4.1** Passer un quiz (1re fois, ex. 60 %). → Une tentative n°1 est historisée
  (`quiz_attempts` : score, date) avec le **détail des réponses**.
- ☐ **S4.2** Refaire le quiz (ex. 90 %). → Tentative **n°2** ajoutée ; la n°1 est **toujours
  là** avec son détail.
- ☐ **S4.3** L'écran actuel de résultat (dernière tentative) affiche bien la n°2 —
  comportement visible inchangé (`quiz_scores` = dernière tentative).
- ☐ **S4.4** (Données migrées) Pour un apprenant qui avait passé des quiz AVANT le
  déploiement : ses scores existants apparaissent comme tentative n°1.

---

| Synthèse | ✅ | ❌ | Commentaire |
|---|---|---|---|
| S1 Connexions | | | |
| S2 Ouvertures supports | | | |
| S3 Durées / reprise | | | |
| S4 Tentatives quiz | | | |

---

## Résultats — recette du 08/08/2026 (branche d'intégration L0+L1+L2)

Déroulée sur l'intégration complète (back + base `efektiv_verif` reconstruite, front branché
dessus). **Périmètre élargi** par rapport à la rédaction initiale : EA-004/005/006/016/017 ont
été livrées entre-temps et sont donc recettées ici.

| Scénario | Résultat | Constat |
|---|---|---|
| S1 Connexions | ✅ 4/4 | Connexion réelle (mot de passe + OTP) → ligne `login_events` avec **IP tronquée** (`127.0.0.0`) et user-agent ; `last_login_at` mis à jour ; 2ᵉ connexion = 2ᵉ ligne (indépendante des jetons). |
| S2 Ouvertures de supports | ✅ 4/4 | 1 ouverture = 1 ligne (`course_id` dénormalisé) ; **rafale de 3 en < 30 s = 1 seule ligne** ; consultation espacée = 2ᵉ ligne ; fenêtre **configurable** (`TRACKING_VIEW_DEBOUNCE_SECONDS`, défaut 30). |
| S3 Durées / reprise | ✅ 4/4 | **Piège dédoublonnage↔durée résolu** : la rafale renvoie le MÊME `view_id` et la durée (120 s) se rattache à la ligne conservée — ni orpheline, ni écrasement. Position de lecture persistée (`media_progress`, 45 s). |
| S4 Tentatives de quiz | ⚠️ **partiel** | Tentative 1 (0 %) puis tentative 2 (100 %) : **les deux sont historisées** avec score et date, `quiz_scores` reflète bien la dernière. **MAIS deux CA non tenus** — voir ci-dessous. |
| EA-004 Téléchargement authentifié | ✅ | Route protégée (`Unauthenticated` sans jeton) ; table `resource_downloads` en place. |
| EA-006 Restrictions upload | ✅ | Validation `mimetypes` (type réel, pas l'extension) + message dédié. |
| EA-016 Exposition API | ✅ | `/api/tracking/learner/{id}` renvoie `login_count`, `distinct_active_days`, `usage_profile`, temps total, vues, téléchargements, tentatives. |
| EA-017 RGPD | ✅ | `TRACKING_RETENTION_MONTHS` (défaut 24) + commande `tracking:purge` avec **essai à blanc** fonctionnel. |

### ⚠️ Écarts au CA d'EA-013 (issue #22) — à traiter, non bloquants
1. **Pas de détail des réponses par tentative** : `quiz_attempts` stocke l'agrégat (score,
   correct_count, question_count) ; le détail des réponses ne vit que dans `quiz_scores`, qui est
   **écrasé à chaque nouvelle tentative**. Le CA demandait « + détail des réponses par tentative ».
   Conséquence : impossible de revoir les réponses données à la tentative 1 après une 2ᵉ. Bloquera
   l'analyse des items (V2) et la remédiation.
2. **Pas de reprise des `quiz_scores` existants comme tentative n°1** : aucune migration de
   rattrapage ; l'historique des apprenants déjà passés par un quiz démarre à vide.

### Note (hors périmètre L1)
Sans en-tête `Accept: application/json`, toute route protégée répond **500** au lieu de 401
(Laravel tente une redirection vers une route `login` inexistante). Comportement **global et
pré-existant**, vérifié sur trois routes dont deux antérieures à L1 — à traiter séparément.

---

## Résultats — recette sur STAGING déployé (08/08/2026)

Environnement : `staging.efektiv-academie-dev.com` + `api-staging…`, **données réelles**
(43 cours, 414 supports, 127 inscriptions). Compte de recette dédié `recette-l1l2@yopmail.com`
(créé pour l'occasion, aucune donnée réelle modifiée).

### L1 — socle tracking

| Scénario | Résultat | Constat sur données réelles |
|---|---|---|
| S1 Connexions | ✅ | Connexion réelle → ligne `login_events` avec **IP réellement tronquée** (`82.127.76.0`, anonymisation RGPD effective) ; `last_login_at` à jour. |
| S2 Ouvertures | ✅ | 1 ouverture = 1 ligne ; rafale de 3 → **1 seule ligne**, `view_id` stable. |
| S3 Durées / reprise | ✅ | Durée (95 s) rattachée à la ligne conservée ; position de lecture (40 s) persistée. |
| S4 Tentatives quiz | ✅ (aux 2 écarts CA près) | Tentative 1 à 0 %, tentative 2 à 100 %, **les deux conservées**. Les 2 écarts de CA restent ouverts (issue #22). |
| EA-016 Exposition | ✅ | `/tracking/learner/142` renvoie connexions, jours actifs, profil d'usage, temps total (95 s), vues, tentatives. |
| EA-017 RGPD | ✅ | `tracking:purge --dry-run` opérationnel sur le serveur. |


**Le détail L2 et le verdict figurent dans `RECETTE_L2.md`.**
