[ERP-76 + ERP-68] Validations d'adresse client (RG → 422) + fixtures démo Catalog/Commercial #41
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Catalog\Infrastructure\DataFixtures;
|
||||||
|
|
||||||
|
use App\Module\Catalog\Domain\Entity\Category;
|
||||||
|
use App\Module\Catalog\Domain\Entity\CategoryType;
|
||||||
|
use App\Module\Catalog\Domain\Repository\CategoryTypeRepositoryInterface;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixtures dev/test du module Catalog : ~12 categories de demonstration reparties
|
||||||
|
* sur les 4 types metier (DISTRIBUTEUR / COURTIER / SECTEUR / AUTRE). Alimente le
|
||||||
|
* repertoire clients (ClientFixtures, module Commercial) avec des donnees
|
||||||
|
* realistes couvrant les categorisations RG-1.03 (DISTRIBUTEUR/COURTIER) et
|
||||||
|
* RG-1.29 (SECTEUR/AUTRE sur adresse).
|
||||||
|
*
|
||||||
|
* Depend de CategoryTypeFixtures : les 4 CategoryType doivent etre seedes avant
|
||||||
|
* de pouvoir y rattacher des Category.
|
||||||
|
*
|
||||||
|
* Idempotence : lookup par (name, categoryType) parmi les categories non
|
||||||
|
* supprimees (deletedAt null), coherent avec l'index unique partiel
|
||||||
|
* uq_category_name_type_active (LOWER(name), category_type_id WHERE deleted_at
|
||||||
|
* IS NULL). Rejouable sans doublon meme si le purger Doctrine est desactive.
|
||||||
|
*
|
||||||
|
* Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
|
||||||
|
* restent null (« Systeme » cote front), c'est attendu.
|
||||||
|
*
|
||||||
|
* Portee : DONNEES DE DEMONSTRATION (dev uniquement). En environnement `test`,
|
||||||
|
* la fixture ne charge rien : les tests seedent et nettoient leurs propres
|
||||||
|
* categories (prefixe dedie) et comptent sur une table `category` vierge — y
|
||||||
|
* injecter 12 categories de demo casserait comptages et cleanups FK
|
||||||
|
* (client_category). Cf. ClientFixtures (meme garde-fou).
|
||||||
|
*/
|
||||||
|
class CategoryFixtures extends Fixture implements DependentFixtureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Source unique des categories de demonstration : code de type metier =>
|
||||||
|
* liste de noms. Les noms sont stockes tels quels (l'unicite est
|
||||||
|
* case-insensitive cote index).
|
||||||
|
*
|
||||||
|
* @var array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private const CATEGORIES = [
|
||||||
|
'SECTEUR' => [
|
||||||
|
'BTP',
|
||||||
|
'Industrie',
|
||||||
|
'Agro-alimentaire',
|
||||||
|
'Transport/Logistique',
|
||||||
|
'Services',
|
||||||
|
],
|
||||||
|
'DISTRIBUTEUR' => [
|
||||||
|
'Distributeur Grand Sud-Ouest',
|
||||||
|
'Distributeur National Premium',
|
||||||
|
'Grossiste régional',
|
||||||
|
],
|
||||||
|
'COURTIER' => [
|
||||||
|
'Cabinet de courtage Léonard',
|
||||||
|
'Cabinet de courtage Bernard',
|
||||||
|
],
|
||||||
|
'AUTRE' => [
|
||||||
|
'Indépendant',
|
||||||
|
'Association',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly CategoryTypeRepositoryInterface $categoryTypeRepository,
|
||||||
|
#[Autowire('%kernel.environment%')]
|
||||||
|
private readonly string $environment,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, class-string>
|
||||||
|
*/
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return [CategoryTypeFixtures::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
// Donnees de demo : dev uniquement. En test, on laisse la table vierge.
|
||||||
|
if ('test' === $this->environment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index des types metier par code (CategoryTypeFixtures les a seedes).
|
||||||
|
$typesByCode = [];
|
||||||
|
foreach ($this->categoryTypeRepository->findAllOrderedByLabel() as $type) {
|
||||||
|
$typesByCode[$type->getCode()] = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::CATEGORIES as $typeCode => $names) {
|
||||||
|
$type = $typesByCode[$typeCode] ?? null;
|
||||||
|
if (!$type instanceof CategoryType) {
|
||||||
|
// Misconfiguration : CategoryTypeFixtures n'a pas tourne avant.
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'CategoryTypeFixtures doit avoir seede le type "%s" avant CategoryFixtures.',
|
||||||
|
$typeCode,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($names as $name) {
|
||||||
|
$this->ensureCategory($manager, $name, $type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cree la categorie (name, type) si elle n'existe pas encore parmi les
|
||||||
|
* categories actives, sinon la laisse en place. Lookup aligne sur l'index
|
||||||
|
* unique partiel (nom + type, hors soft-deleted).
|
||||||
|
*/
|
||||||
|
private function ensureCategory(ObjectManager $manager, string $name, CategoryType $type): void
|
||||||
|
{
|
||||||
|
$existing = $manager->getRepository(Category::class)->findOneBy([
|
||||||
|
'name' => $name,
|
||||||
|
'categoryType' => $type,
|
||||||
|
'deletedAt' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (null !== $existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = new Category();
|
||||||
|
$category->setName($name);
|
||||||
|
$category->setCategoryType($type);
|
||||||
|
$manager->persist($category);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Commercial\Infrastructure\DataFixtures;
|
||||||
|
|
||||||
|
use App\Module\Catalog\Infrastructure\DataFixtures\CategoryFixtures;
|
||||||
|
use App\Module\Commercial\Application\Service\ClientFieldNormalizer;
|
||||||
|
use App\Module\Commercial\Domain\Entity\Bank;
|
||||||
|
use App\Module\Commercial\Domain\Entity\Client;
|
||||||
|
use App\Module\Commercial\Domain\Entity\ClientAddress;
|
||||||
|
use App\Module\Commercial\Domain\Entity\ClientContact;
|
||||||
|
use App\Module\Commercial\Domain\Entity\ClientRib;
|
||||||
|
use App\Module\Commercial\Domain\Entity\PaymentType;
|
||||||
|
use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
|
||||||
|
use App\Shared\Domain\Contract\CategoryInterface;
|
||||||
|
use App\Shared\Domain\Contract\SiteInterface;
|
||||||
|
use App\Shared\Domain\Contract\SiteProviderInterface;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixtures dev/test du module Commercial : ~14 clients de demonstration couvrant
|
||||||
|
* l'ensemble des cas metier RG-1.xx du repertoire clients (M1) :
|
||||||
|
* - client basique ; dependant distributeur / courtier (RG-1.03) ;
|
||||||
|
* - reglement LCR avec 2 RIB (RG-1.13) ; reglement Cheque sans RIB ;
|
||||||
|
* - multi-adresses Prospect / Livraison / Facturation (RG-1.06/07/08/11) ;
|
||||||
|
* - prospect seul ; 3 contacts dont un avec telephone secondaire (RG-1.05/1.02) ;
|
||||||
|
* - client archive (RG-1.22) ; onglet Information complet ; multi-categories M2M.
|
||||||
|
*
|
||||||
|
* Resolution inter-modules conforme a la regle n°1 (pas d'import direct) :
|
||||||
|
* - categories resolues via le contrat Shared CategoryInterface
|
||||||
|
* (resolve_target_entities -> Category) ;
|
||||||
|
* - sites resolus via le contrat Shared SiteProviderInterface.
|
||||||
|
*
|
||||||
|
* Normalisation : les valeurs sont fournies BRUTES (casse libre, telephones
|
||||||
|
* formates) et normalisees par ClientFieldNormalizer avant persist, exactement
|
||||||
|
* comme le ferait le ClientProcessor via l'API (companyName UPPERCASE,
|
||||||
|
* first/last Capitalize, telephones chiffres seuls, emails lowercase).
|
||||||
|
*
|
||||||
|
* Distributeur / courtier auto-references (RG-1.03) : les tiers referencables
|
||||||
|
* (GSO distributeur, Cabinet Leonard courtier) sont crees AVANT les clients qui
|
||||||
|
* les referencent ; un unique flush en fin de load ordonne correctement les
|
||||||
|
* inserts auto-references.
|
||||||
|
*
|
||||||
|
* Idempotence : lookup par companyName normalise (coherent avec l'index unique
|
||||||
|
* partiel uq_client_company_name_active). Un client deja present n'est pas
|
||||||
|
* reconstruit (ses sous-collections ne sont pas redupliquees). Rejouable sans
|
||||||
|
* doublon meme si le purger Doctrine est desactive.
|
||||||
|
*
|
||||||
|
* Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
|
||||||
|
* restent null (« Systeme » cote front), c'est attendu. Les donnees respectent
|
||||||
|
* les CHECK BDD ET les validators applicatifs ERP-76 (exclusivite Prospect,
|
||||||
|
* billingEmail ssi facturation, aucune categorie DISTRIBUTEUR/COURTIER sur une
|
||||||
|
* adresse).
|
||||||
|
*
|
||||||
|
* Depend de CategoryFixtures (categories), SitesFixtures (sites) et
|
||||||
|
* CommercialReferentialFixtures (referentiels comptables Bank / PaymentType).
|
||||||
|
*
|
||||||
|
* Portee : DONNEES DE DEMONSTRATION (dev uniquement). En environnement `test`,
|
||||||
|
* la fixture ne charge rien : les tests seedent et nettoient leurs propres
|
||||||
|
* clients et comptent sur une table `client` vierge — y injecter 14 clients de
|
||||||
|
* demo casserait les comptages de liste et les cleanups. Meme garde-fou que
|
||||||
|
* CategoryFixtures.
|
||||||
|
*/
|
||||||
|
class ClientFixtures extends Fixture implements DependentFixtureInterface
|
||||||
|
{
|
||||||
|
/** Cache des categories resolues par nom (evite des requetes repetees). */
|
||||||
|
private array $categoryCache = [];
|
||||||
|
|
||||||
|
/** Cache des sites resolus par nom. */
|
||||||
|
private array $siteCache = [];
|
||||||
|
|
||||||
|
/** ObjectManager courant, capture en debut de load (resolution categories). */
|
||||||
|
private ObjectManager $manager;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly ClientFieldNormalizer $normalizer,
|
||||||
|
private readonly SiteProviderInterface $siteProvider,
|
||||||
|
#[Autowire('%kernel.environment%')]
|
||||||
|
private readonly string $environment,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, class-string>
|
||||||
|
*/
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CategoryFixtures::class,
|
||||||
|
SitesFixtures::class,
|
||||||
|
CommercialReferentialFixtures::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
// Donnees de demo : dev uniquement. En test, on laisse la table vierge.
|
||||||
|
if ('test' === $this->environment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->manager = $manager;
|
||||||
|
|
||||||
|
// === Tiers referencables (RG-1.03) : crees en premier ===
|
||||||
|
|
||||||
|
// Distributeur reference par d'autres clients.
|
||||||
|
[$gso, $gsoIsNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Distrib Grand Sud-Ouest',
|
||||||
|
firstName: 'Paul',
|
||||||
|
lastName: 'Garnier',
|
||||||
|
phonePrimary: '05 56 10 20 30',
|
||||||
|
email: 'contact@distrib-gso.fr',
|
||||||
|
categoryNames: ['Distributeur Grand Sud-Ouest'],
|
||||||
|
);
|
||||||
|
if ($gsoIsNew) {
|
||||||
|
$this->addContact($gso, 'Paul', 'Garnier', 'Directeur commercial', '05 56 10 20 30', null, 'paul.garnier@distrib-gso.fr');
|
||||||
|
$this->addAddress($gso, ['Pommevic'], '82400', 'Pommevic', '1 Av. Jean Duquesne', isDelivery: true, categoryNames: ['Transport/Logistique']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Courtier reference par d'autres clients.
|
||||||
|
[$leonard, $leonardIsNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Cabinet Léonard Assurances',
|
||||||
|
firstName: 'Sophie',
|
||||||
|
lastName: 'Léonard',
|
||||||
|
phonePrimary: '05 49 11 22 33',
|
||||||
|
email: 'contact@cabinet-leonard.fr',
|
||||||
|
categoryNames: ['Cabinet de courtage Léonard'],
|
||||||
|
);
|
||||||
|
if ($leonardIsNew) {
|
||||||
|
$this->addContact($leonard, 'Sophie', 'Léonard', 'Gérante', '05 49 11 22 33', null, 'sophie.leonard@cabinet-leonard.fr');
|
||||||
|
$this->addAddress($leonard, ['Chatellerault'], '86100', 'Châtellerault', '5 rue des Courtiers', isBilling: true, billingEmail: 'Factures@Cabinet-Leonard.FR');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Client basique ===
|
||||||
|
[$dubois, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Menuiserie Dubois',
|
||||||
|
firstName: 'Jean',
|
||||||
|
lastName: 'Dubois',
|
||||||
|
phonePrimary: '05 49 00 00 01',
|
||||||
|
email: 'contact@menuiserie-dubois.fr',
|
||||||
|
categoryNames: ['BTP'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$dubois->setPaymentType($this->paymentType($manager, 'VIREMENT'));
|
||||||
|
$dubois->setBank($this->bank($manager, 'SG'));
|
||||||
|
$this->addContact($dubois, 'Jean', 'Dubois', 'Gérant', '05 49 00 00 01', null, 'jean.dubois@menuiserie-dubois.fr');
|
||||||
|
$this->addAddress($dubois, ['Chatellerault'], '86100', 'Châtellerault', '12 rue de l\'Atelier', isDelivery: true, categoryNames: ['BTP']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Dependant d'un distributeur (RG-1.03) ===
|
||||||
|
[$garage, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Garage Martin',
|
||||||
|
firstName: 'Luc',
|
||||||
|
lastName: 'Martin',
|
||||||
|
phonePrimary: '05 56 44 55 66',
|
||||||
|
email: 'accueil@garage-martin.fr',
|
||||||
|
categoryNames: ['Services'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$garage->setDistributor($gso);
|
||||||
|
$this->addContact($garage, 'Luc', 'Martin', 'Gérant', '05 56 44 55 66', null, 'luc.martin@garage-martin.fr');
|
||||||
|
$this->addAddress($garage, ['Pommevic'], '82400', 'Pommevic', '8 route de Moissac', isDelivery: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Dependant d'un courtier (RG-1.03) ===
|
||||||
|
[$boulangerie, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Boulangerie Lemoine',
|
||||||
|
firstName: 'Marie',
|
||||||
|
lastName: 'Lemoine',
|
||||||
|
phonePrimary: '05 49 77 88 99',
|
||||||
|
email: 'bonjour@boulangerie-lemoine.fr',
|
||||||
|
categoryNames: ['Agro-alimentaire'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$boulangerie->setBroker($leonard);
|
||||||
|
$this->addContact($boulangerie, 'Marie', 'Lemoine', 'Gérante', '05 49 77 88 99', null, 'marie.lemoine@boulangerie-lemoine.fr');
|
||||||
|
$this->addAddress($boulangerie, ['Chatellerault'], '86100', 'Châtellerault', '3 place du Marché', isDelivery: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Reglement LCR avec 2 RIB (RG-1.13) ===
|
||||||
|
[$transports, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Transports Rapides',
|
||||||
|
firstName: null,
|
||||||
|
lastName: 'Bernard',
|
||||||
|
phonePrimary: '05 56 12 13 14',
|
||||||
|
email: 'exploitation@transports-rapides.fr',
|
||||||
|
categoryNames: ['Transport/Logistique'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$transports->setPaymentType($this->paymentType($manager, 'LCR'));
|
||||||
|
$this->addContact($transports, null, 'Bernard', 'Responsable exploitation', '05 56 12 13 14', null, 'expl@transports-rapides.fr');
|
||||||
|
$this->addAddress($transports, ['Saint-Jean'], '17400', 'Fontenet', '2 zone industrielle', isDelivery: true, categoryNames: ['Transport/Logistique']);
|
||||||
|
$this->addRib($transports, 'Compte principal', 'BNPAFRPPXXX', 'FR1420041010050500013M02606', 0);
|
||||||
|
$this->addRib($transports, 'Compte secondaire', 'SOGEFRPPXXX', 'FR7630006000011234567890189', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Multi-adresses Prospect / Livraison / Facturation (RG-1.06/07/08/11) ===
|
||||||
|
[$industries, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Industries Vertes',
|
||||||
|
firstName: 'Claire',
|
||||||
|
lastName: 'Moreau',
|
||||||
|
phonePrimary: '05 49 21 22 23',
|
||||||
|
email: 'contact@industries-vertes.fr',
|
||||||
|
categoryNames: ['Industrie'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$this->addContact($industries, 'Claire', 'Moreau', 'Directrice', '05 49 21 22 23', null, 'claire.moreau@industries-vertes.fr');
|
||||||
|
// Prospect : exclusif de livraison/facturation (sans billingEmail).
|
||||||
|
$this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '1 avenue de la Prospection', isProspect: true, position: 0);
|
||||||
|
// Livraison.
|
||||||
|
$this->addAddress($industries, ['Saint-Jean'], '17400', 'Fontenet', '4 rue de la Livraison', isDelivery: true, categoryNames: ['Industrie'], position: 1);
|
||||||
|
// Facturation : billingEmail obligatoire.
|
||||||
|
$this->addAddress($industries, ['Chatellerault'], '86100', 'Châtellerault', '7 boulevard des Factures', isBilling: true, billingEmail: 'Compta@Industries-Vertes.FR', position: 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 3 contacts dont un avec telephone secondaire (RG-1.05/1.02) ===
|
||||||
|
[$agro, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Agro Distribution Sud',
|
||||||
|
firstName: 'Thomas',
|
||||||
|
lastName: 'Petit',
|
||||||
|
phonePrimary: '05 56 31 32 33',
|
||||||
|
email: 'contact@agro-sud.fr',
|
||||||
|
categoryNames: ['Agro-alimentaire'],
|
||||||
|
phoneSecondary: '06 01 02 03 04',
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$this->addContact($agro, 'Thomas', 'Petit', 'Directeur des achats', '05 56 31 32 33', '06 01 02 03 04', 'thomas.petit@agro-sud.fr', 0);
|
||||||
|
$this->addContact($agro, 'Julie', 'Roux', 'Assistante commerciale', '05 56 31 32 34', null, 'julie.roux@agro-sud.fr', 1);
|
||||||
|
$this->addContact($agro, 'Marc', 'Girard', 'Logistique', '05 56 31 32 35', null, 'marc.girard@agro-sud.fr', 2);
|
||||||
|
$this->addAddress($agro, ['Pommevic'], '82400', 'Pommevic', '10 rue des Producteurs', isDelivery: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Client archive (RG-1.22) ===
|
||||||
|
[$ancienne, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Ancienne Société Oubliée',
|
||||||
|
firstName: null,
|
||||||
|
lastName: 'Durand',
|
||||||
|
phonePrimary: '05 49 99 99 99',
|
||||||
|
email: 'contact@ancienne-societe.fr',
|
||||||
|
categoryNames: ['Association'],
|
||||||
|
isArchived: true,
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$this->addContact($ancienne, null, 'Durand', 'Ancien contact', '05 49 99 99 99', null, 'contact@ancienne-societe.fr');
|
||||||
|
$this->addAddress($ancienne, ['Chatellerault'], '86100', 'Châtellerault', '99 rue Fermée', isDelivery: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Reglement Cheque sans RIB ===
|
||||||
|
[$services, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Services Pro Conseil',
|
||||||
|
firstName: 'Nadia',
|
||||||
|
lastName: 'Benali',
|
||||||
|
phonePrimary: '05 49 41 42 43',
|
||||||
|
email: 'contact@services-pro.fr',
|
||||||
|
categoryNames: ['Services'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$services->setPaymentType($this->paymentType($manager, 'CHEQUE'));
|
||||||
|
$this->addContact($services, 'Nadia', 'Benali', 'Consultante', '05 49 41 42 43', null, 'nadia.benali@services-pro.fr');
|
||||||
|
$this->addAddress($services, ['Chatellerault'], '86100', 'Châtellerault', '15 rue du Conseil', isDelivery: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Onglet Information complet (RG-1.04) ===
|
||||||
|
[$holding, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Holding Premium Invest',
|
||||||
|
firstName: 'Antoine',
|
||||||
|
lastName: 'Lefèvre',
|
||||||
|
phonePrimary: '05 56 51 52 53',
|
||||||
|
email: 'direction@holding-premium.fr',
|
||||||
|
categoryNames: ['Industrie'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$holding->setDescription('Holding industrielle diversifiée, présente sur le Grand Sud-Ouest.');
|
||||||
|
$holding->setCompetitors('Groupe Atlantique, Sud Industries');
|
||||||
|
$holding->setFoundedAt(new DateTimeImmutable('2005-03-15'));
|
||||||
|
$holding->setEmployeesCount(240);
|
||||||
|
$holding->setRevenueAmount('18500000.00');
|
||||||
|
$holding->setDirectorName('Antoine Lefèvre');
|
||||||
|
$holding->setProfitAmount('1250000.00');
|
||||||
|
$this->addContact($holding, 'Antoine', 'Lefèvre', 'PDG', '05 56 51 52 53', null, 'antoine.lefevre@holding-premium.fr');
|
||||||
|
$this->addAddress($holding, ['Pommevic'], '82400', 'Pommevic', '1 allée des Investisseurs', isDelivery: true, categoryNames: ['Industrie']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Multi-categories M2M ===
|
||||||
|
[$conglo, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Conglomérat Multi Activités',
|
||||||
|
firstName: 'Hélène',
|
||||||
|
lastName: 'Faure',
|
||||||
|
phonePrimary: '05 49 61 62 63',
|
||||||
|
email: 'contact@conglomerat-multi.fr',
|
||||||
|
categoryNames: ['BTP', 'Industrie', 'Services'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$this->addContact($conglo, 'Hélène', 'Faure', 'Directrice générale', '05 49 61 62 63', null, 'helene.faure@conglomerat-multi.fr');
|
||||||
|
$this->addAddress($conglo, ['Chatellerault', 'Saint-Jean'], '86100', 'Châtellerault', '20 rue des Activités', isDelivery: true, categoryNames: ['BTP', 'Services']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Prospect seul ===
|
||||||
|
[$prospect, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Prospect Futur Client',
|
||||||
|
firstName: 'Olivier',
|
||||||
|
lastName: 'Renard',
|
||||||
|
phonePrimary: '05 56 71 72 73',
|
||||||
|
email: 'olivier.renard@prospect-futur.fr',
|
||||||
|
categoryNames: ['BTP'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$this->addContact($prospect, 'Olivier', 'Renard', 'Responsable projet', '05 56 71 72 73', null, 'olivier.renard@prospect-futur.fr');
|
||||||
|
$this->addAddress($prospect, ['Chatellerault'], '86100', 'Châtellerault', '30 rue de la Découverte', isProspect: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Categorie AUTRE ===
|
||||||
|
[$association, $isNew] = $this->ensureClient(
|
||||||
|
$manager,
|
||||||
|
companyName: 'Association des Riverains',
|
||||||
|
firstName: null,
|
||||||
|
lastName: 'Caron',
|
||||||
|
phonePrimary: '05 49 81 82 83',
|
||||||
|
email: 'contact@asso-riverains.fr',
|
||||||
|
categoryNames: ['Association'],
|
||||||
|
);
|
||||||
|
if ($isNew) {
|
||||||
|
$this->addContact($association, null, 'Caron', 'Président', '05 49 81 82 83', null, 'president@asso-riverains.fr');
|
||||||
|
$this->addAddress($association, ['Saint-Jean'], '17400', 'Fontenet', '6 chemin du Village', isDelivery: true, categoryNames: ['Association']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cree un client (base normalisee + categories) s'il n'existe pas encore,
|
||||||
|
* sinon retourne l'existant. Retourne [Client, isNew] : isNew=false bloque la
|
||||||
|
* reconstruction des sous-collections (idempotence sans doublon).
|
||||||
|
*
|
||||||
|
* @param list<string> $categoryNames
|
||||||
|
*
|
||||||
|
* @return array{0: Client, 1: bool}
|
||||||
|
*/
|
||||||
|
private function ensureClient(
|
||||||
|
ObjectManager $manager,
|
||||||
|
string $companyName,
|
||||||
|
?string $firstName,
|
||||||
|
?string $lastName,
|
||||||
|
string $phonePrimary,
|
||||||
|
string $email,
|
||||||
|
array $categoryNames,
|
||||||
|
?string $phoneSecondary = null,
|
||||||
|
bool $isArchived = false,
|
||||||
|
): array {
|
||||||
|
$normalizedName = (string) $this->normalizer->normalizeCompanyName($companyName);
|
||||||
|
|
||||||
|
$existing = $manager->getRepository(Client::class)->findOneBy(['companyName' => $normalizedName]);
|
||||||
|
if ($existing instanceof Client) {
|
||||||
|
return [$existing, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = new Client();
|
||||||
|
$client->setCompanyName($normalizedName);
|
||||||
|
$client->setFirstName($this->normalizer->normalizePersonName($firstName));
|
||||||
|
$client->setLastName($this->normalizer->normalizePersonName($lastName));
|
||||||
|
$client->setPhonePrimary((string) $this->normalizer->normalizePhone($phonePrimary));
|
||||||
|
$client->setPhoneSecondary($this->normalizer->normalizePhone($phoneSecondary));
|
||||||
|
$client->setEmail((string) $this->normalizer->normalizeEmail($email));
|
||||||
|
|
||||||
|
foreach ($categoryNames as $categoryName) {
|
||||||
|
$client->addCategory($this->category($manager, $categoryName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isArchived) {
|
||||||
|
$client->setIsArchived(true);
|
||||||
|
$client->setArchivedAt(new DateTimeImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->persist($client);
|
||||||
|
|
||||||
|
return [$client, true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un contact normalise au client (cascade persist via Client.contacts).
|
||||||
|
* Au moins lastName est toujours fourni (RG-1.05, chk_client_contact_name).
|
||||||
|
*/
|
||||||
|
private function addContact(
|
||||||
|
Client $client,
|
||||||
|
?string $firstName,
|
||||||
|
?string $lastName,
|
||||||
|
?string $jobTitle,
|
||||||
|
?string $phonePrimary,
|
||||||
|
?string $phoneSecondary,
|
||||||
|
?string $email,
|
||||||
|
int $position = 0,
|
||||||
|
): void {
|
||||||
|
$contact = new ClientContact();
|
||||||
|
$contact->setFirstName($this->normalizer->normalizePersonName($firstName));
|
||||||
|
$contact->setLastName($this->normalizer->normalizePersonName($lastName));
|
||||||
|
$contact->setJobTitle($jobTitle);
|
||||||
|
$contact->setPhonePrimary($this->normalizer->normalizePhone($phonePrimary));
|
||||||
|
$contact->setPhoneSecondary($this->normalizer->normalizePhone($phoneSecondary));
|
||||||
|
$contact->setEmail($this->normalizer->normalizeEmail($email));
|
||||||
|
$contact->setPosition($position);
|
||||||
|
|
||||||
|
$client->addContact($contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une adresse au client (cascade persist via Client.addresses). Les
|
||||||
|
* donnees respectent les validators ERP-76 : exclusivite Prospect,
|
||||||
|
* billingEmail ssi facturation, categories limitees a SECTEUR/AUTRE.
|
||||||
|
*
|
||||||
|
* @param list<string> $siteNames au moins un site (RG-1.10)
|
||||||
|
* @param list<string> $categoryNames categories SECTEUR/AUTRE uniquement (RG-1.29)
|
||||||
|
*/
|
||||||
|
private function addAddress(
|
||||||
|
Client $client,
|
||||||
|
array $siteNames,
|
||||||
|
string $postalCode,
|
||||||
|
string $city,
|
||||||
|
string $street,
|
||||||
|
bool $isProspect = false,
|
||||||
|
bool $isDelivery = false,
|
||||||
|
bool $isBilling = false,
|
||||||
|
?string $billingEmail = null,
|
||||||
|
array $categoryNames = [],
|
||||||
|
int $position = 0,
|
||||||
|
): void {
|
||||||
|
$address = new ClientAddress();
|
||||||
|
$address->setIsProspect($isProspect);
|
||||||
|
$address->setIsDelivery($isDelivery);
|
||||||
|
$address->setIsBilling($isBilling);
|
||||||
|
$address->setBillingEmail($this->normalizer->normalizeEmail($billingEmail));
|
||||||
|
$address->setPostalCode($postalCode);
|
||||||
|
$address->setCity($city);
|
||||||
|
$address->setStreet($street);
|
||||||
|
$address->setPosition($position);
|
||||||
|
|
||||||
|
foreach ($siteNames as $siteName) {
|
||||||
|
$address->addSite($this->site($siteName));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($categoryNames as $categoryName) {
|
||||||
|
$address->addCategory($this->category($this->manager, $categoryName));
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->addAddress($address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un RIB au client (cascade persist via Client.ribs). IBAN/BIC valides
|
||||||
|
* (Assert\Iban/Bic non rejouee sur persist direct mais donnees coherentes).
|
||||||
|
*/
|
||||||
|
private function addRib(Client $client, string $label, string $bic, string $iban, int $position = 0): void
|
||||||
|
{
|
||||||
|
$rib = new ClientRib();
|
||||||
|
$rib->setLabel($label);
|
||||||
|
$rib->setBic($bic);
|
||||||
|
$rib->setIban($iban);
|
||||||
|
$rib->setPosition($position);
|
||||||
|
|
||||||
|
$client->addRib($rib);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resout une categorie par son nom via le contrat Shared CategoryInterface
|
||||||
|
* (resolve_target_entities -> Category), sans importer le module Catalog
|
||||||
|
* (regle n°1). Mise en cache par nom.
|
||||||
|
*/
|
||||||
|
private function category(ObjectManager $manager, string $name): CategoryInterface
|
||||||
|
{
|
||||||
|
if (isset($this->categoryCache[$name])) {
|
||||||
|
return $this->categoryCache[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $manager->getRepository(CategoryInterface::class)->findOneBy([
|
||||||
|
'name' => $name,
|
||||||
|
'deletedAt' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$category instanceof CategoryInterface) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Categorie "%s" introuvable : CategoryFixtures doit tourner avant ClientFixtures.',
|
||||||
|
$name,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->categoryCache[$name] = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resout un site par son nom via le contrat Shared SiteProviderInterface,
|
||||||
|
* sans importer le module Sites (regle n°1). Mise en cache par nom.
|
||||||
|
*/
|
||||||
|
private function site(string $name): SiteInterface
|
||||||
|
{
|
||||||
|
if (isset($this->siteCache[$name])) {
|
||||||
|
return $this->siteCache[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$site = $this->siteProvider->findByName($name);
|
||||||
|
|
||||||
|
if (!$site instanceof SiteInterface) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Site "%s" introuvable : SitesFixtures doit tourner avant ClientFixtures.',
|
||||||
|
$name,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->siteCache[$name] = $site;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function paymentType(ObjectManager $manager, string $code): PaymentType
|
||||||
|
{
|
||||||
|
$type = $manager->getRepository(PaymentType::class)->findOneBy(['code' => $code]);
|
||||||
|
|
||||||
|
if (!$type instanceof PaymentType) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'PaymentType "%s" introuvable : CommercialReferentialFixtures doit tourner avant ClientFixtures.',
|
||||||
|
$code,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bank(ObjectManager $manager, string $code): Bank
|
||||||
|
{
|
||||||
|
$bank = $manager->getRepository(Bank::class)->findOneBy(['code' => $code]);
|
||||||
|
|
||||||
|
if (!$bank instanceof Bank) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Bank "%s" introuvable : CommercialReferentialFixtures doit tourner avant ClientFixtures.',
|
||||||
|
$code,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bank;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user