feat: Provisionner automatiquement un nouvel établissement
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
This commit is contained in:
2026-04-08 13:55:41 +02:00
parent bec211ebf0
commit dc2be898d5
171 changed files with 11703 additions and 700 deletions

181
backend/seed-story-6.9.sql Normal file
View File

@@ -0,0 +1,181 @@
-- =============================================================================
-- Jeu de données local — Story 6.9 : GradeVoter & accès notes
-- =============================================================================
-- Prérequis : avoir lancé `make generate-demo` sur un tenant (ecole-beta par défaut)
-- Usage :
-- docker compose exec -T php php bin/console dbal:run-sql "$(cat backend/seed-story-6.9.sql)"
--
-- Mot de passe de tous les comptes démo : DemoPassword123!
-- URL : http://ecole-beta.classeo.local:5174/login
--
-- ┌──────────────────────────────────────────────────────────────────────────┐
-- │ Scénarios à tester │
-- │ │
-- │ AC1 — GradeVoter │
-- │ ✅ Amina Benali (prof maths, affectée 6A) → peut saisir notes │
-- │ ❌ Sophie Lambert (prof SVT, pas affectée 6A en maths) → bloquée │
-- │ ✅ David Nguyen (remplaçant actif maths 6A) → peut saisir notes │
-- │ │
-- │ AC2 — Retrait d'affectation │
-- │ ❌ Julie Caron (prof français, affectation retirée 6A) │
-- │ → peut voir les notes existantes, ne peut plus en saisir │
-- │ │
-- │ AC3 — Badge "Remplaçant" │
-- │ → Se connecter avec David Nguyen, saisir une note │
-- │ → Le badge violet "Remplaçant" apparaît sur la ligne │
-- │ │
-- │ AC4 — Parent StudentVoter │
-- │ ✅ Nadia Martin → voit les notes de Lina Martin (6A) │
-- │ ❌ Claire Bernard → ne voit PAS les notes de Lina Martin │
-- └──────────────────────────────────────────────────────────────────────────┘
DO $$
DECLARE
v_tenant_id UUID;
v_class_6a_id UUID;
v_subject_math_id UUID;
v_subject_fr_id UUID;
v_teacher_amina_id UUID; -- prof maths, affectée
v_teacher_julie_id UUID; -- prof français, affectation retirée
v_teacher_david_id UUID; -- prof anglais, utilisé comme remplaçant
v_student_lina_id UUID;
v_student_chloe_id UUID;
v_student_hugo_id UUID;
v_student_zoe_id UUID;
v_eval_math_id UUID;
v_eval_fr_id UUID;
v_subdomain TEXT := 'ecole-beta';
BEGIN
-- Résoudre le tenant via les utilisateurs démo
SELECT tenant_id INTO v_tenant_id
FROM users WHERE email = 'prof.amina.benali.' || v_subdomain || '@classeo.test' LIMIT 1;
IF v_tenant_id IS NULL THEN
RAISE EXCEPTION 'Aucun compte démo trouvé pour %. Lancez `make generate-demo` d''abord.', v_subdomain;
END IF;
-- Résoudre les entités de démo existantes
SELECT id INTO v_class_6a_id FROM school_classes WHERE tenant_id = v_tenant_id AND name = '6A';
SELECT id INTO v_subject_math_id FROM subjects WHERE tenant_id = v_tenant_id AND code = 'MATH';
SELECT id INTO v_subject_fr_id FROM subjects WHERE tenant_id = v_tenant_id AND code = 'FR';
SELECT id INTO v_teacher_amina_id FROM users WHERE tenant_id = v_tenant_id AND email = 'prof.amina.benali.' || v_subdomain || '@classeo.test';
SELECT id INTO v_teacher_julie_id FROM users WHERE tenant_id = v_tenant_id AND email = 'prof.julie.caron.' || v_subdomain || '@classeo.test';
SELECT id INTO v_teacher_david_id FROM users WHERE tenant_id = v_tenant_id AND email = 'prof.david.nguyen.' || v_subdomain || '@classeo.test';
SELECT id INTO v_student_lina_id FROM users WHERE tenant_id = v_tenant_id AND email = 'eleve.lina.martin.' || v_subdomain || '@classeo.test';
SELECT id INTO v_student_chloe_id FROM users WHERE tenant_id = v_tenant_id AND email = 'eleve.chloe.lopez.' || v_subdomain || '@classeo.test';
SELECT id INTO v_student_hugo_id FROM users WHERE tenant_id = v_tenant_id AND email = 'eleve.hugo.lopez.' || v_subdomain || '@classeo.test';
SELECT id INTO v_student_zoe_id FROM users WHERE tenant_id = v_tenant_id AND email = 'eleve.zoe.moreau.' || v_subdomain || '@classeo.test';
-- Vérifications
IF v_class_6a_id IS NULL THEN RAISE EXCEPTION 'Classe 6A introuvable.'; END IF;
IF v_subject_math_id IS NULL THEN RAISE EXCEPTION 'Matière MATH introuvable.'; END IF;
IF v_teacher_amina_id IS NULL THEN RAISE EXCEPTION 'Prof Amina introuvable.'; END IF;
IF v_teacher_david_id IS NULL THEN RAISE EXCEPTION 'Prof David introuvable.'; END IF;
IF v_student_lina_id IS NULL THEN RAISE EXCEPTION 'Élève Lina introuvable.'; END IF;
-- =========================================================================
-- 1. Évaluation de maths en 6A (propriétaire = Amina, affectée)
-- =========================================================================
v_eval_math_id := gen_random_uuid();
INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, description, evaluation_date, grade_scale, coefficient, status, created_at, updated_at, grades_published_at)
VALUES (v_eval_math_id, v_tenant_id, v_class_6a_id, v_subject_math_id, v_teacher_amina_id,
'Contrôle — Fractions', 'Chapitre 4 : fractions et décimaux',
CURRENT_DATE - INTERVAL '3 days', 20, 1.0, 'published', NOW(), NOW(),
NOW() - INTERVAL '2 days');
-- Notes pour les 4 élèves de 6A
-- Lina et Hugo : saisies par David (remplaçant) → badge "Remplaçant" visible
-- Chloe et Zoe : saisies par Amina (titulaire) → pas de badge
INSERT INTO grades (id, tenant_id, evaluation_id, student_id, value, status, created_by, created_at, updated_at)
VALUES
(gen_random_uuid(), v_tenant_id, v_eval_math_id, v_student_lina_id, 16.5, 'graded', v_teacher_david_id, NOW(), NOW()),
(gen_random_uuid(), v_tenant_id, v_eval_math_id, v_student_chloe_id, 12.0, 'graded', v_teacher_amina_id, NOW(), NOW()),
(gen_random_uuid(), v_tenant_id, v_eval_math_id, v_student_hugo_id, NULL, 'absent', v_teacher_david_id, NOW(), NOW()),
(gen_random_uuid(), v_tenant_id, v_eval_math_id, v_student_zoe_id, 18.0, 'graded', v_teacher_amina_id, NOW(), NOW());
-- Statistiques
INSERT INTO evaluation_statistics (evaluation_id, average, min_grade, max_grade, median_grade, graded_count, updated_at)
VALUES (v_eval_math_id, 15.5, 12.0, 18.0, 16.5, 3, NOW())
ON CONFLICT (evaluation_id) DO NOTHING;
-- =========================================================================
-- 2. Évaluation de français en 6A (propriétaire = Julie)
-- Julie aura son affectation RETIRÉE → AC2
-- =========================================================================
v_eval_fr_id := gen_random_uuid();
INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, description, evaluation_date, grade_scale, coefficient, status, created_at, updated_at, grades_published_at)
VALUES (v_eval_fr_id, v_tenant_id, v_class_6a_id, v_subject_fr_id, v_teacher_julie_id,
'Dictée — Les Misérables', NULL,
CURRENT_DATE - INTERVAL '5 days', 20, 1.0, 'published', NOW(), NOW(),
NOW() - INTERVAL '4 days');
INSERT INTO grades (id, tenant_id, evaluation_id, student_id, value, status, created_by, created_at, updated_at)
VALUES
(gen_random_uuid(), v_tenant_id, v_eval_fr_id, v_student_lina_id, 14.0, 'graded', v_teacher_julie_id, NOW(), NOW()),
(gen_random_uuid(), v_tenant_id, v_eval_fr_id, v_student_chloe_id, 17.5, 'graded', v_teacher_julie_id, NOW(), NOW());
INSERT INTO evaluation_statistics (evaluation_id, average, min_grade, max_grade, median_grade, graded_count, updated_at)
VALUES (v_eval_fr_id, 15.75, 14.0, 17.5, 15.75, 2, NOW())
ON CONFLICT (evaluation_id) DO NOTHING;
-- Retirer l'affectation français 6A de Julie → AC2
UPDATE teacher_assignments
SET status = 'removed', end_date = NOW(), updated_at = NOW()
WHERE tenant_id = v_tenant_id
AND teacher_id = v_teacher_julie_id
AND school_class_id = v_class_6a_id
AND subject_id = v_subject_fr_id;
-- =========================================================================
-- 3. Remplacement actif : David Nguyen remplace Amina en maths 6A → AC1/AC3
-- =========================================================================
INSERT INTO teacher_replacements (id, tenant_id, replaced_teacher_id, replacement_teacher_id, start_date, end_date, status, reason, created_by, created_at, updated_at)
VALUES (gen_random_uuid(), v_tenant_id, v_teacher_amina_id, v_teacher_david_id,
CURRENT_DATE - INTERVAL '1 day', CURRENT_DATE + INTERVAL '14 days',
'active', 'Formation continue — 2 semaines',
v_teacher_amina_id, NOW(), NOW());
-- Lier le remplacement à la classe/matière maths 6A
INSERT INTO replacement_classes (replacement_id, class_id, subject_id)
SELECT tr.id, v_class_6a_id, v_subject_math_id
FROM teacher_replacements tr
WHERE tr.tenant_id = v_tenant_id
AND tr.replacement_teacher_id = v_teacher_david_id
AND tr.replaced_teacher_id = v_teacher_amina_id
AND tr.status = 'active';
-- =========================================================================
-- Résumé
-- =========================================================================
RAISE NOTICE '';
RAISE NOTICE '══════════════════════════════════════════════════════════════';
RAISE NOTICE ' Story 6.9 — Jeu de données créé !';
RAISE NOTICE '══════════════════════════════════════════════════════════════';
RAISE NOTICE ' URL : http://ecole-beta.classeo.local:5174/login';
RAISE NOTICE ' Mot de passe : DemoPassword123!';
RAISE NOTICE '';
RAISE NOTICE ' AC1 — Enseignant affecté :';
RAISE NOTICE ' prof.amina.benali.ecole-beta@classeo.test → saisie maths 6A ✅';
RAISE NOTICE '';
RAISE NOTICE ' AC1 — Enseignant non affecté :';
RAISE NOTICE ' prof.sophie.lambert.ecole-beta@classeo.test';
RAISE NOTICE ' → /dashboard/teacher/evaluations/%/grades → bloqué ❌', v_eval_math_id;
RAISE NOTICE '';
RAISE NOTICE ' AC1/AC3 — Remplaçant actif + badge :';
RAISE NOTICE ' prof.david.nguyen.ecole-beta@classeo.test';
RAISE NOTICE ' → /dashboard/teacher/evaluations/%/grades → saisie ✅ + badge', v_eval_math_id;
RAISE NOTICE '';
RAISE NOTICE ' AC2 — Affectation retirée (lecture seule) :';
RAISE NOTICE ' prof.julie.caron.ecole-beta@classeo.test';
RAISE NOTICE ' → /dashboard/teacher/evaluations/%/grades → lecture ✅ saisie ❌', v_eval_fr_id;
RAISE NOTICE '';
RAISE NOTICE ' AC4 — Parent lié / non lié :';
RAISE NOTICE ' parent.nadia.martin.ecole-beta@classeo.test → notes Lina ✅';
RAISE NOTICE ' parent.claire.bernard.ecole-beta@classeo.test → pas Lina ❌';
RAISE NOTICE '══════════════════════════════════════════════════════════════';
END $$;