Files
Starseed/composer.json
T
matthieu 38f9f164f1
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m40s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m15s
[ERP-58] Implémenter l'export XLSX du répertoire clients (#37)
## 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>
2026-06-01 19:28:34 +00:00

101 lines
2.9 KiB
JSON

{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/doctrine-orm": "^4.2",
"api-platform/symfony": "^4.2",
"doctrine/doctrine-bundle": "^3.2",
"doctrine/doctrine-migrations-bundle": "^4.0",
"doctrine/orm": "^3.6",
"lexik/jwt-authentication-bundle": "^3.2",
"nelmio/cors-bundle": "^2.6",
"nyholm/psr7": "^1.8",
"phpdocumentor/reflection-docblock": "^5.6|^6.0",
"phpoffice/phpspreadsheet": "^5.7",
"phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "8.0.*",
"symfony/console": "8.0.*",
"symfony/dotenv": "8.0.*",
"symfony/expression-language": "8.0.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "8.0.*",
"symfony/intl": "8.0.*",
"symfony/mime": "8.0.*",
"symfony/monolog-bundle": "^4.0",
"symfony/property-access": "8.0.*",
"symfony/property-info": "8.0.*",
"symfony/rate-limiter": "8.0.*",
"symfony/runtime": "8.0.*",
"symfony/security-bundle": "8.0.*",
"symfony/serializer": "8.0.*",
"symfony/twig-bundle": "8.0.*",
"symfony/uid": "8.0.*",
"symfony/validator": "8.0.*",
"symfony/yaml": "8.0.*"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"bump-after-update": true,
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*",
"symfony/polyfill-php83": "*",
"symfony/polyfill-php84": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "8.0.*"
}
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.3",
"friendsofphp/php-cs-fixer": "^3.94",
"phpunit/phpunit": "^13.0",
"symfony/browser-kit": "8.0.*",
"symfony/http-client": "8.0.*"
}
}