feat(commercial) : 2e email de facturation optionnel sur l'adresse client (ERP-119)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m9s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m10s

Pendant du telephone secondaire (max 2). Bump @malio/layer-ui 1.7.8 (prop
addable du MalioInputEmail).

- back : colonne billing_email_secondary (migration + COMMENT + catalogue),
  propriete ClientAddress (Email + Length), Callback etendu (2e email interdit
  hors facturation, optionnel sinon), normalisation lowercase au Processor.
- front : draft + flag UI hasSecondaryBillingEmail, mappers, payloads, champ
  MalioInputEmail :addable -> revele un 2e champ ; layout : 2e email qui coule
  dans la grille et Adresse complementaire sur une colonne.
- tests back (2 emails / 2e email hors facturation) et front (payload).
This commit is contained in:
2026-06-09 20:38:50 +02:00
parent ada4b156fa
commit dacf67535d
14 changed files with 205 additions and 26 deletions
@@ -178,6 +178,15 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
#[Groups(['client_address:read', 'client_address:write'])]
private ?string $billingEmail = null;
// 2e email de facturation, optionnel (max 2 — pendant du telephone secondaire).
// Comme le principal : interdit hors facturation (validateBillingEmailPresence),
// mais jamais obligatoire. Normalise en lowercase par le ClientAddressProcessor.
#[ORM\Column(length: 180, nullable: true)]
#[Assert\Email(message: 'L\'email de facturation secondaire n\'est pas valide.')]
#[Assert\Length(max: 180, maxMessage: 'L\'email de facturation secondaire ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
#[Groups(['client_address:read', 'client_address:write'])]
private ?string $billingEmailSecondary = null;
#[ORM\Column(options: ['default' => 0])]
#[Groups(['client_address:read', 'client_address:write'])]
private int $position = 0;
@@ -308,6 +317,16 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
->addViolation()
;
}
// Le 2e email est OPTIONNEL (jamais requis), mais comme le principal il
// n'a de sens que sur une adresse de facturation.
$hasSecondaryEmail = null !== $this->billingEmailSecondary && '' !== trim($this->billingEmailSecondary);
if (!$this->isBilling && $hasSecondaryEmail) {
$context->buildViolation('L\'email de facturation n\'est autorisé que sur une adresse de facturation.')
->atPath('billingEmailSecondary')
->addViolation()
;
}
}
/**
@@ -497,6 +516,18 @@ class ClientAddress implements TimestampableInterface, BlamableInterface
return $this;
}
public function getBillingEmailSecondary(): ?string
{
return $this->billingEmailSecondary;
}
public function setBillingEmailSecondary(?string $billingEmailSecondary): static
{
$this->billingEmailSecondary = $billingEmailSecondary;
return $this;
}
public function getPosition(): int
{
return $this->position;
@@ -94,5 +94,6 @@ final class ClientAddressProcessor implements ProcessorInterface
private function normalize(ClientAddress $address): void
{
$address->setBillingEmail($this->normalizer->normalizeEmail($address->getBillingEmail()));
$address->setBillingEmailSecondary($this->normalizer->normalizeEmail($address->getBillingEmailSecondary()));
}
}
@@ -219,21 +219,22 @@ final class ColumnCommentsCatalog
] + self::timestampableBlamableComments(),
'client_address' => [
'_table' => 'Adresses d un client (1:n) — prospect exclusif de livraison/facturation (RG-1.06/07/08), >= 1 site rattache (RG-1.10).',
'id' => 'Identifiant interne auto-incremente.',
'client_id' => 'FK -> client.id, ON DELETE CASCADE — client proprietaire de l adresse.',
'is_prospect' => 'Adresse de prospection — exclusive de is_delivery/is_billing (RG-1.06/07/08, chk_client_address_prospect_exclusive). Faux par defaut.',
'is_delivery' => 'Adresse de livraison. Exclusive de is_prospect. Faux par defaut.',
'is_billing' => 'Adresse de facturation. Exclusive de is_prospect. Impose billing_email (RG-1.11). Faux par defaut.',
'is_broker' => 'Adresse Courtier — type autonome exclusif de tout autre usage (chk_client_address_broker_exclusive). Faux par defaut.',
'is_distributor' => 'Adresse Distributeur — type autonome exclusif de tout autre usage (chk_client_address_distributor_exclusive). Faux par defaut.',
'country' => 'Pays de l adresse — defaut France.',
'postal_code' => 'Code postal (4-5 chiffres attendus, RG-1.09).',
'city' => 'Ville — preremplie depuis le code postal via API BAN cote front (RG-1.09).',
'street' => 'Numero et voie de l adresse.',
'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
'billing_email' => 'Email de facturation — obligatoire si is_billing, null sinon (RG-1.11, chk_client_address_billing_email).',
'position' => 'Ordre d affichage de l adresse dans la liste du client (croissant).',
'_table' => 'Adresses d un client (1:n) — prospect exclusif de livraison/facturation (RG-1.06/07/08), >= 1 site rattache (RG-1.10).',
'id' => 'Identifiant interne auto-incremente.',
'client_id' => 'FK -> client.id, ON DELETE CASCADE — client proprietaire de l adresse.',
'is_prospect' => 'Adresse de prospection — exclusive de is_delivery/is_billing (RG-1.06/07/08, chk_client_address_prospect_exclusive). Faux par defaut.',
'is_delivery' => 'Adresse de livraison. Exclusive de is_prospect. Faux par defaut.',
'is_billing' => 'Adresse de facturation. Exclusive de is_prospect. Impose billing_email (RG-1.11). Faux par defaut.',
'is_broker' => 'Adresse Courtier — type autonome exclusif de tout autre usage (chk_client_address_broker_exclusive). Faux par defaut.',
'is_distributor' => 'Adresse Distributeur — type autonome exclusif de tout autre usage (chk_client_address_distributor_exclusive). Faux par defaut.',
'country' => 'Pays de l adresse — defaut France.',
'postal_code' => 'Code postal (4-5 chiffres attendus, RG-1.09).',
'city' => 'Ville — preremplie depuis le code postal via API BAN cote front (RG-1.09).',
'street' => 'Numero et voie de l adresse.',
'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
'billing_email' => 'Email de facturation — obligatoire si is_billing, null sinon (RG-1.11, chk_client_address_billing_email).',
'billing_email_secondary' => '2e email de facturation, optionnel (max 2). Interdit hors facturation, normalise en minuscules (RG-1.21).',
'position' => 'Ordre d affichage de l adresse dans la liste du client (croissant).',
] + self::timestampableBlamableComments(),
'client_address_site' => [