fix : retours métier ERP-193 (4 répertoires) (#139)
Auto Tag Develop / tag (push) Successful in 11s
Auto Tag Develop / tag (push) Successful in 11s
Lot de retours métier **ERP-193** (« Fix tous les retours starseed »), transverse aux 4 répertoires (clients, fournisseurs, prestataires, transporteurs).
## Contenu
- **Pagination** : défaut à 25 items/page sur les 4 répertoires.
- **Libellé** : colonne « Dernière activité » → « Dernière modification ».
- **Consultation** : masquage des onglets vides (coquilles « à venir » + onglets de données sans donnée).
- **Chiffre d'affaires** : plafonné à 999 999 999 999,99 (clamp front + `Assert\LessThanOrEqual` back).
- **Date de création** : interdiction des dates futures (`:max` MalioDate + `Assert\LessThanOrEqual('today')` back).
- **Caractères spéciaux** : blocage des caractères parasites (`²³§~#|…`) dans les champs texte via une allow-list par profil (nom de personne / texte libre / adresse / code alphanumérique) — filtrage front à la frappe + `Assert\Regex` back autoritaire. Email/IBAN/BIC/TVA conservent leurs validateurs de format.
- **UI** : champs en consultation et onglets validés grisés (`readonly` → `disabled`).
- **UI** : boutons « Archiver » en rouge (variant `danger`).
## Tests
- Back : nouveaux tests RG (plafond CA, dates futures, caractères spéciaux) + garde-fou contraintes — suite complète verte (813 tests).
- Front : nouveaux tests unitaires (sanitizers, helpers date/montant) — 615 tests verts, eslint clean.
---------
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #139
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #139.
This commit is contained in:
@@ -19,6 +19,7 @@ use App\Shared\Domain\Contract\ClientInterface;
|
||||
use App\Shared\Domain\Contract\SiteInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use App\Shared\Domain\Validation\TextInputPattern;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -162,6 +163,7 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Assert\NotBlank(message: 'Le nom de l\'entreprise est obligatoire.', normalizer: 'trim')]
|
||||
#[Assert\Length(min: 2, max: 180, minMessage: 'Le nom de l\'entreprise doit comporter au moins {{ limit }} caractères.', maxMessage: 'Le nom de l\'entreprise ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::FREE_TEXT, message: TextInputPattern::FREE_TEXT_MESSAGE)]
|
||||
#[Groups(['client:read', 'client:write:main'])]
|
||||
private ?string $companyName = null;
|
||||
|
||||
@@ -214,11 +216,14 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Assert\Length(max: 255, maxMessage: 'La liste des concurrents ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::FREE_TEXT, message: TextInputPattern::FREE_TEXT_MESSAGE)]
|
||||
#[Groups(['client:read', 'client:write:information'])]
|
||||
private ?string $competitors = null;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['client:read', 'client:write:information'])]
|
||||
// Date de creation jamais dans le futur (retour metier ERP-193) : <= aujourd'hui.
|
||||
#[Assert\LessThanOrEqual(value: 'today', message: 'La date de création ne peut pas être dans le futur.')]
|
||||
// Format d'ENTREE strict ISO `Y-m-d` (le `!` remet l'heure a 00:00:00). Sans
|
||||
// ce format, PHP DateTime accepte des formes ambigues : « 12/25/2026 » (que
|
||||
// le front MalioDate juge invalide en JJ/MM/AAAA) serait sinon interprete en
|
||||
@@ -233,12 +238,16 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa
|
||||
#[Groups(['client:read', 'client:write:information'])]
|
||||
private ?int $employeesCount = null;
|
||||
|
||||
// Chiffre d'affaires plafonne a 999 999 999 999,99 (12 chiffres, retour metier
|
||||
// ERP-193). La colonne (decimal 15,2) tolere plus, mais le metier borne la saisie.
|
||||
#[ORM\Column(type: 'decimal', precision: 15, scale: 2, nullable: true)]
|
||||
#[Assert\LessThanOrEqual(value: 999_999_999_999.99, message: 'Le chiffre d\'affaires ne peut pas dépasser 999 999 999 999,99.')]
|
||||
#[Groups(['client:read', 'client:write:information'])]
|
||||
private ?string $revenueAmount = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'Le nom du dirigeant ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::PERSON_NAME, message: TextInputPattern::PERSON_NAME_MESSAGE)]
|
||||
#[Groups(['client:read', 'client:write:information'])]
|
||||
private ?string $directorName = null;
|
||||
|
||||
@@ -257,6 +266,7 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa
|
||||
|
||||
#[ORM\Column(length: 40, nullable: true)]
|
||||
#[Assert\Length(max: 40, maxMessage: 'Le numéro de compte ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::CODE_ALNUM, message: TextInputPattern::CODE_ALNUM_MESSAGE)]
|
||||
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
||||
private ?string $accountNumber = null;
|
||||
|
||||
@@ -267,6 +277,7 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa
|
||||
|
||||
#[ORM\Column(length: 40, nullable: true)]
|
||||
#[Assert\Length(max: 40, maxMessage: 'Le numéro de TVA ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::CODE_ALNUM, message: TextInputPattern::CODE_ALNUM_MESSAGE)]
|
||||
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
||||
private ?string $nTva = null;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Shared\Domain\Contract\ClientAddressInterface;
|
||||
use App\Shared\Domain\Contract\SiteInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use App\Shared\Domain\Validation\TextInputPattern;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -158,17 +159,20 @@ class ClientAddress implements TimestampableInterface, BlamableInterface, Client
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Assert\NotBlank(message: 'La ville est obligatoire.', normalizer: 'trim')]
|
||||
#[Assert\Length(max: 120, maxMessage: 'La ville ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::ADDRESS, message: TextInputPattern::ADDRESS_MESSAGE)]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $city = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank(message: 'La rue est obligatoire.', normalizer: 'trim')]
|
||||
#[Assert\Length(max: 255, maxMessage: 'La rue ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::ADDRESS, message: TextInputPattern::ADDRESS_MESSAGE)]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $street = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Assert\Length(max: 255, maxMessage: 'Le complément d\'adresse ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::ADDRESS, message: TextInputPattern::ADDRESS_MESSAGE)]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $streetComplement = null;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Shared\Domain\Attribute\Auditable;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use App\Shared\Domain\Validation\TextInputPattern;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -94,16 +95,19 @@ class ClientContact implements TimestampableInterface, BlamableInterface
|
||||
// deux restent nullable au niveau ORM.
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'Le prénom ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::PERSON_NAME, message: TextInputPattern::PERSON_NAME_MESSAGE)]
|
||||
#[Groups(['client_contact:read', 'client_contact:write'])]
|
||||
private ?string $firstName = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'Le nom ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::PERSON_NAME, message: TextInputPattern::PERSON_NAME_MESSAGE)]
|
||||
#[Groups(['client_contact:read', 'client_contact:write'])]
|
||||
private ?string $lastName = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'La fonction ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::FREE_TEXT, message: TextInputPattern::FREE_TEXT_MESSAGE)]
|
||||
#[Groups(['client_contact:read', 'client_contact:write'])]
|
||||
private ?string $jobTitle = null;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Shared\Domain\Contract\SiteInterface;
|
||||
use App\Shared\Domain\Contract\SupplierInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use App\Shared\Domain\Validation\TextInputPattern;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -171,6 +172,7 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Assert\NotBlank(message: 'Le nom du fournisseur est obligatoire.', normalizer: 'trim')]
|
||||
#[Assert\Length(min: 2, max: 180, minMessage: 'Le nom du fournisseur doit comporter au moins {{ limit }} caractères.', maxMessage: 'Le nom du fournisseur ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::FREE_TEXT, message: TextInputPattern::FREE_TEXT_MESSAGE)]
|
||||
#[Groups(['supplier:read', 'supplier:write:main'])]
|
||||
private ?string $companyName = null;
|
||||
|
||||
@@ -195,11 +197,14 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Assert\Length(max: 255, maxMessage: 'La liste des concurrents ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::FREE_TEXT, message: TextInputPattern::FREE_TEXT_MESSAGE)]
|
||||
#[Groups(['supplier:read', 'supplier:write:information'])]
|
||||
private ?string $competitors = null;
|
||||
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
#[Groups(['supplier:read', 'supplier:write:information'])]
|
||||
// Date de creation jamais dans le futur (retour metier ERP-193) : <= aujourd'hui.
|
||||
#[Assert\LessThanOrEqual(value: 'today', message: 'La date de création ne peut pas être dans le futur.')]
|
||||
// Format d'ENTREE strict ISO `Y-m-d` : sans lui, PHP DateTime accepte des
|
||||
// formes ambigues (« 12/25/2026 », jugee invalide par MalioDate en JJ/MM/AAAA,
|
||||
// serait lue en M/J -> 25 decembre et acceptee a tort). Avec le format, toute
|
||||
@@ -212,12 +217,16 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt
|
||||
#[Groups(['supplier:read', 'supplier:write:information'])]
|
||||
private ?int $employeesCount = null;
|
||||
|
||||
// Chiffre d'affaires plafonne a 999 999 999 999,99 (12 chiffres, retour metier
|
||||
// ERP-193). La colonne (decimal 15,2) tolere plus, mais le metier borne la saisie.
|
||||
#[ORM\Column(type: 'decimal', precision: 15, scale: 2, nullable: true)]
|
||||
#[Assert\LessThanOrEqual(value: 999_999_999_999.99, message: 'Le chiffre d\'affaires ne peut pas dépasser 999 999 999 999,99.')]
|
||||
#[Groups(['supplier:read', 'supplier:write:information'])]
|
||||
private ?string $revenueAmount = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'Le nom du dirigeant ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::PERSON_NAME, message: TextInputPattern::PERSON_NAME_MESSAGE)]
|
||||
#[Groups(['supplier:read', 'supplier:write:information'])]
|
||||
private ?string $directorName = null;
|
||||
|
||||
@@ -243,6 +252,7 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt
|
||||
|
||||
#[ORM\Column(length: 40, nullable: true)]
|
||||
#[Assert\Length(max: 40, maxMessage: 'Le numéro de compte ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::CODE_ALNUM, message: TextInputPattern::CODE_ALNUM_MESSAGE)]
|
||||
#[Groups(['supplier:read:accounting', 'supplier:write:accounting'])]
|
||||
private ?string $accountNumber = null;
|
||||
|
||||
@@ -253,6 +263,7 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt
|
||||
|
||||
#[ORM\Column(length: 40, nullable: true)]
|
||||
#[Assert\Length(max: 40, maxMessage: 'Le numéro de TVA ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::CODE_ALNUM, message: TextInputPattern::CODE_ALNUM_MESSAGE)]
|
||||
#[Groups(['supplier:read:accounting', 'supplier:write:accounting'])]
|
||||
private ?string $nTva = null;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Shared\Domain\Contract\SiteInterface;
|
||||
use App\Shared\Domain\Contract\SupplierAddressInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use App\Shared\Domain\Validation\TextInputPattern;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -154,17 +155,20 @@ class SupplierAddress implements TimestampableInterface, BlamableInterface, Supp
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Assert\NotBlank(message: 'La ville est obligatoire.', normalizer: 'trim')]
|
||||
#[Assert\Length(max: 120, maxMessage: 'La ville ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::ADDRESS, message: TextInputPattern::ADDRESS_MESSAGE)]
|
||||
#[Groups(['supplier:item:read', 'supplier:write:addresses', 'supplier_address:read'])]
|
||||
private ?string $city = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank(message: 'La rue est obligatoire.', normalizer: 'trim')]
|
||||
#[Assert\Length(max: 255, maxMessage: 'La rue ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::ADDRESS, message: TextInputPattern::ADDRESS_MESSAGE)]
|
||||
#[Groups(['supplier:item:read', 'supplier:write:addresses', 'supplier_address:read'])]
|
||||
private ?string $street = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Assert\Length(max: 255, maxMessage: 'Le complément d\'adresse ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::ADDRESS, message: TextInputPattern::ADDRESS_MESSAGE)]
|
||||
#[Groups(['supplier:item:read', 'supplier:write:addresses', 'supplier_address:read'])]
|
||||
private ?string $streetComplement = null;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Shared\Domain\Attribute\Auditable;
|
||||
use App\Shared\Domain\Contract\BlamableInterface;
|
||||
use App\Shared\Domain\Contract\TimestampableInterface;
|
||||
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
|
||||
use App\Shared\Domain\Validation\TextInputPattern;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -99,16 +100,19 @@ class SupplierContact implements TimestampableInterface, BlamableInterface
|
||||
// deux restent nullable au niveau ORM.
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'Le prénom ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::PERSON_NAME, message: TextInputPattern::PERSON_NAME_MESSAGE)]
|
||||
#[Groups(['supplier:item:read', 'supplier:write:contacts'])]
|
||||
private ?string $firstName = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'Le nom ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::PERSON_NAME, message: TextInputPattern::PERSON_NAME_MESSAGE)]
|
||||
#[Groups(['supplier:item:read', 'supplier:write:contacts'])]
|
||||
private ?string $lastName = null;
|
||||
|
||||
#[ORM\Column(length: 120, nullable: true)]
|
||||
#[Assert\Length(max: 120, maxMessage: 'La fonction ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: TextInputPattern::FREE_TEXT, message: TextInputPattern::FREE_TEXT_MESSAGE)]
|
||||
#[Groups(['supplier:item:read', 'supplier:write:contacts'])]
|
||||
private ?string $jobTitle = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user