Correctifs post-review M2 fournisseurs (P1 + P2/P3 + alignement M1) (#74)
Auto Tag Develop / tag (push) Successful in 8s

Correctifs issus de la review lead du stack M2 fournisseurs (ERP-84→113), répartis en priorités. Base : `develop`. Suite verte : `make test` 577 tests / 2475 assertions, `php-cs-fixer` 0 correction.

## P1 — défauts bloquants
- **ERP-89** — Le message de complétude Information ne fuit plus le nom de champ technique (`(champ "%s")` retiré). Correction miroir appliquée aux deux validators (Supplier + Client), accent uniformisé. Le `propertyPath` est conservé pour le mapping inline front.
- **ERP-112** — La fixture fournisseurs résout désormais la catégorie en filtrant sur le type `FOURNISSEUR` (via `CategoryInterface::getCategoryTypeCode()`), évitant de rattacher une catégorie homonyme d'un autre type (RG-2.10).
- **ERP-113** — Tests d'export complétés : dédup F3 (fournisseur multi-catégories rendu sur une seule ligne) ; gating SIREN prouvé via un utilisateur minimal non-admin portant `suppliers.view` + `suppliers.accounting.view` (nouveau helper `createUserWithPermissions`).

## P2 / P3
- **ERP-86** — `maxMessage` explicite sur `competitors` (Supplier).
- **ERP-92** — Garde `skipIfSitesModuleDisabled()` sur le test POST adresse sans site (évite un faux positif si le module Sites est désactivé).
- **ERP-89 bis** — Nouveau test : Admin authentifié non-Commerciale + Information incomplète → 200 (distinct du cas `user=null`).
- **ERP-85** — `down()` de la migration fournisseurs en `DROP TABLE IF EXISTS`.
- **ERP-87** — Reset de la mémoïsation payload en début de `process()` du SupplierProcessor + documentation du filtre `?archivedOnly` de l'export (parité avec le provider liste).
- **spec-back.md (M2)** — Alignée sur le code (le code fait foi) : security PATCH `manage or accounting.manage`, gating accounting par ajout de groupe (`SupplierReadGroupContextBuilder`), anti-N+1 via `hydrateListCollections` (pas de fetch-join), types de colonnes réels (`IDENTITY` / `TIMESTAMP(0)`).

## Alignement M1 ↔ M2
- **ERP-86/87 (Client)** — Mêmes corrections appliquées aux jumeaux M1 : message `competitors` explicite + reset mémoïsation `ClientProcessor`.

## Décision actée
- **RG-2.10 (catégorie)** : court-circuit conservé (une seule violation sur `categories`). Les violations partageant path + message sont fusionnées côté front ; ERP-101 (toutes les erreurs en un aller-retour) est déjà respecté car le Callback n'interrompt pas la validation des autres champs.

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #74
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #74.
This commit is contained in:
2026-06-08 08:47:43 +00:00
committed by admin malio
parent f031c70393
commit 9cda225bdf
14 changed files with 220 additions and 45 deletions
@@ -121,6 +121,12 @@ final class ClientProcessor implements ProcessorInterface
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
// Reinitialisation de la memoisation du payload en debut de traitement :
// le service est partage (stateful), on repart du corps de LA requete
// courante et on n'herite jamais des cles decodees d'une requete passee.
$this->decodedContent = null;
$this->decodedPayloadKeys = [];
$writableKeys = $this->writablePayloadKeys();
$isArchiveRequest = $this->guardArchive($data, $writableKeys);
@@ -114,6 +114,12 @@ final class SupplierProcessor implements ProcessorInterface
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
// Reinitialisation de la memoisation du payload en debut de traitement :
// le service est partage (stateful), on repart du corps de LA requete
// courante et on n'herite jamais des cles decodees d'une requete passee.
$this->decodedContent = null;
$this->decodedPayloadKeys = [];
$writableKeys = $this->writablePayloadKeys();
$isArchiveRequest = $this->guardArchive($data, $writableKeys);
@@ -57,6 +57,11 @@ final class SupplierExportController
#[IsGranted('commercial.suppliers.view')]
public function __invoke(Request $request): Response
{
// Memes filtres d'archivage que la vue liste (SupplierProvider) pour que
// l'export reflete exactement ce que l'utilisateur voit a l'ecran :
// - includeArchived : inclut les archives en plus des actifs ;
// - archivedOnly : restreint aux seules archives (prioritaire, cf.
// createListQueryBuilder).
$includeArchived = $this->readBool($request->query->get('includeArchived'));
$archivedOnly = $this->readBool($request->query->get('archivedOnly'));
$search = $request->query->getString('search') ?: null;
@@ -82,6 +82,12 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
*/
class SupplierFixtures extends Fixture implements DependentFixtureInterface
{
/**
* Type de categorie exige pour un fournisseur et ses adresses (RG-2.10).
* Miroir de Supplier::REQUIRED_CATEGORY_TYPE_CODE (non importable — regle n°1).
*/
private const string SUPPLIER_CATEGORY_TYPE_CODE = 'FOURNISSEUR';
/** Cache des categories resolues par nom (evite des requetes repetees). */
private array $categoryCache = [];
@@ -415,19 +421,27 @@ class SupplierFixtures extends Fixture implements DependentFixtureInterface
return $this->categoryCache[$name];
}
$category = $manager->getRepository(CategoryInterface::class)->findOneBy([
// RG-2.10 : on filtre explicitement sur le type FOURNISSEUR. Un lookup par
// le seul `name` rattacherait une categorie homonyme d'un autre type (ex.
// futur PRESTA) — donc du MAUVAIS type — ce qui violerait « au moins une
// categorie de type FOURNISSEUR ». Le filtre type est porte cote PHP
// (findBy ne sait pas filtrer une propriete imbriquee categoryType.code).
$candidates = $manager->getRepository(CategoryInterface::class)->findBy([
'name' => $name,
'deletedAt' => null,
]);
if (!$category instanceof CategoryInterface) {
throw new RuntimeException(sprintf(
'Categorie "%s" introuvable : CategoryFixtures doit tourner avant SupplierFixtures.',
$name,
));
foreach ($candidates as $candidate) {
if ($candidate instanceof CategoryInterface
&& self::SUPPLIER_CATEGORY_TYPE_CODE === $candidate->getCategoryTypeCode()) {
return $this->categoryCache[$name] = $candidate;
}
}
return $this->categoryCache[$name] = $category;
throw new RuntimeException(sprintf(
'Categorie FOURNISSEUR "%s" introuvable : CategoryFixtures doit tourner avant SupplierFixtures.',
$name,
));
}
/**