From ceb54e9cda47724033c28ef92fb8e11b662270c0 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 10:05:46 +0000 Subject: [PATCH 01/13] Ajouter migrations/02_indexes.sql --- migrations/02_indexes.sql | 157 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 migrations/02_indexes.sql diff --git a/migrations/02_indexes.sql b/migrations/02_indexes.sql new file mode 100644 index 0000000..bd912c4 --- /dev/null +++ b/migrations/02_indexes.sql @@ -0,0 +1,157 @@ +-- ============================================= +-- 02_indexes.sql : Index FK + colonnes critiques +-- ============================================= + +-- Recommandation : exécuter après 01_init.sql + +-- =========== +-- UTILISATEURS +-- =========== +-- Recherche par email (insensibilité à la casse pour lookup) +CREATE INDEX IF NOT EXISTS idx_utilisateurs_lower_courriel + ON utilisateurs (LOWER(courriel)); + +-- =========== +-- ASSISTANTES_MATERNELLES +-- =========== +-- FK -> utilisateurs(id) +CREATE INDEX IF NOT EXISTS idx_am_id_utilisateur + ON assistantes_maternelles (id_utilisateur); + +-- ======= +-- PARENTS +-- ======= +-- FK -> utilisateurs(id) +CREATE INDEX IF NOT EXISTS idx_parents_id_utilisateur + ON parents (id_utilisateur); + +-- Co-parent (nullable) +CREATE INDEX IF NOT EXISTS idx_parents_id_co_parent + ON parents (id_co_parent); + +-- ======= +-- ENFANTS +-- ======= +-- (souvent filtrés par statut / date_naissance ? à activer si besoin) +-- CREATE INDEX IF NOT EXISTS idx_enfants_statut ON enfants (statut); +-- CREATE INDEX IF NOT EXISTS idx_enfants_date_naissance ON enfants (date_naissance); + +-- ================ +-- ENFANTS_PARENTS (N:N) +-- ================ +-- PK composite déjà en place (id_parent, id_enfant), ajouter index individuels si jointures unilatérales fréquentes +CREATE INDEX IF NOT EXISTS idx_enfants_parents_id_parent + ON enfants_parents (id_parent); + +CREATE INDEX IF NOT EXISTS idx_enfants_parents_id_enfant + ON enfants_parents (id_enfant); + +-- ======== +-- DOSSIERS +-- ======== +-- FK -> parent / enfant +CREATE INDEX IF NOT EXISTS idx_dossiers_id_parent + ON dossiers (id_parent); + +CREATE INDEX IF NOT EXISTS idx_dossiers_id_enfant + ON dossiers (id_enfant); + +-- Statut (filtrages "à traiter", "envoyé", etc.) +CREATE INDEX IF NOT EXISTS idx_dossiers_statut + ON dossiers (statut); + +-- JSONB : si on fait des requêtes @> sur le planning souhaité +-- CREATE INDEX IF NOT EXISTS idx_dossiers_planning_souhaite_gin +-- ON dossiers USING GIN (planning_souhaite); + +-- ======== +-- MESSAGES +-- ======== +-- Filtrage par dossier + tri chronologique +CREATE INDEX IF NOT EXISTS idx_messages_id_dossier_cree_le + ON messages (id_dossier, cree_le); + +-- Recherche par expéditeur +CREATE INDEX IF NOT EXISTS idx_messages_id_expediteur_cree_le + ON messages (id_expediteur, cree_le); + +-- ========= +-- CONTRATS +-- ========= +-- UNIQUE(id_dossier) existe déjà -> index implicite +-- Tri / filtres fréquents +CREATE INDEX IF NOT EXISTS idx_contrats_statut + ON contrats (statut); + +-- JSONB planning : activer si on requête par clé +-- CREATE INDEX IF NOT EXISTS idx_contrats_planning_gin +-- ON contrats USING GIN (planning); + +-- ================== +-- AVENANTS_CONTRATS +-- ================== +CREATE INDEX IF NOT EXISTS idx_avenants_contrats_id_contrat_cree_le + ON avenants_contrats (id_contrat, cree_le); + +CREATE INDEX IF NOT EXISTS idx_avenants_contrats_initie_par + ON avenants_contrats (initie_par); + +CREATE INDEX IF NOT EXISTS idx_avenants_contrats_statut + ON avenants_contrats (statut); + +-- ========= +-- EVENEMENTS +-- ========= +-- Accès par enfant + période +CREATE INDEX IF NOT EXISTS idx_evenements_id_enfant_date_debut + ON evenements (id_enfant, date_debut); + +-- Filtrage par auteur / AM / parent +CREATE INDEX IF NOT EXISTS idx_evenements_cree_par + ON evenements (cree_par); + +CREATE INDEX IF NOT EXISTS idx_evenements_id_am + ON evenements (id_am); + +CREATE INDEX IF NOT EXISTS idx_evenements_id_parent + ON evenements (id_parent); + +CREATE INDEX IF NOT EXISTS idx_evenements_type + ON evenements (type); + +CREATE INDEX IF NOT EXISTS idx_evenements_statut + ON evenements (statut); + +-- ================= +-- SIGNALEMENTS_BUGS +-- ================= +CREATE INDEX IF NOT EXISTS idx_signalements_bugs_id_utilisateur_cree_le + ON signalements_bugs (id_utilisateur, cree_le); + +-- ======= +-- UPLOADS +-- ======= +CREATE INDEX IF NOT EXISTS idx_uploads_id_utilisateur_cree_le + ON uploads (id_utilisateur, cree_le); + +-- ============= +-- NOTIFICATIONS +-- ============= +-- Requêtes fréquentes : non lues + ordre chrono +CREATE INDEX IF NOT EXISTS idx_notifications_user_lu_cree_le + ON notifications (id_utilisateur, lu, cree_le); + +-- Option : index partiel pour "non lues" +-- CREATE INDEX IF NOT EXISTS idx_notifications_non_lues +-- ON notifications (id_utilisateur, cree_le) +-- WHERE lu = false; + +-- =========== +-- VALIDATIONS +-- =========== +-- Requêtes par utilisateur validé, par statut et par date +CREATE INDEX IF NOT EXISTS idx_validations_id_utilisateur_cree_le + ON validations (id_utilisateur, cree_le); + +CREATE INDEX IF NOT EXISTS idx_validations_statut + ON validations (statut); From c0193ff4bb58ae717d2de5daf1db284fa0142a47 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 10:23:02 +0000 Subject: [PATCH 02/13] Ajouter migrations/03_checks.sql --- migrations/03_checks.sql | 140 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 migrations/03_checks.sql diff --git a/migrations/03_checks.sql b/migrations/03_checks.sql new file mode 100644 index 0000000..9d1e159 --- /dev/null +++ b/migrations/03_checks.sql @@ -0,0 +1,140 @@ +-- ============================================= +-- 03_checks.sql : Contraintes CHECK & NOT NULL +-- A exécuter après 01_init.sql (et 02_indexes.sql) +-- ============================================= + +-- ============== +-- UTILISATEURS +-- ============== +-- (Regex email déjà présente dans 01_init.sql) +-- Optionnel : forcer prenom/nom non vides si fournis +ALTER TABLE utilisateurs + ADD CONSTRAINT chk_utilisateurs_prenom_non_vide + CHECK (prenom IS NULL OR btrim(prenom) <> ''), + ADD CONSTRAINT chk_utilisateurs_nom_non_vide + CHECK (nom IS NULL OR btrim(nom) <> ''); + +-- ========================= +-- ASSISTANTES_MATERNELLES +-- ========================= +-- NIR : aujourd’hui en 15 chiffres (Sprint 2 : chiffrement) +ALTER TABLE assistantes_maternelles + ADD CONSTRAINT chk_am_nir_format + CHECK (nir_chiffre IS NULL OR nir_chiffre ~ '^[0-9]{15}$'), + ADD CONSTRAINT chk_am_nb_max_enfants + CHECK (nb_max_enfants IS NULL OR nb_max_enfants BETWEEN 0 AND 10), + ADD CONSTRAINT chk_am_ville_non_vide + CHECK (ville_residence IS NULL OR btrim(ville_residence) <> ''); + +-- ========= +-- PARENTS +-- ========= +-- Interdiction d’être co-parent de soi-même +ALTER TABLE parents + ADD CONSTRAINT chk_parents_co_parent_diff + CHECK (id_co_parent IS NULL OR id_co_parent <> id_utilisateur); + +-- ========= +-- ENFANTS +-- ========= +-- Cohérence statut / dates de naissance +ALTER TABLE enfants + ADD CONSTRAINT chk_enfants_dates_exclusives + CHECK (NOT (date_naissance IS NOT NULL AND date_prevue_naissance IS NOT NULL)), + ADD CONSTRAINT chk_enfants_statut_dates + CHECK ( + -- a_naitre => date_prevue_naissance requise + (statut = 'a_naitre' AND date_prevue_naissance IS NOT NULL) + OR + -- actif/scolarise => date_naissance requise + (statut IN ('actif','scolarise') AND date_naissance IS NOT NULL) + OR statut IS NULL -- si statut non encore fixé + ), + ADD CONSTRAINT chk_enfants_consentement_coherent + CHECK ( + (consentement_photo = true AND date_consentement_photo IS NOT NULL) + OR + (consentement_photo = false AND date_consentement_photo IS NULL) + ); + +-- ================= +-- ENFANTS_PARENTS +-- ================= +-- (PK composite déjà en place, rien à ajouter ici) + +-- ======== +-- DOSSIERS +-- ======== +ALTER TABLE dossiers + ADD CONSTRAINT chk_dossiers_budget_nonneg + CHECK (budget IS NULL OR budget >= 0), + ADD CONSTRAINT chk_dossiers_type_contrat_non_vide + CHECK (type_contrat IS NULL OR btrim(type_contrat) <> ''), + ADD CONSTRAINT chk_dossiers_planning_json + CHECK (planning_souhaite IS NULL OR jsonb_typeof(planning_souhaite) = 'object'); + +-- ======== +-- MESSAGES +-- ======== +-- Contenu obligatoire, non vide +ALTER TABLE messages + ALTER COLUMN contenu SET NOT NULL; +ALTER TABLE messages + ADD CONSTRAINT chk_messages_contenu_non_vide + CHECK (btrim(contenu) <> ''); + +-- ========= +-- CONTRATS +-- ========= +ALTER TABLE contrats + ADD CONSTRAINT chk_contrats_tarif_nonneg + CHECK (tarif_horaire IS NULL OR tarif_horaire >= 0), + ADD CONSTRAINT chk_contrats_indemnites_nonneg + CHECK (indemnites_repas IS NULL OR indemnites_repas >= 0); + +-- ================== +-- AVENANTS_CONTRATS +-- ================== +-- Rien de spécifique (statut enum déjà en place) + +-- ========= +-- EVENEMENTS +-- ========= +ALTER TABLE evenements + ADD CONSTRAINT chk_evenements_dates_coherentes + CHECK (date_fin IS NULL OR date_debut IS NULL OR date_fin >= date_debut); + +-- ================= +-- SIGNALEMENTS_BUGS +-- ================= +-- Description obligatoire, non vide +ALTER TABLE signalements_bugs + ALTER COLUMN description SET NOT NULL; +ALTER TABLE signalements_bugs + ADD CONSTRAINT chk_bugs_description_non_vide + CHECK (btrim(description) <> ''); + +-- ======= +-- UPLOADS +-- ======= +-- URL obligatoire + format basique (chemin absolu ou http(s)) +ALTER TABLE uploads + ALTER COLUMN fichier_url SET NOT NULL; +ALTER TABLE uploads + ADD CONSTRAINT chk_uploads_url_format + CHECK (fichier_url ~ '^(https?://.+|/[^\\s]+)$'); + +-- ============= +-- NOTIFICATIONS +-- ============= +-- Contenu obligatoire, non vide +ALTER TABLE notifications + ALTER COLUMN contenu SET NOT NULL; +ALTER TABLE notifications + ADD CONSTRAINT chk_notifications_contenu_non_vide + CHECK (btrim(contenu) <> ''); + +-- =========== +-- VALIDATIONS +-- =========== +-- Rien de plus ici (Sprint 1 Ticket 8 enrichira la table) From 17c4717d49a0980163e9138dd72220fa46aeb407 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 10:32:55 +0000 Subject: [PATCH 03/13] Ajouter migrations/04_fk_policies.sql --- migrations/04_fk_policies.sql | 190 ++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 migrations/04_fk_policies.sql diff --git a/migrations/04_fk_policies.sql b/migrations/04_fk_policies.sql new file mode 100644 index 0000000..e1144f1 --- /dev/null +++ b/migrations/04_fk_policies.sql @@ -0,0 +1,190 @@ +-- ========================================================== +-- 04_fk_policies.sql : normalisation des politiques ON DELETE +-- A exécuter après 01_init.sql et 03_checks.sql +-- ========================================================== + +-- Helper: Drop FK d'une table/colonne si elle existe (par son/leurs noms de colonne) +-- puis recrée la contrainte avec la clause fournie +-- Utilise information_schema pour retrouver le nom de contrainte auto-généré +-- NB: schema = public + +-- ========== messages.id_expediteur -> utilisateurs.id : SET NULL (au lieu de CASCADE) +DO $$ +DECLARE + conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='messages' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='id_expediteur'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.messages DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.messages + ADD CONSTRAINT fk_messages_id_expediteur + FOREIGN KEY (id_expediteur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== parents.id_co_parent -> utilisateurs.id : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='parents' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='id_co_parent'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.parents DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.parents + ADD CONSTRAINT fk_parents_id_co_parent + FOREIGN KEY (id_co_parent) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== avenants_contrats.initie_par -> utilisateurs.id : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='avenants_contrats' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='initie_par'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.avenants_contrats DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.avenants_contrats + ADD CONSTRAINT fk_avenants_contrats_initie_par + FOREIGN KEY (initie_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== evenements.id_am -> utilisateurs.id : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='evenements' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='id_am'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.evenements + ADD CONSTRAINT fk_evenements_id_am + FOREIGN KEY (id_am) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== evenements.id_parent -> parents.id_utilisateur : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='evenements' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='id_parent'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.evenements + ADD CONSTRAINT fk_evenements_id_parent + FOREIGN KEY (id_parent) REFERENCES public.parents(id_utilisateur) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== evenements.cree_par -> utilisateurs.id : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='evenements' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='cree_par'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.evenements + ADD CONSTRAINT fk_evenements_cree_par + FOREIGN KEY (cree_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== signalements_bugs.id_utilisateur -> utilisateurs.id : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='signalements_bugs' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='id_utilisateur'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.signalements_bugs DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.signalements_bugs + ADD CONSTRAINT fk_signalements_bugs_id_utilisateur + FOREIGN KEY (id_utilisateur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- ========== validations.id_utilisateur -> utilisateurs.id : SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema='public' + AND tc.table_name='validations' + AND tc.constraint_type='FOREIGN KEY' + AND kcu.column_name='id_utilisateur'; + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.validations DROP CONSTRAINT %I', conname); + END IF; + EXECUTE $sql$ + ALTER TABLE public.validations + ADD CONSTRAINT fk_validations_id_utilisateur + FOREIGN KEY (id_utilisateur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- NB: +-- D'autres FK déjà correctes : CASCADE (assistantes_maternelles, parents, enfants_parents, dossiers, messages.id_dossier, contrats, avenants_contrats.id_contrat, evenements.id_enfant), SET NULL (uploads). +-- On laisse ON UPDATE par défaut (NO ACTION), car les UUID ne changent pas. From 96ae868cf419224d66c16361b7ce3ec2f282afa7 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 10:39:56 +0000 Subject: [PATCH 04/13] Ajouter migrations/05_triggers.sql --- migrations/05_triggers.sql | 150 +++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 migrations/05_triggers.sql diff --git a/migrations/05_triggers.sql b/migrations/05_triggers.sql new file mode 100644 index 0000000..5d4b489 --- /dev/null +++ b/migrations/05_triggers.sql @@ -0,0 +1,150 @@ +-- ============================================= +-- 05_triggers.sql : Timestamps automatiques +-- - Ajoute (si absent) cree_le DEFAULT now() et modifie_le DEFAULT now() +-- - Crée un trigger BEFORE UPDATE pour mettre à jour modifie_le +-- - Idempotent (DROP TRIGGER IF EXISTS / IF NOT EXISTS) +-- A exécuter après 01_init.sql, 02_indexes.sql, 03_checks.sql +-- ============================================= + +-- 1) Fonction unique de mise à jour du timestamp +CREATE OR REPLACE FUNCTION set_modifie_le() +RETURNS TRIGGER AS $$ +BEGIN + NEW.modifie_le := NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Helper macro-like: pour chaque table, s'assurer des colonnes & trigger +-- (on ne peut pas faire de macro, donc on répète pour chaque table) + +-- Liste des tables concernées : +-- utilisateurs, assistantes_maternelles, parents, enfants, enfants_parents, +-- dossiers, messages, contrats, avenants_contrats, evenements, +-- signalements_bugs, uploads, notifications, validations + +-- ========== UTILISATEURS +ALTER TABLE utilisateurs + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_utilisateurs_set_modifie_le ON utilisateurs; +CREATE TRIGGER trg_utilisateurs_set_modifie_le +BEFORE UPDATE ON utilisateurs +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== ASSISTANTES_MATERNELLES +ALTER TABLE assistantes_maternelles + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_am_set_modifie_le ON assistantes_maternelles; +CREATE TRIGGER trg_am_set_modifie_le +BEFORE UPDATE ON assistantes_maternelles +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== PARENTS +ALTER TABLE parents + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_parents_set_modifie_le ON parents; +CREATE TRIGGER trg_parents_set_modifie_le +BEFORE UPDATE ON parents +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== ENFANTS +ALTER TABLE enfants + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_enfants_set_modifie_le ON enfants; +CREATE TRIGGER trg_enfants_set_modifie_le +BEFORE UPDATE ON enfants +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== ENFANTS_PARENTS (table de liaison) +ALTER TABLE enfants_parents + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_enfants_parents_set_modifie_le ON enfants_parents; +CREATE TRIGGER trg_enfants_parents_set_modifie_le +BEFORE UPDATE ON enfants_parents +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== DOSSIERS +ALTER TABLE dossiers + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_dossiers_set_modifie_le ON dossiers; +CREATE TRIGGER trg_dossiers_set_modifie_le +BEFORE UPDATE ON dossiers +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== MESSAGES +ALTER TABLE messages + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_messages_set_modifie_le ON messages; +CREATE TRIGGER trg_messages_set_modifie_le +BEFORE UPDATE ON messages +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== CONTRATS +ALTER TABLE contrats + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_contrats_set_modifie_le ON contrats; +CREATE TRIGGER trg_contrats_set_modifie_le +BEFORE UPDATE ON contrats +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== AVENANTS_CONTRATS +ALTER TABLE avenants_contrats + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_avenants_contrats_set_modifie_le ON avenants_contrats; +CREATE TRIGGER trg_avenants_contrats_set_modifie_le +BEFORE UPDATE ON avenants_contrats +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== EVENEMENTS +ALTER TABLE evenements + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_evenements_set_modifie_le ON evenements; +CREATE TRIGGER trg_evenements_set_modifie_le +BEFORE UPDATE ON evenements +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== SIGNALEMENTS_BUGS +ALTER TABLE signalements_bugs + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_signalements_bugs_set_modifie_le ON signalements_bugs; +CREATE TRIGGER trg_signalements_bugs_set_modifie_le +BEFORE UPDATE ON signalements_bugs +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== UPLOADS +ALTER TABLE uploads + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_uploads_set_modifie_le ON uploads; +CREATE TRIGGER trg_uploads_set_modifie_le +BEFORE UPDATE ON uploads +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== NOTIFICATIONS +ALTER TABLE notifications + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_notifications_set_modifie_le ON notifications; +CREATE TRIGGER trg_notifications_set_modifie_le +BEFORE UPDATE ON notifications +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); + +-- ========== VALIDATIONS +ALTER TABLE validations + ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW(); +DROP TRIGGER IF EXISTS trg_validations_set_modifie_le ON validations; +CREATE TRIGGER trg_validations_set_modifie_le +BEFORE UPDATE ON validations +FOR EACH ROW EXECUTE FUNCTION set_modifie_le(); From a36da6a5f35988138ee7341a4c5c8d497ebeaca1 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 10:47:37 +0000 Subject: [PATCH 05/13] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20v?= =?UTF-8?q?ers=20"/"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FK_POLICIES.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 FK_POLICIES.md diff --git a/FK_POLICIES.md b/FK_POLICIES.md new file mode 100644 index 0000000..3be63b3 --- /dev/null +++ b/FK_POLICIES.md @@ -0,0 +1,142 @@ + +# FK_POLICIES.md +**Politique des clés étrangères (ON DELETE / ON UPDATE)** – Sprint 1 + +## 🎯 Objectif +Documenter, de façon unique et partagée, les règles de suppression/mise à jour appliquées aux **clés étrangères** de la base P’titsPas pour : +- préserver l’**intégrité référentielle** ; +- conserver l’**historique** utile (messages, événements…) ; +- respecter les exigences **RGPD** (suppression en cascade lorsque pertinent). + +> Par défaut, **ON UPDATE = NO ACTION** (UUID immuables). +> Ce document couvre **ON DELETE** table par table. + +--- + +## 🧭 Principes généraux + +- **CASCADE** quand la donnée fille **n’a pas de sens sans le parent** + (ex. `dossiers` d’un parent, `avenants` d’un contrat). +- **SET NULL** quand on veut **préserver l’historique** mais que le référent peut disparaître + (ex. auteur d’un message supprimé, créateur d’un événement). +- **RESTRICT/NO ACTION** non utilisé ici pour éviter des blocages au nettoyage. + +--- + +## 📚 Récapitulatif rapide (matrice) + +| Table (colonne FK) → Référence | ON DELETE | Raison | +|---|---:|---| +| **assistantes_maternelles(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Profil AM supprimé avec son compte | +| **parents(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Extension parent supprimée avec son compte | +| **parents(id_co_parent)** → `utilisateurs(id)` | **SET NULL** | Conserver le parent principal si co-parent disparaît | +| **enfants_parents(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Nettoyage liaisons N:N | +| **enfants_parents(id_enfant)** → `enfants(id)` | **CASCADE** | Idem | +| **dossiers(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Dossier n’a pas de sens sans parent | +| **dossiers(id_enfant)** → `enfants(id)` | **CASCADE** | Dossier n’a pas de sens sans enfant | +| **messages(id_dossier)** → `dossiers(id)` | **CASCADE** | Messages détruits avec le dossier | +| **messages(id_expediteur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique des échanges | +| **contrats(id_dossier)** → `dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé | +| **avenants_contrats(id_contrat)** → `contrats(id)` | **CASCADE** | Avenants détruits avec le contrat | +| **avenants_contrats(initie_par)** → `utilisateurs(id)` | **SET NULL** | Historiser l’avenant sans bloquer | +| **evenements(id_enfant)** → `enfants(id)` | **CASCADE** | Événements n’ont plus de sens | +| **evenements(id_am)** → `utilisateurs(id)` | **SET NULL** | Garder la trace même si AM supprimée | +| **evenements(id_parent)** → `parents(id_utilisateur)` | **SET NULL** | Garder la trace si parent supprimé | +| **evenements(cree_par)** → `utilisateurs(id)` | **SET NULL** | Conserver l’historique de création | +| **signalements_bugs(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé | +| **uploads(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans l’auteur | +| **notifications(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Notifications propres à l’utilisateur | +| **validations(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique de décision | + +> **ON UPDATE** : **NO ACTION** partout (les UUID ne changent pas). + +--- + +## 🔎 Détail par domaine + +### Utilisateurs & extensions +- `assistantes_maternelles.id_utilisateur` → **CASCADE** +- `parents.id_utilisateur` → **CASCADE** +- `parents.id_co_parent` → **SET NULL** (interdit d’être co-parent de soi-même via CHECK déjà posé) + +### Enfants & liaisons +- `enfants_parents.id_parent` → **CASCADE** +- `enfants_parents.id_enfant` → **CASCADE** + +### Dossiers & échanges +- `dossiers.id_parent` → **CASCADE** +- `dossiers.id_enfant` → **CASCADE** +- `messages.id_dossier` → **CASCADE** +- `messages.id_expediteur` → **SET NULL** + +### Contrats & avenants +- `contrats.id_dossier` → **CASCADE** (unique 1:1) +- `avenants_contrats.id_contrat` → **CASCADE** +- `avenants_contrats.initie_par` → **SET NULL** + +### Événements +- `evenements.id_enfant` → **CASCADE** +- `evenements.id_am` → **SET NULL** +- `evenements.id_parent` → **SET NULL** +- `evenements.cree_par` → **SET NULL** + +### Divers +- `signalements_bugs.id_utilisateur` → **SET NULL** +- `uploads.id_utilisateur` → **SET NULL** +- `notifications.id_utilisateur` → **CASCADE** +- `validations.id_utilisateur` → **SET NULL** + +--- + +## 🧪 Scénarios de test (exemples) + +1. **Suppression d’un parent** + - Supprimer `utilisateurs(id=parentX)` + - Attendu : `parents` (CASCADE), ses `dossiers` (CASCADE), `messages` liés aux `dossiers` (CASCADE) sont supprimés. + +2. **Suppression d’un co-parent** + - Supprimer `utilisateurs(id=coParentY)` + - Attendu : `parents.id_co_parent` passe à **NULL**, aucun dossier supprimé. + +3. **Suppression d’un utilisateur auteur de messages** + - Supprimer `utilisateurs(id=uZ)` + - Attendu : les lignes `messages` **restent**, `id_expediteur` devient **NULL**. + +4. **Suppression d’un enfant** + - Supprimer `enfants(id=childA)` + - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements` du childA (CASCADE). + +5. **Suppression d’un utilisateur AM** + - Supprimer `utilisateurs(id=amB)` + - Attendu : `evenements.id_am` devient **NULL** (historique conservé). + +--- + +## 🛠 Migrations associées + +Les ajustements sont implémentés dans : +- **`/bdd/migrations/04_fk_policies.sql`** + – redéfinition des contraintes FK avec les bonnes politiques (**DROP puis ADD CONSTRAINT**), de façon idempotente. + +--- + +## 📄 Notes & futures évolutions + +- **RGPD (Sprint 2)** : si vous activez le **soft delete** (`deleted_at`) côté tables métier, ces politiques restent valides (les suppressions logiques se gèrent au niveau applicatif). +- **Audit** : si vous voulez tracer les suppressions, ajoutez des triggers d’audit (voir ticket Sprint 2 – Audit log). +- **Performance** : chaque FK doit être **indexée côté enfant** (cf. `02_indexes.sql`). + +--- + +## ✅ Checklist de conformité + +- [ ] Toutes les FK listées existent dans la base +- [ ] Politique **ON DELETE** conforme au tableau ci-dessus +- [ ] **ON UPDATE = NO ACTION** partout +- [ ] Tests de suppression réalisés sur une base seedée +- [ ] `04_fk_policies.sql` appliqué sans erreur + +--- + +**Mainteneur** : Équipe BDD +**Dernière mise à jour** : Sprint 1 – Politique FK consolidée From 3f38e3aab851259bfc661424c1a3ea4c71f90309 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 10:52:20 +0000 Subject: [PATCH 06/13] Ajouter seed/02_seed.sql --- seed/02_seed.sql | 221 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 seed/02_seed.sql diff --git a/seed/02_seed.sql b/seed/02_seed.sql new file mode 100644 index 0000000..c8ef3b4 --- /dev/null +++ b/seed/02_seed.sql @@ -0,0 +1,221 @@ +-- ============================================================ +-- 02_seed.sql : Données de test réalistes (Sprint 1) +-- A exécuter après : +-- 01_init.sql (création des tables) +-- 02_indexes.sql +-- 03_checks.sql +-- 04_fk_policies.sql +-- 05_triggers.sql +-- ============================================================ + +BEGIN; + +-- ------------------------------------------------------------ +-- Utilisateurs (super_admin, gestionnaire, 2 parents + co-parent, 1 AM) +-- ------------------------------------------------------------ + +-- UUIDs fixes pour faciliter les tests / jointures +-- super_admin +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('11111111-1111-1111-1111-111111111111', 'admin@ptits-pas.fr', '$2y$10$hashAdminIci', 'Super', 'Admin', 'super_admin', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- gestionnaire +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('22222222-2222-2222-2222-222222222222', 'gestion@ptits-pas.fr', '$2y$10$hashGestionIci', 'Gina', 'Gestion', 'gestionnaire', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- parent #1 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('33333333-3333-3333-3333-333333333333', 'parent1@example.com', '$2y$10$hashParent1', 'Paul', 'Parent', 'parent', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- co-parent du parent #1 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('44444444-4444-4444-4444-444444444444', 'coparent1@example.com', '$2y$10$hashCoParent1', 'Clara', 'CoParent', 'parent', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- parent #2 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('55555555-5555-5555-5555-555555555555', 'parent2@example.com', '$2y$10$hashParent2', 'Nora', 'Parent', 'parent', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- assistante maternelle #1 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('66666666-6666-6666-6666-666666666666', 'am1@example.com', '$2y$10$hashAM1', 'Alice', 'AM', 'am', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Extensions de rôles (parents / AM) +-- ------------------------------------------------------------ + +-- parents (id_co_parent nullable) +INSERT INTO parents (id_utilisateur, id_co_parent) +VALUES ('33333333-3333-3333-3333-333333333333', '44444444-4444-4444-4444-444444444444') -- parent1 avec co-parent +ON CONFLICT (id_utilisateur) DO NOTHING; + +INSERT INTO parents (id_utilisateur, id_co_parent) +VALUES ('55555555-5555-5555-5555-555555555555', NULL) -- parent2 sans co-parent +ON CONFLICT (id_utilisateur) DO NOTHING; + +-- assistantes_maternelles +INSERT INTO assistantes_maternelles (id_utilisateur, numero_agrement, nb_max_enfants, disponible, ville_residence) +VALUES ('66666666-6666-6666-6666-666666666666', 'AGR-2025-0001', 3, true, 'Lille') +ON CONFLICT (id_utilisateur) DO NOTHING; + +-- ------------------------------------------------------------ +-- Enfants +-- - child A : déjà né (statut = 'actif' et date_naissance requise) +-- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise) +-- ------------------------------------------------------------ + +INSERT INTO enfants (id, prenom, nom, statut, date_naissance, jumeau_multiple) +VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'actif', '2022-04-12', false) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance, jumeau_multiple) +VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Mila', 'Parent', 'a_naitre', '2026-02-15', false) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Liaison N:N parents_enfants +-- - parent1 + co-parent ↔ enfant A & B +-- - parent2 ↔ enfant B +-- ------------------------------------------------------------ + +-- parent1 ↔ enfant A +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('33333333-3333-3333-3333-333333333333', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') +ON CONFLICT DO NOTHING; + +-- co-parent1 ↔ enfant A +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('44444444-4444-4444-4444-444444444444', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') +ON CONFLICT DO NOTHING; + +-- parent1 ↔ enfant B +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('33333333-3333-3333-3333-333333333333', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb') +ON CONFLICT DO NOTHING; + +-- parent2 ↔ enfant B +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('55555555-5555-5555-5555-555555555555', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb') +ON CONFLICT DO NOTHING; + +-- ------------------------------------------------------------ +-- Dossier (parent1 ↔ enfant A) +-- ------------------------------------------------------------ +INSERT INTO dossiers (id, id_parent, id_enfant, presentation, type_contrat, repas, budget, planning_souhaite) +VALUES ( + 'dddddddd-dddd-dddd-dddd-dddddddddddd', + '33333333-3333-3333-3333-333333333333', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'Besoin garde périscolaire lundi/mardi/jeudi/vendredi.', + 'mensuel', + true, + 600.00, + '{"lun_ven":{"matin":false,"midi":true,"soir":true}}'::jsonb +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Messages (sur le dossier) +-- ------------------------------------------------------------ +INSERT INTO messages (id, id_dossier, id_expediteur, contenu) +VALUES ('m0000000-0000-0000-0000-000000000001', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '33333333-3333-3333-3333-333333333333', 'Bonjour, nous cherchons une garde périscolaire.') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO messages (id, id_dossier, id_expediteur, contenu) +VALUES ('m0000000-0000-0000-0000-000000000002', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '66666666-6666-6666-6666-666666666666', 'Bonjour, je suis disponible les soirs. Discutons du contrat.') +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Contrat (1:1 avec le dossier) +-- ------------------------------------------------------------ +INSERT INTO contrats (id, id_dossier, planning, tarif_horaire, indemnites_repas, date_debut, statut, signe_parent, signe_am) +VALUES ( + 'cccccccc-cccc-cccc-cccc-cccccccccccc', + 'dddddddd-dddd-dddd-dddd-dddddddddddd', + '{"lun_ven":{"17h-19h":true}}'::jsonb, + 12.50, + 3.50, + '2025-09-01', + 'brouillon', + false, + false +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Avenant de contrat +-- ------------------------------------------------------------ +INSERT INTO avenants_contrats (id, id_contrat, modifications, initie_par, statut) +VALUES ( + 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', + 'cccccccc-cccc-cccc-cccc-cccccccccccc', + '{"changement_horaire":{"vendredi":{"17h-20h":true}}}'::jsonb, + '33333333-3333-3333-3333-333333333333', + 'propose' +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Événement (absence enfant) +-- ------------------------------------------------------------ +INSERT INTO evenements (id, type, id_enfant, id_am, id_parent, cree_par, date_debut, date_fin, commentaires, statut, urgence) +VALUES ( + 'e0000000-0000-0000-0000-000000000001', + 'absence_enfant', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '66666666-6666-6666-6666-666666666666', + '33333333-3333-3333-3333-333333333333', + '33333333-3333-3333-3333-333333333333', + '2025-09-12', + '2025-09-12', + 'Enfant malade (rhume).', + 'propose', + false +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Upload (justificatif lié au dossier) +-- ------------------------------------------------------------ +INSERT INTO uploads (id, id_utilisateur, id_dossier_lie, fichier_url, type_fichier) +VALUES ( + 'u0000000-0000-0000-0000-000000000001', + '33333333-3333-3333-3333-333333333333', + 'dddddddd-dddd-dddd-dddd-dddddddddddd', + '/uploads/justificatifs/dossier_dddddddd_attestation.pdf', + 'application/pdf' +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Notification (pour le parent1) +-- ------------------------------------------------------------ +INSERT INTO notifications (id, id_utilisateur, type, contenu, lu) +VALUES ( + 'n0000000-0000-0000-0000-000000000001', + '33333333-3333-3333-3333-333333333333', + 'nouveau_message', + 'Vous avez un nouveau message sur le dossier #dddd…', + false +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Validation (compte de l’AM validé par le gestionnaire) +-- ------------------------------------------------------------ +INSERT INTO validations (id, id_utilisateur, statut, commentaire, cree_le) +VALUES ( + 'v0000000-0000-0000-0000-000000000001', + '66666666-6666-6666-6666-666666666666', + 'accepte', + 'Dossier AM vérifié par gestionnaire.', + NOW() +) +ON CONFLICT (id) DO NOTHING; + +COMMIT; From 7326e7862943e6e64e5ee2ba7f1b5e613baf3a5d Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:04:11 +0000 Subject: [PATCH 07/13] Ajouter migrations/06_validations_enrich.sql --- migrations/06_validations_enrich.sql | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 migrations/06_validations_enrich.sql diff --git a/migrations/06_validations_enrich.sql b/migrations/06_validations_enrich.sql new file mode 100644 index 0000000..65bacff --- /dev/null +++ b/migrations/06_validations_enrich.sql @@ -0,0 +1,53 @@ +-- ========================================================== +-- 06_validations_enrich.sql : Traçabilité complète des validations +-- - Ajoute la colonne 'valide_par' (FK -> utilisateurs.id) +-- - ON DELETE SET NULL pour conserver l'historique +-- - Ajoute index utiles pour les requêtes (valideur, statut, date) +-- A exécuter après : 01_init.sql, 02_indexes.sql, 03_checks.sql, 04_fk_policies.sql, 05_triggers.sql +-- ========================================================== + +BEGIN; + +-- 1) Colonne 'valide_par' si absente +ALTER TABLE validations + ADD COLUMN IF NOT EXISTS valide_par UUID NULL; + +-- 2) FK vers utilisateurs(id), ON DELETE SET NULL +DO $$ +DECLARE conname text; +BEGIN + SELECT tc.constraint_name INTO conname + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.table_schema = 'public' + AND tc.table_name = 'validations' + AND tc.constraint_type= 'FOREIGN KEY' + AND kcu.column_name = 'valide_par'; + + IF conname IS NOT NULL THEN + EXECUTE format('ALTER TABLE public.validations DROP CONSTRAINT %I', conname); + END IF; + + EXECUTE $sql$ + ALTER TABLE public.validations + ADD CONSTRAINT fk_validations_valide_par + FOREIGN KEY (valide_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL + $sql$; +END $$; + +-- 3) Index pour accélérer les recherches +-- - qui a validé quoi récemment ? +-- - toutes les validations par statut / par date +CREATE INDEX IF NOT EXISTS idx_validations_valide_par_cree_le + ON validations (valide_par, cree_le); + +-- Certains existent peut-être déjà : on sécurise +CREATE INDEX IF NOT EXISTS idx_validations_id_utilisateur_cree_le + ON validations (id_utilisateur, cree_le); + +CREATE INDEX IF NOT EXISTS idx_validations_statut + ON validations (statut); + +COMMIT; From bdf9d0be4fceb730711553a7582f46c9e6eed3c8 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:12:39 +0000 Subject: [PATCH 08/13] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20v?= =?UTF-8?q?ers=20"docs"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ENUMS.md | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/ENUMS.md diff --git a/docs/ENUMS.md b/docs/ENUMS.md new file mode 100644 index 0000000..c24e516 --- /dev/null +++ b/docs/ENUMS.md @@ -0,0 +1,157 @@ + +# ENUMS.md — Référentiel des valeurs énumérées (Sprint 1) + +Ce document recense **toutes les valeurs énumérées** utilisées dans la base P’titsPas, leur **sens fonctionnel**, les **colonnes concernées** et les **transitions** attendues côté métier / API. + +> Objectif : garantir la **cohérence** entre la DB, le backend (NestJS) et le frontend (Flutter). +> Toute nouvelle valeur ou renommage **doit** être ajouté ici **avant** migration DB. + +--- + +## Conventions générales + +- Les valeurs ENUM sont **en minuscules** et **sans espace** (snake_case si nécessaire). +- Côté DB, elles sont implémentées via **types ENUM PostgreSQL** *ou* via `CHECK` (selon ce qui est en place dans `01_init.sql`). +- Côté API, ces valeurs sont **renvoyées telles quelles** et **documentées** dans l’OpenAPI / DTO. + +--- + +## 1) Rôle utilisateur — `role` + +**Tables/colonnes** : `utilisateurs.role` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `super_admin` | Compte technique initial / administration globale | +| `gestionnaire` | Gestion / validation des comptes, supervision | +| `parent` | Parent ou co-parent | +| `am` | Assistante maternelle | + +--- + +## 2) Statut utilisateur — `statut` + +**Tables/colonnes** : `utilisateurs.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `en_attente` | Compte créé mais non validé | +| `accepte` | Compte validé et actif | +| `rejete` | Demande refusée (peut être recréée ultérieurement) | + +--- + +## 3) Statut enfant — `statut` + +**Tables/colonnes** : `enfants.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `a_naitre` | Enfant à naître (date prévue renseignée) | +| `actif` | Enfant pris en charge / en cours de garde | +| `scolarise` | Enfant scolarisé, garde potentiellement périscolaire | + +**Contraintes associées** : +- `a_naitre` → **`date_prevue_naissance` obligatoire** +- `actif`/`scolarise` → **`date_naissance` obligatoire** + +--- + +## 4) Statut dossier — `statut` + +**Tables/colonnes** : `dossiers.statut` +**Valeurs autorisées (MVP)** : + +| Valeur | Description | +|---|---| +| `envoye` | Dossier soumis par le parent (état initial) | +| `en_cours` | Échanges en cours entre parent et AM | +| `clos` | Dossier clôturé (contrat généré ou abandon) | + +--- + +## 5) Statut contrat — `statut` + +**Tables/colonnes** : `contrats.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `brouillon` | Contrat en préparation | +| `valide` | Contrat finalisé (signatures complètes) | +| `archive` | Contrat obsolète / terminé | + +--- + +## 6) Statut avenant — `statut` + +**Tables/colonnes** : `avenants_contrats.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `propose` | Avenant proposé (en attente d’accord) | +| `valide` | Avenant accepté et appliqué | +| `rejete` | Avenant refusé | + +--- + +## 7) Type d’événement — `type` + +**Tables/colonnes** : `evenements.type` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `absence_enfant` | Enfant absent | +| `conge_am` | Congé de l’assistante maternelle | +| `conge_parent` | Congé du parent | +| `arret_maladie_am` | Arrêt maladie AM | +| `evenement_rpe` | Événement RPE | + +--- + +## 8) Statut d’événement — `statut` + +**Tables/colonnes** : `evenements.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `propose` | Événement proposé | +| `valide` | Événement validé | +| `rejete` | Événement refusé | + +--- + +## 9) Statut de validation compte — `statut` + +**Tables/colonnes** : `validations.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `accepte` | Compte validé | +| `rejete` | Compte refusé | + +--- + +## 10) Type de notification — `type` + +**Tables/colonnes** : `notifications.type` +**Valeurs proposées** : + +| Valeur | Description | +|---|---| +| `nouveau_message` | Nouveau message sur un dossier | +| `validation_compte` | Résultat de la validation de compte | +| `maj_contrat` | Contrat mis à jour (avenant / signature) | +| `evenement_a_venir` | Rappel d’un événement proche | + +--- + +**Mainteneur** : Équipe BDD +**Dernière mise à jour** : Sprint 1 — Ticket 9 (ENUMS) From 2075ff33797a1e9258f9572768cf0ba8dd8b0187 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:14:27 +0000 Subject: [PATCH 09/13] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20v?= =?UTF-8?q?ers=20"docs"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FK_POLICIES.md | 142 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 docs/FK_POLICIES.md diff --git a/docs/FK_POLICIES.md b/docs/FK_POLICIES.md new file mode 100644 index 0000000..3be63b3 --- /dev/null +++ b/docs/FK_POLICIES.md @@ -0,0 +1,142 @@ + +# FK_POLICIES.md +**Politique des clés étrangères (ON DELETE / ON UPDATE)** – Sprint 1 + +## 🎯 Objectif +Documenter, de façon unique et partagée, les règles de suppression/mise à jour appliquées aux **clés étrangères** de la base P’titsPas pour : +- préserver l’**intégrité référentielle** ; +- conserver l’**historique** utile (messages, événements…) ; +- respecter les exigences **RGPD** (suppression en cascade lorsque pertinent). + +> Par défaut, **ON UPDATE = NO ACTION** (UUID immuables). +> Ce document couvre **ON DELETE** table par table. + +--- + +## 🧭 Principes généraux + +- **CASCADE** quand la donnée fille **n’a pas de sens sans le parent** + (ex. `dossiers` d’un parent, `avenants` d’un contrat). +- **SET NULL** quand on veut **préserver l’historique** mais que le référent peut disparaître + (ex. auteur d’un message supprimé, créateur d’un événement). +- **RESTRICT/NO ACTION** non utilisé ici pour éviter des blocages au nettoyage. + +--- + +## 📚 Récapitulatif rapide (matrice) + +| Table (colonne FK) → Référence | ON DELETE | Raison | +|---|---:|---| +| **assistantes_maternelles(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Profil AM supprimé avec son compte | +| **parents(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Extension parent supprimée avec son compte | +| **parents(id_co_parent)** → `utilisateurs(id)` | **SET NULL** | Conserver le parent principal si co-parent disparaît | +| **enfants_parents(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Nettoyage liaisons N:N | +| **enfants_parents(id_enfant)** → `enfants(id)` | **CASCADE** | Idem | +| **dossiers(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Dossier n’a pas de sens sans parent | +| **dossiers(id_enfant)** → `enfants(id)` | **CASCADE** | Dossier n’a pas de sens sans enfant | +| **messages(id_dossier)** → `dossiers(id)` | **CASCADE** | Messages détruits avec le dossier | +| **messages(id_expediteur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique des échanges | +| **contrats(id_dossier)** → `dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé | +| **avenants_contrats(id_contrat)** → `contrats(id)` | **CASCADE** | Avenants détruits avec le contrat | +| **avenants_contrats(initie_par)** → `utilisateurs(id)` | **SET NULL** | Historiser l’avenant sans bloquer | +| **evenements(id_enfant)** → `enfants(id)` | **CASCADE** | Événements n’ont plus de sens | +| **evenements(id_am)** → `utilisateurs(id)` | **SET NULL** | Garder la trace même si AM supprimée | +| **evenements(id_parent)** → `parents(id_utilisateur)` | **SET NULL** | Garder la trace si parent supprimé | +| **evenements(cree_par)** → `utilisateurs(id)` | **SET NULL** | Conserver l’historique de création | +| **signalements_bugs(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé | +| **uploads(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans l’auteur | +| **notifications(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Notifications propres à l’utilisateur | +| **validations(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique de décision | + +> **ON UPDATE** : **NO ACTION** partout (les UUID ne changent pas). + +--- + +## 🔎 Détail par domaine + +### Utilisateurs & extensions +- `assistantes_maternelles.id_utilisateur` → **CASCADE** +- `parents.id_utilisateur` → **CASCADE** +- `parents.id_co_parent` → **SET NULL** (interdit d’être co-parent de soi-même via CHECK déjà posé) + +### Enfants & liaisons +- `enfants_parents.id_parent` → **CASCADE** +- `enfants_parents.id_enfant` → **CASCADE** + +### Dossiers & échanges +- `dossiers.id_parent` → **CASCADE** +- `dossiers.id_enfant` → **CASCADE** +- `messages.id_dossier` → **CASCADE** +- `messages.id_expediteur` → **SET NULL** + +### Contrats & avenants +- `contrats.id_dossier` → **CASCADE** (unique 1:1) +- `avenants_contrats.id_contrat` → **CASCADE** +- `avenants_contrats.initie_par` → **SET NULL** + +### Événements +- `evenements.id_enfant` → **CASCADE** +- `evenements.id_am` → **SET NULL** +- `evenements.id_parent` → **SET NULL** +- `evenements.cree_par` → **SET NULL** + +### Divers +- `signalements_bugs.id_utilisateur` → **SET NULL** +- `uploads.id_utilisateur` → **SET NULL** +- `notifications.id_utilisateur` → **CASCADE** +- `validations.id_utilisateur` → **SET NULL** + +--- + +## 🧪 Scénarios de test (exemples) + +1. **Suppression d’un parent** + - Supprimer `utilisateurs(id=parentX)` + - Attendu : `parents` (CASCADE), ses `dossiers` (CASCADE), `messages` liés aux `dossiers` (CASCADE) sont supprimés. + +2. **Suppression d’un co-parent** + - Supprimer `utilisateurs(id=coParentY)` + - Attendu : `parents.id_co_parent` passe à **NULL**, aucun dossier supprimé. + +3. **Suppression d’un utilisateur auteur de messages** + - Supprimer `utilisateurs(id=uZ)` + - Attendu : les lignes `messages` **restent**, `id_expediteur` devient **NULL**. + +4. **Suppression d’un enfant** + - Supprimer `enfants(id=childA)` + - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements` du childA (CASCADE). + +5. **Suppression d’un utilisateur AM** + - Supprimer `utilisateurs(id=amB)` + - Attendu : `evenements.id_am` devient **NULL** (historique conservé). + +--- + +## 🛠 Migrations associées + +Les ajustements sont implémentés dans : +- **`/bdd/migrations/04_fk_policies.sql`** + – redéfinition des contraintes FK avec les bonnes politiques (**DROP puis ADD CONSTRAINT**), de façon idempotente. + +--- + +## 📄 Notes & futures évolutions + +- **RGPD (Sprint 2)** : si vous activez le **soft delete** (`deleted_at`) côté tables métier, ces politiques restent valides (les suppressions logiques se gèrent au niveau applicatif). +- **Audit** : si vous voulez tracer les suppressions, ajoutez des triggers d’audit (voir ticket Sprint 2 – Audit log). +- **Performance** : chaque FK doit être **indexée côté enfant** (cf. `02_indexes.sql`). + +--- + +## ✅ Checklist de conformité + +- [ ] Toutes les FK listées existent dans la base +- [ ] Politique **ON DELETE** conforme au tableau ci-dessus +- [ ] **ON UPDATE = NO ACTION** partout +- [ ] Tests de suppression réalisés sur une base seedée +- [ ] `04_fk_policies.sql` appliqué sans erreur + +--- + +**Mainteneur** : Équipe BDD +**Dernière mise à jour** : Sprint 1 – Politique FK consolidée From 3ec0eb8d89fab65c14f0b767b1494108c99d403e Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:14:41 +0000 Subject: [PATCH 10/13] Supprimer FK_POLICIES.md --- FK_POLICIES.md | 142 ------------------------------------------------- 1 file changed, 142 deletions(-) delete mode 100644 FK_POLICIES.md diff --git a/FK_POLICIES.md b/FK_POLICIES.md deleted file mode 100644 index 3be63b3..0000000 --- a/FK_POLICIES.md +++ /dev/null @@ -1,142 +0,0 @@ - -# FK_POLICIES.md -**Politique des clés étrangères (ON DELETE / ON UPDATE)** – Sprint 1 - -## 🎯 Objectif -Documenter, de façon unique et partagée, les règles de suppression/mise à jour appliquées aux **clés étrangères** de la base P’titsPas pour : -- préserver l’**intégrité référentielle** ; -- conserver l’**historique** utile (messages, événements…) ; -- respecter les exigences **RGPD** (suppression en cascade lorsque pertinent). - -> Par défaut, **ON UPDATE = NO ACTION** (UUID immuables). -> Ce document couvre **ON DELETE** table par table. - ---- - -## 🧭 Principes généraux - -- **CASCADE** quand la donnée fille **n’a pas de sens sans le parent** - (ex. `dossiers` d’un parent, `avenants` d’un contrat). -- **SET NULL** quand on veut **préserver l’historique** mais que le référent peut disparaître - (ex. auteur d’un message supprimé, créateur d’un événement). -- **RESTRICT/NO ACTION** non utilisé ici pour éviter des blocages au nettoyage. - ---- - -## 📚 Récapitulatif rapide (matrice) - -| Table (colonne FK) → Référence | ON DELETE | Raison | -|---|---:|---| -| **assistantes_maternelles(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Profil AM supprimé avec son compte | -| **parents(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Extension parent supprimée avec son compte | -| **parents(id_co_parent)** → `utilisateurs(id)` | **SET NULL** | Conserver le parent principal si co-parent disparaît | -| **enfants_parents(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Nettoyage liaisons N:N | -| **enfants_parents(id_enfant)** → `enfants(id)` | **CASCADE** | Idem | -| **dossiers(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Dossier n’a pas de sens sans parent | -| **dossiers(id_enfant)** → `enfants(id)` | **CASCADE** | Dossier n’a pas de sens sans enfant | -| **messages(id_dossier)** → `dossiers(id)` | **CASCADE** | Messages détruits avec le dossier | -| **messages(id_expediteur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique des échanges | -| **contrats(id_dossier)** → `dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé | -| **avenants_contrats(id_contrat)** → `contrats(id)` | **CASCADE** | Avenants détruits avec le contrat | -| **avenants_contrats(initie_par)** → `utilisateurs(id)` | **SET NULL** | Historiser l’avenant sans bloquer | -| **evenements(id_enfant)** → `enfants(id)` | **CASCADE** | Événements n’ont plus de sens | -| **evenements(id_am)** → `utilisateurs(id)` | **SET NULL** | Garder la trace même si AM supprimée | -| **evenements(id_parent)** → `parents(id_utilisateur)` | **SET NULL** | Garder la trace si parent supprimé | -| **evenements(cree_par)** → `utilisateurs(id)` | **SET NULL** | Conserver l’historique de création | -| **signalements_bugs(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé | -| **uploads(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans l’auteur | -| **notifications(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Notifications propres à l’utilisateur | -| **validations(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique de décision | - -> **ON UPDATE** : **NO ACTION** partout (les UUID ne changent pas). - ---- - -## 🔎 Détail par domaine - -### Utilisateurs & extensions -- `assistantes_maternelles.id_utilisateur` → **CASCADE** -- `parents.id_utilisateur` → **CASCADE** -- `parents.id_co_parent` → **SET NULL** (interdit d’être co-parent de soi-même via CHECK déjà posé) - -### Enfants & liaisons -- `enfants_parents.id_parent` → **CASCADE** -- `enfants_parents.id_enfant` → **CASCADE** - -### Dossiers & échanges -- `dossiers.id_parent` → **CASCADE** -- `dossiers.id_enfant` → **CASCADE** -- `messages.id_dossier` → **CASCADE** -- `messages.id_expediteur` → **SET NULL** - -### Contrats & avenants -- `contrats.id_dossier` → **CASCADE** (unique 1:1) -- `avenants_contrats.id_contrat` → **CASCADE** -- `avenants_contrats.initie_par` → **SET NULL** - -### Événements -- `evenements.id_enfant` → **CASCADE** -- `evenements.id_am` → **SET NULL** -- `evenements.id_parent` → **SET NULL** -- `evenements.cree_par` → **SET NULL** - -### Divers -- `signalements_bugs.id_utilisateur` → **SET NULL** -- `uploads.id_utilisateur` → **SET NULL** -- `notifications.id_utilisateur` → **CASCADE** -- `validations.id_utilisateur` → **SET NULL** - ---- - -## 🧪 Scénarios de test (exemples) - -1. **Suppression d’un parent** - - Supprimer `utilisateurs(id=parentX)` - - Attendu : `parents` (CASCADE), ses `dossiers` (CASCADE), `messages` liés aux `dossiers` (CASCADE) sont supprimés. - -2. **Suppression d’un co-parent** - - Supprimer `utilisateurs(id=coParentY)` - - Attendu : `parents.id_co_parent` passe à **NULL**, aucun dossier supprimé. - -3. **Suppression d’un utilisateur auteur de messages** - - Supprimer `utilisateurs(id=uZ)` - - Attendu : les lignes `messages` **restent**, `id_expediteur` devient **NULL**. - -4. **Suppression d’un enfant** - - Supprimer `enfants(id=childA)` - - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements` du childA (CASCADE). - -5. **Suppression d’un utilisateur AM** - - Supprimer `utilisateurs(id=amB)` - - Attendu : `evenements.id_am` devient **NULL** (historique conservé). - ---- - -## 🛠 Migrations associées - -Les ajustements sont implémentés dans : -- **`/bdd/migrations/04_fk_policies.sql`** - – redéfinition des contraintes FK avec les bonnes politiques (**DROP puis ADD CONSTRAINT**), de façon idempotente. - ---- - -## 📄 Notes & futures évolutions - -- **RGPD (Sprint 2)** : si vous activez le **soft delete** (`deleted_at`) côté tables métier, ces politiques restent valides (les suppressions logiques se gèrent au niveau applicatif). -- **Audit** : si vous voulez tracer les suppressions, ajoutez des triggers d’audit (voir ticket Sprint 2 – Audit log). -- **Performance** : chaque FK doit être **indexée côté enfant** (cf. `02_indexes.sql`). - ---- - -## ✅ Checklist de conformité - -- [ ] Toutes les FK listées existent dans la base -- [ ] Politique **ON DELETE** conforme au tableau ci-dessus -- [ ] **ON UPDATE = NO ACTION** partout -- [ ] Tests de suppression réalisés sur une base seedée -- [ ] `04_fk_policies.sql` appliqué sans erreur - ---- - -**Mainteneur** : Équipe BDD -**Dernière mise à jour** : Sprint 1 – Politique FK consolidée From 53a31e352d8a2b7106e21364a67b375be0eaa3d6 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:19:56 +0000 Subject: [PATCH 11/13] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20v?= =?UTF-8?q?ers=20"tests/sql"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/sql/verify.sql | 205 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/sql/verify.sql diff --git a/tests/sql/verify.sql b/tests/sql/verify.sql new file mode 100644 index 0000000..05593dd --- /dev/null +++ b/tests/sql/verify.sql @@ -0,0 +1,205 @@ + +-- ============================================================ +-- verify.sql — Jeux de requêtes de vérification (Sprint 1) +-- Objectifs : +-- 1) Vérifier l'intégrité fonctionnelle (joins, données seedées) +-- 2) Détecter rapidement des problèmes d'index/perf (EXPLAIN) +-- 3) Servir de référence pour le back/front (requêtes typiques) +-- +-- Usage (Docker) : +-- docker compose exec -T postgres \ +-- psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} \ +-- -f /docker-entrypoint-initdb.d/tests/verify.sql +-- +-- Pré-requis : +-- - 01_init.sql +-- - 02_indexes.sql +-- - 03_checks.sql +-- - 04_fk_policies.sql +-- - 05_triggers.sql +-- - 02_seed.sql (pour résultats non vides) +-- ============================================================ + +\echo '=== 0) Version & horodatage ====================================' +SELECT version(); +SELECT NOW() AS executed_at; + +\echo '=== 1) Comptes & répartition par rôle ==========================' +SELECT role, COUNT(*) AS nb +FROM utilisateurs +GROUP BY role +ORDER BY nb DESC; + +\echo '=== 2) Utilisateurs en attente / acceptés / rejetés ============' +SELECT statut, COUNT(*) AS nb +FROM utilisateurs +GROUP BY statut +ORDER BY nb DESC; + +\echo '=== 3) Parents avec co-parents (NULL si pas de co-parent) ======' +SELECT p.id_utilisateur AS parent_id, + u.courriel AS parent_email, + p.id_co_parent, + uc.courriel AS co_parent_email +FROM parents p +JOIN utilisateurs u ON u.id = p.id_utilisateur +LEFT JOIN utilisateurs uc ON uc.id = p.id_co_parent +ORDER BY parent_email; + +\echo '=== 4) Enfants (statut, dates cohérentes) ======================' +SELECT id, prenom, nom, statut, date_naissance, date_prevue_naissance +FROM enfants +ORDER BY nom, prenom; + +\echo '=== 5) Liaison N:N parents_enfants =============================' +SELECT ep.id_parent, up.courriel AS parent_email, ep.id_enfant, e.prenom AS enfant +FROM enfants_parents ep +JOIN utilisateurs up ON up.id = ep.id_parent +JOIN enfants e ON e.id = ep.id_enfant +ORDER BY parent_email, enfant; + +\echo '=== 6) Dossiers (parent, enfant, statut) =======================' +SELECT d.id, up.courriel AS parent_email, e.prenom AS enfant, d.statut, d.budget +FROM dossiers d +JOIN utilisateurs up ON up.id = d.id_parent +JOIN enfants e ON e.id = d.id_enfant +ORDER BY d.cree_le DESC; + +\echo '=== 7) Messages par dossier (ordre chronologique) ==============' +SELECT m.id, m.id_dossier, m.id_expediteur, ue.courriel AS expediteur_email, m.contenu, m.cree_le +FROM messages m +LEFT JOIN utilisateurs ue ON ue.id = m.id_expediteur +ORDER BY m.id_dossier, m.cree_le; + +\echo '=== 8) Contrats 1:1 avec dossier + avenants ====================' +SELECT c.id AS contrat_id, c.id_dossier, c.statut, + COUNT(a.id) AS nb_avenants +FROM contrats c +LEFT JOIN avenants_contrats a ON a.id_contrat = c.id +GROUP BY c.id, c.id_dossier, c.statut +ORDER BY c.cree_le DESC; + +\echo '=== 9) Evénements par enfant (30 derniers jours) ==============' +SELECT ev.id, ev.type, ev.id_enfant, e.prenom AS enfant, ev.date_debut, ev.date_fin, ev.statut +FROM evenements ev +JOIN enfants e ON e.id = ev.id_enfant +WHERE ev.date_debut >= (NOW()::date - INTERVAL '30 days') +ORDER BY ev.date_debut DESC; + +\echo '=== 10) Uploads & notifications récentes =======================' +SELECT u.courriel, up.fichier_url, up.type_fichier, up.cree_le +FROM uploads up +LEFT JOIN utilisateurs u ON u.id = up.id_utilisateur +ORDER BY up.cree_le DESC; + +SELECT u.courriel, n.type, n.contenu, n.lu, n.cree_le +FROM notifications n +LEFT JOIN utilisateurs u ON u.id = n.id_utilisateur +ORDER BY n.cree_le DESC; + +\echo '=== 11) Validations (qui a validé quoi) ========================' +SELECT v.id, uu.courriel AS utilisateur_valide, + uv.courriel AS valide_par, v.statut, v.commentaire, v.cree_le +FROM validations v +LEFT JOIN utilisateurs uu ON uu.id = v.id_utilisateur +LEFT JOIN utilisateurs uv ON uv.id = v.valide_par +ORDER BY v.cree_le DESC; + +-- ============================================================ +-- Vérifications d'intégrité (requêtes de contrôle) +-- ============================================================ +\echo '=== 12) Orphelins potentiels (doivent renvoyer 0 ligne) =======' + +-- Messages orphelins (dossier manquant) +SELECT m.* +FROM messages m +LEFT JOIN dossiers d ON d.id = m.id_dossier +WHERE d.id IS NULL; + +-- Liaisons enfants_parents orphelines +SELECT ep.* +FROM enfants_parents ep +LEFT JOIN parents p ON p.id_utilisateur = ep.id_parent +LEFT JOIN enfants e ON e.id = ep.id_enfant +WHERE p.id_utilisateur IS NULL OR e.id IS NULL; + +-- Contrats sans dossier +SELECT c.* +FROM contrats c +LEFT JOIN dossiers d ON d.id = c.id_dossier +WHERE d.id IS NULL; + +-- Avenants sans contrat +SELECT a.* +FROM avenants_contrats a +LEFT JOIN contrats c ON c.id = a.id_contrat +WHERE c.id IS NULL; + +-- Evénements sans enfant +SELECT ev.* +FROM evenements ev +LEFT JOIN enfants e ON e.id = ev.id_enfant +WHERE e.id IS NULL; + +\echo '=== 13) Performance : EXPLAIN sur requêtes clés ===============' + +-- Messages par dossier (doit utiliser idx_messages_id_dossier_cree_le) +EXPLAIN ANALYZE +SELECT m.* +FROM messages m +WHERE m.id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd' +ORDER BY m.cree_le DESC +LIMIT 20; + +-- Evénements par enfant et période (idx_evenements_id_enfant_date_debut) +EXPLAIN ANALYZE +SELECT ev.* +FROM evenements ev +WHERE ev.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + AND ev.date_debut >= '2025-01-01'; + +-- Notifications non lues (idx_notifications_user_lu_cree_le) +EXPLAIN ANALYZE +SELECT n.* +FROM notifications n +WHERE n.id_utilisateur = '33333333-3333-3333-3333-333333333333' + AND n.lu = false +ORDER BY n.cree_le DESC +LIMIT 20; + +-- Dossiers par parent/enfant (idx_dossiers_id_parent/id_enfant/statut) +EXPLAIN ANALYZE +SELECT d.* +FROM dossiers d +WHERE d.id_parent = '33333333-3333-3333-3333-333333333333' + AND d.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +ORDER BY d.cree_le DESC; + +\echo '=== 14) JSONB : exemples de filtrage ===========================' +-- Recherche de dossiers où planning_souhaite contient midi=true un jour ouvré +-- (Index GIN recommandé si usage intensif : cf. 02_indexes.sql) +SELECT d.id, d.planning_souhaite +FROM dossiers d +WHERE d.planning_souhaite @> '{"lun_ven":{"midi":true}}'; + +-- Contrats : présence d’un créneau donné +SELECT c.id, c.planning +FROM contrats c +WHERE c.planning @> '{"lun_ven":{"17h-19h":true}}'; + +\echo '=== 15) Sanity check final ====================================' +-- Quelques totaux utiles +SELECT + (SELECT COUNT(*) FROM utilisateurs) AS nb_utilisateurs, + (SELECT COUNT(*) FROM parents) AS nb_parents, + (SELECT COUNT(*) FROM assistantes_maternelles) AS nb_am, + (SELECT COUNT(*) FROM enfants) AS nb_enfants, + (SELECT COUNT(*) FROM enfants_parents) AS nb_liens_parent_enfant, + (SELECT COUNT(*) FROM dossiers) AS nb_dossiers, + (SELECT COUNT(*) FROM messages) AS nb_messages, + (SELECT COUNT(*) FROM contrats) AS nb_contrats, + (SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants, + (SELECT COUNT(*) FROM evenements) AS nb_evenements, + (SELECT COUNT(*) FROM uploads) AS nb_uploads, + (SELECT COUNT(*) FROM notifications) AS nb_notifications, + (SELECT COUNT(*) FROM validations) AS nb_validations; From e8445bb3cec4c57099f84b7c187d411aacdc3194 Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:20:17 +0000 Subject: [PATCH 12/13] Supprimer tests/sql/verify.sql --- tests/sql/verify.sql | 205 ------------------------------------------- 1 file changed, 205 deletions(-) delete mode 100644 tests/sql/verify.sql diff --git a/tests/sql/verify.sql b/tests/sql/verify.sql deleted file mode 100644 index 05593dd..0000000 --- a/tests/sql/verify.sql +++ /dev/null @@ -1,205 +0,0 @@ - --- ============================================================ --- verify.sql — Jeux de requêtes de vérification (Sprint 1) --- Objectifs : --- 1) Vérifier l'intégrité fonctionnelle (joins, données seedées) --- 2) Détecter rapidement des problèmes d'index/perf (EXPLAIN) --- 3) Servir de référence pour le back/front (requêtes typiques) --- --- Usage (Docker) : --- docker compose exec -T postgres \ --- psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} \ --- -f /docker-entrypoint-initdb.d/tests/verify.sql --- --- Pré-requis : --- - 01_init.sql --- - 02_indexes.sql --- - 03_checks.sql --- - 04_fk_policies.sql --- - 05_triggers.sql --- - 02_seed.sql (pour résultats non vides) --- ============================================================ - -\echo '=== 0) Version & horodatage ====================================' -SELECT version(); -SELECT NOW() AS executed_at; - -\echo '=== 1) Comptes & répartition par rôle ==========================' -SELECT role, COUNT(*) AS nb -FROM utilisateurs -GROUP BY role -ORDER BY nb DESC; - -\echo '=== 2) Utilisateurs en attente / acceptés / rejetés ============' -SELECT statut, COUNT(*) AS nb -FROM utilisateurs -GROUP BY statut -ORDER BY nb DESC; - -\echo '=== 3) Parents avec co-parents (NULL si pas de co-parent) ======' -SELECT p.id_utilisateur AS parent_id, - u.courriel AS parent_email, - p.id_co_parent, - uc.courriel AS co_parent_email -FROM parents p -JOIN utilisateurs u ON u.id = p.id_utilisateur -LEFT JOIN utilisateurs uc ON uc.id = p.id_co_parent -ORDER BY parent_email; - -\echo '=== 4) Enfants (statut, dates cohérentes) ======================' -SELECT id, prenom, nom, statut, date_naissance, date_prevue_naissance -FROM enfants -ORDER BY nom, prenom; - -\echo '=== 5) Liaison N:N parents_enfants =============================' -SELECT ep.id_parent, up.courriel AS parent_email, ep.id_enfant, e.prenom AS enfant -FROM enfants_parents ep -JOIN utilisateurs up ON up.id = ep.id_parent -JOIN enfants e ON e.id = ep.id_enfant -ORDER BY parent_email, enfant; - -\echo '=== 6) Dossiers (parent, enfant, statut) =======================' -SELECT d.id, up.courriel AS parent_email, e.prenom AS enfant, d.statut, d.budget -FROM dossiers d -JOIN utilisateurs up ON up.id = d.id_parent -JOIN enfants e ON e.id = d.id_enfant -ORDER BY d.cree_le DESC; - -\echo '=== 7) Messages par dossier (ordre chronologique) ==============' -SELECT m.id, m.id_dossier, m.id_expediteur, ue.courriel AS expediteur_email, m.contenu, m.cree_le -FROM messages m -LEFT JOIN utilisateurs ue ON ue.id = m.id_expediteur -ORDER BY m.id_dossier, m.cree_le; - -\echo '=== 8) Contrats 1:1 avec dossier + avenants ====================' -SELECT c.id AS contrat_id, c.id_dossier, c.statut, - COUNT(a.id) AS nb_avenants -FROM contrats c -LEFT JOIN avenants_contrats a ON a.id_contrat = c.id -GROUP BY c.id, c.id_dossier, c.statut -ORDER BY c.cree_le DESC; - -\echo '=== 9) Evénements par enfant (30 derniers jours) ==============' -SELECT ev.id, ev.type, ev.id_enfant, e.prenom AS enfant, ev.date_debut, ev.date_fin, ev.statut -FROM evenements ev -JOIN enfants e ON e.id = ev.id_enfant -WHERE ev.date_debut >= (NOW()::date - INTERVAL '30 days') -ORDER BY ev.date_debut DESC; - -\echo '=== 10) Uploads & notifications récentes =======================' -SELECT u.courriel, up.fichier_url, up.type_fichier, up.cree_le -FROM uploads up -LEFT JOIN utilisateurs u ON u.id = up.id_utilisateur -ORDER BY up.cree_le DESC; - -SELECT u.courriel, n.type, n.contenu, n.lu, n.cree_le -FROM notifications n -LEFT JOIN utilisateurs u ON u.id = n.id_utilisateur -ORDER BY n.cree_le DESC; - -\echo '=== 11) Validations (qui a validé quoi) ========================' -SELECT v.id, uu.courriel AS utilisateur_valide, - uv.courriel AS valide_par, v.statut, v.commentaire, v.cree_le -FROM validations v -LEFT JOIN utilisateurs uu ON uu.id = v.id_utilisateur -LEFT JOIN utilisateurs uv ON uv.id = v.valide_par -ORDER BY v.cree_le DESC; - --- ============================================================ --- Vérifications d'intégrité (requêtes de contrôle) --- ============================================================ -\echo '=== 12) Orphelins potentiels (doivent renvoyer 0 ligne) =======' - --- Messages orphelins (dossier manquant) -SELECT m.* -FROM messages m -LEFT JOIN dossiers d ON d.id = m.id_dossier -WHERE d.id IS NULL; - --- Liaisons enfants_parents orphelines -SELECT ep.* -FROM enfants_parents ep -LEFT JOIN parents p ON p.id_utilisateur = ep.id_parent -LEFT JOIN enfants e ON e.id = ep.id_enfant -WHERE p.id_utilisateur IS NULL OR e.id IS NULL; - --- Contrats sans dossier -SELECT c.* -FROM contrats c -LEFT JOIN dossiers d ON d.id = c.id_dossier -WHERE d.id IS NULL; - --- Avenants sans contrat -SELECT a.* -FROM avenants_contrats a -LEFT JOIN contrats c ON c.id = a.id_contrat -WHERE c.id IS NULL; - --- Evénements sans enfant -SELECT ev.* -FROM evenements ev -LEFT JOIN enfants e ON e.id = ev.id_enfant -WHERE e.id IS NULL; - -\echo '=== 13) Performance : EXPLAIN sur requêtes clés ===============' - --- Messages par dossier (doit utiliser idx_messages_id_dossier_cree_le) -EXPLAIN ANALYZE -SELECT m.* -FROM messages m -WHERE m.id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd' -ORDER BY m.cree_le DESC -LIMIT 20; - --- Evénements par enfant et période (idx_evenements_id_enfant_date_debut) -EXPLAIN ANALYZE -SELECT ev.* -FROM evenements ev -WHERE ev.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' - AND ev.date_debut >= '2025-01-01'; - --- Notifications non lues (idx_notifications_user_lu_cree_le) -EXPLAIN ANALYZE -SELECT n.* -FROM notifications n -WHERE n.id_utilisateur = '33333333-3333-3333-3333-333333333333' - AND n.lu = false -ORDER BY n.cree_le DESC -LIMIT 20; - --- Dossiers par parent/enfant (idx_dossiers_id_parent/id_enfant/statut) -EXPLAIN ANALYZE -SELECT d.* -FROM dossiers d -WHERE d.id_parent = '33333333-3333-3333-3333-333333333333' - AND d.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' -ORDER BY d.cree_le DESC; - -\echo '=== 14) JSONB : exemples de filtrage ===========================' --- Recherche de dossiers où planning_souhaite contient midi=true un jour ouvré --- (Index GIN recommandé si usage intensif : cf. 02_indexes.sql) -SELECT d.id, d.planning_souhaite -FROM dossiers d -WHERE d.planning_souhaite @> '{"lun_ven":{"midi":true}}'; - --- Contrats : présence d’un créneau donné -SELECT c.id, c.planning -FROM contrats c -WHERE c.planning @> '{"lun_ven":{"17h-19h":true}}'; - -\echo '=== 15) Sanity check final ====================================' --- Quelques totaux utiles -SELECT - (SELECT COUNT(*) FROM utilisateurs) AS nb_utilisateurs, - (SELECT COUNT(*) FROM parents) AS nb_parents, - (SELECT COUNT(*) FROM assistantes_maternelles) AS nb_am, - (SELECT COUNT(*) FROM enfants) AS nb_enfants, - (SELECT COUNT(*) FROM enfants_parents) AS nb_liens_parent_enfant, - (SELECT COUNT(*) FROM dossiers) AS nb_dossiers, - (SELECT COUNT(*) FROM messages) AS nb_messages, - (SELECT COUNT(*) FROM contrats) AS nb_contrats, - (SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants, - (SELECT COUNT(*) FROM evenements) AS nb_evenements, - (SELECT COUNT(*) FROM uploads) AS nb_uploads, - (SELECT COUNT(*) FROM notifications) AS nb_notifications, - (SELECT COUNT(*) FROM validations) AS nb_validations; From 08a260a8985f3b1494254612f5ee5cdfbdf5f1ad Mon Sep 17 00:00:00 2001 From: vdorge Date: Mon, 25 Aug 2025 11:20:46 +0000 Subject: [PATCH 13/13] =?UTF-8?q?T=C3=A9l=C3=A9verser=20les=20fichiers=20v?= =?UTF-8?q?ers=20"tests/sql"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/sql/verify.sql | 205 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 tests/sql/verify.sql diff --git a/tests/sql/verify.sql b/tests/sql/verify.sql new file mode 100644 index 0000000..05593dd --- /dev/null +++ b/tests/sql/verify.sql @@ -0,0 +1,205 @@ + +-- ============================================================ +-- verify.sql — Jeux de requêtes de vérification (Sprint 1) +-- Objectifs : +-- 1) Vérifier l'intégrité fonctionnelle (joins, données seedées) +-- 2) Détecter rapidement des problèmes d'index/perf (EXPLAIN) +-- 3) Servir de référence pour le back/front (requêtes typiques) +-- +-- Usage (Docker) : +-- docker compose exec -T postgres \ +-- psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} \ +-- -f /docker-entrypoint-initdb.d/tests/verify.sql +-- +-- Pré-requis : +-- - 01_init.sql +-- - 02_indexes.sql +-- - 03_checks.sql +-- - 04_fk_policies.sql +-- - 05_triggers.sql +-- - 02_seed.sql (pour résultats non vides) +-- ============================================================ + +\echo '=== 0) Version & horodatage ====================================' +SELECT version(); +SELECT NOW() AS executed_at; + +\echo '=== 1) Comptes & répartition par rôle ==========================' +SELECT role, COUNT(*) AS nb +FROM utilisateurs +GROUP BY role +ORDER BY nb DESC; + +\echo '=== 2) Utilisateurs en attente / acceptés / rejetés ============' +SELECT statut, COUNT(*) AS nb +FROM utilisateurs +GROUP BY statut +ORDER BY nb DESC; + +\echo '=== 3) Parents avec co-parents (NULL si pas de co-parent) ======' +SELECT p.id_utilisateur AS parent_id, + u.courriel AS parent_email, + p.id_co_parent, + uc.courriel AS co_parent_email +FROM parents p +JOIN utilisateurs u ON u.id = p.id_utilisateur +LEFT JOIN utilisateurs uc ON uc.id = p.id_co_parent +ORDER BY parent_email; + +\echo '=== 4) Enfants (statut, dates cohérentes) ======================' +SELECT id, prenom, nom, statut, date_naissance, date_prevue_naissance +FROM enfants +ORDER BY nom, prenom; + +\echo '=== 5) Liaison N:N parents_enfants =============================' +SELECT ep.id_parent, up.courriel AS parent_email, ep.id_enfant, e.prenom AS enfant +FROM enfants_parents ep +JOIN utilisateurs up ON up.id = ep.id_parent +JOIN enfants e ON e.id = ep.id_enfant +ORDER BY parent_email, enfant; + +\echo '=== 6) Dossiers (parent, enfant, statut) =======================' +SELECT d.id, up.courriel AS parent_email, e.prenom AS enfant, d.statut, d.budget +FROM dossiers d +JOIN utilisateurs up ON up.id = d.id_parent +JOIN enfants e ON e.id = d.id_enfant +ORDER BY d.cree_le DESC; + +\echo '=== 7) Messages par dossier (ordre chronologique) ==============' +SELECT m.id, m.id_dossier, m.id_expediteur, ue.courriel AS expediteur_email, m.contenu, m.cree_le +FROM messages m +LEFT JOIN utilisateurs ue ON ue.id = m.id_expediteur +ORDER BY m.id_dossier, m.cree_le; + +\echo '=== 8) Contrats 1:1 avec dossier + avenants ====================' +SELECT c.id AS contrat_id, c.id_dossier, c.statut, + COUNT(a.id) AS nb_avenants +FROM contrats c +LEFT JOIN avenants_contrats a ON a.id_contrat = c.id +GROUP BY c.id, c.id_dossier, c.statut +ORDER BY c.cree_le DESC; + +\echo '=== 9) Evénements par enfant (30 derniers jours) ==============' +SELECT ev.id, ev.type, ev.id_enfant, e.prenom AS enfant, ev.date_debut, ev.date_fin, ev.statut +FROM evenements ev +JOIN enfants e ON e.id = ev.id_enfant +WHERE ev.date_debut >= (NOW()::date - INTERVAL '30 days') +ORDER BY ev.date_debut DESC; + +\echo '=== 10) Uploads & notifications récentes =======================' +SELECT u.courriel, up.fichier_url, up.type_fichier, up.cree_le +FROM uploads up +LEFT JOIN utilisateurs u ON u.id = up.id_utilisateur +ORDER BY up.cree_le DESC; + +SELECT u.courriel, n.type, n.contenu, n.lu, n.cree_le +FROM notifications n +LEFT JOIN utilisateurs u ON u.id = n.id_utilisateur +ORDER BY n.cree_le DESC; + +\echo '=== 11) Validations (qui a validé quoi) ========================' +SELECT v.id, uu.courriel AS utilisateur_valide, + uv.courriel AS valide_par, v.statut, v.commentaire, v.cree_le +FROM validations v +LEFT JOIN utilisateurs uu ON uu.id = v.id_utilisateur +LEFT JOIN utilisateurs uv ON uv.id = v.valide_par +ORDER BY v.cree_le DESC; + +-- ============================================================ +-- Vérifications d'intégrité (requêtes de contrôle) +-- ============================================================ +\echo '=== 12) Orphelins potentiels (doivent renvoyer 0 ligne) =======' + +-- Messages orphelins (dossier manquant) +SELECT m.* +FROM messages m +LEFT JOIN dossiers d ON d.id = m.id_dossier +WHERE d.id IS NULL; + +-- Liaisons enfants_parents orphelines +SELECT ep.* +FROM enfants_parents ep +LEFT JOIN parents p ON p.id_utilisateur = ep.id_parent +LEFT JOIN enfants e ON e.id = ep.id_enfant +WHERE p.id_utilisateur IS NULL OR e.id IS NULL; + +-- Contrats sans dossier +SELECT c.* +FROM contrats c +LEFT JOIN dossiers d ON d.id = c.id_dossier +WHERE d.id IS NULL; + +-- Avenants sans contrat +SELECT a.* +FROM avenants_contrats a +LEFT JOIN contrats c ON c.id = a.id_contrat +WHERE c.id IS NULL; + +-- Evénements sans enfant +SELECT ev.* +FROM evenements ev +LEFT JOIN enfants e ON e.id = ev.id_enfant +WHERE e.id IS NULL; + +\echo '=== 13) Performance : EXPLAIN sur requêtes clés ===============' + +-- Messages par dossier (doit utiliser idx_messages_id_dossier_cree_le) +EXPLAIN ANALYZE +SELECT m.* +FROM messages m +WHERE m.id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd' +ORDER BY m.cree_le DESC +LIMIT 20; + +-- Evénements par enfant et période (idx_evenements_id_enfant_date_debut) +EXPLAIN ANALYZE +SELECT ev.* +FROM evenements ev +WHERE ev.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + AND ev.date_debut >= '2025-01-01'; + +-- Notifications non lues (idx_notifications_user_lu_cree_le) +EXPLAIN ANALYZE +SELECT n.* +FROM notifications n +WHERE n.id_utilisateur = '33333333-3333-3333-3333-333333333333' + AND n.lu = false +ORDER BY n.cree_le DESC +LIMIT 20; + +-- Dossiers par parent/enfant (idx_dossiers_id_parent/id_enfant/statut) +EXPLAIN ANALYZE +SELECT d.* +FROM dossiers d +WHERE d.id_parent = '33333333-3333-3333-3333-333333333333' + AND d.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +ORDER BY d.cree_le DESC; + +\echo '=== 14) JSONB : exemples de filtrage ===========================' +-- Recherche de dossiers où planning_souhaite contient midi=true un jour ouvré +-- (Index GIN recommandé si usage intensif : cf. 02_indexes.sql) +SELECT d.id, d.planning_souhaite +FROM dossiers d +WHERE d.planning_souhaite @> '{"lun_ven":{"midi":true}}'; + +-- Contrats : présence d’un créneau donné +SELECT c.id, c.planning +FROM contrats c +WHERE c.planning @> '{"lun_ven":{"17h-19h":true}}'; + +\echo '=== 15) Sanity check final ====================================' +-- Quelques totaux utiles +SELECT + (SELECT COUNT(*) FROM utilisateurs) AS nb_utilisateurs, + (SELECT COUNT(*) FROM parents) AS nb_parents, + (SELECT COUNT(*) FROM assistantes_maternelles) AS nb_am, + (SELECT COUNT(*) FROM enfants) AS nb_enfants, + (SELECT COUNT(*) FROM enfants_parents) AS nb_liens_parent_enfant, + (SELECT COUNT(*) FROM dossiers) AS nb_dossiers, + (SELECT COUNT(*) FROM messages) AS nb_messages, + (SELECT COUNT(*) FROM contrats) AS nb_contrats, + (SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants, + (SELECT COUNT(*) FROM evenements) AS nb_evenements, + (SELECT COUNT(*) FROM uploads) AS nb_uploads, + (SELECT COUNT(*) FROM notifications) AS nb_notifications, + (SELECT COUNT(*) FROM validations) AS nb_validations;