test(technique) : couvrir RG-3.x PHPUnit + capturer le contrat JSON (ERP-139) (#100)
Auto Tag Develop / tag (push) Successful in 11s

Ticket Lesstime #139 (M3 — Répertoire prestataires, position 1.9). DoD back avant le front : suite PHPUnit consolidée sur la matrice § 8.1 + captures JSON réelles dans la spec § 4.0.bis.

## Contenu
- **Fix réfs comptables** : `provider:read:accounting` ajouté sur `TvaMode`/`PaymentDelay`/`PaymentType`/`Bank` — sans ça elles sortaient en IRI nu dans le détail prestataire (réplique du fix ERP-92 du M2, piège #1 § 4.0.bis).
- **`ProviderSerializationContractTest`** (13 tests) : gating RIB/scalaires par omission, réfs compta en objet `{id,code,label}`, `isArchived`, embed categories/sites liste+détail, sous-collections, enveloppe AP4 ; `testDodReferenceJsonShape` dumpe le JSON réel (`PROVIDER_DOD_DUMP=1`).
- **`ProviderAuditTest`** (5 tests) : create/update/archive (`technique.Provider`), iban/bic dans le diff (`technique.ProviderRib`, pas dAuditIgnore), trace M2M `sites`.
- **`ProviderListTest`** étendu : `?pagination=false`, anti-N+1, filtre `?typeCode=PRESTATAIRE`.
- **`ProviderRbacGatingTest`** étendu : restauration en conflit de nom → 409 (RG-3.14).
- **`ProviderFixtures`** (§ 8.4) : démo idempotente (complet VIREMENT+banque+RIB, LCR+RIB, CHEQUE multi-cat, minimal, archivé) répartie sur sites 86/17/82 ; skip en env `test`.
- Helper `seedCompleteProvider` ; spec § 4.0.bis : gabarits remplacés par les captures réelles (liste + détail avec/sans accounting.view).

## Vérifications
- `make php-cs-fixer-allow-risky` → 0 fichier
- `make test` → OK, 677 tests, 3328 assertions (garde-fous globaux verts)

## Notes
- MR stackée sur ERP-138 (base = sa branche).
- Fixtures démo exercées en dev via `make fixtures` (autowiring vérifié).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #100
This commit was merged in pull request #100.
This commit is contained in:
2026-06-12 14:44:43 +00:00
parent d5462bcf42
commit 3fe0f676f6
41 changed files with 5725 additions and 100 deletions
+62 -10
View File
@@ -13,6 +13,8 @@ use App\Module\Commercial\Domain\Entity\Bank;
use App\Module\Commercial\Domain\Entity\PaymentDelay;
use App\Module\Commercial\Domain\Entity\PaymentType;
use App\Module\Commercial\Domain\Entity\TvaMode;
use App\Module\Technique\Infrastructure\ApiPlatform\State\Processor\ProviderProcessor;
use App\Module\Technique\Infrastructure\ApiPlatform\State\Provider\ProviderProvider;
use App\Module\Technique\Infrastructure\Doctrine\DoctrineProviderRepository;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Contract\BlamableInterface;
@@ -53,11 +55,12 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* reference de donnees de reference, pas de logique inter-module.
*
* Contrat de serialisation (RETEX M1, 3 maillons — spec § 4.0) : les read-groups
* sont poses ICI (source unique). L'#[ApiResource] est ici un SQUELETTE (operations
* + contextes + security) ; le ProviderProvider (liste paginee anti-N+1, exclusion
* archives, cloisonnement site, gating accounting) et le ProviderProcessor
* (normalisation, archivage, 409 doublon, RG-3.07 / RG-3.08) sont cables au ticket
* suivant (ERP-134) — ils ne sont volontairement PAS references ici.
* sont poses ICI (source unique). L'#[ApiResource] cable (ERP-134) le ProviderProvider
* (liste paginee anti-N+1, exclusion archives, cloisonnement site lecture + detail
* 404) et le ProviderProcessor (normalisation, archivage, mode strict par groupe,
* cloisonnement site ecriture, 409 doublon). Le groupe provider:read:accounting est
* ajoute dynamiquement au contexte par le ProviderReadGroupContextBuilder selon la
* permission accounting.view (ERP-134) — jamais pose en dur sur l'operation.
*
* Audite (#[Auditable], tous champs — y compris RIB embarques, § 2.7) +
* Timestampable / Blamable via le trait Shared.
@@ -69,17 +72,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
// La liste embarque les categories (code/name, groupe category:read) et
// les sites du prestataire (name/postalCode, groupe site:read — relation
// DIRECTE provider.sites, RG-3.03). Maillon (c) : category:read +
// site:read presents dans le contexte. L'hydratation anti-N+1 sera
// cablee par le ProviderProvider (ERP-134, cf. DoctrineProviderRepository).
// site:read presents dans le contexte. Hydratation anti-N+1 cablee par
// le ProviderProvider (cf. DoctrineProviderRepository::hydrateListCollections).
normalizationContext: ['groups' => ['provider:read', 'category:read', 'site:read', 'default:read']],
provider: ProviderProvider::class,
),
new Get(
security: "is_granted('technique.providers.view')",
// Detail : prestataire + sous-collections embarquees (contacts, adresses
// + leurs sites/categories/contacts) + RIB (gates compta). Le groupe
// provider:read:accounting est volontairement ABSENT : il sera ajoute au
// contexte par le ProviderProvider / ReadGroupContextBuilder selon la
// permission accounting.view (ERP-134, parade fuite IBAN/BIC — bug #4 M1).
// provider:read:accounting est volontairement ABSENT : il est ajoute au
// contexte par le ProviderReadGroupContextBuilder selon la permission
// accounting.view (parade fuite IBAN/BIC — bug #4 M1).
normalizationContext: ['groups' => [
'provider:read',
'provider:item:read',
@@ -87,11 +91,13 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
'site:read',
'default:read',
]],
provider: ProviderProvider::class,
),
new Post(
security: "is_granted('technique.providers.manage')",
normalizationContext: ['groups' => ['provider:read', 'category:read', 'site:read', 'default:read']],
denormalizationContext: ['groups' => ['provider:write:main']],
processor: ProviderProcessor::class,
),
new Patch(
// Security elargie : `manage` OU `accounting.manage` — le role Compta n'a
@@ -105,6 +111,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
'provider:write:accounting',
'provider:write:archive',
]],
provider: ProviderProvider::class,
processor: ProviderProcessor::class,
),
// Pas de Delete au M3 (HP M4). Archivage via PATCH { isArchived: true }.
],
@@ -132,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]
@@ -283,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;