fc9746df68
Résolution du conflit de contrat de sérialisation (Client.php) après merge du fix #45 (ERP-80/81/82/83) dans develop : - GetCollection : ajout category:read + site:read + accesseur getSites() (delta nécessaire à ERP-62, absent du contrat mergé) — un seul endroit. - Get : client_rib:read NON réintroduit (fix sécu #45 conservé : contenu RIB gaté par client:read:accounting, plus de fuite IBAN/BIC). - getSites() conservé en sus du gating RIB de develop. - Repository : fetch-joins categories/addresses/sites conservés (anti N+1 liste).
763 lines
25 KiB
PHP
763 lines
25 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Commercial\Domain\Entity;
|
|
|
|
use ApiPlatform\Metadata\ApiResource;
|
|
use ApiPlatform\Metadata\Get;
|
|
use ApiPlatform\Metadata\GetCollection;
|
|
use ApiPlatform\Metadata\Patch;
|
|
use ApiPlatform\Metadata\Post;
|
|
use App\Module\Commercial\Infrastructure\ApiPlatform\State\Processor\ClientProcessor;
|
|
use App\Module\Commercial\Infrastructure\ApiPlatform\State\Provider\ClientProvider;
|
|
use App\Module\Commercial\Infrastructure\Doctrine\DoctrineClientRepository;
|
|
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 DateTimeImmutable;
|
|
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\Serializer\Attribute\SerializedName;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
|
|
/**
|
|
* Client (M1 Commercial) — entite racine du repertoire clients. Porte le
|
|
* formulaire principal, l'onglet Information, l'onglet Comptabilite, le
|
|
* mecanisme d'archivage (is_archived / archived_at) et le soft-delete technique
|
|
* prepare mais non expose au M1 (deleted_at, HP-M2-1).
|
|
*
|
|
* Decisions structurantes :
|
|
* - Audit complet (#[Auditable]) sur tous les champs (M2M categories audite
|
|
* automatiquement). Timestampable/Blamable via le trait Shared.
|
|
* - PAS de #[ORM\UniqueConstraint] : l'unicite du nom de societe (RG-1.16) est
|
|
* portee par l'index partiel fonctionnel uq_client_company_name_active
|
|
* (LOWER(company_name) WHERE is_archived = FALSE AND deleted_at IS NULL),
|
|
* inexprimable en attribut ORM, donc possede par la seule migration. Le SIREN
|
|
* et l'email NE SONT PAS uniques (RG-1.15/1.17 supprimees, decision Q4).
|
|
* - distributor / broker : 2 FK auto-referentes mutuellement exclusives
|
|
* (RG-1.03, CHECK chk_client_distrib_or_broker en base).
|
|
* - categories : M2M vers Category (module Catalog) via le contrat
|
|
* CategoryInterface + resolve_target_entities (regle n°1, pas d'import direct).
|
|
*
|
|
* Operations API (Provider + Processor) branchees en ERP-55 :
|
|
* - GetCollection / Get : security commercial.clients.view. La liste expose le
|
|
* groupe client:read ; le detail embarque en plus contacts/adresses/ribs
|
|
* (groupe client:item:read). Les champs comptables (client:read:accounting)
|
|
* sont ajoutes DYNAMIQUEMENT par ClientReadGroupContextBuilder si l'user a
|
|
* la permission accounting.view (§ 2.7 / § 4.1 / § 4.2).
|
|
* - Post / Patch : security commercial.clients.manage ; le ClientProcessor
|
|
* applique normalisation, gating accounting/archive et regles metier.
|
|
* - Pas de Delete au M1 (HP-M2-1) : l'archivage passe par PATCH isArchived.
|
|
*/
|
|
#[ApiResource(
|
|
operations: [
|
|
new GetCollection(
|
|
security: "is_granted('commercial.clients.view')",
|
|
// La liste embarque les categories (avec leur code, groupe
|
|
// category:read) et les sites agreges des adresses (groupe
|
|
// site:read) pour alimenter les colonnes « Catégories » et
|
|
// « Site(s) » du Repertoire (ERP-62). Cf. getSites() plus bas.
|
|
normalizationContext: ['groups' => ['client:read', 'default:read', 'category:read', 'site:read']],
|
|
provider: ClientProvider::class,
|
|
),
|
|
new Get(
|
|
security: "is_granted('commercial.clients.view')",
|
|
// Detail : client + sous-collections embarquees.
|
|
// - client:read:accounting est ajoute par le context builder selon la
|
|
// permission (gate les scalaires comptables ET les RIB embarques),
|
|
// donc absent ici volontairement.
|
|
// - client_rib:read N'EST PLUS dans le contexte : le contenu des RIB
|
|
// embarques est desormais porte par client:read:accounting (gate),
|
|
// ce qui retire la fuite IBAN/BIC vers les users sans accounting.view.
|
|
// - category:read et site:read sont indispensables pour embarquer le
|
|
// code/libelle des categories et des sites (sinon stub IRI nu) :
|
|
// Category.code/name vivent sous category:read, Site.name sous site:read.
|
|
normalizationContext: ['groups' => [
|
|
'client:read',
|
|
'client:item:read',
|
|
'client_contact:read',
|
|
'client_address:read',
|
|
'category:read',
|
|
'site:read',
|
|
'default:read',
|
|
]],
|
|
provider: ClientProvider::class,
|
|
),
|
|
new Post(
|
|
security: "is_granted('commercial.clients.manage')",
|
|
normalizationContext: ['groups' => ['client:read', 'default:read', 'category:read', 'site:read']],
|
|
denormalizationContext: ['groups' => ['client:write:main']],
|
|
processor: ClientProcessor::class,
|
|
),
|
|
new Patch(
|
|
// Security elargie (ERP-74) : `manage` OU `accounting.manage`. Le
|
|
// role Compta n'a pas `manage` mais doit pouvoir editer l'onglet
|
|
// Comptabilite d'un client existant (§ 2.7). Le ClientProcessor
|
|
// re-gate ensuite onglet par onglet :
|
|
// - champs accounting -> accounting.manage (guardAccounting, RG-1.28) ;
|
|
// - champs main/information -> manage (guardManage : empeche Compta
|
|
// d'editer les autres onglets) ;
|
|
// - isArchived -> archive (guardArchive, RG-1.22).
|
|
security: "is_granted('commercial.clients.manage') or is_granted('commercial.clients.accounting.manage')",
|
|
// Le ClientProcessor inspecte les champs reellement envoyes pour
|
|
// autoriser/refuser onglet par onglet (RG-1.22 / RG-1.28) : les
|
|
// champs accounting exigent accounting.manage, isArchived exige
|
|
// archive, le reste (main/information) exige manage.
|
|
normalizationContext: ['groups' => ['client:read', 'default:read', 'category:read', 'site:read']],
|
|
denormalizationContext: ['groups' => [
|
|
'client:write:main',
|
|
'client:write:information',
|
|
'client:write:accounting',
|
|
'client:write:archive',
|
|
]],
|
|
provider: ClientProvider::class,
|
|
processor: ClientProcessor::class,
|
|
),
|
|
],
|
|
)]
|
|
#[ORM\Entity(repositoryClass: DoctrineClientRepository::class)]
|
|
#[ORM\Table(name: 'client')]
|
|
// Index nommes pour matcher la migration (Version20260601000000). L'index
|
|
// unique partiel uq_client_company_name_active reste possede par la migration :
|
|
// Doctrine ORM ne sait pas exprimer un index fonctionnel (LOWER) + partiel
|
|
// (WHERE) via attribut. Pas de #[ORM\UniqueConstraint] (decision Q4).
|
|
#[ORM\Index(name: 'idx_client_is_archived', columns: ['is_archived'])]
|
|
#[ORM\Index(name: 'idx_client_deleted_at', columns: ['deleted_at'])]
|
|
#[ORM\Index(name: 'idx_client_distributor_id', columns: ['distributor_id'])]
|
|
#[ORM\Index(name: 'idx_client_broker_id', columns: ['broker_id'])]
|
|
#[ORM\Index(name: 'idx_client_created_by', columns: ['created_by'])]
|
|
#[ORM\Index(name: 'idx_client_updated_by', columns: ['updated_by'])]
|
|
#[Auditable]
|
|
class Client implements TimestampableInterface, BlamableInterface
|
|
{
|
|
use TimestampableBlamableTrait;
|
|
|
|
#[ORM\Id]
|
|
#[ORM\GeneratedValue]
|
|
#[ORM\Column]
|
|
#[Groups(['client:read'])]
|
|
private ?int $id = null;
|
|
|
|
// === Formulaire principal ===
|
|
#[ORM\Column(length: 180)]
|
|
#[Assert\NotBlank(message: 'Le nom de l\'entreprise est obligatoire.', normalizer: 'trim')]
|
|
#[Assert\Length(min: 2, max: 180, normalizer: 'trim')]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?string $companyName = null;
|
|
|
|
// RG-1.01 : firstName OU lastName obligatoire (validation au futur Processor).
|
|
#[ORM\Column(length: 120, nullable: true)]
|
|
#[Assert\Length(max: 120, normalizer: 'trim')]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?string $firstName = null;
|
|
|
|
#[ORM\Column(length: 120, nullable: true)]
|
|
#[Assert\Length(max: 120, normalizer: 'trim')]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?string $lastName = null;
|
|
|
|
#[ORM\Column(length: 20)]
|
|
#[Assert\NotBlank]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?string $phonePrimary = null;
|
|
|
|
#[ORM\Column(length: 20, nullable: true)]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?string $phoneSecondary = null;
|
|
|
|
#[ORM\Column(length: 180)]
|
|
#[Assert\NotBlank]
|
|
#[Assert\Email]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?string $email = null;
|
|
|
|
// RG-1.03 : distributor / broker auto-references mutuellement exclusives
|
|
// (CHECK chk_client_distrib_or_broker en base).
|
|
#[ORM\ManyToOne(targetEntity: self::class)]
|
|
#[ORM\JoinColumn(name: 'distributor_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?Client $distributor = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: self::class)]
|
|
#[ORM\JoinColumn(name: 'broker_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private ?Client $broker = null;
|
|
|
|
#[ORM\Column(name: 'triage_service', options: ['default' => false])]
|
|
#[Groups(['client:read', 'client:write:main'])]
|
|
private bool $triageService = false;
|
|
|
|
// RG : au moins une categorie (Count min 1). M2M vers Category via le contrat
|
|
// CategoryInterface (resolve_target_entities -> Category).
|
|
/** @var Collection<int, CategoryInterface> */
|
|
#[ORM\ManyToMany(targetEntity: CategoryInterface::class)]
|
|
#[ORM\JoinTable(name: 'client_category')]
|
|
#[ORM\JoinColumn(name: 'client_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(['client:read', 'client:write:main'])]
|
|
private Collection $categories;
|
|
|
|
// === Onglet Information ===
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?string $description = null;
|
|
|
|
#[ORM\Column(length: 255, nullable: true)]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?string $competitors = null;
|
|
|
|
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?DateTimeImmutable $foundedAt = null;
|
|
|
|
#[ORM\Column(nullable: true)]
|
|
#[Assert\PositiveOrZero]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?int $employeesCount = null;
|
|
|
|
#[ORM\Column(type: 'decimal', precision: 15, scale: 2, nullable: true)]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?string $revenueAmount = null;
|
|
|
|
#[ORM\Column(length: 120, nullable: true)]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?string $directorName = null;
|
|
|
|
#[ORM\Column(type: 'decimal', precision: 15, scale: 2, nullable: true)]
|
|
#[Groups(['client:read', 'client:write:information'])]
|
|
private ?string $profitAmount = null;
|
|
|
|
// === Onglet Comptabilite ===
|
|
// Lecture conditionnee via le groupe `client:read:accounting` (ajoute par le
|
|
// futur Provider si l'user a la permission accounting.view). Ecriture via
|
|
// `client:write:accounting` (le futur Processor exige accounting.manage).
|
|
#[ORM\Column(length: 20, nullable: true)]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?string $siren = null;
|
|
|
|
#[ORM\Column(length: 40, nullable: true)]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?string $accountNumber = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: TvaMode::class)]
|
|
#[ORM\JoinColumn(name: 'tva_mode_id', referencedColumnName: 'id', nullable: true, onDelete: 'RESTRICT')]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?TvaMode $tvaMode = null;
|
|
|
|
#[ORM\Column(length: 40, nullable: true)]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?string $nTva = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: PaymentDelay::class)]
|
|
#[ORM\JoinColumn(name: 'payment_delay_id', referencedColumnName: 'id', nullable: true, onDelete: 'RESTRICT')]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?PaymentDelay $paymentDelay = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: PaymentType::class)]
|
|
#[ORM\JoinColumn(name: 'payment_type_id', referencedColumnName: 'id', nullable: true, onDelete: 'RESTRICT')]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?PaymentType $paymentType = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: Bank::class)]
|
|
#[ORM\JoinColumn(name: 'bank_id', referencedColumnName: 'id', nullable: true, onDelete: 'RESTRICT')]
|
|
#[Groups(['client:read:accounting', 'client:write:accounting'])]
|
|
private ?Bank $bank = null;
|
|
|
|
// === Sous-collections (exposees via sous-ressources API dediees, ulterieur) ===
|
|
/** @var Collection<int, ClientContact> */
|
|
#[ORM\OneToMany(mappedBy: 'client', targetEntity: ClientContact::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
|
private Collection $contacts;
|
|
|
|
/** @var Collection<int, ClientAddress> */
|
|
#[ORM\OneToMany(mappedBy: 'client', targetEntity: ClientAddress::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
|
private Collection $addresses;
|
|
|
|
/** @var Collection<int, ClientRib> */
|
|
#[ORM\OneToMany(mappedBy: 'client', targetEntity: ClientRib::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
|
private Collection $ribs;
|
|
|
|
// === Archive / Soft delete ===
|
|
// Groupe d'ECRITURE uniquement sur la propriete (denormalisation PATCH
|
|
// archive). Le groupe de LECTURE est declare sur le getter isArchived()
|
|
// avec SerializedName('isArchived') : sans cela, Symfony strip le prefixe
|
|
// "is" et exposerait la cle JSON "archived" (meme pattern que User::isAdmin
|
|
// et Role::isSystem).
|
|
#[ORM\Column(name: 'is_archived', options: ['default' => false])]
|
|
#[Groups(['client:write:archive'])]
|
|
private bool $isArchived = false;
|
|
|
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
|
#[Groups(['client:read'])]
|
|
private ?DateTimeImmutable $archivedAt = null;
|
|
|
|
// Soft delete technique (HP-M2-1) : non expose en lecture/ecriture au M1.
|
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
|
private ?DateTimeImmutable $deletedAt = null;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->categories = new ArrayCollection();
|
|
$this->contacts = new ArrayCollection();
|
|
$this->addresses = new ArrayCollection();
|
|
$this->ribs = new ArrayCollection();
|
|
}
|
|
|
|
public function getId(): ?int
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
public function getCompanyName(): ?string
|
|
{
|
|
return $this->companyName;
|
|
}
|
|
|
|
public function setCompanyName(string $companyName): static
|
|
{
|
|
$this->companyName = $companyName;
|
|
|
|
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 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 getDistributor(): ?Client
|
|
{
|
|
return $this->distributor;
|
|
}
|
|
|
|
public function setDistributor(?Client $distributor): static
|
|
{
|
|
$this->distributor = $distributor;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getBroker(): ?Client
|
|
{
|
|
return $this->broker;
|
|
}
|
|
|
|
public function setBroker(?Client $broker): static
|
|
{
|
|
$this->broker = $broker;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function isTriageService(): bool
|
|
{
|
|
return $this->triageService;
|
|
}
|
|
|
|
public function setTriageService(bool $triageService): static
|
|
{
|
|
$this->triageService = $triageService;
|
|
|
|
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;
|
|
}
|
|
|
|
public function getDescription(): ?string
|
|
{
|
|
return $this->description;
|
|
}
|
|
|
|
public function setDescription(?string $description): static
|
|
{
|
|
$this->description = $description;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getCompetitors(): ?string
|
|
{
|
|
return $this->competitors;
|
|
}
|
|
|
|
public function setCompetitors(?string $competitors): static
|
|
{
|
|
$this->competitors = $competitors;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getFoundedAt(): ?DateTimeImmutable
|
|
{
|
|
return $this->foundedAt;
|
|
}
|
|
|
|
public function setFoundedAt(?DateTimeImmutable $foundedAt): static
|
|
{
|
|
$this->foundedAt = $foundedAt;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getEmployeesCount(): ?int
|
|
{
|
|
return $this->employeesCount;
|
|
}
|
|
|
|
public function setEmployeesCount(?int $employeesCount): static
|
|
{
|
|
$this->employeesCount = $employeesCount;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getRevenueAmount(): ?string
|
|
{
|
|
return $this->revenueAmount;
|
|
}
|
|
|
|
public function setRevenueAmount(?string $revenueAmount): static
|
|
{
|
|
$this->revenueAmount = $revenueAmount;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getDirectorName(): ?string
|
|
{
|
|
return $this->directorName;
|
|
}
|
|
|
|
public function setDirectorName(?string $directorName): static
|
|
{
|
|
$this->directorName = $directorName;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getProfitAmount(): ?string
|
|
{
|
|
return $this->profitAmount;
|
|
}
|
|
|
|
public function setProfitAmount(?string $profitAmount): static
|
|
{
|
|
$this->profitAmount = $profitAmount;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getSiren(): ?string
|
|
{
|
|
return $this->siren;
|
|
}
|
|
|
|
public function setSiren(?string $siren): static
|
|
{
|
|
$this->siren = $siren;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getAccountNumber(): ?string
|
|
{
|
|
return $this->accountNumber;
|
|
}
|
|
|
|
public function setAccountNumber(?string $accountNumber): static
|
|
{
|
|
$this->accountNumber = $accountNumber;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getTvaMode(): ?TvaMode
|
|
{
|
|
return $this->tvaMode;
|
|
}
|
|
|
|
public function setTvaMode(?TvaMode $tvaMode): static
|
|
{
|
|
$this->tvaMode = $tvaMode;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getNTva(): ?string
|
|
{
|
|
return $this->nTva;
|
|
}
|
|
|
|
public function setNTva(?string $nTva): static
|
|
{
|
|
$this->nTva = $nTva;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getPaymentDelay(): ?PaymentDelay
|
|
{
|
|
return $this->paymentDelay;
|
|
}
|
|
|
|
public function setPaymentDelay(?PaymentDelay $paymentDelay): static
|
|
{
|
|
$this->paymentDelay = $paymentDelay;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getPaymentType(): ?PaymentType
|
|
{
|
|
return $this->paymentType;
|
|
}
|
|
|
|
public function setPaymentType(?PaymentType $paymentType): static
|
|
{
|
|
$this->paymentType = $paymentType;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getBank(): ?Bank
|
|
{
|
|
return $this->bank;
|
|
}
|
|
|
|
public function setBank(?Bank $bank): static
|
|
{
|
|
$this->bank = $bank;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/** @return Collection<int, ClientContact> */
|
|
#[Groups(['client:item:read'])]
|
|
public function getContacts(): Collection
|
|
{
|
|
return $this->contacts;
|
|
}
|
|
|
|
public function addContact(ClientContact $contact): static
|
|
{
|
|
if (!$this->contacts->contains($contact)) {
|
|
$this->contacts->add($contact);
|
|
$contact->setClient($this);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function removeContact(ClientContact $contact): static
|
|
{
|
|
if ($this->contacts->removeElement($contact) && $contact->getClient() === $this) {
|
|
$contact->setClient(null);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/** @return Collection<int, ClientAddress> */
|
|
#[Groups(['client:item:read'])]
|
|
public function getAddresses(): Collection
|
|
{
|
|
return $this->addresses;
|
|
}
|
|
|
|
public function addAddress(ClientAddress $address): static
|
|
{
|
|
if (!$this->addresses->contains($address)) {
|
|
$this->addresses->add($address);
|
|
$address->setClient($this);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function removeAddress(ClientAddress $address): static
|
|
{
|
|
if ($this->addresses->removeElement($address) && $address->getClient() === $this) {
|
|
$address->setClient(null);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Sites distincts rattaches a au moins une adresse du client (RG-1.10).
|
|
* Le Client ne porte pas de sites en propre : ils vivent sur les adresses.
|
|
* Agrege en lecture seule pour la colonne « Site(s) » du Repertoire (badges
|
|
* colores) — expose en LISTE via le groupe client:read (les adresses
|
|
* completes restent reservees au detail, client:item:read).
|
|
*
|
|
* @return list<SiteInterface>
|
|
*/
|
|
#[Groups(['client:read'])]
|
|
public function getSites(): array
|
|
{
|
|
$sites = [];
|
|
foreach ($this->addresses as $address) {
|
|
foreach ($address->getSites() as $site) {
|
|
// Deduplication par identite d'objet : un meme site peut etre
|
|
// rattache a plusieurs adresses du client.
|
|
$sites[spl_object_id($site)] = $site;
|
|
}
|
|
}
|
|
|
|
return array_values($sites);
|
|
}
|
|
|
|
// Embed gate sur le groupe COMPTABLE (et non client:item:read comme contacts/
|
|
// adresses) : client:read:accounting n'est ajoute au contexte que si l'user a
|
|
// accounting.view (ClientReadGroupContextBuilder). Resultat : la cle `ribs` est
|
|
// TOTALEMENT ABSENTE du detail pour un user sans accounting.view (ex. Commerciale),
|
|
// au meme titre que les scalaires comptables — corrige la fuite de RIB ou la
|
|
// Commerciale recevait IBAN/BIC en clair.
|
|
/** @return Collection<int, ClientRib> */
|
|
#[Groups(['client:read:accounting'])]
|
|
public function getRibs(): Collection
|
|
{
|
|
return $this->ribs;
|
|
}
|
|
|
|
public function addRib(ClientRib $rib): static
|
|
{
|
|
if (!$this->ribs->contains($rib)) {
|
|
$this->ribs->add($rib);
|
|
$rib->setClient($this);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function removeRib(ClientRib $rib): static
|
|
{
|
|
if ($this->ribs->removeElement($rib) && $rib->getClient() === $this) {
|
|
$rib->setClient(null);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Groupe de lecture + nom serialise explicite : sans SerializedName, Symfony
|
|
// exposerait la cle "archived" (strip du prefixe "is" sur les getters).
|
|
#[Groups(['client:read'])]
|
|
#[SerializedName('isArchived')]
|
|
public function isArchived(): bool
|
|
{
|
|
return $this->isArchived;
|
|
}
|
|
|
|
public function setIsArchived(bool $isArchived): static
|
|
{
|
|
$this->isArchived = $isArchived;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getArchivedAt(): ?DateTimeImmutable
|
|
{
|
|
return $this->archivedAt;
|
|
}
|
|
|
|
public function setArchivedAt(?DateTimeImmutable $archivedAt): static
|
|
{
|
|
$this->archivedAt = $archivedAt;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getDeletedAt(): ?DateTimeImmutable
|
|
{
|
|
return $this->deletedAt;
|
|
}
|
|
|
|
public function setDeletedAt(?DateTimeImmutable $deletedAt): static
|
|
{
|
|
$this->deletedAt = $deletedAt;
|
|
|
|
return $this;
|
|
}
|
|
}
|