Files
benevoles/CLAUDE.md
T
AdamThomasandClaude Sonnet 5 b268e308ab Étape 10 : envoi d'emails par SMTP
- app/email/ : sender (smtplib, STARTTLS ou SSL, connexion réutilisée pour un
  lot) + templates (rendu, textes dans locales/fr.json). EMAIL_ENABLED=false
  (défaut) ⇒ journalisé seulement, sans destinataire ni corps (RGPD).
- Lien magique : POST .../auth/magic-link envoie désormais l'email
  (PUBLIC_BASE_URL + magic_link_path + ?token=). Helper app/auth/magic_link.py
  partagé. Sans SMTP, le jeton non nominatif reste journalisé pour le dev.
- Récapitulatif de planning : POST /editions/{id}/notifications/planning
  (COORDINATION). subject/body sont des modèles ({prenom}, {nom}, {nb_creneaux},
  {nb_heures}, {creneaux}, {lien_connexion} → lien magique personnel émis).
  Cible tous les bénévoles avec email ou une liste. Envoi en BackgroundTasks.
- Config : PUBLIC_BASE_URL, EMAIL_ENABLED, SMTP_* ; .env.example,
  docker-compose (dev + prod), deploy/README.
- 9 tests (sender activé/désactivé, rendu, notifications : contenu, gabarit par
  défaut, lien magique embarqué, sous-ensemble, réservé COORDINATION).
  137 tests au total ; ruff + alembic check verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-10 08:08:55 +02:00

18 KiB
Raw Blame History

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
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, source (AssignmentSource, défaut MANUAL), locked (défaut false). Unique (volunteer_id, shift_id). Pas de statut : l'affectation est valide dès sa création (aucune étape de confirmation) ; la retirer, c'est supprimer la ligne.
  • 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 (app/auth/)

  • COORDINATOR : accès complet, construit et publie les plannings.
  • LEAD (référent) : lecture sur toute l'édition, écriture limitée aux postes dont il est lead_account_id (et à leurs créneaux et affectations).
  • VOLUNTEER : lecture de sa seule fiche et de ses seules affectations ; aucune écriture.

Matrice appliquée par les endpoints :

Ressource COORDINATOR LEAD VOLUNTEER
volunteers liste/écriture ✔ liste seule —
volunteers/{id} lecture ✔ ✔ soi-même
positions/shifts lecture ✔ ✔ —
positions/shifts écriture ✔ ses postes —
assignments écriture ✔ ses postes —
assignments lecture ✔ ✔ les siennes
matrix, import, accounts ✔ / ✔ / ✔ ✔ / — / — —

Authentification — deux systèmes selon le compte, une même session JWT HS256 (Authorization: Bearer <jwt>, TTL session_ttl_hours) à l'arrivée. Le compte porte password_hash : défini ⇒ mot de passe, NULL ⇒ lien magique (Account.auth_method = "password" / "magic_link").

Mot de passe — comptes COORDINATOR :

  1. POST /editions/{id}/auth/login {email, password} → JWT + AccountRead. Échec = 401 auth.invalid_credentials (aucune divulgation).
  2. POST /editions/{id}/auth/change-password {current_password, new_password} (compte courant). Hachage bcrypt, longueur mini MIN_LENGTH (10).

Lien magique — comptes LEAD / VOLUNTEER (sans mot de passe) :

  1. POST /editions/{id}/auth/magic-link {email} → 202 constant. Si un compte sans mot de passe correspond, un MagicLinkToken est créé (jeton aléatoire, haché SHA-256 en base, TTL magic_link_ttl_minutes).
  2. Envoi d'email stubé : le lien est écrit dans les logs (app.auth, INFO, jeton=… — identifiants seulement, aucune donnée nominative).
  3. POST /editions/{id}/auth/verify {token} → JWT + AccountRead. Jeton à usage unique.

GET /auth/me → compte courant.

La coordination crée les comptes via POST /editions/{id}/accounts : un COORDINATOR exige un password (≥ 10) ; un LEAD / VOLUNTEER le refuse (lien magique). Un bénévole sans email n'a pas de Account.

Emails (app/email/)

Envoi par SMTP (auto-hébergé). EMAIL_ENABLED=false (défaut dev/tests) ⇒ les emails sont seulement journalisés (app.email, INFO, sans destinataire ni corps — RGPD). Config : SMTP_HOST/PORT/USER/PASSWORD/STARTTLS/SSL/FROM, PUBLIC_BASE_URL (pour les liens).

  • Lien magique : POST .../auth/magic-link envoie l'email contenant PUBLIC_BASE_URL + magic_link_path + ?token=. Sans SMTP, le jeton (non nominatif) reste journalisé pour le dev.
  • Récapitulatif de planning : POST /editions/{id}/notifications/planning (COORDINATION), corps { subject?, body?, volunteer_ids? }. subject/body sont des modèles avec {prenom}, {nom}, {nb_creneaux}, {nb_heures}, {creneaux}, {lien_connexion} (si présent ⇒ un lien magique personnel est émis pour le compte du bénévole). volunteer_ids absent ⇒ tous les bénévoles de l'édition qui ont un email. Envoi en tâche de fond (BackgroundTasks, une connexion SMTP réutilisée). Réponse : { queued, skipped_no_email, recipients }.

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 :

  • Toutes les affectations du bénévole comptent pour chevauchement / cumul / repos (une affectation = une décision, toujours active).
  • « 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, assigned }] }].
  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 · SMS · constructeur de formulaire · pointage sur site · fonctions d'IA. (L'envoi d'emails par SMTP est en place — voir § 2.)


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, auth, accounts, imports,
│   │   │                           #   volunteers, positions, shifts,
│   │   │                           #   assignments, matrix, notifications
│   │   ├── auth/                   # mot de passe + lien magique + JWT + rôles
│   │   ├── email/                  # SMTP (sender) + rendu des emails (templates)
│   │   ├── 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/<owner>/<repo>-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, PUBLIC_BASE_URL et les variables EMAIL_ENABLED / SMTP_* (voir deploy/README.md).

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) validée
7 Authentification : lien magique + JWT + application des rôles validée
8 Second système : connexion mot de passe (bcrypt) pour la coordination validée
9 Suppression du statut d'affectation (plus d'étape de confirmation) validée
10 Envoi d'emails par SMTP : lien magique + récapitulatif de planning à valider

Reste hors périmètre (modèle prêt à les accueillir) : solveur OR-Tools, SMS, constructeur de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans + connexion en place (voir frontend/).