diff --git a/src/Module/Technique/Domain/Entity/Provider.php b/src/Module/Technique/Domain/Entity/Provider.php index 3b1da19..8c95e8a 100644 --- a/src/Module/Technique/Domain/Entity/Provider.php +++ b/src/Module/Technique/Domain/Entity/Provider.php @@ -140,6 +140,12 @@ class Provider implements TimestampableInterface, BlamableInterface */ private const string REQUIRED_CATEGORY_TYPE_CODE = 'PRESTATAIRE'; + /** Code pivot du type de reglement imposant une banque (RG-3.07). */ + private const string PAYMENT_TYPE_VIREMENT = 'VIREMENT'; + + /** Code pivot du type de reglement imposant au moins un RIB (RG-3.08). */ + private const string PAYMENT_TYPE_LCR = 'LCR'; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -291,6 +297,44 @@ class Provider implements TimestampableInterface, BlamableInterface } } + /** + * RG-3.07 / RG-3.08 : coherence du type de reglement comptable. Comme au M2 + * (decision figee ERP-89, jumeau Supplier::validatePaymentTypeConsistency), + * ces RG inter-champs passent par une contrainte d'entite (Assert\Callback + + * ->atPath()) et NON par le ProviderProcessor, afin que chaque 422 porte un + * propertyPath exploitable par extractApiViolations (mapping inline sous le + * champ, pas un toast — convention ERP-101). + * - RG-3.07 : paymentType = VIREMENT impose une banque -> violation sur `bank`. + * - RG-3.08 : paymentType = LCR impose au moins un RIB -> violation sur + * `paymentType` (les RIB n'ont pas de champ de formulaire ou s'ancrer quand + * la liste est vide ; l'erreur s'affiche donc sous le select « Type de + * règlement », binde cote front). Le 409 sur DELETE du dernier RIB en LCR est + * porte par le ProviderRibProcessor (ERP-135). + * + * Ces champs vivant dans le groupe d'ecriture comptable (absent du POST, qui + * n'expose que provider:write:main), la contrainte ne mord en pratique que sur + * le PATCH de l'onglet Comptabilite. + */ + #[Assert\Callback] + public function validatePaymentTypeConsistency(ExecutionContextInterface $context): void + { + $paymentCode = $this->paymentType?->getCode(); + + if (self::PAYMENT_TYPE_VIREMENT === $paymentCode && null === $this->bank) { + $context->buildViolation('La banque est obligatoire pour le type de règlement Virement.') + ->atPath('bank') + ->addViolation() + ; + } + + if (self::PAYMENT_TYPE_LCR === $paymentCode && $this->ribs->isEmpty()) { + $context->buildViolation('Au moins un RIB est obligatoire pour le type de règlement LCR.') + ->atPath('paymentType') + ->addViolation() + ; + } + } + public function getId(): ?int { return $this->id; diff --git a/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderProcessor.php b/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderProcessor.php index a439a13..0552d13 100644 --- a/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderProcessor.php +++ b/src/Module/Technique/Infrastructure/ApiPlatform/State/Processor/ProviderProcessor.php @@ -52,12 +52,13 @@ use Symfony\Component\Validator\ConstraintViolationList; * collisions d'unicite en 409 (RG-3.10 doublon de nom ; RG-3.14 conflit de * restauration). * - * La RG-3.09 (categorie de type PRESTATAIRE) est portee par un Assert\Callback + - * ->atPath() sur l'entite Provider (joue par API Platform AVANT ce processor), - * pour que la 422 porte un propertyPath consommable par extractApiViolations - * (mapping inline, pas un toast — convention ERP-101). Les RG-3.07 (Virement -> - * banque) et RG-3.08 (LCR -> RIB) relevent de l'onglet Comptabilite / sous-ressource - * RIB (ticket dedie) et ne sont pas portees ici. + * Les RG inter-champs RG-3.07 (Virement -> banque), RG-3.08 (LCR -> >= 1 RIB) et + * RG-3.09 (categorie de type PRESTATAIRE) sont portees par des Assert\Callback + + * ->atPath() sur les entites Provider / ProviderAddress (jouees par API Platform + * AVANT ce processor), pour que chaque 422 porte un propertyPath consommable par + * extractApiViolations (mapping inline, pas un toast — convention ERP-101). Le 409 + * sur DELETE du dernier RIB en LCR (volet ecriture de RG-3.08) est porte par le + * ProviderRibProcessor (ERP-135). * * @implements ProcessorInterface */ diff --git a/tests/Module/Technique/Api/AbstractProviderApiTestCase.php b/tests/Module/Technique/Api/AbstractProviderApiTestCase.php index 15c6522..b4326ba 100644 --- a/tests/Module/Technique/Api/AbstractProviderApiTestCase.php +++ b/tests/Module/Technique/Api/AbstractProviderApiTestCase.php @@ -7,6 +7,7 @@ namespace App\Tests\Module\Technique\Api; use ApiPlatform\Symfony\Bundle\Test\Client; use App\Module\Catalog\Domain\Entity\Category; use App\Module\Catalog\Domain\Entity\CategoryType; +use App\Module\Commercial\Domain\Entity\Bank; use App\Module\Commercial\Domain\Entity\PaymentType; use App\Module\Core\Domain\Entity\Permission; use App\Module\Core\Domain\Entity\Role; @@ -335,6 +336,22 @@ abstract class AbstractProviderApiTestCase extends AbstractApiTestCase return $paymentType; } + /** + * Recupere une banque seedee (CommercialReferentialFixtures) par code (ex. SG). + * Echoue explicitement si absente (fixtures non chargees). + */ + protected function bank(string $code): Bank + { + $bank = $this->getEm()->getRepository(Bank::class)->findOneBy(['code' => $code]); + + self::assertNotNull( + $bank, + sprintf('Banque "%s" introuvable : fixtures comptables chargees (make test-db-setup) ?', $code), + ); + + return $bank; + } + /** * Indexe les violations d'un corps 422 par propertyPath (assert ciblee). * diff --git a/tests/Module/Technique/Api/ProviderAccountingValidationTest.php b/tests/Module/Technique/Api/ProviderAccountingValidationTest.php new file mode 100644 index 0000000..aecbd2f --- /dev/null +++ b/tests/Module/Technique/Api/ProviderAccountingValidationTest.php @@ -0,0 +1,85 @@ +createAdminClient(); + $seed = $this->seedProvider('Virement No Bank'); + + $response = $client->request('PATCH', '/api/providers/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE, 'Accept' => self::LD], + 'json' => ['paymentType' => '/api/payment_types/'.$this->paymentType('VIREMENT')->getId()], + ]); + + self::assertResponseStatusCodeSame(422); + self::assertArrayHasKey('bank', $this->violationsByPath($response->toArray(false))); + } + + public function testVirementWithBankReturns200(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedProvider('Virement With Bank'); + + $client->request('PATCH', '/api/providers/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => [ + 'paymentType' => '/api/payment_types/'.$this->paymentType('VIREMENT')->getId(), + 'bank' => '/api/banks/'.$this->bank('SG')->getId(), + ], + ]); + + self::assertResponseStatusCodeSame(200); + } + + // === RG-3.08 : LCR impose au moins un RIB (volet ecriture du formulaire) === + + public function testLcrWithoutRibReturns422OnPaymentTypePath(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedProvider('Lcr No Rib'); + + $response = $client->request('PATCH', '/api/providers/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE, 'Accept' => self::LD], + 'json' => ['paymentType' => '/api/payment_types/'.$this->paymentType('LCR')->getId()], + ]); + + self::assertResponseStatusCodeSame(422); + // Miroir client : violation portee sur `paymentType` (select « Type de + // règlement »), les RIB n'ayant pas de champ de formulaire pour l'ancrer. + self::assertArrayHasKey('paymentType', $this->violationsByPath($response->toArray(false))); + } + + public function testLcrWithRibReturns200(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedProvider('Lcr With Rib'); + $this->addRib($seed); + + $client->request('PATCH', '/api/providers/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['paymentType' => '/api/payment_types/'.$this->paymentType('LCR')->getId()], + ]); + + self::assertResponseStatusCodeSame(200); + } + + // violationsByPath() : helper mutualise dans AbstractProviderApiTestCase. +}