[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