fix(front+back) : suppression de la categorie du bloc adresse prestataire (ERP-193)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 47s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 5m13s

La categorie portee par l'ADRESSE du prestataire (M2M provider_address_category)
est retiree de toutes les couches : champ + prop du bloc adresse, type/payload/
mapping front, entite ProviderAddress (M2M, Assert\Count, validateCategoryType,
accesseurs), fixtures, contexte de serialisation. Nouvelle migration de drop de la
table (namespace racine pour l'ordre post-creation). La categorie du PRESTATAIRE
lui-meme (provider_category, repertoire, filtre, formulaire principal) est conservee.
This commit is contained in:
2026-06-22 09:44:44 +02:00
parent a0c356c9fb
commit bd71caa289
21 changed files with 109 additions and 198 deletions
@@ -278,8 +278,7 @@ class Provider implements TimestampableInterface, BlamableInterface
/**
* RG-3.09 : toute categorie posee sur le prestataire doit etre de type
* PRESTATAIRE -> sinon 422 avec violation sur le champ `categories`
* (propertyPath aligne ERP-101, message FR ERP-107). Miroir de
* ProviderAddress::validateCategoryType. S'appuie sur
* (propertyPath aligne ERP-101, message FR ERP-107). S'appuie sur
* CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est
* acceptee des qu'elle PORTE le type PRESTATAIRE ; pas d'import du module
* Catalog, regle ABSOLUE n°1). Joue avant la base via la validation API
@@ -14,7 +14,6 @@ use App\Module\Technique\Infrastructure\ApiPlatform\State\Processor\ProviderAddr
use App\Module\Technique\Infrastructure\ApiPlatform\State\Provider\ProviderSubResourceItemProvider;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\CategoryInterface;
use App\Shared\Domain\Contract\SiteInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
@@ -24,21 +23,17 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Adresse d'un prestataire (1:n) — onglet Adresse. Version SIMPLIFIEE de
* SupplierAddress : PAS de address_type (PROSPECT/DEPART/RENDU), PAS de bennes,
* PAS de triage_provider (champs specifiques fournisseur). Champs : country /
* postal_code / city / street / street_complement + M2M sites / contacts /
* categories.
* postal_code / city / street / street_complement + M2M sites / contacts.
*
* Relations M2M :
* - sites : SiteInterface (module Sites) via resolve_target_entities — au moins
* un site obligatoire (RG-3.05, Assert\Count). Site n'a pas de `code`.
* - contacts : ProviderContact (meme module).
* - categories : CategoryInterface (module Catalog) via resolve_target_entities —
* type PRESTATAIRE attendu (RG-3.09, Assert\Callback validateCategoryType).
*
* Embarquee sous `provider.addresses` au detail (groupe provider:item:read,
* maillon (a)).
@@ -50,7 +45,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* - GET /api/provider_addresses/{id} : lecture unitaire (security view) — la lecture
* courante reste via le parent. Pas de GET collection autonome.
* Tout passe par le ProviderAddressProcessor (rattachement parent + cloisonnement
* d'ecriture des sites, § 2.13). Les regles RG-3.05/3.06/3.09 sont portees par les
* d'ecriture des sites, § 2.13). Les regles RG-3.05/3.06 sont portees par les
* contraintes de l'entite (jouees avant le processor).
*
* Audite (#[Auditable]) + Timestampable / Blamable.
@@ -59,9 +54,9 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
operations: [
new Get(
security: "is_granted('technique.providers.view')",
// site:read + category:read : embarquent les Site / Category lies
// (maillon (c)) plutot que des IRI nus dans le retour.
normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'category:read', 'default:read']],
// site:read : embarque les Site lies (maillon (c)) plutot que des IRI
// nus dans le retour.
normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'default:read']],
// Cloisonnement par site du prestataire parent (§ 2.13) : 404 hors perimetre.
provider: ProviderSubResourceItemProvider::class,
),
@@ -76,13 +71,13 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
// manuellement par ProviderAddressProcessor::linkParent (404 si absent).
read: false,
security: "is_granted('technique.providers.manage')",
normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'category:read', 'default:read']],
normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'default:read']],
denormalizationContext: ['groups' => ['provider:write:addresses']],
processor: ProviderAddressProcessor::class,
),
new Patch(
security: "is_granted('technique.providers.manage')",
normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'category:read', 'default:read']],
normalizationContext: ['groups' => ['provider:item:read', 'site:read', 'default:read']],
denormalizationContext: ['groups' => ['provider:write:addresses']],
provider: ProviderSubResourceItemProvider::class,
processor: ProviderAddressProcessor::class,
@@ -102,13 +97,6 @@ class ProviderAddress implements TimestampableInterface, BlamableInterface, Prov
{
use TimestampableBlamableTrait;
/**
* RG-3.09 : seules les categories PORTANT ce type sont autorisees sur une
* adresse prestataire. S'appuie sur CategoryInterface::getCategoryTypeCodes()
* (pas d'import du module Catalog — regle ABSOLUE n°1).
*/
private const string REQUIRED_CATEGORY_TYPE_CODE = 'PRESTATAIRE';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
@@ -175,46 +163,10 @@ class ProviderAddress implements TimestampableInterface, BlamableInterface, Prov
#[Groups(['provider:item:read', 'provider:write:addresses'])]
private Collection $contacts;
// RG-3.09 : au moins une categorie de type PRESTATAIRE par adresse (le type est
// controle par validateCategoryType ; le minimum par Assert\Count, miroir sites).
/** @var Collection<int, CategoryInterface> */
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
#[ORM\JoinTable(name: 'provider_address_category')]
#[ORM\JoinColumn(name: 'provider_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'RESTRICT')]
#[Assert\Count(min: 1, minMessage: 'Au moins une catégorie est obligatoire.')]
#[Groups(['provider:item:read', 'provider:write:addresses'])]
private Collection $categories;
public function __construct()
{
$this->sites = new ArrayCollection();
$this->contacts = new ArrayCollection();
$this->categories = new ArrayCollection();
}
/**
* RG-3.09 : toute categorie posee sur une adresse prestataire doit etre de
* type PRESTATAIRE -> sinon 422 avec violation sur le champ `categories`
* (propertyPath aligne ERP-101, message FR ERP-107). S'appuie sur
* CategoryInterface::getCategoryTypeCodes() (multi-type — la categorie est
* acceptee des qu'elle PORTE le type PRESTATAIRE ; pas d'import du module
* Catalog, regle ABSOLUE n°1). Joue avant la base via la validation API Platform.
*/
#[Assert\Callback]
public function validateCategoryType(ExecutionContextInterface $context): void
{
foreach ($this->categories as $category) {
if ($category instanceof CategoryInterface
&& !in_array(self::REQUIRED_CATEGORY_TYPE_CODE, $category->getCategoryTypeCodes(), true)) {
$context->buildViolation('Type de catégorie non autorisé (PRESTATAIRE attendu).')
->atPath('categories')
->addViolation()
;
return;
}
}
$this->sites = new ArrayCollection();
$this->contacts = new ArrayCollection();
}
public function getId(): ?int
@@ -349,26 +301,4 @@ class ProviderAddress implements TimestampableInterface, BlamableInterface, Prov
return $this;
}
/** @return Collection<int, CategoryInterface> */
public function getCategories(): Collection
{
return $this->categories;
}
public function addCategory(CategoryInterface $category): static
{
if (!$this->categories->contains($category)) {
$this->categories->add($category);
}
return $this;
}
public function removeCategory(CategoryInterface $category): static
{
$this->categories->removeElement($category);
return $this;
}
}
@@ -33,8 +33,7 @@ use Symfony\Component\Validator\ConstraintViolationList;
* sites de l'adresse (RG-3.05 / § 2.13). Les regles de l'onglet Adresse sont
* garanties en amont par des contraintes sur l'entite, jouees par API Platform
* avant ce processor : RG-3.06 (code postal, Assert\Regex), RG-3.05 (>= 1 site,
* Assert\Count), RG-3.09 (categorie de type PRESTATAIRE, Assert\Callback
* ProviderAddress::validateCategoryType).
* Assert\Count).
* - DELETE : aucune regle metier specifique (suppression physique directe).
*
* La security de l'operation (technique.providers.manage) est appliquee par API
@@ -63,7 +63,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ProviderFixtures extends Fixture implements DependentFixtureInterface
{
/**
* Type de categorie exige pour un prestataire et ses adresses (RG-3.09).
* Type de categorie exige pour un prestataire (RG-3.09).
* Miroir de Provider::REQUIRED_CATEGORY_TYPE_CODE (non importable — regle n°1).
*/
private const string PROVIDER_CATEGORY_TYPE_CODE = 'PRESTATAIRE';
@@ -117,7 +117,7 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface
$maintenance->setPaymentType($this->paymentType($manager, 'VIREMENT'));
$maintenance->setBank($this->bank($manager, 'SG'));
$this->addContact($maintenance, 'Marie', 'Martin', 'Responsable', '05 49 00 00 01', null, 'marie.martin@maintenance-pro.fr');
$this->addAddress($maintenance, ['Chatellerault', 'Saint-Jean'], '86000', 'Poitiers', '12 rue des Acacias', categoryNames: ['Maintenance industrielle']);
$this->addAddress($maintenance, ['Chatellerault', 'Saint-Jean'], '86000', 'Poitiers', '12 rue des Acacias');
$this->addRib($maintenance, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
}
@@ -140,7 +140,7 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface
$transport->setPaymentDelay($this->paymentDelay($manager, 'A_RECEPTION'));
$transport->setPaymentType($this->paymentType($manager, 'CHEQUE'));
$this->addContact($transport, 'Thomas', 'Petit', 'Responsable logistique', '05 56 31 32 33', null, 'thomas.petit@transport-express.fr');
$this->addAddress($transport, ['Saint-Jean'], '17400', 'Fontenet', '4 zone des Transporteurs', categoryNames: ['Transport']);
$this->addAddress($transport, ['Saint-Jean'], '17400', 'Fontenet', '4 zone des Transporteurs');
}
// === Prestataire minimal — contact par le seul nom (RG-3.04), site 86 ===
@@ -237,8 +237,7 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface
* moins un site est rattache (RG-3.05) ; categories d'adresse de type
* PRESTATAIRE (RG-3.09).
*
* @param list<string> $siteNames au moins un site (RG-3.05)
* @param list<string> $categoryNames categories de type PRESTATAIRE (RG-3.09)
* @param list<string> $siteNames au moins un site (RG-3.05)
*/
private function addAddress(
Provider $provider,
@@ -247,7 +246,6 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface
string $city,
string $street,
?string $streetComplement = null,
array $categoryNames = [],
int $position = 0,
): void {
$address = new ProviderAddress();
@@ -262,9 +260,6 @@ class ProviderFixtures extends Fixture implements DependentFixtureInterface
foreach ($siteNames as $siteName) {
$address->addSite($this->site($siteName));
}
foreach ($categoryNames as $categoryName) {
$address->addCategory($this->category($this->manager, $categoryName));
}
$provider->addAddress($address);
}
@@ -444,12 +444,6 @@ final class ColumnCommentsCatalog
'provider_contact_id' => 'FK -> provider_contact.id, ON DELETE CASCADE — contact associe a l adresse.',
],
'provider_address_category' => [
'_table' => 'Jointure M2M provider_address <-> category — categories d adresse de type PRESTATAIRE (RG-3.09).',
'provider_address_id' => 'FK -> provider_address.id, ON DELETE CASCADE — adresse concernee.',
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type PRESTATAIRE (RG-3.09).',
],
'provider_rib' => [
'_table' => 'Coordonnees bancaires d un prestataire (1:n) — >= 1 RIB attendu selon le type de reglement (RG-3.08). Tous les champs audites (pas d AuditIgnore).',
'id' => 'Identifiant interne auto-incremente.',