6f9bb68170
Auto Tag Develop / tag (push) Successful in 7s
## ERP-92 — Tests PHPUnit M2 fournisseurs (#521) Suite fonctionnelle M2 assertant sur le **corps JSON** (jamais les annotations), jumelle de la suite clients M1. ### Couverture - **Contrat de sérialisation** (`SupplierSerializationContractTest`) : 4 régressions M1 re-testées — RIB gaté **absent** pour la Commerciale, booléens `triageProvider`/`isArchived` présents, embed `categories[].code/name`, embed `sites[].name/postalCode` (objet, pas IRI) — + enveloppe AP4 (`member`/`totalItems`/`view`, archivés exclus) + suppression du contact inline. - **Matrice RBAC réelle** (`app:seed-rbac`, pas de mock) : bureau/compta/commerciale/usine 200/403, gating `accounting` par **omission de clé**, mode strict PATCH (RG-2.16). - **Matrice RG-2.03 → RG-2.17** (création, normalisation RG-2.12, catégorie FOURNISSEUR RG-2.10, unicité RG-2.11, archivage RG-2.14/2.15, RG-2.07/2.08 compta, sous-ressources RG-2.04/2.05/2.06/2.09). - **Anti N+1 liste** : nombre de requêtes constant entre 2 et 4 fournisseurs. **Audit** Supplier + RIB (`iban`/`bic` dans le diff). ### Fix de contrat (découvert par la DoD) Les référentiels comptables (`TvaMode`/`PaymentType`/`PaymentDelay`/`Bank`) ne portaient que `client:read:accounting` (M1) → sur un fournisseur ils sortaient en **IRI nu**. Ajout de `supplier:read:accounting` → objet `{id, code, label}` embarqué (additif, zéro impact M1). Sans ce fix, #95/#96 auraient été développés contre un contrat faux. ### Infra `makefile` : `test-db-setup` recrée l'index partiel `uq_supplier_company_name_active` (droppé par `schema:update` comme celui du client — oubli M2). ### DoD ✅ § 4.0.bis : réponses JSON **réelles** (liste + détail admin/commerciale) collées. Front #93→#96 peuvent démarrer. ### Vérifs - `make test` : **574 tests OK** (suite complète verte) - `make php-cs-fixer-allow-risky` : 0 correction --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #71 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
181 lines
6.9 KiB
PHP
181 lines
6.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Commercial\Api;
|
|
|
|
use App\Module\Commercial\Domain\Entity\Supplier;
|
|
|
|
/**
|
|
* Tests fonctionnels du formulaire principal fournisseur (M2, spec § 4.3 / § 4.4)
|
|
* sur le CORPS JSON : creation (companyName + categories), normalisation serveur
|
|
* (RG-2.12 UPPERCASE), categorie de type FOURNISSEUR (RG-2.10), unicite du nom
|
|
* (RG-2.11) et archivage nominal (RG-2.14). Jumeau de ClientApiTest (M1).
|
|
*
|
|
* @internal
|
|
*/
|
|
final class SupplierApiTest extends AbstractSupplierApiTestCase
|
|
{
|
|
// === POST formulaire principal ===
|
|
|
|
public function testPostMainFormUppercasesCompanyName(): void
|
|
{
|
|
$client = $this->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.');
|
|
}
|
|
}
|