This commit is contained in:
ynov-deploy 2025-08-26 12:03:35 +02:00
commit ef2622f8ed
9 changed files with 1415 additions and 0 deletions

157
docs/ENUMS.md Normal file
View File

@ -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 PtitsPas, 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 lOpenAPI / 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 daccord) |
| `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 lassistante 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 dun événement proche |
---
**Mainteneur** : Équipe BDD
**Dernière mise à jour** : Sprint 1 — Ticket 9 (ENUMS)

142
docs/FK_POLICIES.md Normal file
View File

@ -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 PtitsPas 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 **na pas de sens sans le parent**
(ex. `dossiers` dun parent, `avenants` dun contrat).
- **SET NULL** quand on veut **préserver lhistorique** mais que le référent peut disparaître
(ex. auteur dun message supprimé, créateur dun é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 na pas de sens sans parent |
| **dossiers(id_enfant)**`enfants(id)` | **CASCADE** | Dossier na 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 lhistorique 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 lavenant sans bloquer |
| **evenements(id_enfant)**`enfants(id)` | **CASCADE** | Événements nont 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 lhistorique 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 lauteur |
| **notifications(id_utilisateur)**`utilisateurs(id)` | **CASCADE** | Notifications propres à lutilisateur |
| **validations(id_utilisateur)**`utilisateurs(id)` | **SET NULL** | Garder lhistorique 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 dun parent**
- Supprimer `utilisateurs(id=parentX)`
- Attendu : `parents` (CASCADE), ses `dossiers` (CASCADE), `messages` liés aux `dossiers` (CASCADE) sont supprimés.
2. **Suppression dun co-parent**
- Supprimer `utilisateurs(id=coParentY)`
- Attendu : `parents.id_co_parent` passe à **NULL**, aucun dossier supprimé.
3. **Suppression dun utilisateur auteur de messages**
- Supprimer `utilisateurs(id=uZ)`
- Attendu : les lignes `messages` **restent**, `id_expediteur` devient **NULL**.
4. **Suppression dun enfant**
- Supprimer `enfants(id=childA)`
- Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements` du childA (CASCADE).
5. **Suppression dun 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 daudit (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

157
migrations/02_indexes.sql Normal file
View File

@ -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);

140
migrations/03_checks.sql Normal file
View File

@ -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 : aujourdhui 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)

View File

@ -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.

150
migrations/05_triggers.sql Normal file
View File

@ -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();

View File

@ -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;

221
seed/02_seed.sql Normal file
View File

@ -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 lAM 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;

205
tests/sql/verify.sql Normal file
View File

@ -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 dun 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;