- Fichier unique, processus en deux temps : POST /editions/{id}/import/analyze
(colonnes détectées, aperçu, mapping suggéré, demi-journées de l'édition)
puis POST /editions/{id}/import/execute (mapping validé)
- Parser CSV (délimiteur et encodage devinés, BOM) et XLSX (openpyxl),
détection XLSX par nombre magique
- Vocabulaire des valeurs de disponibilité (tri-état, accents/casse) et des
tailles de t-shirt ; compétences créées à la volée dans le référentiel
- Rapport ligne par ligne : codes de traduction + paramètres, jamais de
phrase. Doublons : upsert sur email, signalement sur (nom, prénom) sans
email, doublons intra-fichier. Mode simulation (rollback, rien de persisté)
- 40 tests : parser, valeurs, service (tous les cas), endpoints
- Dépendances : python-multipart, openpyxl
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
270 lines
14 KiB
Markdown
270 lines
14 KiB
Markdown
# 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 (health, imports)
|
||
│ │ ├── importing/ # import CSV/Excel (parser, values, service)
|
||
│ │ ├── models/ # tables SQLModel
|
||
│ │ ├── validation/ # moteur de règles (cœur du produit)
|
||
│ │ ├── 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 | *(étape ultérieure)* `docker compose run --rm backend uv run python -m app.seed` |
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
---
|
||
|
||
## 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) | à valider |
|
||
|
||
> **En attente** : la couche d'authentification (lien magique, `Account`,
|
||
> JWT, dépendances de rôle) fera l'objet d'une étape dédiée. Les endpoints
|
||
> actuels ne sont pas encore protégés.
|