# CLAUDE.md — Gestion des bénévoles Application web de gestion des bénévoles pour un festival associatif (50–200 bénévoles, 4 jours montage/démontage inclus). Interface en français, code en anglais. Ce fichier est le contrat de travail. Il est tenu à jour à chaque étape. --- ## 1. Vocabulaire du domaine (glossaire FR ↔ EN) Le métier parle français, le code et la base parlent anglais. Toute entité, colonne, route et variable utilise la colonne « Code / base ». | Métier (brief) | Code / base | |------------------------------------|-----------------------------------------------| | Édition | `Edition` | | Bénévole | `Volunteer` | | Compétence | `Skill` | | Poste | `Position` | | Référent d'un poste | `Position.lead_account_id` → `Account` | | Créneau | `Shift` | | Disponibilité | `Availability` | | Affectation | `Assignment` | | Compte (utilisateur connecté) | `Account` | | Découpage de journée (paramétrable)| `DayPart` (`code`, `label`, `start_time`, `end_time`, `order`) | | Lien magique | `MagicLinkToken` | Énumérations : | Métier | Code | |------------------------------------|-----------------------------------------------| | Statut d'affectation | `PROPOSED` / `CONFIRMED` / `DECLINED` / `CANCELLED` | | Origine d'affectation | `SOLVER` / `MANUAL` | | Verrouillée | `Assignment.locked: bool` | | Rôle | `COORDINATOR` / `LEAD` / `VOLUNTEER` | | Disponibilité (tri-état) | `AVAILABLE` / `IF_NEEDED` / `UNAVAILABLE` | | Catégorie de poste | `BAR` / `TICKETING` / `WELCOME` / `PARKING` / `CLEANING` / `CATERING` / `SETUP` / `STAGE` / `OTHER` | ### Distinction critique (ne jamais confondre, ni en base ni dans l'UI) - **`Availability`** = une *déclaration* du bénévole : « je peux venir tel jour, telle demi-journée ». - **`Assignment`** = une *décision* de l'organisation : « tu tiens le bar samedi 18 h – 22 h ». Deux tables distinctes, deux écrans distincts, deux vocabulaires distincts. --- ## 2. Modèle de domaine Défini dans `backend/app/models/` (`enums.py`, `base.py`, `domain.py`). Clés primaires `UUID`. Champs d'audit `created_at` / `updated_at` sur toutes les tables sauf les tables d'association. - **`Edition`** : `year`, `name` (opt.), `start_date`, `end_date`, `retention_days` (durée de rétention des données nominatives en jours, `NULL` = pas de purge). - **`DayPart`** : découpage d'une journée propre à l'édition (nombre et bornes libres, paramétrable par la coordination). `code`, `label`, `start_time`, `end_time` (`end_time < start_time` ⇒ passe minuit), `order`. Unique (`edition_id`, `code`). - **`Volunteer`** : `email` (unique **par édition**, nullable pour les bénévoles créés à la main — plusieurs `NULL` autorisés), `first_name`, `last_name`, `phone`, `tshirt_size`, `diet`, `comment`. Pas de date de naissance. - **`Skill`** : référentiel par édition (`code`, `label`). N-N `Volunteer` (`volunteer_skill`). N-N `Shift` (`shift_required_skill`). - **`Position`** : `name`, `location`, `category` (`PositionCategory`), `lead_account_id` (→ `Account`, `SET NULL` à la suppression du compte). - **`Shift`** : `position_id`, `starts_at`, `ends_at` (peut finir le lendemain), `min_staff`, `ideal_staff`, compétences requises (N-N `Skill`). - **`Availability`** : `volunteer_id`, `date`, `day_part_id`, `status` (`AvailabilityStatus`). Unique (`volunteer_id`, `date`, `day_part_id`). - **`Assignment`** : `volunteer_id`, `shift_id`, `status` (`AssignmentStatus`, défaut `PROPOSED`), `source` (`AssignmentSource`, défaut `MANUAL`), `locked` (défaut `false`). Unique (`volunteer_id`, `shift_id`). - **`Account`** : `email` (unique par édition), `role` (`Role`), `edition_id`, `volunteer_id` (nullable, unique). - **`MagicLinkToken`** : `account_id`, `token_hash` (unique), `expires_at`, `used_at`. Conventions techniques : - Multi-édition : toute entité métier porte un `edition_id`. - Énumérations stockées en `VARCHAR` + `CHECK` (`native_enum=False`), pas de type ENUM natif PostgreSQL. - Suppression en cascade au niveau base (`ON DELETE CASCADE`) sur tous les `edition_id` et sur les liens vers `volunteer`, `shift`, `position`, `skill`, `account`. La suppression complète d'un bénévole (RGPD) efface donc ses `Availability`, `Assignment`, liens de compétence et son `Account`. - Dates/heures **naïves**, heure locale Europe/Paris (`TZ` du conteneur + `tzdata`). `app.models.local_now()` pour les horodatages. ### Rôles et accès - **`COORDINATOR`** : accès complet, construit et publie les plannings. - **`LEAD`** (référent) : lecture sur toute l'édition, écriture limitée à ses propres postes. - **`VOLUNTEER`** : accès à ses seules données. Authentification par lien magique envoyé par email, sans mot de passe. L'envoi d'email est **stubé** pour l'instant (le lien est écrit dans les logs). Un bénévole sans email n'a pas de `Account` et ne se connecte pas ; il est géré entièrement par la coordination. --- ## 3. Moteur de validation des affectations — cœur du produit Module isolé `backend/app/validation/`. **Tests écrits avant l'implémentation**, un cas passant + un cas déclenchant par règle. Retourne une liste de violations typées `BLOCKING` ou `WARNING`. Chaque violation est un **code** + des paramètres structurés, jamais une phrase (la traduction se fait via `locales/fr.json`). | Code | Sévérité | Règle | |---------------------------|------------|-------------------------------------------------------------| | `SHIFT_OVERLAP` | `BLOCKING` | Deux créneaux d'un même bénévole se chevauchent. | | `OUTSIDE_AVAILABILITY` | `WARNING` | Un `DayPart` couvert par le créneau est `UNAVAILABLE` ou sans déclaration. | | `DAILY_HOURS_EXCEEDED` | `WARNING` | > 8 h 00 d'affectations cumulées sur une même journée. | | `INSUFFICIENT_REST` | `WARNING` | < 8 h 00 entre la fin d'un créneau et le début du suivant. | | `MISSING_REQUIRED_SKILL` | `WARNING` | Le bénévole n'a pas une compétence requise par le créneau. | Sémantique figée : - Statuts pris en compte pour chevauchement / cumul / repos : `PROPOSED` + `CONFIRMED`. `DECLINED` et `CANCELLED` sont ignorés. - « Journée » (cumul 8 h) : du début du premier `DayPart` du jour au début du premier `DayPart` du lendemain. - Cumul : `> 8 h 00` strict (8 h 00 pile passe). - Repos : `< 8 h 00` strict. - Créneaux jointifs (`end_at == start_at`) : **pas** un chevauchement. - Une violation `BLOCKING` empêche l'enregistrement, sans dérogation. - Une violation `WARNING` est retournée à l'appelant mais n'empêche rien (l'équipe connaît des exceptions légitimes). --- ## 4. Périmètre ### Premier incrément (en cours) 1. Modèle de données complet + migrations Alembic. 2. Import CSV/Excel (un seul fichier bénévoles + disponibilités), en deux temps : analyse (colonnes détectées, aperçu) puis exécution avec mapping validé. Rapport ligne par ligne. Détection des doublons : upsert sur `email` ; sans email, détection sur (`last_name`, `first_name`) signalée sans upsert automatique. Mode simulation qui ne persiste rien. 3. API CRUD sur `Position`, `Shift`, `Volunteer`, `Assignment`. 4. Moteur de validation (section 3). 5. Endpoint matrice d'une édition : `[{ position, shifts: [{ shift, min_staff, ideal_staff, confirmed, proposed }] }]`. 6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours, quelques trous d'effectif, 2–3 avertissements actifs. ### Hors périmètre (le modèle doit juste pouvoir les accueillir sans refonte) Solveur OR-Tools · envoi d'emails / SMS · constructeur de formulaire · pointage sur site · fonctions d'IA. --- ## 5. Arborescence ``` . ├── CLAUDE.md ├── README.md ├── docker-compose.yml # dev : db + backend (hot reload) ├── .env.example ├── .github/workflows/deploy.yml # CI/CD : build → GHCR → SSH sur le VPS ├── deploy/ │ ├── docker-compose.prod.yml │ └── README.md # secrets requis, setup VPS ├── docker/postgres-init/ # création de la base de test au 1er boot ├── backend/ │ ├── pyproject.toml # géré par uv │ ├── Dockerfile │ ├── alembic.ini │ ├── app/ │ │ ├── main.py # instance FastAPI │ │ ├── config.py # settings (pydantic-settings) │ │ ├── db.py # engine + session │ │ ├── logging_config.py # RGPD : pas de PII dans les logs │ │ ├── api/ # routers + schemas Pydantic + deps │ │ │ # health, imports, volunteers, positions, │ │ │ # shifts, assignments, matrix │ │ ├── importing/ # import CSV/Excel (parser, values, service) │ │ ├── models/ # tables SQLModel │ │ ├── validation/ # moteur pur (models, engine) + loader (base) │ │ ├── seed.py # jeu de démonstration (python -m app.seed) │ │ ├── i18n/ │ │ │ ├── __init__.py # t(key, **params) │ │ │ └── locales/fr.json │ │ └── migrations/ # Alembic (env.py, versions/) │ └── tests/ │ ├── conftest.py # vraie base de test PostgreSQL │ └── ... └── frontend/ # React + Vite + TS + Tailwind (étape ultérieure) ``` --- ## 6. Commandes Aucune dépendance locale requise hors Docker. | Action | Commande | |-------------------------------|-------------------------------------------------------------------------| | Lancer en dev | `docker compose up --build` | | API + docs interactives | http://localhost:8000/docs | | Tests backend | `docker compose run --rm backend uv run pytest` | | Couverture | `docker compose run --rm backend uv run pytest --cov=app` | | Lint | `docker compose run --rm backend uv run ruff check .` | | Format | `docker compose run --rm backend uv run ruff format .` | | Créer une migration | `docker compose run --rm backend uv run alembic revision --autogenerate -m "..."` | | Appliquer les migrations | `docker compose run --rm backend uv run alembic upgrade head` | | Vérifier migration ↔ modèle | `docker compose run --rm backend uv run alembic check` (après `upgrade head`) | | Charger le jeu de démo | `docker compose run --rm backend uv run python -m app.seed` (après `alembic upgrade head`) | --- ## 7. Conventions - **Code, base, noms de fichiers en anglais.** Le français vit dans les fichiers de traduction et les commits. - **Aucun texte visible par l'utilisateur en dur.** Backend : `app.i18n.t(key)` + `locales/fr.json`. Frontend : fichier i18n dédié. Vrai même en monolingue. - **RGPD :** - Pas de log des noms, emails, téléphones. On logue des identifiants. - `Edition.retention_days` : durée de rétention paramétrable. - Endpoint de suppression complète d'un bénévole : *hard delete* en cascade. - **Dates et heures :** stockées naïves, interprétées en heure locale Europe/Paris (heure murale). Les règles « 8 h », « journée », « demi-journée » sont toutes en heure locale. - **Validation TDD :** tests avant implémentation, un cas passant + un cas déclenchant par règle. - **Git :** un commit atomique par étape validée, message en français. - **Étapes courtes :** après chaque étape, montrer le résultat et attendre la validation avant de continuer. --- ## 8. Déploiement - Dépôt GitHub. VPS avec Docker. - `.github/workflows/deploy.yml` : sur chaque push `main` → build de l'image backend → push sur `ghcr.io//-backend` → SSH sur le VPS → `docker compose -f docker-compose.prod.yml pull && up -d`. - Secrets requis (voir `deploy/README.md`) : `SSH_HOST`, `SSH_USER`, `SSH_KEY`, `DEPLOY_PATH`, `GHCR_TOKEN`. - Le VPS porte un fichier `.env` (non versionné) avec `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `JWT_SECRET`. --- ## 9. Journal des étapes | Étape | Description | État | |-------|-------------------------------------------------------|----------| | 1 | Squelette : monorepo, Docker, CI/CD, backend qui démarre | validée | | 2 | Modèle de données complet + migration Alembic initiale | validée | | 3 | Import CSV/Excel (analyse + exécution, rapport, doublons, simulation) | validée | | 4 | Moteur de validation des affectations (pur, TDD, 5 règles) | validée | | 5 | API CRUD (Position, Shift, Volunteer, Assignment) + matrice + validation branchée | validée | | 6 | Jeu de données de démonstration (`app/seed.py`) | à valider | Premier incrément backend terminé. **Reste hors périmètre** (modèle prêt à les accueillir) : authentification par lien magique (`Account` + `MagicLinkToken` existent, endpoints non encore protégés), solveur OR-Tools, envoi d'emails/SMS, constructeur de formulaire, pointage, IA. Le frontend (React/Vite/TS/Tailwind) reste à démarrer.