feat(transport) : export XLSX répertoire + prix (ERP-162) #118

Merged
matthieu merged 2 commits from feat/erp-162-carrier-export into feat/erp-161-carrier-prices 2026-06-16 13:47:06 +00:00
Owner

GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived, search, certificationType) + GET /api/carriers/{id}/prices/export.xlsx (tableau Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec #[Route(priority: 1)], génération déléguée au service Shared SpreadsheetExporterInterface. Tests : 200 + en-têtes fichier, respect des filtres, regroupement par contenant, 404 transporteur inconnu, 403 sans transport.carriers.view, 401 anonyme. Lien ticket ERP-162.

GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived, search, certificationType) + GET /api/carriers/{id}/prices/export.xlsx (tableau Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec #[Route(priority: 1)], génération déléguée au service Shared SpreadsheetExporterInterface. Tests : 200 + en-têtes fichier, respect des filtres, regroupement par contenant, 404 transporteur inconnu, 403 sans transport.carriers.view, 401 anonyme. Lien ticket ERP-162.
matthieu added the backM4-Transporteurtype/feat labels 2026-06-16 09:08:54 +00:00
tristan reviewed 2026-06-16 10:05:41 +00:00
tristan left a comment
Owner

Code review ERP-162 (export XLSX répertoire + prix transporteur) — relue contre spec-back § 4.6.

Verdict : PR solide, aucun constat bloquant. Deux controllers custom bien faits :

  • priority: 1 présent et documenté sur les deux routes (/api/carriers/export.xlsx et /api/carriers/{id}/prices/export.xlsx) → évite le conflit avec l'item API Platform {id}.{_format} (règle CLAUDE.md respectée).
  • Security transport.carriers.view, soft-delete → 404 (cohérent CarrierProvider), génération déléguée au service Shared SpreadsheetExporterInterface (le COMMENT générique séparé du QUOI métier), cross-module via contrats Shared (aucun use App\Module\Commercial\…).
  • Export répertoire = mêmes filtres que la liste (l'export reflète l'écran) ; colonnes exactement celles de spec-back § 4.6. Export prix regroupé Benne/Fond mouvant, Forfait/Tonne ventilés.

Tests = les meilleurs de la stack : ils chargent réellement le XLSX (PhpSpreadsheet) et assertent le contenu des cellules (colonnes QUALIMAT, regroupement, libellés cross-module), + RBAC 403/401 + 404 + filtres.

Quelques 🟡 mineurs en inline (sémantique colonne APRO/Sites à confirmer, cohérence archivedOnly). Rien qui bloque.

Code review ERP-162 (export XLSX répertoire + prix transporteur) — relue contre spec-back § 4.6. **Verdict : PR solide, aucun constat bloquant.** Deux controllers custom bien faits : - **`priority: 1`** présent et documenté sur les deux routes (`/api/carriers/export.xlsx` et `/api/carriers/{id}/prices/export.xlsx`) → évite le conflit avec l'item API Platform `{id}.{_format}` (règle CLAUDE.md respectée). - Security `transport.carriers.view`, soft-delete → 404 (cohérent `CarrierProvider`), génération déléguée au service Shared `SpreadsheetExporterInterface` (le COMMENT générique séparé du QUOI métier), cross-module via contrats Shared (aucun `use App\Module\Commercial\…`). - Export répertoire = **mêmes filtres que la liste** (l'export reflète l'écran) ; colonnes exactement celles de spec-back § 4.6. Export prix regroupé Benne/Fond mouvant, Forfait/Tonne ventilés. **Tests = les meilleurs de la stack** : ils chargent réellement le XLSX (PhpSpreadsheet) et assertent le **contenu des cellules** (colonnes QUALIMAT, regroupement, libellés cross-module), + RBAC 403/401 + 404 + filtres. Quelques 🟡 mineurs en inline (sémantique colonne APRO/Sites à confirmer, cohérence `archivedOnly`). Rien qui bloque.
@@ -0,0 +45,4 @@
private readonly SpreadsheetExporterInterface $exporter,
) {}
#[Route('/api/carriers/export.xlsx', name: 'transport_carriers_export_xlsx', methods: ['GET'], priority: 1)]
Owner

🟢 priority: 1 correct et bien justifié (sans lui, API Platform capterait /api/carriers/export.xlsx comme GET /api/carriers/{id}.{_format} avec id="export"). IsGranted('transport.carriers.view'), génération déléguée au service Shared, et surtout les mêmes filtres que GET /api/carriers (via createListQueryBuilder) → l'export reflète exactement la vue liste. Colonnes conformes à spec-back § 4.6. RAS.

🟢 `priority: 1` correct et bien justifié (sans lui, API Platform capterait `/api/carriers/export.xlsx` comme `GET /api/carriers/{id}.{_format}` avec id="export"). `IsGranted('transport.carriers.view')`, génération déléguée au service Shared, et surtout les **mêmes filtres que `GET /api/carriers`** (via `createListQueryBuilder`) → l'export reflète exactement la vue liste. Colonnes conformes à spec-back § 4.6. RAS.
@@ -0,0 +54,4 @@
// - includeArchived : reintegre les archives en plus des actifs ;
// - search : recherche fuzzy sur le nom ;
// - certificationType : filtre repetable (?certificationType[]=A&...).
$includeArchived = $this->readBool($request->query->get('includeArchived'));
Owner

