54d8327fa5
Auto Tag Develop / tag (push) Successful in 9s
PR **empilée sur ERP-132** (#90) — base = \`feature/ERP-132-migrer-schema-bdd-m3\` (ERP-132 pas encore mergé dans develop). À rebaser sur develop une fois #90 mergée. ## Périmètre (ticket Lesstime #133, M3 § 3.3/3.4/2.12/4.0) Entités Doctrine + mapping ApiResource (squelette) + repository avec hydratation anti-N+1. Miroir des entités `Supplier*` (M2), **amputé de l'onglet Information** et **augmenté de `provider.sites`** (M2M direct, RG-3.03). ### Créé - `Provider`, `ProviderContact`, `ProviderAddress` (simplifiée : pas de `addressType`/`bennes`/`triageProvider`), `ProviderRib` — `#[Auditable]` + Timestampable/Blamable. - `ProviderRepositoryInterface` + `DoctrineProviderRepository` : `createListQueryBuilder` (filtres + tri seuls) + `hydrateListCollections` anti-N+1 (catégories puis **sites en relation directe**, requêtes `IN` bornées séparées — § 2.12). ### Contrat de sérialisation (RETEX M1 — 3 maillons) Groupes posés sur l'entité (source unique) : liste = `provider:read`+`category:read`+`site:read` ; détail = +`provider:item:read`. Piège booléen `isArchived` traité (`#[Groups]`+`#[SerializedName]` sur le getter). Embed `categories[].code/name` + `sites[].name/postalCode` (objet, pas IRI). ### Consommation cross-module (§ 2.1) - Site/Category via contrats Shared (`SiteInterface`/`CategoryInterface` + `resolve_target_entities`) — comme Supplier, conforme règle ABSOLUE n°1. - Référentiels comptables (`TvaMode`/`PaymentDelay`/`PaymentType`/`Bank`) en relation ORM partagée directe (décision § 2.1, remontée Shared tracée HP-M4-2). ### Garde-fous / infra (requis pour le vert) - Mapping ORM du module `Technique` dans `doctrine.yaml` (sinon les 9 tables `provider*` vues orphelines → DROP). - Tables `provider*` ajoutées à `ColumnCommentsCatalog` + ligne `dbal:run-sql uq_provider_company_name_active` au makefile `test-db-setup`. - 4 libellés `audit.entity.technique_*` (fr.json) ; `ProviderAddress::postalCode` whitelisté dans `EXCLUDED_LENGTH_MIRROR` (Regex CP {4,5}). ## Hors périmètre (→ ERP-134) ApiResource **sans** `ProviderProvider`/`ProviderProcessor` ; sous-entités **sans** `#[ApiResource]`. Hydratation effective, gating accounting, cloisonnement par site, normalisation, 409 doublon, RG-3.07/3.08 → ERP-134. Sous-ressources POST/PATCH/DELETE → ticket ultérieur. ## Tests - \`make test\` → **589/589 ✓** · \`php-cs-fixer\` → 0 correction. - \`schema:validate\` : mapping OK ; « not in sync » résiduel strictement homologue à supplier (COMMENT via catalogue + index FK auto-Doctrine), non régressif. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #91
190 lines
5.8 KiB
PHP
190 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Technique\Domain\Entity;
|
|
|
|
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 Doctrine\ORM\Mapping as ORM;
|
|
use Symfony\Component\Serializer\Attribute\Groups;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
/**
|
|
* Contact d'un prestataire (1:n) — onglet Contacts. Un bloc est valide des qu'au
|
|
* moins un champ est rempli (RG-3.04) : garantie portee par un CHECK BDD
|
|
* (chk_provider_contact_name) + le ProviderProcessor (ERP-134) ; l'entite reste
|
|
* permissive (tous les champs nullable).
|
|
*
|
|
* Embarque sous `provider.contacts` au detail (groupe provider:item:read,
|
|
* maillon (a) du contrat de serialisation). Maximum 2 telephones
|
|
* (phonePrimary + phoneSecondary).
|
|
*
|
|
* L'exposition en SOUS-RESSOURCE API (POST /providers/{id}/contacts, PATCH /
|
|
* DELETE) est un ticket ulterieur du M3 : pas d'#[ApiResource] ici (l'entite est
|
|
* pour l'instant uniquement embarquee via le detail du prestataire).
|
|
*
|
|
* Audite (#[Auditable]) + Timestampable / Blamable (pattern Shared standard).
|
|
*/
|
|
#[ORM\Entity]
|
|
#[ORM\Table(name: 'provider_contact')]
|
|
#[ORM\Index(name: 'idx_provider_contact_provider', columns: ['provider_id'])]
|
|
#[Auditable]
|
|
class ProviderContact implements TimestampableInterface, BlamableInterface
|
|
{
|
|
use TimestampableBlamableTrait;
|
|
|
|
#[ORM\Id]
|
|
#[ORM\GeneratedValue]
|
|
#[ORM\Column]
|
|
#[Groups(['provider:item:read'])]
|
|
private ?int $id = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: Provider::class, inversedBy: 'contacts')]
|
|
#[ORM\JoinColumn(name: 'provider_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
|
private ?Provider $provider = null;
|
|
|
|
// RG-3.04 : au moins un champ du contact renseigne (CHECK BDD + Processor). Les
|
|
// champs 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')]
|
|
#[Groups(['provider:item:read', 'provider: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')]
|
|
#[Groups(['provider:item:read', 'provider: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')]
|
|
#[Groups(['provider:item:read', 'provider:write:contacts'])]
|
|
private ?string $jobTitle = null;
|
|
|
|
// Pas de validation de format telephone (saisie libre), mais une Assert\Length
|
|
// calee sur la colonne VARCHAR(20) evite l'erreur Postgres (500 non rattachee au
|
|
// champ) au profit d'une 422 propre (ERP-107).
|
|
#[ORM\Column(length: 20, nullable: true)]
|
|
#[Assert\Length(max: 20, maxMessage: 'Le téléphone ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
|
#[Groups(['provider:item:read', 'provider:write:contacts'])]
|
|
private ?string $phonePrimary = null;
|
|
|
|
#[ORM\Column(length: 20, nullable: true)]
|
|
#[Assert\Length(max: 20, maxMessage: 'Le téléphone secondaire ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
|
#[Groups(['provider:item:read', 'provider:write:contacts'])]
|
|
private ?string $phoneSecondary = null;
|
|
|
|
#[ORM\Column(length: 180, nullable: true)]
|
|
#[Assert\Email(message: 'L\'adresse email n\'est pas valide.')]
|
|
#[Assert\Length(max: 180, maxMessage: 'L\'email ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
|
#[Groups(['provider:item:read', 'provider:write:contacts'])]
|
|
private ?string $email = null;
|
|
|
|
// Ordre d'affichage du contact (gere serveur, non expose au M3).
|
|
#[ORM\Column(options: ['default' => 0])]
|
|
private int $position = 0;
|
|
|
|
public function getId(): ?int
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
public function getProvider(): ?Provider
|
|
{
|
|
return $this->provider;
|
|
}
|
|
|
|
public function setProvider(?Provider $provider): static
|
|
{
|
|
$this->provider = $provider;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getFirstName(): ?string
|
|
{
|
|
return $this->firstName;
|
|
}
|
|
|
|
public function setFirstName(?string $firstName): static
|
|
{
|
|
$this->firstName = $firstName;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getLastName(): ?string
|
|
{
|
|
return $this->lastName;
|
|
}
|
|
|
|
public function setLastName(?string $lastName): static
|
|
{
|
|
$this->lastName = $lastName;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getJobTitle(): ?string
|
|
{
|
|
return $this->jobTitle;
|
|
}
|
|
|
|
public function setJobTitle(?string $jobTitle): static
|
|
{
|
|
$this->jobTitle = $jobTitle;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getPhonePrimary(): ?string
|
|
{
|
|
return $this->phonePrimary;
|
|
}
|
|
|
|
public function setPhonePrimary(?string $phonePrimary): static
|
|
{
|
|
$this->phonePrimary = $phonePrimary;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getPhoneSecondary(): ?string
|
|
{
|
|
return $this->phoneSecondary;
|
|
}
|
|
|
|
public function setPhoneSecondary(?string $phoneSecondary): static
|
|
{
|
|
$this->phoneSecondary = $phoneSecondary;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getEmail(): ?string
|
|
{
|
|
return $this->email;
|
|
}
|
|
|
|
public function setEmail(?string $email): static
|
|
{
|
|
$this->email = $email;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getPosition(): int
|
|
{
|
|
return $this->position;
|
|
}
|
|
|
|
public function setPosition(int $position): static
|
|
{
|
|
$this->position = $position;
|
|
|
|
return $this;
|
|
}
|
|
}
|