[ERP-76 + ERP-68] Validations d'adresse client (RG → 422) + fixtures démo Catalog/Commercial (#41)
Auto Tag Develop / tag (push) Successful in 11s
Auto Tag Develop / tag (push) Successful in 11s
Stack de 2 tickets sur une branche (squash sur `develop`). ## ERP-76 (#500) — Validations d'adresse Client → 422 Les règles d'intégrité de l'onglet Adresse étaient soit non implémentées (RG-1.29), soit rejetées en 500 par les CHECK Postgres (RG-1.06/07/08/11). Elles sont désormais portées par des `Assert\Callback` applicatifs sur `ClientAddress`, qui remontent une **422 Hydra avant la base** ; les CHECK BDD restent en filet de sécurité. - `validateProspectExclusivity` — `isProspect` exclusif de `isDelivery`/`isBilling` (RG-1.06/07/08). - `validateBillingEmailPresence` — `billingEmail` obligatoire ssi `isBilling` (RG-1.11). - `validateCategoryTypes` — refuse une catégorie de type DISTRIBUTEUR/COURTIER sur une adresse (RG-1.29, violation `categories`), via `CategoryInterface` (règle n°1 respectée). Tests `ClientAddressTest` durcis (≥400 → **422 explicite**) + 4 cas RG-1.29. Cahier de test M1 mis à jour. ## ERP-68 (#486) — Fixtures démo Catalog + Commercial (dev only) - `CategoryFixtures` (Catalog) : 12 catégories sur les 4 types. - `ClientFixtures` (Commercial) : 14 clients couvrant les cas RG (dépendant distributeur/courtier RG-1.03, LCR + 2 RIB RG-1.13, Chèque sans RIB, multi-adresses Prospect/Livraison/Facturation RG-1.06/07/08/11, prospect seul, 3 contacts + tél. secondaire RG-1.05/1.02, archivé RG-1.22, onglet Information complet, multi-catégories M2M). Résolution inter-modules via les seuls contrats Shared (`CategoryInterface`, `SiteProviderInterface`). Valeurs brutes normalisées par `ClientFieldNormalizer`. Données conformes aux CHECK BDD **et** aux validators ERP-76. Idempotentes (lookup `companyName`/`name`). **Garde-fou** : les deux fixtures sont no-op en environnement `test` (la base de test reste un socle minimal ; pas de pollution des comptages ni des cleanups FK). ## Bonus — idempotence fixtures `AppFixtures` (admin/alice/bob) rendu idempotent via lookup par username : `doctrine:fixtures:load --append` est désormais rejouable sans erreur sur tout le jeu de fixtures. ## Vérifications - `make test` : **436/436 vert** (0 échec/erreur). - `make php-cs-fixer-allow-risky` OK. - `make db-reset` charge sans erreur ; 2 runs `--append` consécutifs = idempotent (0 doublon ; 7 users / 14 clients / 12 catégories stables). - `admin/admin` intact. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #41 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #41.
This commit is contained in:
@@ -23,19 +23,23 @@ 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 client (1:n) — onglet Adresse. Une adresse de prospection
|
||||
* (isProspect) est exclusive d'une adresse de livraison/facturation
|
||||
* (RG-1.06/07/08, CHECK BDD). Un email de facturation est obligatoire ssi
|
||||
* isBilling (RG-1.11, CHECK BDD). Au moins un site doit etre rattache
|
||||
* (RG-1.10, Assert\Count).
|
||||
* (RG-1.06/07/08). Un email de facturation est obligatoire ssi isBilling
|
||||
* (RG-1.11). Au moins un site doit etre rattache (RG-1.10, Assert\Count). Ces
|
||||
* regles sont portees par des Assert\Callback (cf. validateProspectExclusivity /
|
||||
* validateBillingEmailPresence, ERP-76) qui remontent une 422 avant la base ;
|
||||
* les CHECK Postgres (chk_client_address_prospect_exclusive /
|
||||
* chk_client_address_billing_email) restent en filet de securite.
|
||||
*
|
||||
* Relations M2M :
|
||||
* - sites : SiteInterface (module Sites) via resolve_target_entities
|
||||
* - contacts : ClientContact (meme module)
|
||||
* - categories : CategoryInterface (module Catalog) via resolve_target_entities
|
||||
* — limitees aux types SECTEUR/AUTRE cote validation (RG-1.29, hors ERP-57)
|
||||
* — limitees aux types SECTEUR/AUTRE (RG-1.29, validateCategoryTypes, ERP-76)
|
||||
*
|
||||
* Audite (#[Auditable]) + Timestampable/Blamable.
|
||||
*
|
||||
@@ -83,6 +87,9 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
/** RG-1.29 : seuls ces types de categorie qualifient une adresse physique. */
|
||||
private const array ALLOWED_CATEGORY_TYPES = ['SECTEUR', 'AUTRE'];
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
@@ -130,7 +137,7 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $streetComplement = null;
|
||||
|
||||
// RG-1.11 : obligatoire ssi isBilling (CHECK BDD + futur Processor).
|
||||
// RG-1.11 : obligatoire ssi isBilling (validateBillingEmailPresence + CHECK BDD).
|
||||
#[ORM\Column(length: 180, nullable: true)]
|
||||
#[Assert\Email]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
@@ -158,7 +165,7 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private Collection $contacts;
|
||||
|
||||
// RG-1.29 : categories de type SECTEUR/AUTRE uniquement (filtre au Processor).
|
||||
// RG-1.29 : categories de type SECTEUR/AUTRE uniquement (validateCategoryTypes).
|
||||
/** @var Collection<int, CategoryInterface> */
|
||||
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
||||
#[ORM\JoinTable(name: 'client_address_category')]
|
||||
@@ -174,6 +181,79 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
|
||||
$this->categories = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.06 / RG-1.07 / RG-1.08 : une adresse de prospection est exclusive
|
||||
* d'une adresse de livraison ou de facturation. Mirror applicatif (422) du
|
||||
* CHECK chk_client_address_prospect_exclusive, joue avant la base afin de
|
||||
* remonter une violation Hydra plutot qu'une 500 DBAL.
|
||||
*/
|
||||
#[Assert\Callback]
|
||||
public function validateProspectExclusivity(ExecutionContextInterface $context): void
|
||||
{
|
||||
if ($this->isProspect && ($this->isDelivery || $this->isBilling)) {
|
||||
$context->buildViolation('Une adresse de prospection ne peut pas être une adresse de livraison ni de facturation.')
|
||||
->atPath('isProspect')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.11 : l'email de facturation est obligatoire si l'adresse est de
|
||||
* facturation, et interdit sinon. Mirror applicatif (422) du CHECK
|
||||
* chk_client_address_billing_email.
|
||||
*
|
||||
* On raisonne sur la PRESENCE effective de l'email : null ET chaine vide
|
||||
* sont traites comme « absent », car le ClientAddressProcessor normalise une
|
||||
* chaine vide en null APRES la validation (RG-1.21). Sans ce traitement,
|
||||
* billingEmail="" passerait les callbacks (null === "" est faux) puis serait
|
||||
* persiste en null avec is_billing=true -> violation du CHECK -> 500 au lieu
|
||||
* du 422 attendu (et symetriquement, "" sur une adresse non facturable
|
||||
* serait rejete a tort).
|
||||
*/
|
||||
#[Assert\Callback]
|
||||
public function validateBillingEmailPresence(ExecutionContextInterface $context): void
|
||||
{
|
||||
$hasBillingEmail = null !== $this->billingEmail && '' !== trim($this->billingEmail);
|
||||
|
||||
if ($this->isBilling && !$hasBillingEmail) {
|
||||
$context->buildViolation('L\'email de facturation est obligatoire pour une adresse de facturation.')
|
||||
->atPath('billingEmail')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (!$this->isBilling && $hasBillingEmail) {
|
||||
$context->buildViolation('L\'email de facturation n\'est autorisé que sur une adresse de facturation.')
|
||||
->atPath('billingEmail')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-1.29 : seules les categories de type SECTEUR / AUTRE qualifient une
|
||||
* adresse physique. Les types DISTRIBUTEUR / COURTIER decrivent une relation
|
||||
* entre clients (RG-1.03) et n'ont pas de sens sur une adresse -> 422 avec
|
||||
* violation sur le champ `categories`. S'appuie sur
|
||||
* CategoryInterface::getCategoryTypeCode() (pas d'import du module Catalog).
|
||||
*/
|
||||
#[Assert\Callback]
|
||||
public function validateCategoryTypes(ExecutionContextInterface $context): void
|
||||
{
|
||||
foreach ($this->categories as $category) {
|
||||
if ($category instanceof CategoryInterface
|
||||
&& !in_array($category->getCategoryTypeCode(), self::ALLOWED_CATEGORY_TYPES, true)) {
|
||||
$context->buildViolation('Type de catégorie non autorisé sur une adresse.')
|
||||
->atPath('categories')
|
||||
->addViolation()
|
||||
;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
||||
Reference in New Issue
Block a user