createAdminClient(); $cat = $this->supplierCategory('NEGOCIANT'); $data = $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'recycla sas', 'categories' => ['/api/categories/'.$cat->getId()], ], ])->toArray(); self::assertResponseStatusCodeSame(201); // RG-2.12 : companyName normalise en MAJUSCULES sur la valeur RENVOYEE. self::assertSame('RECYCLA SAS', $data['companyName']); // Embed categorie : code/name presents (category:read dans le contexte). self::assertSame('NEGOCIANT', $data['categories'][0]['code']); } public function testPostMainFormHasNoInlineContactFields(): void { // refonte-contact V0.2 : plus aucun champ de contact inline au POST. $client = $this->createAdminClient(); $cat = $this->supplierCategory('NEGOCIANT'); $data = $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'No Inline Co', // Champs historiques : ignores par le denormaliseur. 'firstName' => 'Ignored', 'lastName' => 'Ignored', 'phonePrimary' => '0612345678', 'email' => 'ignored@test.fr', 'categories' => ['/api/categories/'.$cat->getId()], ], ])->toArray(); self::assertResponseStatusCodeSame(201); foreach (['firstName', 'lastName', 'phonePrimary', 'phoneSecondary', 'email'] as $key) { self::assertArrayNotHasKey($key, $data); } } // === RG-2.10 : categorie de type FOURNISSEUR === public function testPostWithNonFournisseurCategoryReturns422OnCategoriesPath(): void { $client = $this->createAdminClient(); // createCategory() (parent) cree une categorie de type CLIENT -> interdite. $clientTypedCategory = $this->createCategory('SECTEUR'); $response = $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD], 'json' => [ 'companyName' => 'Wrong Cat Type', 'categories' => ['/api/categories/'.$clientTypedCategory->getId()], ], ]); self::assertResponseStatusCodeSame(422); $byPath = []; foreach ($response->toArray(false)['violations'] ?? [] as $v) { $byPath[$v['propertyPath']] = $v['message']; } // ERP-101 : la violation porte propertyPath=categories (mapping inline front). self::assertArrayHasKey('categories', $byPath); self::assertSame('Type de catégorie non autorisé (FOURNISSEUR attendu).', $byPath['categories']); } // === RG-2.11 : unicite du nom de societe === public function testPostDuplicateCompanyNameReturns409(): void { $client = $this->createAdminClient(); $this->seedSupplier('Dup Name Co'); $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Dup Name Co'), ]); // RG-2.11 : doublon parmi les actifs -> 409 (index uq_supplier_company_name_active). self::assertResponseStatusCodeSame(409); } public function testPostSameNameAfterArchivingPreviousReturns201(): void { $client = $this->createAdminClient(); // L'homonyme est archive -> hors index partiel : le nom redevient disponible. $this->seedSupplier('Reuse After Archive', true); $client->request('POST', '/api/suppliers', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Reuse After Archive'), ]); self::assertResponseStatusCodeSame(201); } // === RG-2.14 : archivage (admin) === public function testAdminArchiveSetsArchivedAt(): void { $client = $this->createAdminClient(); $seed = $this->seedSupplier('Archive Me'); $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true], ]); self::assertResponseStatusCodeSame(200); $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(Supplier::class)->find($seed->getId()); self::assertNotNull($reloaded); self::assertTrue($reloaded->isArchived()); self::assertNotNull($reloaded->getArchivedAt(), 'RG-2.14 : archivedAt doit etre rempli a l\'archivage.'); } public function testArchiveWithOtherFieldReturns422(): void { $client = $this->createAdminClient(); $seed = $this->seedSupplier('Archive Plus Field'); // RG-2.14 : une requete d'archivage ne modifie aucun autre champ. $response = $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => true, 'companyName' => 'Renamed While Archiving'], ]); self::assertResponseStatusCodeSame(422); // Le 422 doit etre celui de RG-2.14 (archivage exclusif) et non un 422 // orthogonal : on verifie le message porte par l'exception. self::assertStringContainsString('archivage', $response->getContent(false)); } public function testRestoreSetsArchivedAtNull(): void { $client = $this->createAdminClient(); $seed = $this->seedSupplier('Restore Me', true); $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['isArchived' => false], ]); self::assertResponseStatusCodeSame(200); $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(Supplier::class)->find($seed->getId()); self::assertNotNull($reloaded); self::assertFalse($reloaded->isArchived()); self::assertNull($reloaded->getArchivedAt(), 'RG-2.15 : archivedAt repasse a null a la restauration.'); } }