[ERP-58] Implémenter l'export XLSX du répertoire clients #37

Merged
malio merged 2 commits from feature/ERP-58-export-xlsx-clients into feature/ERP-57-sous-ressources-contacts-adresses-rib 2026-06-01 19:28:36 +00:00
Owner

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.

## 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.
matthieu changed title from feat(commercial) : export XLSX du répertoire clients (ERP-58) to [ERP-58] Implémenter l'export XLSX du répertoire clients 2026-06-01 13:53:17 +00:00
matthieu added the backtype/feat labels 2026-06-01 13:53:20 +00:00
matthieu added 2 commits 2026-06-01 14:31:18 +00:00
matthieu force-pushed feature/ERP-58-export-xlsx-clients from c25b3146e8 to 9f3037c5c4 2026-06-01 14:31:18 +00:00 Compare
malio merged commit 38f9f164f1 into feature/ERP-57-sous-ressources-contacts-adresses-rib 2026-06-01 19:28:36 +00:00
malio deleted branch feature/ERP-58-export-xlsx-clients 2026-06-01 19:28:36 +00:00
matthieu added the M1-Client label 2026-06-01 21:15:00 +00:00
Sign in to join this conversation.