Files
Starseed/tests/Module/Commercial/Domain/Entity/SupplierValidationTest.php
T
matthieu a9c14704b7
Auto Tag Develop / tag (push) Successful in 7s
feat(catalog) : categories multi-types (M:N) + bouton Filtres liste (#75)
## Contexte

Une `Category` ne pouvait appartenir qu'à **un seul** `CategoryType` (ManyToOne). Le besoin métier : plusieurs types par catégorie. Cette MR fait passer la relation en **ManyToMany** et ajoute le bouton **« Filtres »** à droite de la liste des catégories (modèle Répertoire Clients).

Slice vertical complet (le passage M:N casse mécaniquement le contrat inter-module `CategoryInterface`, consommé par la RG-2.10 fournisseurs).

## Volet A — Relation M:N
- `Category.categoryType` (ManyToOne) → `categoryTypes` (ManyToMany, jonction `category_category_type`). Au moins un type obligatoire (`Assert\\Count(min:1)`).
- **Unicité du nom GLOBALE** parmi les actifs (`uq_category_name_active`, remplace `uq_category_name_type_active`). Message 409 reformulé.
- Migration : table de jonction + backfill + drop colonne `category_type_id` + nouvel index. Validée **rejouable sur base fraîche**.
- Contrat Shared : `getCategoryTypeCode()` → `getCategoryTypeCodes(): array`. `Supplier`/`SupplierAddress`/`SupplierFixtures` revalident « contient FOURNISSEUR » (RG-2.10).
- Provider/Repository : filtre type via sous-requête `EXISTS` (ne tronque pas la collection embarquée), eager-load anti-N+1.

## Volet B — Bouton « Filtres »
- Drawer recherche par nom + types multi (OR). Compteur de filtres actifs. État local, jamais persisté en URL.
- Back : filtres `?name=` et `?typeId[]=` sur la collection.

## Front
- Multi-select `MalioSelectCheckbox`, `useCategoryForm` en `categoryTypeIds[]`, colonne « Types », clés i18n.

## Tests / vérifs
- `make test` : **582 tests, 2474 assertions, 0 échec** 
- `make nuxt-test` : **236 tests** 
- `make php-cs-fixer-allow-risky` 
- Migration rejouée sur base fraîche (`make db-reset`) 
- Nouveau `CategoryFilterTest` (name partiel + typeId[] OR + multi-type non dupliqué)

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #75
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 09:47:15 +00:00

189 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\Commercial\Domain\Entity;
use App\Module\Commercial\Domain\Entity\Bank;
use App\Module\Commercial\Domain\Entity\PaymentType;
use App\Module\Commercial\Domain\Entity\Supplier;
use App\Module\Commercial\Domain\Entity\SupplierRib;
use App\Shared\Domain\Contract\CategoryInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Tests des contraintes inter-champs de l'entite Supplier portees par
* Assert\Callback (decision figee ERP-89) : RG-2.10 (categorie de type
* FOURNISSEUR), RG-2.07 (Virement -> banque), RG-2.08 (LCR -> >= 1 RIB).
*
* On valide l'entite avec le validator Symfony (mapping par attributs) et on
* assert le propertyPath exact de chaque violation (contrat ERP-101 :
* exploitable par extractApiViolations). Pas de base : les Callback ne touchent
* que des champs en memoire (categories via un double CategoryInterface).
*
* @internal
*/
final class SupplierValidationTest extends TestCase
{
private ValidatorInterface $validator;
protected function setUp(): void
{
$this->validator = Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator()
;
}
// === RG-2.10 : categories de type FOURNISSEUR ===
public function testFournisseurCategoryIsAccepted(): void
{
$supplier = $this->validSupplier();
self::assertSame([], $this->violationPaths($supplier));
}
public function testNonFournisseurCategoryIsRejectedOnCategoriesPath(): void
{
$supplier = new Supplier();
$supplier->setCompanyName('Recycla SAS');
$supplier->addCategory($this->category('CLIENT'));
self::assertContains('categories', $this->violationPaths($supplier));
}
public function testMultiTypeCategoryContainingFournisseurIsAccepted(): void
{
// RG-2.10 sous ManyToMany : une categorie qui PORTE FOURNISSEUR (parmi
// d'autres types) reste autorisee sur un fournisseur.
$supplier = new Supplier();
$supplier->setCompanyName('Recycla SAS');
$supplier->addCategory($this->category('CLIENT', 'FOURNISSEUR'));
self::assertNotContains('categories', $this->violationPaths($supplier));
}
// === RG-2.07 : Virement impose une banque ===
public function testVirementWithoutBankIsRejectedOnBankPath(): void
{
$supplier = $this->validSupplier();
$supplier->setPaymentType($this->paymentType('VIREMENT'));
self::assertContains('bank', $this->violationPaths($supplier));
}
public function testVirementWithBankPasses(): void
{
$supplier = $this->validSupplier();
$supplier->setPaymentType($this->paymentType('VIREMENT'));
$supplier->setBank(new Bank());
self::assertNotContains('bank', $this->violationPaths($supplier));
}
// === RG-2.08 : LCR impose au moins un RIB ===
public function testLcrWithoutRibIsRejectedOnRibsPath(): void
{
$supplier = $this->validSupplier();
$supplier->setPaymentType($this->paymentType('LCR'));
self::assertContains('ribs', $this->violationPaths($supplier));
}
public function testLcrWithRibPasses(): void
{
$supplier = $this->validSupplier();
$supplier->setPaymentType($this->paymentType('LCR'));
$supplier->addRib(new SupplierRib());
self::assertNotContains('ribs', $this->violationPaths($supplier));
}
public function testNeutralPaymentTypeRequiresNeitherBankNorRib(): void
{
// Un type de reglement neutre (ni VIREMENT ni LCR) n'exige ni banque ni RIB.
$supplier = $this->validSupplier();
$supplier->setPaymentType($this->paymentType('CHEQUE'));
$paths = $this->violationPaths($supplier);
self::assertNotContains('bank', $paths);
self::assertNotContains('ribs', $paths);
}
/**
* Fournisseur valide (nom + 1 categorie FOURNISSEUR), sans onglet
* Comptabilite renseigne : sert de base aux tests RG-2.07/2.08.
*/
private function validSupplier(): Supplier
{
$supplier = new Supplier();
$supplier->setCompanyName('Recycla SAS');
$supplier->addCategory($this->category('FOURNISSEUR'));
return $supplier;
}
/**
* @return list<string> propertyPaths des violations levees par le validator
*/
private function violationPaths(Supplier $supplier): array
{
$paths = [];
foreach ($this->validator->validate($supplier) as $violation) {
$paths[] = $violation->getPropertyPath();
}
return $paths;
}
/**
* Double minimal de CategoryInterface (pas d'acces base) PORTANT les codes de
* type voulus — seul element regarde par validateCategoryType. Variadic pour
* couvrir le cas multi-types (ManyToMany).
*
* @return list<string> n'est pas le type de retour : helper renvoyant un double
*/
private function category(string ...$typeCodes): CategoryInterface
{
return new class(array_values($typeCodes)) implements CategoryInterface {
/** @param list<string> $typeCodes */
public function __construct(private readonly array $typeCodes) {}
public function getId(): ?int
{
return 1;
}
public function getName(): ?string
{
return 'Categorie test';
}
public function getCode(): ?string
{
return 'TEST';
}
/** @return list<string> */
public function getCategoryTypeCodes(): array
{
return $this->typeCodes;
}
};
}
private function paymentType(string $code): PaymentType
{
$type = new PaymentType();
$type->setCode($code);
$type->setLabel($code);
return $type;
}
}