getEm()->createQuery('DELETE FROM '.Supplier::class)->execute(); parent::tearDown(); } /** * Fetch-or-create une categorie de type FOURNISSEUR par code (defaut * Negociant). Type FOURNISSEUR exige par RG-2.10 : un POST fournisseur portant * cette categorie passe la validation. Idempotent (lookup par code, aligne sur * l'index unique partiel uq_category_code) et auto-suffisant : ne depend pas du * seed CategoryFixtures (que d'autres tests de la suite peuvent purger). Une * categorie creee ici porte le prefixe de nom de test -> purgee par le parent. */ protected function supplierCategory(string $code = 'NEGOCIANT'): Category { $em = $this->getEm(); $existing = $em->getRepository(Category::class)->findOneBy(['code' => $code, 'deletedAt' => null]); if (null !== $existing) { return $existing; } $category = new Category(); $category->setName(self::TEST_CATEGORY_PREFIX.'fr_'.strtolower($code)); $category->setCode($code); $category->setCategoryType($this->supplierCategoryType()); $em->persist($category); $em->flush(); return $category; } /** * Recupere (ou cree) le type FOURNISSEUR. Idempotent : la contrainte d'unicite * sur category_type.code interdit les doublons. */ protected function supplierCategoryType(): CategoryType { $em = $this->getEm(); $existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'FOURNISSEUR']); if (null !== $existing) { return $existing; } $type = new CategoryType(); $type->setCode('FOURNISSEUR'); $type->setLabel('Fournisseur'); $em->persist($type); $em->flush(); return $type; } /** * Seede directement un Supplier minimal (sans passer par l'API), pour les * tests de liste / archivage / serialisation. Nom stocke en MAJUSCULES pour * refleter l'etat normalise (RG-2.12) qu'aurait produit le SupplierProcessor. * Porte une categorie FOURNISSEUR (defaut Negociant). */ protected function seedSupplier(string $companyName, bool $isArchived = false, string $categoryCode = 'NEGOCIANT'): Supplier { $em = $this->getEm(); $supplier = new Supplier(); $supplier->setCompanyName(mb_strtoupper($companyName, 'UTF-8')); $supplier->addCategory($this->supplierCategory($categoryCode)); $supplier->setIsArchived($isArchived); if ($isArchived) { $supplier->setArchivedAt(new DateTimeImmutable()); } $em->persist($supplier); $em->flush(); return $supplier; } /** * Seede un fournisseur COMPLET (sans passer par l'API — validations * applicatives non rejouees mais CHECK BDD respectes) : onglet Information * rempli, bloc comptable non nul (SIREN + refs), >= 1 RIB, >= 1 adresse * multi-sites (>= 2 sites, triageProvider=true) avec >= 1 categorie * FOURNISSEUR, >= 1 contact, >= 1 categorie sur le fournisseur. Sert de socle * au contrat de serialisation et a la DoD (§ 4.0.bis). * * @param string $paymentTypeCode code du type de reglement a poser (defaut LCR, * coherent avec le RIB seede ; RG-2.08) */ protected function seedCompleteSupplier(string $companyName, string $paymentTypeCode = 'LCR'): Supplier { $em = $this->getEm(); // Nom unique parmi les actifs (index partiel uq_supplier_company_name_active). $suffix = substr(bin2hex(random_bytes(3)), 0, 6); $supplier = new Supplier(); $supplier->setCompanyName(mb_strtoupper($companyName.' '.$suffix, 'UTF-8')); $supplier->addCategory($this->supplierCategory('NEGOCIANT')); // Onglet Information complet (RG-2.03 : exige pour la Commerciale). $supplier->setDescription('Fournisseur de test complet.'); $supplier->setCompetitors('Concurrent A, Concurrent B'); $supplier->setFoundedAt(new DateTimeImmutable('2008-04-01')); $supplier->setEmployeesCount(42); $supplier->setRevenueAmount('1500000.00'); $supplier->setDirectorName('Jean Dupont'); $supplier->setProfitAmount('120000.00'); $supplier->setVolumeForecast(8000); // Bloc comptable non nul (gating par omission cote Commerciale). $supplier->setSiren('123456789'); $supplier->setAccountNumber('F0001'); $supplier->setNTva('FR00123456789'); $supplier->setTvaMode($this->tvaMode('FRANCE_VENTES')); $supplier->setPaymentDelay($this->paymentDelay('J30')); $supplier->setPaymentType($this->paymentType($paymentTypeCode)); if ('VIREMENT' === $paymentTypeCode) { $supplier->setBank($this->bank('SG')); } $em->persist($supplier); // >= 2 sites fixtures pour une adresse multi-sites (RG-2.06). $sites = $em->getRepository(Site::class)->findBy([], null, 2); self::assertGreaterThanOrEqual(2, count($sites), 'Au moins 2 sites fixtures requis (SitesFixtures).'); $contact = new SupplierContact(); $contact->setSupplier($supplier); $contact->setFirstName('Marie'); $contact->setLastName('Martin'); $contact->setJobTitle('Responsable achats'); $contact->setPhonePrimary('0612345678'); $contact->setEmail('marie.martin@seed.test'); $supplier->addContact($contact); $em->persist($contact); $address = new SupplierAddress(); $address->setSupplier($supplier); $address->setAddressType('DEPART'); $address->setPostalCode('86000'); $address->setCity('Poitiers'); $address->setStreet('12 rue des Acacias'); $address->setBennes(3); // triageProvider=true : prouve qu'un booleen `true` est bien serialise // (piege n°3 du M1 — la cle etait droppee). $address->setTriageProvider(true); foreach ($sites as $site) { $address->addSite($site); } $address->addCategory($this->supplierCategory('NEGOCIANT')); $address->addContact($contact); $supplier->addAddress($address); $em->persist($address); $rib = new SupplierRib(); $rib->setSupplier($supplier); $rib->setLabel('Compte principal'); $rib->setBic(self::VALID_BIC); $rib->setIban(self::VALID_IBAN); $supplier->addRib($rib); $em->persist($rib); $em->flush(); return $supplier; } /** * Ajoute un contact a un fournisseur deja persiste (seed direct). */ protected function addContact( Supplier $supplier, ?string $firstName = 'Marie', ?string $lastName = 'Martin', ?string $phonePrimary = null, ?string $email = null, int $position = 0, ): SupplierContact { $contact = new SupplierContact(); $contact->setSupplier($supplier); $contact->setFirstName($firstName); $contact->setLastName($lastName); $contact->setPhonePrimary($phonePrimary); $contact->setEmail($email); $contact->setPosition($position); $supplier->addContact($contact); $this->getEm()->persist($contact); $this->getEm()->flush(); return $contact; } /** * Ajoute un RIB a un fournisseur deja persiste (seed direct). */ protected function addRib(Supplier $supplier, string $label = 'Compte principal'): SupplierRib { $rib = new SupplierRib(); $rib->setSupplier($supplier); $rib->setLabel($label); $rib->setBic(self::VALID_BIC); $rib->setIban(self::VALID_IBAN); $supplier->addRib($rib); $this->getEm()->persist($rib); $this->getEm()->flush(); return $rib; } /** * Payload minimal valide de l'onglet principal (companyName + 1 categorie * FOURNISSEUR). Si $categoryId est null, la categorie Negociant seedee est * utilisee. * * @return array */ protected function validMainPayload(string $companyName, ?int $categoryId = null): array { $categoryId ??= $this->supplierCategory('NEGOCIANT')->getId(); return [ 'companyName' => $companyName, 'categories' => ['/api/categories/'.$categoryId], ]; } protected function paymentType(string $code): PaymentType { return $this->referential(PaymentType::class, $code); } protected function paymentDelay(string $code): PaymentDelay { return $this->referential(PaymentDelay::class, $code); } protected function tvaMode(string $code): TvaMode { return $this->referential(TvaMode::class, $code); } protected function bank(string $code): Bank { return $this->referential(Bank::class, $code); } /** * Recupere un referentiel comptable seede (CommercialReferentialFixtures) par * code. Echoue explicitement si absent (fixtures non chargees). * * @template T of object * * @param class-string $entityClass * * @return T */ private function referential(string $entityClass, string $code): object { $entity = $this->getEm()->getRepository($entityClass)->findOneBy(['code' => $code]); self::assertNotNull( $entity, sprintf('Referentiel %s "%s" introuvable : fixtures comptables chargees (make test-db-setup) ?', $entityClass, $code), ); return $entity; } /** * Indexe les violations d'un corps de reponse 422 par propertyPath. Permet * d'asserter qu'un 422 porte bien sur le champ attendu (et n'est pas un 422 * orthogonal) : un test qui se contente du code 422 passerait meme si la RG * visee etait cassee pour une autre raison. * * @param array $body corps decode de la reponse (toArray(false)) * * @return array propertyPath => message */ protected function violationsByPath(array $body): array { $byPath = []; foreach ($body['violations'] ?? [] as $v) { $byPath[$v['propertyPath']] = $v['message']; } return $byPath; } }