# Spec — Extension des outils MCP : module Absences + trous CRUD

Date : 2026-05-22
Branche : `feat/absence-management`

## Contexte & objectif

Le serveur MCP Lesstime (`src/Mcp/Tool/`) expose aujourd'hui les projets, tâches,
métadonnées de tâches, time tracking et workflows. Le nouveau **module Absences**
(`AbsenceRequest`, `AbsencePolicy`, `AbsenceBalance`) n'est pas exposé, et plusieurs
entités existantes n'ont qu'une couverture partielle (souvent `list` sans
create/update/delete).

Objectif : permettre de piloter l'app via MCP (assistant) sur ces domaines, en
respectant strictement la logique métier déjà en place.

## Conventions reprises de l'existant

- Une classe par outil, attribut `#[McpTool(name, description)]` sur la **classe**.
- Discovery automatique : `config/packages/mcp.yaml` scanne `src/` (exclut `DataFixtures`).
  Aucune config à ajouter — créer la classe suffit.
- Constructeur : injection de repos/services + `Security`.
- `__invoke(...)` : check de rôle en première ligne (`AccessDeniedException` sinon),
  validation des IDs (`InvalidArgumentException` si introuvable), retour `json_encode(...)`.
- Sérialisation centralisée dans `App\Mcp\Tool\Serializer`.
- Rôle : `ROLE_USER` pour la lecture/écriture courante ; `ROLE_ADMIN` pour les
  opérations sensibles (déjà le cas pour `list-clients`/`list-users`, et pour les
  opérations admin du module absences).

## Décision clé — réutilisation de la logique métier (pas les Processors)