🟡 Cohérence filtres (lié à mon constat #112). L'export ne lit que includeArchived (miroir de CarrierProvider, qui n'expose pas archivedOnly contrairement aux 3 autres répertoires). C'est cohérent avec le provider actuel — mais si tu ajoutes archivedOnly au CarrierProvider (pour le toggle « Voir les archivés » du front ERP-164), pense à le répliquer ici sinon l'export divergera de la liste affichée. À traiter ensemble.

🟡 Cohérence filtres (lié à mon constat #112). L'export ne lit que `includeArchived` (miroir de `CarrierProvider`, qui n'expose pas `archivedOnly` contrairement aux 3 autres répertoires). C'est cohérent avec le provider actuel — mais si tu ajoutes `archivedOnly` au `CarrierProvider` (pour le toggle « Voir les archivés » du front ERP-164), **pense à le répliquer ici** sinon l'export divergera de la liste affichée. À traiter ensemble.
@@ -0,0 +133,4 @@
* - branche FOURNISSEUR : l'adresse d'approvisionnement, identifiee par la
* raison sociale du fournisseur (cf. note de classe sur les contrats Shared).
*/
private function formatDeparture(CarrierPrice $price): string
Owner

🟡 À confirmer (sémantique colonne). Le controller mappe « Adresse APRO ou Adresse Sites » ainsi : CLIENT → site de départ (86/17/82), FOURNISSEUR → adresse d'appro (fournisseur). C'est business-correct (un prix CLIENT n'a pas d'appro) et c'est bien testé (testExportRendersGroupedPriceRows). Mais le texte littéral du docx p.10 dit « Si Client → Adresse APRO sinon Adresse Sites » — soit l'inverse. Le docx est vraisemblablement imprécis et ton interprétation est la bonne ; juste s'assurer que l'écran Consultation onglet Prix (ERP-170) appliquera exactement le même mapping pour la cohérence export↔écran.

🟡 À confirmer (sémantique colonne). Le controller mappe « Adresse APRO ou Adresse Sites » ainsi : CLIENT → site de départ (86/17/82), FOURNISSEUR → adresse d'appro (fournisseur). C'est **business-correct** (un prix CLIENT n'a pas d'appro) et c'est bien **testé** (`testExportRendersGroupedPriceRows`). Mais le texte littéral du docx p.10 dit « Si Client → Adresse APRO sinon Adresse Sites » — soit l'inverse. Le docx est vraisemblablement imprécis et ton interprétation est la bonne ; juste s'assurer que l'écran Consultation onglet Prix (ERP-170) appliquera **exactement** le même mapping pour la cohérence export↔écran.
@@ -0,0 +59,4 @@
* cross-module (le prix CLIENT livre chez le client, le prix FOURNISSEUR part
* de l'adresse du fournisseur).
*/
public function testExportRendersGroupedPriceRows(): void
Owner

🟢 Très bon niveau de test (le meilleur de la stack) : le test charge réellement le XLSX via PhpSpreadsheet et assert le contenu — regroupement Benne/Fond mouvant, état du prix (« Validé »/« En cours »), et les libellés cross-module (TESTCARRIERREF CLI en colonne livraison pour un prix CLIENT, TESTCARRIERREF FRN en colonne départ pour un FOURNISSEUR). Ça verrouille à la fois le mapping de branche et l'intégration resolve_target_entities. 👍

🟢 Très bon niveau de test (le meilleur de la stack) : le test charge réellement le XLSX via PhpSpreadsheet et assert le **contenu** — regroupement Benne/Fond mouvant, état du prix (« Validé »/« En cours »), et les libellés cross-module (`TESTCARRIERREF CLI` en colonne livraison pour un prix CLIENT, `TESTCARRIERREF FRN` en colonne départ pour un FOURNISSEUR). Ça verrouille à la fois le mapping de branche et l'intégration `resolve_target_entities`. 👍
matthieu force-pushed feat/erp-162-carrier-export from f0c09d6961 to 15c6207c78 2026-06-16 13:06:28 +00:00 Compare
matthieu added 2 commits 2026-06-16 13:15:02 +00:00
GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived,
search, certificationType) et GET /api/carriers/{id}/prices/export.xlsx (tableau
Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec
#[Route(priority: 1)] pour éviter le conflit API Platform {id}, génération
déléguée au service Shared SpreadsheetExporterInterface.
L'export XLSX du repertoire reflete la vue liste : il propage desormais
?archivedOnly comme CarrierProvider (sinon l'export divergerait de l'ecran
quand le toggle « Voir les archives » est actif).
matthieu force-pushed feat/erp-162-carrier-export from 15c6207c78 to c0fa00c9c5 2026-06-16 13:15:02 +00:00 Compare
matthieu merged commit c0fa00c9c5 into feat/erp-161-carrier-prices 2026-06-16 13:47:06 +00:00
matthieu deleted branch feat/erp-162-carrier-export 2026-06-16 13:47:07 +00:00
Sign in to join this conversation.