getEm(); // Carrier d'abord : ON DELETE CASCADE purge carrier_price (FK RESTRICT vers // client/supplier), liberant les Client/Supplier de test pour leur purge. $em->createQuery('DELETE FROM '.Carrier::class)->execute(); $em->createQuery('DELETE FROM '.ClientEntity::class.' c WHERE c.companyName LIKE :p') ->setParameter('p', self::TEST_REF_PREFIX.'%')->execute() ; $em->createQuery('DELETE FROM '.Supplier::class.' s WHERE s.companyName LIKE :p') ->setParameter('p', self::TEST_REF_PREFIX.'%')->execute() ; // qualimat_carrier : insere en DBAL brut (entite lecture seule) -> purge DBAL. $em->getConnection()->executeStatement( 'DELETE FROM qualimat_carrier WHERE siret LIKE :p', ['p' => self::TEST_SIRET_PREFIX.'%'], ); parent::tearDown(); } protected function createAdminClient(): Client { return $this->authenticatedClient('admin', 'admin'); } /** * Garde-fou ERP-101 : verifie qu'une reponse 422 porte une violation sur le * `propertyPath` attendu (et pas seulement le bon code HTTP). Sans cette * assertion, une 422 venue d'une AUTRE cause (autre champ manquant, IRI 404) * ferait passer le test au vert sans prouver le mapping inline par champ. * * Mutualise dans la base (au lieu d'un duplicata par fichier) pour que toute * la stack d'ecriture (formulaire principal + sous-ressources) l'utilise. */ protected static function assertViolationOnPath(object $response, string $path): void { /** @var ResponseInterface $response */ $paths = array_column($response->toArray(false)['violations'] ?? [], 'propertyPath'); self::assertContains( $path, $paths, sprintf('Aucune violation sur "%s" (paths: %s).', $path, implode(', ', $paths)), ); } /** * Payload minimal valide du formulaire principal (transporteur non-QUALIMAT, * non affrete) : nom + certification GMP_PLUS. Sert de base aux tests * d'ecriture / RBAC. * * @return array */ protected function validMainPayload(string $name): array { return [ 'name' => $name, 'certificationType' => 'GMP_PLUS', 'isChartered' => false, ]; } /** * Seede un transporteur minimal (nom en MAJUSCULES, comme le ferait le * futur Processor). Sert aux tests de liste / archivage. */ protected function seedCarrier(string $name, bool $isArchived = false): Carrier { $em = $this->getEm(); $carrier = new Carrier(); $carrier->setName(mb_strtoupper($name, 'UTF-8')); $carrier->setCertificationType('GMP_PLUS'); $carrier->setIsArchived($isArchived); if ($isArchived) { $carrier->setArchivedAt(new DateTimeImmutable()); } $em->persist($carrier); $em->flush(); return $carrier; } /** * Seede un transporteur COMPLET (sans passer par l'API) : lien QUALIMAT, * 1 adresse, 1 contact, et 2 prix couvrant les deux branches (CLIENT avec * client + adresse de livraison + site de depart ; FOURNISSEUR avec * fournisseur + adresse d'appro + site de livraison). Socle du contrat de * serialisation et de la DoD (§ 4.0.bis). */ protected function seedCompleteCarrier(string $name): Carrier { $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(3)), 0, 6); $qualimat = $this->seedQualimatCarrier($name); $carrier = new Carrier(); $carrier->setName(mb_strtoupper($name.' '.$suffix, 'UTF-8')); $carrier->setQualimatCarrier($qualimat); $carrier->setCertificationType('QUALIMAT'); $em->persist($carrier); $address = new CarrierAddress(); $address->setCarrier($carrier); $address->setPostalCode('86000'); $address->setCity('Poitiers'); $address->setStreet('12 rue des Acacias'); $carrier->addAddress($address); $em->persist($address); $contact = new CarrierContact(); $contact->setCarrier($carrier); $contact->setFirstName('Marie'); $contact->setLastName('Martin'); $contact->setPhonePrimary('0612345678'); $contact->setEmail('marie.martin@seed.test'); $carrier->addContact($contact); $em->persist($contact); // Refs cross-module : seedees localement (en test, les fixtures M1/M2 ne // sont pas chargees — seuls les sites le sont). Prouve l'embed via les // contrats Shared + resolve_target_entities (regle n°1). $site = $em->getRepository(Site::class)->findOneBy([]); self::assertNotNull($site, 'Un site fixture est requis (SitesFixtures).'); $clientAddress = $this->seedClientWithAddress($name.' '.$suffix); $supplierAddress = $this->seedSupplierWithAddress($name.' '.$suffix); // Branche CLIENT (RG-4.10). $clientPrice = new CarrierPrice(); $clientPrice->setCarrier($carrier); $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); $em->persist($clientPrice); // Branche FOURNISSEUR (RG-4.11). $supplierPrice = new CarrierPrice(); $supplierPrice->setCarrier($carrier); $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); $em->persist($supplierPrice); $em->flush(); return $carrier; } /** * Seede un Client minimal (companyName prefixe pour la purge) + une adresse * de livraison valide (CHECKs client_address respectes). Retourne l'adresse. */ protected function seedClientWithAddress(string $label): ClientAddress { $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(3)), 0, 6); $client = new ClientEntity(); $client->setCompanyName(mb_strtoupper(self::TEST_REF_PREFIX.' CLI '.$label.' '.$suffix, 'UTF-8')); $em->persist($client); $address = new ClientAddress(); $address->setClient($client); // Adresse de livraison : is_delivery=true, is_prospect=false, is_billing=false // -> satisfait chk_client_address_prospect_exclusive + chk_client_address_billing_email. $address->setIsDelivery(true); $address->setPostalCode('86000'); $address->setCity('Poitiers'); $address->setStreet('1 rue de la Livraison'); $em->persist($address); return $address; } /** * Seede un Supplier minimal (companyName prefixe pour la purge) + une adresse * d'approvisionnement valide (address_type DEPART). Retourne l'adresse. */ protected function seedSupplierWithAddress(string $label): SupplierAddress { $em = $this->getEm(); $suffix = substr(bin2hex(random_bytes(3)), 0, 6); $supplier = new Supplier(); $supplier->setCompanyName(mb_strtoupper(self::TEST_REF_PREFIX.' FRN '.$label.' '.$suffix, 'UTF-8')); $em->persist($supplier); $address = new SupplierAddress(); $address->setSupplier($supplier); $address->setAddressType('DEPART'); $address->setPostalCode('17000'); $address->setCity('La Rochelle'); $address->setStreet('2 quai de l Appro'); $em->persist($address); return $address; } /** * Insere une ligne qualimat_carrier de test en DBAL brut (l'entite mappee est * en lecture seule) et retourne l'entite rechargee. SIRET prefixe pour la purge. */ protected function seedQualimatCarrier(string $name): QualimatCarrier { $em = $this->getEm(); $siret = self::TEST_SIRET_PREFIX.substr(bin2hex(random_bytes(6)), 0, 9); $em->getConnection()->insert('qualimat_carrier', [ 'siret' => $siret, 'name' => mb_strtoupper($name, 'UTF-8'), 'address' => '12 rue des Acacias', 'postal_code' => '86000', 'city' => 'Poitiers', 'status' => 'Valide', 'validity_date' => '2027-12-31', 'is_active' => 'true', 'last_synced_at' => new DateTimeImmutable()->format('Y-m-d H:i:s'), ]); $qualimat = $em->getRepository(QualimatCarrier::class)->findOneBy(['siret' => $siret]); self::assertNotNull($qualimat, 'La ligne qualimat_carrier de test doit etre rechargeable.'); return $qualimat; } }