Les Processors API Platform (`AbsenceRequestProcessor`, `AbsenceReviewProcessor`,
`AbsenceCancelProcessor`) sont liés à `Security::getUser()` (l'utilisateur courant)
et à l'`Operation` HTTP. En MCP, l'utilisateur courant est le **propriétaire du
token** (admin), or on veut pouvoir agir **au nom d'un employé**.

→ Les outils MCP **n'appellent pas les Processors** ; ils répliquent leur
orchestration en réutilisant les **services partagés** qui portent la vraie règle
métier :

- `AbsenceDayCalculator::countWorkingDays(...)` — calcul des jours décomptés.
- `AbsenceBalanceService` — `reservePending`, `applyApproval`, `release`, `periodFor`.
- `AbsencePolicyRepository::findOneByType(...)` — politique active du type.
- `AbsenceRequestRepository::hasOverlap(...)` — règle anti-chevauchement.

`create-absence-request` prend un `userId` explicite (l'employé cible) ;
`review`/`cancel` posent `reviewedBy` = utilisateur du token MCP.

Ainsi soldes (`pending`/`taken`/`acquired`) et statuts restent cohérents avec ce
que produit l'UI.

## Inventaire des outils

### Module Absences — `src/Mcp/Tool/Absence/` (10 outils)

| Outil | Rôle | Paramètres | Logique |
|---|---|---|---|
| `list-absence-requests` | USER | `userId?`, `status?`, `type?`, `from?`, `to?` | Filtre ; sans `userId` renvoie tout (token admin). |
| `get-absence-request` | USER | `id` | — |
| `create-absence-request` | USER | `userId`, `type`, `startDate`, `endDate`, `startHalfDay?`, `endHalfDay?`, `reason?` | Vérifie policy active + overlap, calcule `countedDays`, refuse si ≤ 0, statut `Pending`, `reservePending`. |
| `review-absence-request` | ADMIN | `id`, `decision` (`approve`\|`reject`), `rejectionReason?` | Seulement si `Pending`. Approve → `applyApproval` ; reject → `rejectionReason` requis + `release(false)`. Pose `reviewedAt`/`reviewedBy`. |
| `cancel-absence-request` | USER | `id` | `Pending` → `release(false)` ; `Approved` → ADMIN requis + `release(true)` ; sinon conflit. Statut `Cancelled`. |
| `delete-absence-request` | ADMIN | `id` | Suppression définitive. |
| `list-absence-policies` | USER | — | Toutes les policies (ordre `type`). |
| `update-absence-policy` | ADMIN | `id`, `daysPerYear?`, `daysPerEvent?`, `justificationRequired?`, `noticeDays?`, `countWorkingDaysOnly?`, `active?` | Seuls les champs fournis changent. |
| `list-absence-balances` | USER | `userId?`, `type?`, `period?` | Soldes filtrés. |
| `update-absence-balance` | ADMIN | `id`, `acquired?`, `acquiring?`, `taken?` | Ajustement manuel (régularisation). |

`type` et `status` acceptés en valeur d'enum string (ex. `cp`, `maladie`,
`pending`) ; erreur de validation explicite si invalide. `startDate`/`endDate`
au format `YYYY-MM-DD`.

### Trous CRUD sur l'existant

**Projets / groupes**
- `delete-project` (ADMIN) — `id`.
- `delete-group` (USER) — `id`.

**Métadonnées de tâches** — `src/Mcp/Tool/TaskMeta/`
- `create-tag` / `update-tag` / `delete-tag` (USER) — `label`, `color?`.
- `create-effort` / `update-effort` / `delete-effort` (USER) — `label` (+ ordre éventuel).
- `create-priority` / `update-priority` / `delete-priority` (USER) — `label`, `color?`.
- `create-status` / `update-status` / `delete-status` (ADMIN) — **`workflowId` requis**
  + `category` (`todo`|`in_progress`|`blocked`|`review`|`done`), `label`, `color?`,
  `position?`, `isFinal?`. (Les statuts ne sont PAS globaux : ils appartiennent à un workflow.)

**Clients** — `src/Mcp/Tool/Reference/` (ADMIN, aligné sur `list-clients`)
- `get-client` — `id`.
- `create-client` — `name` (+ `email?`, `phone?`, `street?`, `city?`, `postalCode?`).
- `update-client` — `id` + champs optionnels.
- `delete-client` — `id`.

**Utilisateurs** — `src/Mcp/Tool/Reference/` (ADMIN)
- `get-user` — `id` (profil complet RH).
- `update-user` — `id` + champs RH/profil : `isEmployee?`, `hireDate?`, `endDate?`,
  `contractType?`, `workTimeRatio?`, `annualLeaveDays?`, `referencePeriodStart?`,
  `initialLeaveBalance?`, `familySituation?`, `nbChildren?`.
  **Hors périmètre (décision utilisateur) : pas de `create-user`, pas de modification
  de `password` ni `roles` via MCP.**

## Sérialisation — ajouts à `Serializer.php`

- `absenceRequest(AbsenceRequest)` : id, user{id,username}, type{value,label},
  startDate, endDate, startHalfDay, endHalfDay, countedDays, reason, status{value,label},
  rejectionReason, createdAt, reviewedAt, reviewedBy, justificationFileName.
- `absencePolicy(AbsencePolicy)` : id, type{value,label}, daysPerYear, daysPerEvent,
  justificationRequired, noticeDays, countWorkingDaysOnly, active.
- `absenceBalance(AbsenceBalance)` : id, user, type{value,label}, period, acquired,
  acquiring, taken, pending, acquiredTotal, available.
- `client(Client)` : id, name, email, phone, street, city, postalCode.
- `userFull(User)` : id, username, roles, isEmployee, hireDate, endDate, contractType,
  workTimeRatio, annualLeaveDays, referencePeriodStart, initialLeaveBalance,
  familySituation, nbChildren.

## Mise à jour de la doc MCP

`config/packages/mcp.yaml` — bloc `instructions` :
- corriger la mention « statuses … are GLOBAL » (faux : par workflow) ;
- ajouter une phrase sur le domaine Absences (requests/policies/balances, lifecycle
  approve/reject/cancel, `userId` pour agir au nom d'un employé).

## Découpage du plan d'implémentation (jalons)

1. **Absences** : Serializer + 10 outils + tests de cohérence des soldes.
2. **Métadonnées tâches** : delete-project/group, CRUD tag/effort/priority/status.
3. **Clients & users** : CRUD clients, get/update user + maj `mcp.yaml`.

Chaque jalon est livrable et testable indépendamment.

## Tests

Tests fonctionnels MCP (si une infra de test MCP existe) ou tests unitaires sur la
réplication de la logique de solde : créer → review(approve) → cancel et vérifier
`pending`/`taken` à chaque étape ; vérifier le refus sur chevauchement et sur
plage sans jour ouvré.

## Hors périmètre

- Mail, BookStack, Gitea, Zimbra, Notifications, TaskDocument (non demandés).
- Création d'utilisateurs et gestion mot de passe/rôles via MCP.
- Upload de justificatif d'absence via MCP.
