Deux colonnes nullable firstName/lastName sur User (groupes me:read,
user:list, user:write), éditables dans le drawer utilisateur (admin).
L'affichage reste basé sur le username pour l'instant. Migration +
valeurs de démo dans les fixtures.
Sur une demande d'un seul jour, le formulaire recopie la demi-journée de
début sur la fin (même date), si bien que les deux bornes portaient une
demi-journée et 0,5 était soustrait deux fois (1 - 0,5 - 0,5 = 0).
Quand start et end tombent le même jour, les deux bornes se confondent :
on ne soustrait désormais 0,5 qu'une seule fois. Comparaison par
getTimestamp() pour rester compatible strict_comparison (=== sur deux
DateTimeImmutable distincts teste l'identité d'instance, pas la valeur).
Couverture complétée : mono-jour plein, week-end, férié, inversion de
dates, demi-journée de fin seule, demi sur samedi en mode ouvrables.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AbsenceBalanceService::availableForRequest() : jours disponibles (acquis N-1
+ en cours N − pris) pour la période de la demande, null si type non suivi.
- Blocage de l'approbation si countedDays > disponible, dans les deux chemins
(REST AbsenceReviewProcessor + MCP ReviewAbsenceRequestTool), comme le motif
décès. Les CP en cours d'acquisition restent posables, mais pas au-delà du
droit total (plus de solde négatif silencieux à l'approbation).
- Fixture : demande pending CP d'alice replacée dans sa période de référence
2025-2026 (26→29/05/2026, 4 j ouvrés) et solde pending aligné (5 → 4) ;
plus de "en attente" orphelin non lié à une demande.
- Test fonctionnel testApproveBeyondAvailableBalanceIsBlocked + employé de test
doté d'un droit pour que les approbations existantes passent le garde-fou.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Périmètre 1-6 du design 2026-05-22-absence-legal-compliance-fixes (points
lourds — ancienneté, CP pendant maladie, rétention — reportés en backlog).
- Événements familiaux sans solde : AbsenceType::decrementsBalance() ne vaut
true que pour les CP. Mariage/PACS, naissance, décès = droits par événement ;
congé parental = suspension ; maladie = Sécu. Plus de solde fantôme.
- Décès : daysPerEvent = null (selon lien de parenté) + motif obligatoire à la
création (REST + MCP), les minimums légaux étant rappelés dans l'aide.
- Ajout du congé naissance (type, policy 3 j, justificatif, libellés/couleur front).
- Garde-fou demi-journée : -0,5 appliqué uniquement si le jour-borne est
réellement décompté (corrige un sous-décompte week-end/férié) — TDD.
- CCN documentée : paramètre app.absence.convention = "Syntec (IDCC 1486)",
rappelée en sous-titre admin et dans l'aide /help.
Tests : AbsenceDayCalculatorTest (garde-fou demi-journée), AbsenceRequestLifecycle
(motif décès obligatoire + aucun solde touché). make test 52/52, build Nuxt OK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Suite à la revue de conformité du module absences.
Fuite corrigée : GET /api/users et /api/users/{id} n'avaient aucun contrôle
d'accès alors que le groupe user:list exposait les données RH/familiales
(date d'embauche, contrat, soldes de CP, rôles…). Tout utilisateur authentifié
pouvait donc lire ces informations sur tous ses collègues.
- chaque champ RH (isEmployee, hireDate, endDate, contractType, workTimeRatio,
annualLeaveDays, referencePeriodStart, initialLeaveBalance) ainsi que roles
est désormais exposé via #[ApiProperty(security: "is_granted('ROLE_ADMIN') or
object == user")] : visible uniquement par un admin ou par l'utilisateur
lui-même. id et username restent publics (sélecteurs d'assigné, avatars).
Minimisation : suppression de familySituation et nbChildren, collectés et
exposés (form RH, API, outil MCP) mais utilisés par aucun calcul.
- entité User + enum FamilySituation + migration de drop des colonnes
- Serializer MCP, update-user (MCP), EmployeeDrawer, DTO, fixtures, i18n
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose le module Absences via le serveur MCP et comble les trous CRUD
existants (projets, groupes, métadonnées de tâches, clients, users RH).
Absences (réutilise AbsenceDayCalculator + AbsenceBalanceService pour ne
pas contourner la logique de soldes) :
- list/get/create/review/cancel/delete-absence-request
- list/update-absence-policy, list/update-absence-balance
- create-absence-request prend un userId explicite (agir au nom d'un employé) ;
review/cancel maintiennent les soldes (pending/taken) cohérents
- AbsenceRequestRepository::findFiltered pour les filtres de liste
Trous CRUD comblés :
- delete-project, delete-group
- CRUD tag, effort, priority
- CRUD status (couplé au workflow, avec category)
- CRUD client, get/update-user (champs RH, sans password ni roles)
Sérialisation centralisée (Serializer::absenceRequest/Policy/Balance/client/userFull).
Instructions MCP (mcp.yaml) mises à jour : statuts par workflow + domaine absences.
Tests : tests/Functional/Mcp/AbsenceRequestLifecycleTest (création / approbation /
annulation admin) vérifient le cycle complet et la cohérence des soldes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Décode les encoded-words MIME (RFC 2047) des sujets et noms d'expéditeur
via App\Mail\MimeHeaderDecoder, appliqué dans ImapMailProvider (sync propre)
- Commande app:mail:redecode-headers (--dry-run) pour re-décoder l'existant en base
- Aperçu inline images + PDF en visionneuse modale plein écran (MailAttachmentPreview),
téléchargement conservé pour les autres types
- Tests unitaires du décodeur + maj docs/mail-integration.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trois causes racines révélées par une vraie synchro complète (139 dossiers) :
- contrainte UNIQUE globale sur message_id : fausse pour IMAP (un même Message-ID
existe dans plusieurs dossiers) → violation → fermeture de l'EntityManager →
cascade qui tuait tous les dossiers suivants. Migration : index simple à la place.
- 139 connexions IMAP (une par dossier) → throttling OVH (failed to authenticate) :
réutilisation d'une seule connexion (closeConnection() ajouté à l'interface).
- état de connexion corrompu après un dossier en erreur (must be in SELECTED state) :
reconnexion ciblée après chaque dossier en échec.
- garde anti-cascade : reset du ManagerRegistry + arrêt propre si l'EM se ferme.
Résultat : 456 messages sur 57 dossiers (avant : 188/30 puis crash). Les rares
dossiers à encodage spécial sont skippés proprement et réessayés au cycle suivant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quatre bugs révélés en testant contre une vraie boîte OVH (les tests mockaient
le provider, donc jamais exercés) :
- requête sans critère → "BAD parse error: zero-length content" : ajout de whereAll()
- getDate()/getSubject() renvoient des Attribute webklex v6, pas des scalaires : casts explicites
- séquence par défaut ST_MSGN → le peek() de webklex faisait un STORE par numéro de
séquence rejeté par OVH ("flag could not be removed") : force ST_UID sur toutes les requêtes
- snippet via getTextBody() forçait un fetch de corps par mail (sync 179s + peek) :
setFetchBody(false) au listing, snippet désormais optionnel
Sync INBOX : 9 messages en 1,6s (avant : échec en 179s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le guard enabled dans getClient() bloquait le test de connexion alors que le
workflow naturel est configurer → tester → activer. getClient(requireEnabled)
permet au nouveau testConnection() de se connecter sans exiger enabled=true.
Le controller (ROLE_ADMIN) renvoie désormais le détail de l'erreur pour faciliter
le diagnostic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- dispatch MailSyncRequested au bus Messenger, retourne 202 immediat
- folderPath optionnel via body JSON pour sync ciblee
- en test : transport in-memory route le message en sync
- securite via MailAccessChecker
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- App\Message\MailSyncRequested (optionnel folderPath)
- App\MessageHandler\MailSyncRequestedHandler delegue a MailSyncService::syncFolder ou syncAll
- messenger.yaml : transport async via Doctrine DSN, retry 3x exponentiel, failure transport
- en test : transport in-memory (sync immediat)
- migration Version20260519220000 : cree messenger_messages table (idempotente, IF NOT EXISTS)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- downloadId = base64url(messageDbId:partNumber)
- Content-Disposition: attachment systematique (jamais inline pour eviter XSS via HTML attachments)
- X-Content-Type-Options: nosniff
- filename sanitise via basename pour eviter path traversal
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- POST /api/mail/messages/{id}/link-task body {taskId} : cree TaskMailLink (idempotent)
- DELETE /api/mail/messages/{id}/link-task/{taskId} : supprime le lien (204)
- GET /api/tasks/{id}/mails : liste les mails lies a une tache
- securite via MailAccessChecker, tests fonctionnels 401/403
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cree une Task avec titre = subject du mail (max 255 chars)
- utilise findMaxNumberByProjectForUpdate pour numero (advisory lock PG)
- transaction wrapInTransaction pour eviter race conditions
- taskGroupId et priorityId optionnels via body JSON
- cree automatiquement le TaskMailLink (mail <-> tache)
- retourne 201 + taskId/taskNumber/taskTitle/messageId
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- POST /api/mail/messages/{id}/read body {read: bool} - synchro IMAP + BDD
- POST /api/mail/messages/{id}/flag body {flagged: bool} - synchro IMAP + BDD
- IMAP-side non bloquant : BDD est mise a jour meme si IMAP fail (resync au prochain cycle)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- recupere headers + body + attachments via ImapMailProvider::fetchMessage
- cache Symfony pool cache.app, cle mail_body_{md5(messageId)}, TTL 300s
- attachments serialises sans contenu binaire, avec downloadId base64url(messageDbId:partNumber)
- 503 si IMAP indisponible, 404 si message inconnu
- les tests read/flag ROLE_CLIENT/auth seront ajoutes en Task 10 (route deja existante)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ensureCanAccessMail : refuse ROLE_CLIENT pur (sans ROLE_ADMIN)
- ensureIsAdmin : helper pour endpoints config
- service utilise par tous les controllers metier mail
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- endpoint ROLE_ADMIN qui teste la connexion IMAP via listFolders
- retourne ok:bool + foldersCount ou error sanitise (pas de leak interne)
- priority:1 obligatoire pour eviter conflit avec route API Platform {id}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ApiResource MailSettings expose les operations Get + Patch sur /api/mail/configuration
- Provider + Processor relient le DTO a l'entite MailConfiguration (singleton)
- password en write-only (jamais retourne) + hasPassword en lecture
- chiffrement password via TokenEncryptor (sodium)
- securite ROLE_ADMIN sur les deux operations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ajoute symfony/messenger ^8.0 et symfony/doctrine-messenger ^8.0 pour la sync mail async
- ajoute symfony/browser-kit + css-selector en dev pour tests fonctionnels WebTestCase
- ENCRYPTION_KEY ajoutee dans phpunit.dist.xml pour permettre le chiffrement en test
- MESSENGER_TRANSPORT_DSN configure (Doctrine), messenger.yaml minimal (sera enrichi en Task 12)
- fix(orm) : ClientTicket - migre uniqueConstraints en attribut separe (Doctrine ORM 4 deprecation)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend :
- POST /api/me/regenerate-api-token : nouveau controller, ROLE_USER (exclut CLIENT)
- User.apiToken exposé via groupe me:read sur GET /api/me
Frontend :
- Section 'Token API MCP' sur /profile (masquée pour les CLIENT du portail)
- Boutons Copier + Régénérer avec modal de confirmation
- Service api-token + DTO mis à jour + clés i18n fr