Files
Lesstime/docs/superpowers/plans/2026-06-20-lst-58-directory-prospect.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

4.8 KiB

LST-58 (2.4) — Module Directory : Prospect + front répertoire (plan)

Suite de la migration Directory. Client (back) déjà livré (c5738d2). Reste : entité Prospect (nouvelle) + front répertoire (Clients + Prospects). Spec produit non fournie → design défini ici de façon raisonnable, à valider au test. Additif, sans régression. Branche integration/modular-monolith-0.1-1.3.

Design Prospect (décidé, à valider)

Aligné sur Client (même module Directory), enrichi des concepts de prospection commerciale.

Entité App\Module\Directory\Domain\Entity\Prospect (table prospect) :

  • id int PK
  • name string(255) NOT NULL — contact ou société
  • company string(255) nullable
  • email string(255) nullable
  • phone string(50) nullable
  • street string(255) nullable / city string(255) nullable / postalCode string(20) nullable (alignés Client)
  • status enum ProspectStatus NOT NULL (default New)
  • source string(255) nullable — origine (recommandation, salon, site web…)
  • notes text nullable
  • convertedClient ManyToOne ClientInterface nullable, JoinColumn ON DELETE SET NULL — rempli à la conversion
  • Timestampable/Blamable (trait) + #[Auditable]
  • Groupes : prospect:read / prospect:write

Enum App\Module\Directory\Domain\Enum\ProspectStatus : New (nouveau), Contacted (contacté), Qualified (qualifié), Won (gagné/converti), Lost (perdu). Méthode label(): string (FR), comme les autres enums.

API Platform (aligné Client) :

  • GetCollection paginationEnabled:false, is_granted('ROLE_USER')
  • Get ROLE_USER ; Post/Patch/Delete ROLE_ADMIN
  • Opération custom Post /prospects/{id}/convert (processor ConvertProspectProcessor) : crée un Client à partir du Prospect (name/company→name, email, phone, adresse), lie convertedClient, passe status=Won. Sécurité ROLE_ADMIN. Renvoie le Prospect mis à jour. Idempotent si déjà converti (renvoie l'existant).
  • #[ApiFilter] SearchFilter sur status (filtre répertoire).

Repo : ProspectRepositoryInterface (Domain) + DoctrineProspectRepository (Infra) + binding.

MCP (cohérent avec clients, sous Infrastructure/Mcp/Tool/) : list-prospects, get-prospect, create-prospect, update-prospect, delete-prospect, convert-prospect. Serializer : ajouter prospect() dans src/Mcp/Tool/Serializer.php.

DirectoryModule.permissions() : ajouter directory.prospects.view, directory.prospects.manage (additif).

Migration additive : CREATE TABLE prospect (colonnes + FK converted_client→client ON DELETE SET NULL + created_by/updated_by FK user + index + COMMENT). Down = DROP TABLE.

Fixtures : 2-3 prospects de démo (statuts variés), dont un converti.

Front répertoire (frontend/modules/directory/)

Aujourd'hui : pas de page client dédiée (AdminClientTab + picker ProjectDrawer). On crée un vrai répertoire.

  • nuxt.config.ts vide.
  • services/ : clients.ts (move depuis racine), prospects.ts (nouveau) + dto/{client,prospect}.ts.
  • pages/directory.vue : page à 2 onglets (Clients / Prospects), tableaux paginés côté client (paginationEnabled:false back), recherche/filtre statut pour prospects.
  • components/ : ClientDrawer.vue (move depuis components/client/), ProspectDrawer.vue (nouveau, create/edit + bouton « Convertir en client »).
  • Sidebar : ajouter item sidebar.general.directory/directory, 'module' => 'directory', gate ROLE_ADMIN (gestion référentiel).
  • Réécrire imports consommateurs de ~/services/clients / ~/services/dto/client (AdminClientTab, ProjectDrawer, pages projects) → ~/modules/directory/services/.... AdminClientTab : soit le retirer de /admin au profit de /directory, soit le laisser pointer le nouveau service. Décision : garder AdminClientTab fonctionnel (repoint service) ET ajouter la page /directory (les deux coexistent ; /directory = vue dédiée).
  • i18n global : ajouter clés directory.*, prospects.*, sidebar.general.directory.

Vagues d'exécution

  1. Back Prospect : enum + entité + repo + API (CRUD + convert) + MCP (6 tools) + Serializer + permissions module + fixtures + migration. Vérif cache:clear/migrate/phpunit/cs-fixer → commit.
  2. Front Directory : layer (move client front + page répertoire + ProspectDrawer + prospects service/dto) + sidebar + imports + i18n. Vérif nuxt build → commit.

Critères d'acceptation (ticket #58)

  • Clients en module (fait, c5738d2)
  • Prospects en module + front répertoire fonctionnel
  • resolve_target_entities → Directory\Client
  • make test vert, aucune migration destructive
  • toggle module directory (sidebar + route /directory)

Suite phase 2 (après 2.4)

  • 2.5 (#67) Module Mail — WIP docs/mail-integration.md, à traiter avec précaution.
  • 2.6 (#68) Module Integration (Gitea/BookStack/Zimbra/Share).