Compare commits

..

1 Commits

Author SHA1 Message Date
Matthieu 9a6ec71981 feat(technique) : validations RG comptables server-side (RG-3.07 Virement/banque, RG-3.08 LCR/RIB) (ERP-136)
- Provider::validatePaymentTypeConsistency (Assert\Callback, miroir Supplier ERP-89) :
  RG-3.07 VIREMENT impose une banque (violation sur bank),
  RG-3.08 LCR impose au moins un RIB (violation sur paymentType).
- ProviderProcessor : docblock realigne (RG-3.07/3.08 portees par l'entite).
- AbstractProviderApiTestCase::bank() helper referentiel.
- ProviderAccountingValidationTest : 4 cas (negatif 422 / positif 200) par RG.

Les RG-3.03/3.05/3.09 (contraintes d'entite) et l'ecriture cloisonnee (gardes
processors, RG-3.17/2.13) etaient deja posees en ERP-133/134/135 et restent couvertes.
2026-06-12 11:51:12 +02:00
4 changed files with 153 additions and 6 deletions
@@ -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;
@@ -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<Provider, Provider>
*/
@@ -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).
*
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Tests\Module\Technique\Api;
/**
* Tests fonctionnels des RG comptables inter-champs portees par les Assert\Callback
* de l'entite Provider (M3, RG-3.07 / RG-3.08), via le PATCH de l'onglet
* Comptabilite (groupe provider:write:accounting). On asserte le code HTTP et le
* propertyPath de la violation (consommable par extractApiViolations cote front,
* ERP-101). Jumeau de SupplierAccountingApiTest (M2), sans le bloc « completude de
* l'onglet » : le prestataire est minimal et n'impose pas les six scalaires
* comptables (spec M3 § 3.1).
*
* @internal
*/
final class ProviderAccountingValidationTest extends AbstractProviderApiTestCase
{
// === RG-3.07 : Virement impose une banque ===
public function testVirementWithoutBankReturns422OnBankPath(): void
{
$client = $this->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.
}