[ERP-57] Implémenter les sous-ressources Contacts / Adresses / RIB #35
Reference in New Issue
Block a user
Delete Branch "feature/ERP-57-sous-ressources-contacts-adresses-rib"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Expose les sous-ressources Contacts / Adresses / RIB du repertoire clients (M1, spec § 4.5) : - 3 Processors dedies (ClientContactProcessor, ClientAddressProcessor, ClientRibProcessor) : normalisation serveur reutilisant ClientFieldNormalizer (RG-1.19 capitalize, RG-1.20 telephones chiffres, RG-1.21 emails/billingEmail lowercase) + regles metier. - Operations API Platform : - POST /api/clients/{id}/contacts|addresses, PATCH/DELETE /api/client_contacts|addresses/{id} (security commercial.clients.manage) - POST /api/clients/{id}/ribs, PATCH/DELETE /api/client_ribs/{id} (security commercial.clients.accounting.manage) - GET item par sous-ressource (lecture unitaire) ; pas de GET collection autonome (lecture via le parent, non concernee par la pagination ERP-72). - Regles de gestion : - RG-1.13 : DELETE du dernier RIB d'un client en reglement LCR -> 409. - RG-1.14 : DELETE du dernier contact d'un client -> 409 (completude front au M1). - RG-1.05 : prenom OU nom du contact obligatoire -> 422. - Validations deja portees par l'entite et desormais exercees : Assert\Count(min:1) sur ClientAddress.sites (RG-1.10), Assert\Regex code postal (RG-1.09), Assert\Iban / Assert\Bic sur ClientRib. - SiteReferenceDenormalizer : resout les IRIs /api/sites vers SiteInterface (meme pattern que CategoryReferenceDenormalizer, sans import cross-module). - Ajout de symfony/intl, requis par Assert\Bic. Tests : ClientSubResourceApiTest (13 cas) couvrant CRUD, normalisation, RG-1.13/1.14, gating 403 sur client_ribs sans accounting.manage. Suite back complete au vert (383 tests).## Contexte Ticket ERP-58 (M1 Commercial, spec-back § 4.6) — export XLSX du répertoire clients. Branche stackée sur ERP-57. **Cible la MR sur \`feature/ERP-57-...\`** (squash merge). ## Objectif d'archi : un service d'export RÉUTILISABLE Le générique vit dans \`Shared\`, le module Client ne déclare que QUOI exporter. ### Shared (le COMMENT — sans métier) - \`Shared/Domain/Contract/SpreadsheetExporterInterface\` : \`export(string $sheetTitle, array $headers, iterable $rows): string\`. Zéro connaissance métier. - \`Shared/Infrastructure/Export/PhpSpreadsheetExporter\` : implémentation PhpSpreadsheet (en-tête ligne 1 + lignes, retour binaire via fichier temporaire). Titre d'onglet assaini (≤ 31 car., caractères Excel interdits retirés). Supporte un \`iterable\` paresseux (generator). - Auto-aliasé (un seul implémenteur) → \`SpreadsheetExporterInterface\` résout vers \`PhpSpreadsheetExporter\`. > Tout futur module réutilise \`SpreadsheetExporterInterface\` sans toucher au Client. ### Commercial (le QUOI) - \`ClientExportController\` (controller custom, \`#[Route('/api/clients/export.xlsx', priority: 1)]\` — **priority:1 obligatoire** pour éviter le conflit API Platform \`{id}\`). Security \`commercial.clients.view\`. - Mêmes filtres que \`GET /api/clients\` (non archivés par défaut, \`?search\`, \`?categoryType\`, \`?includeArchived\`). **Filtrage factorisé dans \`ClientRepository::createListQueryBuilder()\`** (search + categoryType déplacés depuis \`ClientProvider\`) → liste paginée et export partagent strictement la même logique, zéro duplication. - Colonnes (§ 4.6) : Nom entreprise, Nom contact principal, Prénom, Tél. principal, Tél. secondaire, Email, Catégories (CSV), Sites (CSV = union distincte des sites des adresses), **SIREN (omis si pas \`commercial.clients.accounting.view\`)**, Date de création. - Réponse : \`Content-Type: …spreadsheetml.sheet\`, \`Content-Disposition: attachment; filename="repertoire-clients-{YYYYMMDD}.xlsx"\`. ## Dépendance \`composer require phpoffice/phpspreadsheet\` (^5.7). Nettoyage recipes vérifié : seuls \`composer.json\`/\`composer.lock\` modifiés (pas de scaffolding parasite, \`symfony.lock\` désormais versionné). ## Tests (404 OK) - **Unitaire Shared** : XLSX relisible (en-têtes + 2 lignes), generator, titre assaini. - **Fonctionnel** : 200 (Content-Type + filename), exclusion archives par défaut, \`?search\`/\`?categoryType\`, SIREN présent (accounting.view) / absent (view seul), 403 sans \`clients.view\`, 401 anonyme. ## Note Au démarrage, \`symfony/intl\` (requis par ERP-57, contrainte \`Bic\`) manquait du vendor → \`composer install\` joué pour rétablir une base saine. ## ⚠️ Heads-up review (@Tristan) — fichiers « propriété » d'ERP-55 touchés Cette MR refactore deux fichiers introduits par ERP-55 : - **`ClientRepository::createListQueryBuilder()`** accueille désormais le filtrage `search` + `categoryType` (signature `(bool $includeArchived, ?string $search, ?string $categoryType)`). - **`ClientProvider`** délègue ce filtrage au repository → il **perd sa dépendance `EntityManager`** et ses méthodes privées `applySearch` / `applyCategoryType`. **Pourquoi** : DRY entre la liste paginée (`GET /api/clients`) et l'export — une seule source de vérité pour la sélection des clients. Effet de bord positif : ça résout **plus proprement la fuite d'abstraction** que tu avais pointée en revue ERP-55 (P2) — la sous-requête `categoryType` n'est plus construite via l'`EntityManager` injecté dans le provider, mais à l'intérieur du repository (là où l'accès Doctrine est légitime). Pas de changement de comportement de l'API liste : régression couverte par `ClientApiTest` (tri, exclusion archives, includeArchived, pagination) — tout vert. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #37 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>Pull request closed