diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +.env diff --git a/database/BDD.sql b/database/BDD.sql new file mode 100644 index 0000000..042ca39 --- /dev/null +++ b/database/BDD.sql @@ -0,0 +1,243 @@ +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ========================================================== +-- ENUMS +-- ========================================================== +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN + CREATE TYPE role_type AS ENUM ('parent', 'gestionnaire', 'super_admin', 'administrateur', 'assistante_maternelle'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN + CREATE TYPE genre_type AS ENUM ('H', 'F'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN + CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN + CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN + CREATE TYPE statut_dossier_type AS ENUM ('envoye','accepte','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN + CREATE TYPE statut_contrat_type AS ENUM ('brouillon','en_attente_signature','valide','resilie'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN + CREATE TYPE statut_avenant_type AS ENUM ('propose','accepte','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN + CREATE TYPE type_evenement_type AS ENUM ('absence_enfant','conge_am','conge_parent','arret_maladie_am','evenement_rpe'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN + CREATE TYPE statut_evenement_type AS ENUM ('propose','valide','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN + CREATE TYPE statut_validation_type AS ENUM ('en_attente','valide','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'situation_familiale_type') THEN + CREATE TYPE situation_familiale_type AS ENUM ('celibataire','marie','concubinage','pacse','separe','divorce','veuf','parent_isole'); + END IF; +END $$; + +-- ========================================================== +-- Table : utilisateurs +-- ========================================================== +CREATE TABLE utilisateurs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL UNIQUE, + CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + password TEXT NOT NULL, + prenom VARCHAR(100), + nom VARCHAR(100), + genre genre_type, + role role_type NOT NULL, + statut statut_utilisateur_type DEFAULT 'en_attente', + mobile VARCHAR(20), + telephone_fixe VARCHAR(20), + adresse TEXT, + date_naissance DATE, + photo_url TEXT, + consentement_photo BOOLEAN DEFAULT false, + date_consentement_photo TIMESTAMPTZ, + changement_mdp_obligatoire BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now(), + ville VARCHAR(150), + code_postal VARCHAR(10), + profession VARCHAR(150), + situation_familiale situation_familiale_type +); + +-- ========================================================== +-- Table : assistantes_maternelles +-- ========================================================== +CREATE TABLE assistantes_maternelles ( + id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, + numero_agrement VARCHAR(50), + date_agrement DATE, + nir_chiffre CHAR(15), + annee_experience SMALLINT, + specialite VARCHAR (100), + nb_max_enfants INT, + place_disponible INT, + biographie TEXT, + disponible BOOLEAN DEFAULT true +); + +-- ========================================================== +-- Table : parents +-- ========================================================== +CREATE TABLE parents ( + id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, + id_co_parent UUID REFERENCES utilisateurs(id) +); + +-- ========================================================== +-- Table : enfants +-- ========================================================== +CREATE TABLE enfants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + statut statut_enfant_type, + prenom VARCHAR(100), + nom VARCHAR(100), + genre genre_type, + date_naissance DATE, + date_prevue_naissance DATE, + photo_url TEXT, + consentement_photo BOOLEAN DEFAULT false, + date_consentement_photo TIMESTAMPTZ, + est_multiple BOOLEAN DEFAULT false +); + +-- ========================================================== +-- Table : enfants_parents +-- ========================================================== +CREATE TABLE enfants_parents ( + id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + PRIMARY KEY (id_parent, id_enfant) +); + +-- ========================================================== +-- Table : dossiers +-- ========================================================== +CREATE TABLE dossiers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + presentation TEXT, + type_contrat VARCHAR(50), + repas BOOLEAN DEFAULT false, + budget NUMERIC(10,2), + planning_souhaite JSONB, + statut statut_dossier_type DEFAULT 'envoye', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : messages +-- ========================================================== +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_dossier UUID REFERENCES dossiers(id) ON DELETE CASCADE, + id_expediteur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + contenu TEXT, + re_redige_par_ia BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : contrats +-- ========================================================== +CREATE TABLE contrats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_dossier UUID UNIQUE REFERENCES dossiers(id) ON DELETE CASCADE, + planning JSONB, + tarif_horaire NUMERIC(6,2), + indemnites_repas NUMERIC(6,2), + date_debut DATE, + statut statut_contrat_type DEFAULT 'brouillon', + signe_parent BOOLEAN DEFAULT false, + signe_am BOOLEAN DEFAULT false, + finalise_le TIMESTAMPTZ, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : avenants_contrats +-- ========================================================== +CREATE TABLE avenants_contrats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_contrat UUID REFERENCES contrats(id) ON DELETE CASCADE, + modifications JSONB, + initie_par UUID REFERENCES utilisateurs(id), + statut statut_avenant_type DEFAULT 'propose', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : evenements +-- ========================================================== +CREATE TABLE evenements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type type_evenement_type, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + id_am UUID REFERENCES utilisateurs(id), + id_parent UUID REFERENCES parents(id_utilisateur), + cree_par UUID REFERENCES utilisateurs(id), + date_debut TIMESTAMPTZ, + date_fin TIMESTAMPTZ, + commentaires TEXT, + statut statut_evenement_type DEFAULT 'propose', + delai_grace TIMESTAMPTZ, + urgent BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : signalements_bugs +-- ========================================================== +CREATE TABLE signalements_bugs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id), + description TEXT, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : uploads +-- ========================================================== +CREATE TABLE uploads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL, + fichier_url TEXT NOT NULL, + type VARCHAR(50), + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : notifications +-- ========================================================== +CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + contenu TEXT, + lu BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : validations +-- ========================================================== +CREATE TABLE validations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id), + type VARCHAR(50), + statut statut_validation_type DEFAULT 'en_attente', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); diff --git a/database/README.md b/database/README.md new file mode 100644 index 0000000..4ebcd90 --- /dev/null +++ b/database/README.md @@ -0,0 +1,80 @@ + + +# PtitsPas Ynov - Base de Données + +Ce projet contient la **base de données** pour l'application PtitsPas, avec scripts de migration, import de données, documentation et configuration Docker. + +--- + +## Prérequis + +- Docker Desktop (https://www.docker.com/products/docker-desktop/) +- Docker Compose + +--- + +## Structure du projet + +- `migrations/` : scripts SQL pour la création et l'import de la base +- `bdd/data_test/` : fichiers CSV pour l'import de données de test +- `docs/` : documentation métier et technique +- `seed/` : scripts de seed +- `tests/` : tests SQL +- `docker-compose.dev.yml` : configuration Docker pour le développement + +--- + +## Lancer la base de données en local + +Dans le terminal, depuis le dossier du projet : + +```bash +docker compose -f docker-compose.dev.yml up -d +``` + +Pour arrêter et supprimer les volumes : + +```bash +docker compose -f docker-compose.dev.yml down -v +``` + +--- + + +## Importation automatique des données de test + +Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`. + +Il n'est pas nécessaire de lancer manuellement le script d'import. + +--- + +## Accéder à pgAdmin4 + +### Via Docker (local) + +Ouvre ton navigateur sur : + +``` +http://localhost:8081 +``` + +**Email** : `admin@ptits-pas.fr` +**Mot de passe** : `admin123` + +**Mot de passse pour se connecter au server local** : `admin123` + + +## Conseils et bonnes pratiques + +- Vérifie la cohérence des identifiants dans les CSV avant import +- Pour modifier la structure, utilise les scripts de migration dans `migrations/` +- Pour ajouter des scripts d'automatisation, crée un dossier `scripts/` +- Documente les étapes spécifiques dans le README ou dans `docs/` + +--- + +## Contact + +Pour toute question ou contribution, consulte la documentation ou contacte l'équipe PtitsPas. + diff --git a/database/bdd/data_test/assistantes_maternelles.csv b/database/bdd/data_test/assistantes_maternelles.csv new file mode 100644 index 0000000..6403aba --- /dev/null +++ b/database/bdd/data_test/assistantes_maternelles.csv @@ -0,0 +1,3 @@ +"id_utilisateur","numero_agrement","nir_chiffre","nb_max_enfants","biographie","disponible","ville_residence","date_agrement","annee_experience","specialite","place_disponible" +"a1f3d5c7-8b9a-4e2f-9c1d-3b2a4f6e7d8c","AGR5678",,3,"Agrément 3 enfants - Spécialité 1-3 ans - 1 place disponible",True,,"2010-09-01",14,"1-3 ans",1 +"d9c2e3f4-5b6a-4c3d-9f1a-2e7b3c5d8a1f","AGR1234",,4,"Agrément 4 enfants - Spécialité bébés 0-18 mois - 2 places disponibles",True,,"2005-06-15",18,"Bébés 0-18 mois",2 diff --git a/database/bdd/data_test/contrats.csv b/database/bdd/data_test/contrats.csv new file mode 100644 index 0000000..ad9ff5d --- /dev/null +++ b/database/bdd/data_test/contrats.csv @@ -0,0 +1,2 @@ +"id","id_dossier","planning","tarif_horaire","indemnites_repas","date_debut","statut","signe_parent","signe_am","finalise_le","cree_le","modifie_le" +"f09c6ffa-4627-4aa8-b20b-829c2c828f0d","bb9c30a0-60b4-4832-9947-8a7d2366673d","{""jours"": [""Lundi"", ""Mardi"", ""Mercredi""]}","10.50","4.50","2024-09-01","brouillon",True,False,,"2025-09-09 11:05:47.933418+00","2025-09-09 11:05:47.933418+00" diff --git a/database/bdd/data_test/dossiers.csv b/database/bdd/data_test/dossiers.csv new file mode 100644 index 0000000..8088f6d --- /dev/null +++ b/database/bdd/data_test/dossiers.csv @@ -0,0 +1,2 @@ +"id","id_parent","id_enfant","presentation","type_contrat","repas","budget","planning_souhaite","statut","cree_le","modifie_le" +"bb9c30a0-60b4-4832-9947-8a7d2366673d","f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","Contrat test pour garde à temps plein","temps_plein",False,"1200.00",,"envoye","2025-09-09 10:58:28.718654+00","2025-09-09 10:58:28.718654+00" diff --git a/database/bdd/data_test/enfants.csv b/database/bdd/data_test/enfants.csv new file mode 100644 index 0000000..1cf2ab6 --- /dev/null +++ b/database/bdd/data_test/enfants.csv @@ -0,0 +1,10 @@ +"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo","est_multiple" +"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","actif","Emma","Dupont","F","2020-06-01",,,False,,False +"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","actif",,,,"2020-01-01","2025-01-01",,False,,False +"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","actif","Emma","Martin",,"2023-02-15",,,False,,False +"e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","actif","Noah","Martin",,"2023-02-15",,,False,,False +"e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e","actif","Léa","Martin",,"2023-02-15",,,False,,False +"e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","actif","Chloé","Rousseau",,"2022-04-20",,,False,,False +"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","actif","Hugo","Rousseau",,"2024-03-10",,,False,,False +"e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","actif","Maxime","Lecomte",,"2023-04-15",,,False,,False +"edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,,False diff --git a/database/bdd/data_test/enfants_parents.csv b/database/bdd/data_test/enfants_parents.csv new file mode 100644 index 0000000..910c800 --- /dev/null +++ b/database/bdd/data_test/enfants_parents.csv @@ -0,0 +1,9 @@ +"id_parent","id_enfant" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e" +"d3e5f7a9-1c2b-4d6e-8f3a-2b4c6d8e9f1a","e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b" +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f" +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a" diff --git a/database/bdd/data_test/evenements.csv b/database/bdd/data_test/evenements.csv new file mode 100644 index 0000000..1a63298 --- /dev/null +++ b/database/bdd/data_test/evenements.csv @@ -0,0 +1,2 @@ +"id","type","id_enfant","id_am","id_parent","cree_par","date_debut","date_fin","commentaires","statut","delai_grace","urgent","cree_le","modifie_le" +"9f09425c-a374-4c9f-b3b1-be7258b60cd3","absence_enfant","5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","62de8e71-8082-4383-a3a2-4277bdd07516",,"bbcae75c-0e60-4b84-b281-079dba23b44e","2024-11-10 00:00:00+00","2024-11-11 00:00:00+00","Enfant malade","propose",,True,"2025-09-02 12:55:43.781467+00","2025-09-05 14:39:38.390126+00" diff --git a/database/bdd/data_test/notifications.csv b/database/bdd/data_test/notifications.csv new file mode 100644 index 0000000..7a177c1 --- /dev/null +++ b/database/bdd/data_test/notifications.csv @@ -0,0 +1,2 @@ +"id","id_utilisateur","contenu","lu","cree_le" +"71e90c37-f2cb-4aff-ad34-1c728f620afb","bbcae75c-0e60-4b84-b281-079dba23b44e","Votre dossier a été accepté",False,"2025-09-02 12:57:42.845264+00" diff --git a/database/bdd/data_test/parents.csv b/database/bdd/data_test/parents.csv new file mode 100644 index 0000000..c3d98f7 --- /dev/null +++ b/database/bdd/data_test/parents.csv @@ -0,0 +1,5 @@ +"id_utilisateur","id_co_parent" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f", +"d3e5f7a9-1c2b-4d6e-8f3a-2b4c6d8e9f1a", +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e" diff --git a/database/bdd/data_test/uploads.csv b/database/bdd/data_test/uploads.csv new file mode 100644 index 0000000..25709b6 --- /dev/null +++ b/database/bdd/data_test/uploads.csv @@ -0,0 +1,2 @@ +"id","id_utilisateur","fichier_url","type","cree_le" +"db1eb36d-5f30-4027-b529-1d972b79180a","bbcae75c-0e60-4b84-b281-079dba23b44e","https://placeholder.local/file","image","2025-09-02 12:57:35.140078+00" diff --git a/database/bdd/data_test/utilisateurs.csv b/database/bdd/data_test/utilisateurs.csv new file mode 100644 index 0000000..c4539ce --- /dev/null +++ b/database/bdd/data_test/utilisateurs.csv @@ -0,0 +1,12 @@ +"id","email","password","prenom","nom","genre","role","statut","telephone","adresse","photo_url","consentement_photo","date_consentement_photo","changement_mdp_obligatoire","cree_le","modifie_le","ville","code_postal","mobile","telephone_fixe","profession","situation_familiale","date_naissance" +"62de8e71-8082-4383-a3a2-4277bdd07516","am1@example.com","hash125","Claire","Martin","F","assistante_maternelle","actif","0609091011","5 place Bellecour",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Lyon","69002",,,,, +"76c40571-5da6-4d27-8e07-303185875b36","gest1@example.com","hash126","Paul","Lemoine","H","gestionnaire","actif","0612131415","10 rue Victor Hugo",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Paris","75002",,,,, +"9bc30373-91a4-45ed-9c05-ffe1651ad906","coparent@example.com","hash124","Marc","Durand","H","parent","actif","0605060708","45 avenue de Lyon",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Lyon","69000",,,,, +"a1f3d5c7-8b9a-4e2f-9c1d-3b2a4f6e7d8c","fatima.elmansouri@ptits-pas.fr","password","Fatima","El Mansouri",,"assistante_maternelle","actif",,"17 Boulevard Aristide Briand",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 75 45 67 89","01 39 98 78 90","Assistante maternelle","marie","1975-11-12" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","julien.rousseau@ptits-pas.fr","password","Julien","Rousseau",,"parent","actif",,"14 Rue Pasteur",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 56 67 78 89","01 39 98 01 23","Commercial","divorce","1985-08-29" +"bbcae75c-0e60-4b84-b281-079dba23b44e","parent1@example.com","hash123","Alice","Dupont","F","parent","actif","0601020304","12 rue de Paris",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Paris","75001",,,,, +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","claire.martin@ptits-pas.fr","password","Claire","Martin",,"parent","actif",,"5 Avenue du Général de Gaulle",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 89 56 78 90","01 39 98 89 01","Infirmière","marie","1990-04-03" +"d3e5f7a9-1c2b-4d6e-8f3a-2b4c6d8e9f1a","david.lecomte@ptits-pas.fr","password","David","Lecomte",,"parent","actif",,"31 Rue Émile Zola",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 45 56 67 78","01 39 98 12 34","Développeur web","celibataire","1992-10-07" +"d9c2e3f4-5b6a-4c3d-9f1a-2e7b3c5d8a1f","marie.dubois@ptits-pas.fr","password","Marie","Dubois",,"assistante_maternelle","actif",,"25 Rue de la République",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 12:44:51.654512+00","Bezons","95870","06 96 34 56 78","01 39 98 67 89","Assistante maternelle","marie","1980-06-08" +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","amelie.durand@ptits-pas.fr","password","Amélie","Durand",,"parent","actif",,"23 Rue Victor Hugo",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 67 78 89 90","01 39 98 90 12","Comptable","divorce","1987-12-14" +"f3b1a2d4-1c7e-4c2f-8b1a-9d3a8e2f5b6c","sophie.bernard@ptits-pas.fr","password","Sophie","Bernard",,"administrateur","actif",,"12 Avenue Gabriel Péri",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 78 12 34 56","01 39 98 45 67","Responsable administrative","marie","1978-03-15" diff --git a/database/bdd/data_test/validations.csv b/database/bdd/data_test/validations.csv new file mode 100644 index 0000000..a93c5da --- /dev/null +++ b/database/bdd/data_test/validations.csv @@ -0,0 +1,4 @@ +"id","id_utilisateur","type","statut","cree_le","modifie_le","valide_par","commentaire" +"8ec99565-e8c2-469f-9641-01b99b8281eb","62de8e71-8082-4383-a3a2-4277bdd07516","identité","valide","2025-09-02 12:57:49.846424+00","2025-09-02 12:57:49.846424+00",, +"be1c4779-341b-436d-b17e-8bc486d22ef8","62de8e71-8082-4383-a3a2-4277bdd07516",,"valide","2025-09-09 14:47:01.963573+00","2025-09-09 14:47:01.963573+00",,"Contrôle OK" +"fcc45701-5708-4368-b467-b95ddb7e1580","d9c2e3f4-5b6a-4c3d-9f1a-2e7b3c5d8a1f",,"valide","2025-09-09 14:52:47.339858+00","2025-09-09 14:52:47.339858+00","76c40571-5da6-4d27-8e07-303185875b36","Contrôle OK" diff --git a/database/bdd/schemas/bdd_simple.png b/database/bdd/schemas/bdd_simple.png new file mode 100644 index 0000000..8983acc Binary files /dev/null and b/database/bdd/schemas/bdd_simple.png differ diff --git a/database/bdd/schemas/bdd_type.png b/database/bdd/schemas/bdd_type.png new file mode 100644 index 0000000..69af700 Binary files /dev/null and b/database/bdd/schemas/bdd_type.png differ diff --git a/database/docker-compose.dev.yml b/database/docker-compose.dev.yml new file mode 100644 index 0000000..6c608be --- /dev/null +++ b/database/docker-compose.dev.yml @@ -0,0 +1,46 @@ +# Docker Compose pour développement local de la base de données uniquement +# Usage: docker compose -f docker-compose.dev.yml up -d + +services: + # Base de données PostgreSQL + postgres: + image: postgres:17 + container_name: ptitspas-postgres-standalone + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123} + POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db} + ports: + - "5433:5432" + volumes: + - ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql + - ./migrations/07_import.sql:/docker-entrypoint-initdb.d/07_import.sql + - ./bdd/data_test:/bdd/data_test + - postgres_standalone_data:/var/lib/postgresql/data + networks: + - ptitspas_dev + + # Interface d'administration DB + pgadmin: + image: dpage/pgadmin4:9.8 + container_name: ptitspas-pgadmin-standalone + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@ptits-pas.fr} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123} + ports: + - "8081:80" + depends_on: + - postgres + networks: + - ptitspas_dev + volumes: + - ./pgadmin/servers.json:/pgadmin4/servers.json + +volumes: + postgres_standalone_data: + +networks: + ptitspas_dev: + driver: bridge diff --git a/database/docker-compose.yml b/database/docker-compose.yml new file mode 100644 index 0000000..d6babdc --- /dev/null +++ b/database/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres:17 + container_name: ynov-postgres + restart: unless-stopped + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: ptitpas_db + volumes: + - ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql + - postgres_data:/var/lib/postgresql/data + networks: + - ynov_network + + pgadmin: + image: dpage/pgadmin4 + container_name: ynov-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@ptits-pas.fr + PGADMIN_DEFAULT_PASSWORD: admin123 + depends_on: + - db + labels: + - "traefik.enable=true" + - "traefik.http.routers.ynov-pgadmin.rule=Host(\"ynov-pgadmin.ptits-pas.fr\")" + - "traefik.http.routers.ynov-pgadmin.entrypoints=websecure" + - "traefik.http.routers.ynov-pgadmin.tls.certresolver=leresolver" + - "traefik.http.services.ynov-pgadmin.loadbalancer.server.port=80" + networks: + - ynov_network + - proxy_network + +volumes: + postgres_data: + +networks: + ynov_network: + driver: bridge + proxy_network: + external: true diff --git a/database/docs/ENUMS.md b/database/docs/ENUMS.md new file mode 100644 index 0000000..c24e516 --- /dev/null +++ b/database/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) diff --git a/database/docs/FK_POLICIES.md b/database/docs/FK_POLICIES.md new file mode 100644 index 0000000..3be63b3 --- /dev/null +++ b/database/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 diff --git a/database/migrations/01_init.sql b/database/migrations/01_init.sql new file mode 100644 index 0000000..a2076a4 --- /dev/null +++ b/database/migrations/01_init.sql @@ -0,0 +1,277 @@ +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ========================================================== +-- ENUMS +-- ========================================================== +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN + CREATE TYPE role_type AS ENUM ('parent', 'gestionnaire', 'super_admin', 'assistante_maternelle','administrateur'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN + CREATE TYPE genre_type AS ENUM ('H', 'F', 'Autre'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN + CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN + CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN + CREATE TYPE statut_dossier_type AS ENUM ('envoye','accepte','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN + CREATE TYPE statut_contrat_type AS ENUM ('brouillon','en_attente_signature','valide','resilie'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN + CREATE TYPE statut_avenant_type AS ENUM ('propose','accepte','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN + CREATE TYPE type_evenement_type AS ENUM ('absence_enfant','conge_am','conge_parent','arret_maladie_am','evenement_rpe'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN + CREATE TYPE statut_evenement_type AS ENUM ('propose','valide','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN + CREATE TYPE statut_validation_type AS ENUM ('en_attente','valide','refuse'); + END IF; +END $$; + +-- ========================================================== +-- Table : utilisateurs +-- ========================================================== +CREATE TABLE utilisateurs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL UNIQUE, + CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + password TEXT NOT NULL, + prenom VARCHAR(100), + nom VARCHAR(100), + genre genre_type, + role role_type NOT NULL, + statut statut_utilisateur_type DEFAULT 'en_attente', + telephone VARCHAR(20), + adresse TEXT, + photo_url TEXT, + consentement_photo BOOLEAN DEFAULT false, + date_consentement_photo TIMESTAMPTZ, + changement_mdp_obligatoire BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now(), + ville VARCHAR(150), + code_postal VARCHAR(10), + mobile VARCHAR(20), + telephone_fixe VARCHAR(20), + profession VARCHAR(150), + situation_familiale VARCHAR(50), + date_naissance DATE +); + +-- ========================================================== +-- Table : assistantes_maternelles +-- ========================================================== +CREATE TABLE assistantes_maternelles ( + id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, + numero_agrement VARCHAR(50), + nir_chiffre CHAR(15), + nb_max_enfants INT, + biographie TEXT, + disponible BOOLEAN DEFAULT true, + ville_residence VARCHAR(100), + date_agrement DATE, + annee_experience SMALLINT, + specialite VARCHAR(100), + place_disponible INT +); + +-- ========================================================== +-- Table : parentschange les donnée de init +-- ========================================================== +CREATE TABLE parents ( + id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, + id_co_parent UUID REFERENCES utilisateurs(id) +); + +-- ========================================================== +-- Table : enfants +-- ========================================================== +CREATE TABLE enfants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + statut statut_enfant_type, + prenom VARCHAR(100), + nom VARCHAR(100), + genre genre_type, + date_naissance DATE, + date_prevue_naissance DATE, + photo_url TEXT, + consentement_photo BOOLEAN DEFAULT false, + date_consentement_photo TIMESTAMPTZ, + est_multiple BOOLEAN DEFAULT false +); + +-- ========================================================== +-- Table : enfants_parents +-- ========================================================== +CREATE TABLE enfants_parents ( + id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + PRIMARY KEY (id_parent, id_enfant) +); + +-- ========================================================== +-- Table : dossiers +-- ========================================================== +CREATE TABLE dossiers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + presentation TEXT, + type_contrat VARCHAR(50), + repas BOOLEAN DEFAULT false, + budget NUMERIC(10,2), + planning_souhaite JSONB, + statut statut_dossier_type DEFAULT 'envoye', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : messages +-- ========================================================== +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_dossier UUID REFERENCES dossiers(id) ON DELETE CASCADE, + id_expediteur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + contenu TEXT, + re_redige_par_ia BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : contrats +-- ========================================================== +CREATE TABLE contrats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_dossier UUID UNIQUE REFERENCES dossiers(id) ON DELETE CASCADE, + planning JSONB, + tarif_horaire NUMERIC(6,2), + indemnites_repas NUMERIC(6,2), + date_debut DATE, + statut statut_contrat_type DEFAULT 'brouillon', + signe_parent BOOLEAN DEFAULT false, + signe_am BOOLEAN DEFAULT false, + finalise_le TIMESTAMPTZ, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : avenants_contrats +-- ========================================================== +CREATE TABLE avenants_contrats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_contrat UUID REFERENCES contrats(id) ON DELETE CASCADE, + modifications JSONB, + initie_par UUID REFERENCES utilisateurs(id), + statut statut_avenant_type DEFAULT 'propose', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : evenements +-- ========================================================== +CREATE TABLE evenements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type type_evenement_type, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + id_am UUID REFERENCES utilisateurs(id), + id_parent UUID REFERENCES parents(id_utilisateur), + cree_par UUID REFERENCES utilisateurs(id), + date_debut TIMESTAMPTZ, + date_fin TIMESTAMPTZ, + commentaires TEXT, + statut statut_evenement_type DEFAULT 'propose', + delai_grace TIMESTAMPTZ, + urgent BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : signalements_bugs +-- ========================================================== +CREATE TABLE signalements_bugs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id), + description TEXT, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : uploads +-- ========================================================== +CREATE TABLE uploads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL, + fichier_url TEXT NOT NULL, + type VARCHAR(50), + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : notifications +-- ========================================================== +CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + contenu TEXT, + lu BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : validations +-- ========================================================== +CREATE TABLE validations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id), + type VARCHAR(50), + statut statut_validation_type DEFAULT 'en_attente', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now(), + valide_par UUID REFERENCES utilisateurs(id), + commentaire TEXT +); + + +-- ========================================================== +-- Initialisation d'un administrateur par défaut +-- ========================================================== + +-- ========================================================== +-- SEED: Super Administrateur par défaut +-- ========================================================== +-- Mot de passe: 4dm1n1strateur +INSERT INTO utilisateurs ( + id, + email, + password, + prenom, + nom, + role, + statut, + cree_le, + modifie_le +) +VALUES ( + gen_random_uuid(), + 'admin@ptits-pas.fr', + '$2b$12$Fo5ly1YlTj3O6lXf.IUgoeUqEebBGpmoM5zLbzZx.CueorSE7z2E2', + 'Admin', + 'Système', + 'super_admin', + 'actif', + now(), + now() +) +ON CONFLICT (email) DO NOTHING; diff --git a/database/migrations/02_indexes.sql b/database/migrations/02_indexes.sql new file mode 100644 index 0000000..bd912c4 --- /dev/null +++ b/database/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); diff --git a/database/migrations/03_checks.sql b/database/migrations/03_checks.sql new file mode 100644 index 0000000..9d1e159 --- /dev/null +++ b/database/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) diff --git a/database/migrations/04_fk_policies.sql b/database/migrations/04_fk_policies.sql new file mode 100644 index 0000000..e1144f1 --- /dev/null +++ b/database/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. diff --git a/database/migrations/05_triggers.sql b/database/migrations/05_triggers.sql new file mode 100644 index 0000000..5d4b489 --- /dev/null +++ b/database/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(); diff --git a/database/migrations/06_validations_enrich.sql b/database/migrations/06_validations_enrich.sql new file mode 100644 index 0000000..65bacff --- /dev/null +++ b/database/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; diff --git a/database/migrations/07_import.sql b/database/migrations/07_import.sql new file mode 100644 index 0000000..957e8af --- /dev/null +++ b/database/migrations/07_import.sql @@ -0,0 +1,42 @@ +-- Script d'importation des données CSV dans la base Postgres du docker dev +-- À exécuter dans le conteneur ou via psql connecté à la base +-- psql -U admin -d ptitpas_db -f /docker-entrypoint-initdb.d/07_import.sql +-- Exemple d'utilisation : + + +-- Import utilisateurs +\copy utilisateurs FROM 'bdd/data_test/utilisateurs.csv' DELIMITER ',' CSV HEADER; + +-- Import assistantes_maternelles +\copy assistantes_maternelles FROM 'bdd/data_test/assistantes_maternelles.csv' DELIMITER ',' CSV HEADER; + +-- Import parents +\copy parents FROM 'bdd/data_test/parents.csv' DELIMITER ',' CSV HEADER; + +-- Import enfants +\copy enfants FROM 'bdd/data_test/enfants.csv' DELIMITER ',' CSV HEADER; + +-- Import enfants_parents +\copy enfants_parents FROM 'bdd/data_test/enfants_parents.csv' DELIMITER ',' CSV HEADER; + +-- Import dossiers +\copy dossiers FROM 'bdd/data_test/dossiers.csv' DELIMITER ',' CSV HEADER; + +-- Import contrats +\copy contrats FROM 'bdd/data_test/contrats.csv' DELIMITER ',' CSV HEADER; + +-- Import validations +\copy validations FROM 'bdd/data_test/validations.csv' DELIMITER ',' CSV HEADER; + +-- Import notifications +\copy notifications FROM 'bdd/data_test/notifications.csv' DELIMITER ',' CSV HEADER; + +-- Import uploads +\copy uploads FROM 'bdd/data_test/uploads.csv' DELIMITER ',' CSV HEADER; + +-- Import evenements +\copy evenements FROM 'bdd/data_test/evenements.csv' DELIMITER ',' CSV HEADER; + +-- Remarque : +-- Les chemins doivent être accessibles depuis le conteneur Docker (monter le dossier si besoin) +-- Adapter l'utilisateur, la base et le chemin si nécessaire diff --git a/database/pgadmin/servers.json b/database/pgadmin/servers.json new file mode 100644 index 0000000..b9f8a5e --- /dev/null +++ b/database/pgadmin/servers.json @@ -0,0 +1,13 @@ +{ +"Servers": { + "1": { + "Name": "Postgres Dev", + "Group": "Local", + "Host": "postgres", + "Port": 5432, + "Username": "admin", + "SSLMode": "prefer", + "MaintenanceDB": "ptitpas_db" + } + } +} diff --git a/database/seed/02_seed.sql b/database/seed/02_seed.sql new file mode 100644 index 0000000..c8ef3b4 --- /dev/null +++ b/database/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; diff --git a/database/tests/sql/verify.sql b/database/tests/sql/verify.sql new file mode 100644 index 0000000..05593dd --- /dev/null +++ b/database/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;