feat(commercial) : add M1 client entities + accounting referentials + repositories
Entites metier (Client, ClientContact, ClientAddress, ClientRib) avec #[Auditable] + Timestampable/Blamable, et 4 referentiels comptables statiques (TvaMode, PaymentDelay, PaymentType, Bank). 8 repositories interfaces + impl Doctrine. Aucun ApiResource (Provider/Processor = ERP-55). - Client : 2 FK auto-referentes distributor/broker (mutuellement exclusives, CHECK en base), M2M categories, FK referentiels comptables, groupes de serialisation par onglet. Pas de #[ORM\UniqueConstraint] : unicite du nom de societe portee par l'index partiel Postgres (decision Q4). - ClientRib : tous les champs audites, aucun #[AuditIgnore] sur iban/bic (decision 29/05, audit admin-only). - M2M Category via le contrat Shared CategoryInterface + resolve_target_entities (regle n°1, pas d'import inter-modules) ; sites via SiteInterface. - CommercialReferentialFixtures : re-seed idempotent des 4 referentiels (sinon vides apres db-reset car desormais tables mappees, purgees par les fixtures). - Referentiels whitelistes dans EntitiesAreTimestampableBlamableTest::EXCLUDED. - doctrine.yaml : mapping ORM du module Commercial + resolve CategoryInterface. - ColumnCommentsCatalog : ajout des colonnes M1 (chemin schema:update/test) ; migration retrofit Version20260528120000 filtree sur les tables existantes pour ne pas casser sur les tables des modules crees plus tard. - makefile test-db-setup : recreation de l'index partiel uq_client_company_name_active. Refs ERP-54.
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Commercial\Domain\Entity;
|
||||
|
||||
use App\Module\Commercial\Infrastructure\Doctrine\DoctrineClientAddressRepository;
|
||||
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;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* 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, futur Processor)
|
||||
*
|
||||
* Audite (#[Auditable]) + Timestampable/Blamable. Aucun ApiResource au M1.1
|
||||
* (sous-ressources branchees a un ticket dedie).
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: DoctrineClientAddressRepository::class)]
|
||||
#[ORM\Table(name: 'client_address')]
|
||||
#[ORM\Index(name: 'idx_client_address_client', columns: ['client_id'])]
|
||||
#[Auditable]
|
||||
class ClientAddress implements TimestampableInterface, BlamableInterface
|
||||
{
|
||||
use TimestampableBlamableTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['client_address:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'addresses')]
|
||||
#[ORM\JoinColumn(name: 'client_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ?Client $client = null;
|
||||
|
||||
#[ORM\Column(name: 'is_prospect', options: ['default' => false])]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private bool $isProspect = false;
|
||||
|
||||
#[ORM\Column(name: 'is_delivery', options: ['default' => false])]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private bool $isDelivery = false;
|
||||
|
||||
#[ORM\Column(name: 'is_billing', options: ['default' => false])]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private bool $isBilling = false;
|
||||
|
||||
#[ORM\Column(length: 80, options: ['default' => 'France'])]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private string $country = 'France';
|
||||
|
||||
// RG-1.09 : code postal a 4 ou 5 chiffres (pas de controle CP/ville serveur).
|
||||
#[ORM\Column(length: 20)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Regex(pattern: '/^[0-9]{4,5}$/', message: 'Le code postal doit comporter 4 ou 5 chiffres.')]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $postalCode = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $city = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $street = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $streetComplement = null;
|
||||
|
||||
// RG-1.11 : obligatoire ssi isBilling (CHECK BDD + futur Processor).
|
||||
#[ORM\Column(length: 180, nullable: true)]
|
||||
#[Assert\Email]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private ?string $billingEmail = null;
|
||||
|
||||
#[ORM\Column(options: ['default' => 0])]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private int $position = 0;
|
||||
|
||||
// RG-1.10 : au moins un site rattache a chaque adresse.
|
||||
/** @var Collection<int, SiteInterface> */
|
||||
#[ORM\ManyToMany(targetEntity: SiteInterface::class)]
|
||||
#[ORM\JoinTable(name: 'client_address_site')]
|
||||
#[ORM\JoinColumn(name: 'client_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'site_id', referencedColumnName: 'id', onDelete: 'RESTRICT')]
|
||||
#[Assert\Count(min: 1, minMessage: 'Au moins un site est obligatoire.')]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private Collection $sites;
|
||||
|
||||
/** @var Collection<int, ClientContact> */
|
||||
#[ORM\ManyToMany(targetEntity: ClientContact::class)]
|
||||
#[ORM\JoinTable(name: 'client_address_contact')]
|
||||
#[ORM\JoinColumn(name: 'client_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'client_contact_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private Collection $contacts;
|
||||
|
||||
// RG-1.29 : categories de type SECTEUR/AUTRE uniquement (filtre au Processor).
|
||||
/** @var Collection<int, CategoryInterface> */
|
||||
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
||||
#[ORM\JoinTable(name: 'client_address_category')]
|
||||
#[ORM\JoinColumn(name: 'client_address_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'RESTRICT')]
|
||||
#[Groups(['client_address:read', 'client_address:write'])]
|
||||
private Collection $categories;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->sites = new ArrayCollection();
|
||||
$this->contacts = new ArrayCollection();
|
||||
$this->categories = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getClient(): ?Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function setClient(?Client $client): static
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isProspect(): bool
|
||||
{
|
||||
return $this->isProspect;
|
||||
}
|
||||
|
||||
public function setIsProspect(bool $isProspect): static
|
||||
{
|
||||
$this->isProspect = $isProspect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDelivery(): bool
|
||||
{
|
||||
return $this->isDelivery;
|
||||
}
|
||||
|
||||
public function setIsDelivery(bool $isDelivery): static
|
||||
{
|
||||
$this->isDelivery = $isDelivery;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isBilling(): bool
|
||||
{
|
||||
return $this->isBilling;
|
||||
}
|
||||
|
||||
public function setIsBilling(bool $isBilling): static
|
||||
{
|
||||
$this->isBilling = $isBilling;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCountry(): string
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
public function setCountry(string $country): static
|
||||
{
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPostalCode(): ?string
|
||||
{
|
||||
return $this->postalCode;
|
||||
}
|
||||
|
||||
public function setPostalCode(?string $postalCode): static
|
||||
{
|
||||
$this->postalCode = $postalCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCity(): ?string
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
public function setCity(?string $city): static
|
||||
{
|
||||
$this->city = $city;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreet(): ?string
|
||||
{
|
||||
return $this->street;
|
||||
}
|
||||
|
||||
public function setStreet(?string $street): static
|
||||
{
|
||||
$this->street = $street;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStreetComplement(): ?string
|
||||
{
|
||||
return $this->streetComplement;
|
||||
}
|
||||
|
||||
public function setStreetComplement(?string $streetComplement): static
|
||||
{
|
||||
$this->streetComplement = $streetComplement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillingEmail(): ?string
|
||||
{
|
||||
return $this->billingEmail;
|
||||
}
|
||||
|
||||
public function setBillingEmail(?string $billingEmail): static
|
||||
{
|
||||
$this->billingEmail = $billingEmail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function setPosition(int $position): static
|
||||
{
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, SiteInterface> */
|
||||
public function getSites(): Collection
|
||||
{
|
||||
return $this->sites;
|
||||
}
|
||||
|
||||
public function addSite(SiteInterface $site): static
|
||||
{
|
||||
if (!$this->sites->contains($site)) {
|
||||
$this->sites->add($site);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSite(SiteInterface $site): static
|
||||
{
|
||||
$this->sites->removeElement($site);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, ClientContact> */
|
||||
public function getContacts(): Collection
|
||||
{
|
||||
return $this->contacts;
|
||||
}
|
||||
|
||||
public function addContact(ClientContact $contact): static
|
||||
{
|
||||
if (!$this->contacts->contains($contact)) {
|
||||
$this->contacts->add($contact);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeContact(ClientContact $contact): static
|
||||
{
|
||||
$this->contacts->removeElement($contact);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user