Files
Lesstime/docs/superpowers/plans/2026-06-20-lst-65-module-projectmanagement.md
T
matthieu 8313c759c6
Auto Tag Develop / tag (push) Successful in 9s
Migration modular monolith DDD (0.1 → 3.3) (#17)
## Migration modular monolith DDD — Lesstime (0.1 → 3.3)

Cette MR regroupe l'intégralité de la refonte en monolithe modulaire (strangler progressif, additif). Elle remplace les MR stackées de Phase 1 (#12–#16), désormais incluses ici.

**Ne pas merger avant validation fonctionnelle** : branche destinée à être testée telle quelle.

### Périmètre — 9 modules sous `src/Module/`
| Phase | Module | Contenu |
|------|--------|---------|
| 0.1 | (socle) | infrastructure modulaire, `ModuleInterface`, mapping Doctrine par module |
| 0.2 | (socle front) | auto-détection des layers Nuxt sous `frontend/modules/*` |
| 1.1 | **Core** | Identité (User/Auth), Notifications, Notifier |
| 1.2 | Core | RBAC fin (permissions `module.resource.action`, sidebar gated) |
| 1.3 | Core | Audit log (`#[Auditable]`, listener, provider DBAL) |
| 2.1 | **TimeTracking** | TimeEntry + MCP + export |
| 2.2 | **ProjectManagement** | cœur métier Projets/Tâches + 38 MCP tools |
| 2.3 | **Absence** | demandes, soldes, policies, justificatifs |
| 2.4 | **Directory** | Clients (migrés) + **Prospects** (nouveau, conversion → Client) |
| 2.5 | **Mail** | intégration IMAP OVH + liens tâches |
| 2.6 | **Integration** | Gitea / BookStack / Zimbra / Share |
| 3.1 | **Reporting** | rapports transverses (DBAL read-only, 0 import inter-module) |
| 3.2 | **ClientPortal** | portail client (ROLE_CLIENT cloisonné, tickets, notifications) |
| 3.3 | (finition) | nettoyage legacy — `src/Entity` vide, app 100% modulaire |

### Architecture
- Découplage inter-modules par **contrats** (`UserInterface`, `ProjectInterface`, `TaskInterface`, `TaskTagInterface`, `ClientInterface`, `ClientTicketInterface`, `LeaveProfileInterface`) + `resolve_target_entities` 100% modulaire (aucune cible legacy).
- Repositories : interface `Domain/Repository` + implémentation `Infrastructure/Doctrine`, bindées.
- Reporting en DBAL read-only pur (aucun import d'entité d'un autre module).
- Chaque migration de module : déplacement à comportement préservé (API publique et noms d'outils MCP inchangés), migrations **additives** uniquement (zéro destructif).

### Sécurité
- ROLE_CLIENT cloisonné : un utilisateur client n'accède qu'à `/portal` et à ses propres tickets (filtrés par `allowedProjects`), interdit sur toute l'API interne.
- Correctif : interdiction pour un client de créer un lien vers le partage SMB (upload uniquement).

### QA non-régression (branche reconstruite from scratch)
- Migrations from scratch + fixtures : OK.
- Compilation dev + prod : OK.
- **180 tests PHPUnit verts**, php-cs-fixer clean, ~96 routes, **66 outils MCP** tous sous `App\Module\*`.
- Smoke test runtime multi-rôles (admin / ROLE_USER / ROLE_CLIENT) : 44 vérifications HTTP, **0 écart**, cloisonnement client étanche.
- Build Nuxt OK, 9 layers, 0 import legacy résiduel.

### Points à arbitrer (hors périmètre de cette migration)
- Durcissement MCP/IDOR pré-existant (`userId` explicite sans scoping sur certains tools TimeTracking/Absence/TaskDocument) — ticket dédié recommandé.
- Validation fonctionnelle de **Prospect** et **ClientPortal** (conçus depuis les specs disque).
- **Harmonisation visuelle Malio finale** (3.3) — finition esthétique inter-modules laissée au PO.

---

## ⚠️ Déploiement / migration des données — à ne pas oublier

### 1. Resynchroniser les séquences PostgreSQL après tout import/restore de dump
Si la prod (ou tout environnement) est **montée depuis un dump** (`pg_restore` / `COPY`), les lignes sont chargées avec leurs `id` explicites **sans avancer les séquences** → au premier `INSERT` : `duplicate key value violates unique constraint "..._pkey"` (constaté en local sur `notification`, `task`, `time_entry`…).

À lancer **juste après chaque restore/import** :

```sql
DO $$
DECLARE r RECORD; maxid BIGINT; seq TEXT;
BEGIN
  FOR r IN SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public'
  LOOP
    seq := pg_get_serial_sequence(quote_ident(r.table_name), r.column_name);
    IF seq IS NOT NULL THEN
      EXECUTE format('SELECT COALESCE(MAX(%I),0) FROM %I', r.column_name, r.table_name) INTO maxid;
      PERFORM setval(seq, GREATEST(maxid,1), maxid > 0);
    END IF;
  END LOOP;
END $$;
```

> Ne concerne **pas** une prod qui tourne déjà (séquences avancées organiquement) — uniquement le cas restore/import. Idempotent, sans risque.

### 2. Fix dénormalisation des collections typées-contrat (code, inclus dans la branche)
Les relations **to-many** typées par une interface `Shared\Domain\Contract\*` (`TimeEntry::tags` → `TaskTagInterface`, `Task::collaborators` → `UserInterface`) étaient **indénormalisables par API Platform** (mono-valué OK via IRI, collection KO) → **tout POST/PATCH portant une telle collection renvoyait 400/500**. Corrigé par un dénormaliseur générique `ContractRelationDenormalizer` (réutilise `resolve_target_entities`, zéro couplage par-entité) + test fonctionnel de non-régression.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #17
2026-06-23 13:50:42 +00:00

9.7 KiB

LST-65 (2.2) — Module ProjectManagement : plan de migration

Migration strangler du cœur métier Projets/Tâches vers src/Module/ProjectManagement/. Additive, sans régression API. Exécution en 4 tranches incrémentalement vertes (chaque tranche compile + phpunit vert + commit ; aucun état cassé committé).

Branche : integration/modular-monolith-0.1-1.3 (empilement phase 2). Vérif container : docker exec -u www-data php-lesstime-fpm php bin/console cache:clear Tests : docker exec -u www-data php-lesstime-fpm php vendor/bin/phpunit (baseline = 159 verts). Style : make php-cs-fixer-allow-risky. PHP declare(strict_types=1). SQL colonnes minuscules.

Périmètre (10 entités + écosystème)

Entités : Project, Task, Workflow, TaskStatus, TaskGroup, TaskEffort, TaskPriority, TaskTag, TaskRecurrence, TaskDocument. Enums : StatusCategory, RecurrenceType. Repos (9), State (7), MCP (38), Controller (1), Services (2 : CalDavService, RecurrenceCalculator), Listeners (3), ApiResource (SwitchWorkflowOutput), fixtures, tests.

Décisions d'architecture (figées)

  1. Contrats inter-modules uniquement (src/Shared/Domain/Contract/), surface minimale :
    • ProjectInterface : getId(): ?int, getCode(): ?string, getName(): ?string
    • TaskInterface : getId(): ?int, getNumber(): ?int, getTitle(): ?string
    • TaskTagInterface : getId(): ?int, getLabel(): ?string, getColor(): ?string
    • ClientInterface : getId(): ?int, getName(): ?string
    • PAS de WorkflowInterface (Workflow est intra-module PM).
  2. Consommateur contractuel : seul le module TimeTracking (TimeEntry) bascule Project/Task/TaskTag → interfaces. Project (PM) bascule client → ClientInterface.
  3. Legacy non modularisé (Gitea/BookStack/Mail : src/Controller/Mail/*, src/State/Gitea*, src/State/BookStack*, src/Service/GiteaApiService.php, src/ApiResource/BookStack*, src/Entity/TaskMailLink.php, src/Entity/TaskBookStackLink.php), Serializer MCP partagé (src/Mcp/Tool/Serializer.php), fixtures, tests : bascule du FQCN concret App\Entity\XApp\Module\ProjectManagement\Domain\Entity\X. Couplage transitoire legacy→module, nettoyé en 2.4/2.5/2.6.
  4. Repos : pattern Core/TimeTracking — interface Domain/Repository/XxxRepositoryInterface + Infrastructure/Doctrine/DoctrineXxxRepository extends ServiceEntityRepository implements … + binding services.yaml. Conserver les méthodes métier (findMaxNumberByProjectForUpdate, findFirstNonFinal, findDefault).
  5. Services CalDavService + RecurrenceCalculatorInfrastructure/ du module (dépendance résiduelle ZimbraConfiguration legacy tolérée jusqu'à 2.6).
  6. Serializer.php reste à src/Mcp/Tool/ (helper multi-domaines), import concret PM.
  7. Timestampable additif : sur Task et Project uniquement (agrégats), pas les référentiels. Migration additive (4 colonnes nullable + FK SET NULL + COMMENT).
  8. Table inchangée (naming strategy → mêmes tables). Aucune migration destructive.
  9. resolve_target_entities final :
    UserInterface       -> App\Module\Core\Domain\Entity\User            (existant)
    ProjectInterface    -> App\Module\ProjectManagement\Domain\Entity\Project
    TaskInterface       -> App\Module\ProjectManagement\Domain\Entity\Task
    TaskTagInterface    -> App\Module\ProjectManagement\Domain\Entity\TaskTag
    ClientInterface     -> App\Entity\Client                              (Client legacy jusqu'à 2.4)
    

Tranche 1 — Découplage EN PLACE (entités non déplacées)

But : créer les contrats et basculer les consommateurs inter-modules, sans déplacer les entités → diff minimal, isole le risque architectural.

  1. Créer les 4 interfaces dans src/Shared/Domain/Contract/ (signatures ci-dessus).
  2. src/Entity/Project.php implements ProjectInterface ; Task.php implements TaskInterface ; TaskTag.php implements TaskTagInterface ; Client.php implements ClientInterface. (Méthodes déjà présentes — juste implements + use.)
  3. Project.php : client → type ?ClientInterface (targetEntity: ClientInterface::class, import, getter/setter).
  4. src/Module/TimeTracking/Domain/Entity/TimeEntry.php : project?ProjectInterface, task?TaskInterface, tagsCollection<TaskTagInterface> (targetEntity = interfaces, imports, getters/setters/addTag/removeTag). MAJ TimeEntryRepositoryInterface/DoctrineTimeEntryRepository/ActiveTimeEntryProvider/TimeEntryExportController si typage Project/Task.
  5. config/packages/doctrine.yaml : ajouter les 4 lignes resolve_target_entities (cibles = App\Entity\Project/Task/TaskTag + App\Entity\Client — encore legacy à ce stade).
  6. Vérif : cache:clear OK + phpunit vert. Commit refactor(project-management) : introduce Project/Task/TaskTag/Client contracts, decouple TimeTracking.

Tranche 2 — Move mécanique vers le module

But : déplacer entités + écosystème, bascule namespaces, sans changement de comportement.

  1. git mv entités → src/Module/ProjectManagement/Domain/Entity/ (namespace App\Module\ProjectManagement\Domain\Entity). Relations intra-module = concret ; client=ClientInterface ; assignee/collaborators/uploadedBy=UserInterface (inchangé). repositoryClassDoctrineXxxRepository::class.
  2. git mv enums → src/Module/ProjectManagement/Domain/Enum/ (namespace adapté).
  3. Repos → Infrastructure/Doctrine/DoctrineXxxRepository.php + interfaces Domain/Repository/XxxRepositoryInterface.php (méthodes métier dans l'interface). Bindings services.yaml (9).
  4. State (7), MCP (38), Controller (1), Services (2), Listeners (3), ApiResource SwitchWorkflowOutput → sous-dossiers Infrastructure/… du module, namespaces adaptés, injecter les interfaces de repo. services.yaml : repointer App\State\TaskDocumentProcessor, App\Controller\TaskDocumentDownloadController, App\Mcp\Tool\Task\AddTaskDocumentTool, App\Mcp\Tool\Task\UpdateTaskDocumentTool, App\EventListener\TaskDocumentListener vers les nouveaux FQCN (garder $uploadDir + tag doctrine.orm.entity_listener).
  5. resolve_target_entities : repointer ProjectInterface/TaskInterface/TaskTagInterface vers les FQCN module. (ClientInterface reste App\Entity\Client.)
  6. Swap FQCN concret legacy : remplacer App\Entity\{Task,Project,Workflow,TaskStatus,TaskGroup,TaskEffort,TaskPriority,TaskTag,TaskRecurrence,TaskDocument}App\Module\ProjectManagement\Domain\Entity\… et App\Enum\{StatusCategory,RecurrenceType}App\Module\ProjectManagement\Domain\Enum\… et App\Repository\Xxx → interfaces/Doctrine, dans : Serializer.php, Controller/Mail/, State/Gitea, State/BookStack*, ApiResource/BookStack*, Service/GiteaApiService.php, Entity/TaskMailLink.php, Entity/TaskBookStackLink.php, DataFixtures/AppFixtures.php, tests/*. (NE PAS toucher App\Entity\Client.)
  7. config/modules.php : ajouter ProjectManagementModule (id project-management, label Projets & Tâches, isRequired false, permissions project-management.projects.view/manage, project-management.tasks.view/manage — non recâblées, additif).
  8. config/packages/doctrine.yaml : mapping ProjectManagement (dir src/Module/ProjectManagement/Domain/Entity).
  9. config/sidebar.php : 'module' => 'project-management' sur items my-tasks et projects.
  10. Vérif : cache:clear OK + doctrine:schema:validate mapping OK + phpunit vert + cs-fixer. Commit feat(project-management) : migrate core Projects/Tasks domain into module (back).

Tranche 3 — Timestampable additif (Task + Project)

  1. Ajouter TimestampableBlamableTrait + interfaces à Task et Project.
  2. Migration additive manuscrite : created_at/updated_at (TIMESTAMP(0) null), created_by/updated_by (INT null, FK "user" ON DELETE SET NULL) + index + COMMENT, sur task et project. down() = DROP des ajouts.
  3. Champs hors groupes API existants (le trait porte ses propres groupes).
  4. Vérif : migrations:migrate -n (dev+test) + phpunit vert. Commit feat(project-management) : add timestampable/blamable to Task and Project (additive).

Tranche 4 — Front layer project-management

  1. git mv vers frontend/modules/project-management/ : pages (my-tasks, projects/index, projects/[id]/{index,groups,archives}), components/{project,task}/, services (projects, tasks, workflows, task-statuses, task-priorities, task-efforts, task-tags, task-groups, task-documents, task-recurrences) + services/dto/ correspondants. nuxt.config.ts = export default defineNuxtConfig({}).
  2. Réécrire imports explicites ~/services/<x> + ~/services/dto/<x>~/modules/project-management/... dans : les fichiers déplacés, components/admin/{AdminEffortTab,AdminPriorityTab,AdminTagTab,AdminWorkflowTab,WorkflowDrawer}.vue, components/mail/{MailCreateTaskModal,MailLinkTaskModal}.vue, pages/index.vue, pages/mail.vue, app/layouts/default.vue, et frontend/modules/time-tracking/ (dto/time-entry, stores/timer, pages/time-tracking, components/TimeEntryDrawer importent project/task/task-tag dto). clients.ts reste racine.
  3. Préserver routes /my-tasks, /projects, /projects/:id, /projects/:id/groups, /projects/:id/archives. i18n global inchangé.
  4. Vérif : cd frontend && npx nuxt build OK + routes présentes. Commit feat(project-management) : extract Projects/Tasks front into Nuxt module layer.

Critères d'acceptation (ticket)

  • Cœur Projets/Tâches en module sans régression API (opérations/securities/uriTemplates conservés).
  • Aucun import direct inter-modules établis (contrats) — legacy en transit toléré.
  • make test vert, aucune migration destructive.
  • Toggle module project-management (sidebar + routes) prouvé.