6dab7cfd17
2 nits cs preexistants masques par le cache local (.php-cs-fixer.cache) et revele par la CI (check projet entier, sans cache) : QualimatCarrierSearchProvider et CarrierFixtures. Sans incidence fonctionnelle.
315 lines
14 KiB
PHP
315 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Transport\Infrastructure\DataFixtures;
|
|
|
|
use App\Module\Commercial\Infrastructure\DataFixtures\ClientFixtures;
|
|
use App\Module\Commercial\Infrastructure\DataFixtures\SupplierFixtures;
|
|
use App\Module\Sites\Infrastructure\DataFixtures\SitesFixtures;
|
|
use App\Module\Transport\Application\Service\CarrierFieldNormalizer;
|
|
use App\Module\Transport\Domain\Entity\Carrier;
|
|
use App\Module\Transport\Domain\Entity\CarrierAddress;
|
|
use App\Module\Transport\Domain\Entity\CarrierContact;
|
|
use App\Module\Transport\Domain\Entity\CarrierPrice;
|
|
use App\Module\Transport\Domain\Entity\QualimatCarrier;
|
|
use App\Shared\Domain\Contract\ClientAddressInterface;
|
|
use App\Shared\Domain\Contract\SiteInterface;
|
|
use App\Shared\Domain\Contract\SiteProviderInterface;
|
|
use App\Shared\Domain\Contract\SupplierAddressInterface;
|
|
use App\Shared\Domain\Entity\UploadedDocument;
|
|
use DateTimeImmutable;
|
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Doctrine\Persistence\ObjectManager;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
|
|
/**
|
|
* Fixtures dev/demo du repertoire transporteurs (M4) couvrant l'ensemble des cas
|
|
* metier RG-4.xx, jumelles des fixtures fournisseurs (M2). C'est ICI que vivent
|
|
* les fixtures COMPLETES (les maillons WT precedents s'etaient limites a un stub
|
|
* de lecture). Cas pivots seedes (§ 8.4) :
|
|
* - 1 transporteur QUALIMAT (lien `qualimat_carrier` + adresse copiee +
|
|
* validityDate PASSEE pour exercer le fond rouge RG-4.04) ;
|
|
* - 1 transporteur AUTRE + Decharge (UploadedDocument, RG-4.02) ;
|
|
* - 1 transporteur affrete (indexation + benne + volume obligatoires, RG-4.03) ;
|
|
* - 1 transporteur LIOT (immatriculations, certification non requise, RG-4.01) ;
|
|
* - 1 transporteur COMPLET : contacts + adresses + prix CLIENT et FOURNISSEUR ;
|
|
* - 1 transporteur archive (exclusion liste + restauration, RG-4.14).
|
|
*
|
|
* Resolution inter-modules conforme a la regle n°1 (pas d'import de logique) :
|
|
* - sites resolus via le contrat Shared SiteProviderInterface ;
|
|
* - client/adresse et fournisseur/adresse des prix resolus via les contrats
|
|
* Shared ClientAddressInterface / SupplierAddressInterface (relations ORM
|
|
* partagees, RG-4.10/4.11). Si la demo Commercial/Sites n'est pas chargee, les
|
|
* prix sont simplement omis (le reste de la fiche reste seede).
|
|
*
|
|
* Normalisation : valeurs fournies BRUTES puis normalisees par
|
|
* CarrierFieldNormalizer avant persist, comme le ferait le CarrierProcessor via
|
|
* l'API (name UPPERCASE, first/last Capitalize, telephones chiffres seuls, email
|
|
* lowercase, liotPlates « ; »-normalise).
|
|
*
|
|
* Idempotence : lookup par `name` normalise (coherent avec l'index unique partiel
|
|
* uq_carrier_name_active). Un transporteur deja present n'est pas reconstruit (ses
|
|
* sous-collections ne sont pas redupliquees). Rejouable sans doublon.
|
|
*
|
|
* 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
|
|
* transporteurs et comptent sur une table `carrier` vierge — y injecter des
|
|
* transporteurs de demo casserait les comptages de liste et les cleanups. Meme
|
|
* garde-fou que ClientFixtures / SupplierFixtures.
|
|
*/
|
|
class CarrierFixtures extends Fixture implements DependentFixtureInterface
|
|
{
|
|
/** SIRET de la ligne qualimat_carrier de demo (cle naturelle, insert idempotent). */
|
|
private const string QUALIMAT_DEMO_SIRET = '90000000000017';
|
|
|
|
public function __construct(
|
|
private readonly CarrierFieldNormalizer $normalizer,
|
|
private readonly SiteProviderInterface $siteProvider,
|
|
#[Autowire('%kernel.environment%')]
|
|
private readonly string $environment,
|
|
) {}
|
|
|
|
/**
|
|
* @return array<int, class-string>
|
|
*/
|
|
public function getDependencies(): array
|
|
{
|
|
// Les prix referencent des Client/Supplier/Site de demo (relations ORM
|
|
// partagees) : ces fixtures doivent tourner avant.
|
|
return [
|
|
SitesFixtures::class,
|
|
ClientFixtures::class,
|
|
SupplierFixtures::class,
|
|
];
|
|
}
|
|
|
|
public function load(ObjectManager $manager): void
|
|
{
|
|
// Donnees de demo : dev uniquement. En test, on laisse la table vierge.
|
|
if ('test' === $this->environment) {
|
|
return;
|
|
}
|
|
|
|
// === Transporteur QUALIMAT (RG-4.01) — adresse copiee + validite PASSEE (RG-4.04) ===
|
|
[$grelillier, $isNew] = $this->ensureCarrier($manager, 'Transports Grelillier');
|
|
if ($isNew) {
|
|
$grelillier->setQualimatCarrier($this->ensureQualimatDemoLine($manager));
|
|
$grelillier->setCertificationType('QUALIMAT');
|
|
// Adresse pre-remplie depuis la copie QUALIMAT (RG-4.05).
|
|
$this->addAddress($grelillier, '86000', 'Poitiers', '12 rue des Acacias');
|
|
$this->addContact($grelillier, 'Marie', 'Martin', 'Exploitation', '06 12 34 56 78', null, 'marie.martin@grelillier.fr');
|
|
}
|
|
|
|
// === Transporteur AUTRE + Decharge (RG-4.02) ===
|
|
[$pandele, $isNew] = $this->ensureCarrier($manager, 'Transports Pandele');
|
|
if ($isNew) {
|
|
$pandele->setCertificationType('AUTRE');
|
|
$pandele->setDischargeDocument($this->buildDischargeDocument($manager));
|
|
$this->addContact($pandele, 'Luc', 'Pandele', 'Gerant', '05 49 11 22 33', null, 'luc.pandele@pandele.fr');
|
|
}
|
|
|
|
// === Transporteur affrete (RG-4.03) — indexation + benne + volume ===
|
|
[$affrete, $isNew] = $this->ensureCarrier($manager, 'Affreteurs Reunis');
|
|
if ($isNew) {
|
|
$affrete->setCertificationType('GMP_PLUS');
|
|
$affrete->setIsChartered(true);
|
|
$affrete->setIndexationRate('5.00');
|
|
$affrete->setContainerType('BENNE');
|
|
$affrete->setVolumeM3('90.00');
|
|
$this->addAddress($affrete, '17000', 'La Rochelle', '4 quai des Affreteurs');
|
|
}
|
|
|
|
// === Cas LIOT (RG-4.01) — immatriculations, certification non requise ===
|
|
[$liot, $isNew] = $this->ensureCarrier($manager, 'LIOT');
|
|
if ($isNew) {
|
|
$liot->setLiotPlates($this->normalizer->normalizeLiotPlates('ab-123-cd ; ef-456-gh ; gh-789-ij'));
|
|
}
|
|
|
|
// === Transporteur COMPLET — contacts + adresses + prix CLIENT et FOURNISSEUR ===
|
|
[$complet, $isNew] = $this->ensureCarrier($manager, 'Transports Logistique Globale');
|
|
if ($isNew) {
|
|
$complet->setCertificationType('OVOCOM');
|
|
$this->addAddress($complet, '86100', 'Châtellerault', '20 zone des Transporteurs');
|
|
$this->addContact($complet, 'Sophie', 'Bernard', 'Directrice', '05 49 44 55 66', '06 99 88 77 66', 'sophie.bernard@logistique-globale.fr', 0);
|
|
$this->addContact($complet, 'Marc', 'Lopez', 'Affretement', '05 49 44 55 67', null, 'marc.lopez@logistique-globale.fr', 1);
|
|
$this->addPrices($manager, $complet);
|
|
}
|
|
|
|
// === Transporteur archive (RG-4.14) ===
|
|
[$archive, $isNew] = $this->ensureCarrier($manager, 'Transports Anciens', isArchived: true);
|
|
if ($isNew) {
|
|
$archive->setCertificationType('COMPTE_PROPRE');
|
|
$this->addContact($archive, 'Paul', 'Ancien', 'Ex-gerant', '05 49 00 00 00', null, 'paul.ancien@anciens.fr');
|
|
}
|
|
|
|
$manager->flush();
|
|
}
|
|
|
|
/**
|
|
* Cree un transporteur (nom normalise UPPERCASE) s'il n'existe pas encore,
|
|
* sinon retourne l'existant. Retourne [Carrier, isNew] : isNew=false bloque la
|
|
* reconstruction des sous-collections (idempotence sans doublon).
|
|
*
|
|
* @return array{0: Carrier, 1: bool}
|
|
*/
|
|
private function ensureCarrier(ObjectManager $manager, string $name, bool $isArchived = false): array
|
|
{
|
|
$normalizedName = (string) $this->normalizer->normalizeName($name);
|
|
|
|
$existing = $manager->getRepository(Carrier::class)->findOneBy(['name' => $normalizedName]);
|
|
if ($existing instanceof Carrier) {
|
|
return [$existing, false];
|
|
}
|
|
|
|
$carrier = new Carrier();
|
|
$carrier->setName($normalizedName);
|
|
|
|
if ($isArchived) {
|
|
$carrier->setIsArchived(true);
|
|
$carrier->setArchivedAt(new DateTimeImmutable());
|
|
}
|
|
|
|
$manager->persist($carrier);
|
|
|
|
return [$carrier, true];
|
|
}
|
|
|
|
/**
|
|
* Ajoute une adresse au transporteur (cascade persist via Carrier.addresses).
|
|
*/
|
|
private function addAddress(Carrier $carrier, string $postalCode, string $city, string $street): void
|
|
{
|
|
$address = new CarrierAddress();
|
|
$address->setPostalCode($postalCode);
|
|
$address->setCity($city);
|
|
$address->setStreet($street);
|
|
$carrier->addAddress($address);
|
|
}
|
|
|
|
/**
|
|
* Ajoute un contact normalise au transporteur (cascade persist via
|
|
* Carrier.contacts). Au moins un champ est toujours fourni (RG-4.08).
|
|
*/
|
|
private function addContact(
|
|
Carrier $carrier,
|
|
?string $firstName,
|
|
?string $lastName,
|
|
?string $jobTitle,
|
|
?string $phonePrimary,
|
|
?string $phoneSecondary,
|
|
?string $email,
|
|
int $position = 0,
|
|
): void {
|
|
$contact = new CarrierContact();
|
|
$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);
|
|
|
|
$carrier->addContact($contact);
|
|
}
|
|
|
|
/**
|
|
* Ajoute un prix CLIENT et un prix FOURNISSEUR au transporteur (RG-4.10/4.11),
|
|
* en resolvant les relations cross-module (client/adresse de livraison + site
|
|
* de depart ; fournisseur/adresse d'appro + site de livraison) via les contrats
|
|
* Shared. Si la demo Commercial/Sites n'est pas disponible, les prix sont omis.
|
|
*/
|
|
private function addPrices(ObjectManager $manager, Carrier $carrier): void
|
|
{
|
|
$site = $this->siteProvider->findByName('Chatellerault');
|
|
|
|
// Branche CLIENT (RG-4.10) : 1ere adresse de livraison de la demo M1.
|
|
$clientAddress = $manager->getRepository(ClientAddressInterface::class)->findOneBy(['isDelivery' => true]);
|
|
if ($site instanceof SiteInterface && $clientAddress instanceof ClientAddressInterface && null !== $clientAddress->getClient()) {
|
|
$clientPrice = new CarrierPrice();
|
|
$clientPrice->setDirection('CLIENT');
|
|
$clientPrice->setClient($clientAddress->getClient());
|
|
$clientPrice->setClientDeliveryAddress($clientAddress);
|
|
$clientPrice->setDepartureSite($site);
|
|
$clientPrice->setContainerType('BENNE');
|
|
$clientPrice->setPricingUnit('TONNE');
|
|
$clientPrice->setPrice('42.50');
|
|
$clientPrice->setPriceState('VALIDE');
|
|
$carrier->addPrice($clientPrice);
|
|
}
|
|
|
|
// Branche FOURNISSEUR (RG-4.11) : 1ere adresse de DEPART de la demo M2.
|
|
$supplierAddress = $manager->getRepository(SupplierAddressInterface::class)->findOneBy(['addressType' => 'DEPART']);
|
|
if ($site instanceof SiteInterface && $supplierAddress instanceof SupplierAddressInterface && null !== $supplierAddress->getSupplier()) {
|
|
$supplierPrice = new CarrierPrice();
|
|
$supplierPrice->setDirection('FOURNISSEUR');
|
|
$supplierPrice->setSupplier($supplierAddress->getSupplier());
|
|
$supplierPrice->setSupplierSupplyAddress($supplierAddress);
|
|
$supplierPrice->setDeliverySite($site);
|
|
$supplierPrice->setContainerType('FOND_MOUVANT');
|
|
$supplierPrice->setPricingUnit('FORFAIT');
|
|
$supplierPrice->setPrice('320.00');
|
|
$supplierPrice->setPriceState('EN_COURS');
|
|
$carrier->addPrice($supplierPrice);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construit (non persiste explicitement — cascade via la FK Carrier) un
|
|
* UploadedDocument de demo pour la Decharge (RG-4.02). Pas de fichier reel sur
|
|
* disque : metadonnees factices suffisantes pour la demo.
|
|
*/
|
|
private function buildDischargeDocument(ObjectManager $manager): UploadedDocument
|
|
{
|
|
$document = new UploadedDocument(
|
|
'decharge-demo.pdf',
|
|
'demo/decharge-demo.pdf',
|
|
'application/pdf',
|
|
12_345,
|
|
str_repeat('0', 64),
|
|
new DateTimeImmutable(),
|
|
);
|
|
$manager->persist($document);
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Insere (idempotent, par SIRET) une ligne `qualimat_carrier` de demo a
|
|
* validite PASSEE (RG-4.04) puis retourne l'entite (lecture seule) rechargee.
|
|
* La table est normalement alimentee par `app:qualimat:sync` ; en demo on pose
|
|
* une ligne directe en DBAL (l'entite mappee n'expose aucune ecriture API).
|
|
*/
|
|
private function ensureQualimatDemoLine(ObjectManager $manager): QualimatCarrier
|
|
{
|
|
$repository = $manager->getRepository(QualimatCarrier::class);
|
|
$existing = $repository->findOneBy(['siret' => self::QUALIMAT_DEMO_SIRET]);
|
|
if ($existing instanceof QualimatCarrier) {
|
|
return $existing;
|
|
}
|
|
|
|
if ($manager instanceof EntityManagerInterface) {
|
|
$manager->getConnection()->insert('qualimat_carrier', [
|
|
'siret' => self::QUALIMAT_DEMO_SIRET,
|
|
'name' => 'TRANSPORTS GRELILLIER',
|
|
'address' => '12 rue des Acacias',
|
|
'postal_code' => '86000',
|
|
'city' => 'Poitiers',
|
|
'status' => 'Valide',
|
|
// Validite PASSEE : exerce le fond rouge RG-4.04 cote front.
|
|
'validity_date' => '2024-12-31',
|
|
'is_active' => 'true',
|
|
'last_synced_at' => new DateTimeImmutable()->format('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
// @var QualimatCarrier $line
|
|
return $repository->findOneBy(['siret' => self::QUALIMAT_DEMO_SIRET]);
|
|
}
|
|
}
|