feat(transport) : CarrierProcessor + champs conditionnels formulaire principal (ERP-158)
Ecriture du formulaire principal transporteur (M4, WT4) : POST/PATCH via CarrierProcessor + CarrierFieldNormalizer, contraintes conditionnelles sur l'entite Carrier. - RG-4.01 : POST qualimatCarrier -> certificationType=QUALIMAT + FK persistee ; cas LIOT (name=LIOT) -> certification non requise, liotPlates accepte. - RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 (Assert\Callback). - RG-4.03 : isChartered=true sans indexationRate/containerType/volumeM3 -> 422. - RG-4.12 : doublon de nom (parmi actifs) -> 409 (index partiel uq_carrier_name_active). - RG-4.13 : normalisation serveur (name UPPER, liotPlates ;-split/trim/UPPER) + methodes personne/telephone/email pour les sous-ressources Contact (WT7). - RG-4.14 : PATCH isArchived exige transport.carriers.archive (Admin seul), mode strict -> 403 + 422 si autre champ ; restauration en conflit -> 409. Operations Post/Patch ajoutees a l'ApiResource (lecture posee au WT3 conservee). RG conditionnelles portees par validateMainFormConsistency (Assert\Callback + ->atPath()) pour un propertyPath mappable inline (useFormErrors, ERP-101). certificationType / containerType whitelistes dans EXCLUDED_LENGTH_MIRROR (Choice borne deja les valeurs, miroir SupplierAddress::addressType). Tests : CarrierWriteApiTest (RG-4.01->4.03/4.12->4.14), CarrierRBACMatrixTest (matrice bureau/compta/commerciale/usine), CarrierArchiveTest (409 restauration), CarrierFieldNormalizerTest (RG-4.13). make test vert (750).
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Transport\Api;
|
||||
|
||||
/**
|
||||
* Ecriture du formulaire principal transporteur (M4, WT4 — ERP-158). Couvre les
|
||||
* RG du CarrierProcessor + contraintes conditionnelles : RG-4.01 (QUALIMAT + cas
|
||||
* LIOT), RG-4.02 (AUTRE -> decharge), RG-4.03 (affrete -> indexation/benne/volume),
|
||||
* RG-4.12 (doublon de nom -> 409), RG-4.13 (normalisation), RG-4.14 (archivage +
|
||||
* mode strict). Jumeau des SupplierApiTest / SupplierPatchStrictTest (M2).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CarrierWriteApiTest extends AbstractCarrierApiTestCase
|
||||
{
|
||||
/**
|
||||
* RG-4.01 : POST avec qualimatCarrier -> certificationType=QUALIMAT accepte et
|
||||
* FK persistee (verifiee au detail, qualimatCarrier embarque).
|
||||
*/
|
||||
public function testPostQualimatPersistsCertificationAndForeignKey(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
$qualimat = $this->seedQualimatCarrier('Transports Grelillier');
|
||||
|
||||
$created = $client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => [
|
||||
'name' => 'Transports Grelillier',
|
||||
'qualimatCarrier' => '/api/qualimat_carriers/'.$qualimat->getId(),
|
||||
'certificationType' => 'QUALIMAT',
|
||||
'isChartered' => false,
|
||||
],
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertSame('QUALIMAT', $created['certificationType']);
|
||||
|
||||
$detail = $client->request('GET', $created['@id'], ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
self::assertIsArray($detail['qualimatCarrier']);
|
||||
self::assertSame((int) $qualimat->getId(), (int) $detail['qualimatCarrier']['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.01 (cas LIOT) : nom = LIOT -> certificationType non requis (champ masque)
|
||||
* et liotPlates accepte (et normalise, RG-4.13).
|
||||
*/
|
||||
public function testPostLiotAcceptsPlatesWithoutCertification(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$created = $client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => [
|
||||
'name' => 'LIOT',
|
||||
'liotPlates' => 'ab-123-cd ; ef-456-gh',
|
||||
'isChartered' => false,
|
||||
],
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertNull($created['certificationType']);
|
||||
self::assertSame('AB-123-CD; EF-456-GH', $created['liotPlates']);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.01 : hors cas LIOT, l'absence de certification est rejetee (422 cible
|
||||
* sur certificationType).
|
||||
*/
|
||||
public function testPostWithoutCertificationOutsideLiotIsRejected(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$response = $client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => ['name' => 'Sans Certif', 'isChartered' => false],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'certificationType');
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 cible ; une
|
||||
* certification != AUTRE sans decharge passe (201).
|
||||
*/
|
||||
public function testAutreCertificationRequiresDischarge(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$response = $client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => ['name' => 'Sans Decharge', 'certificationType' => 'AUTRE', 'isChartered' => false],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'dischargeDocument');
|
||||
|
||||
// Certification != AUTRE : pas de decharge requise.
|
||||
$client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => ['name' => 'Avec GmpPlus', 'certificationType' => 'GMP_PLUS', 'isChartered' => false],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.03 : isChartered=true sans indexationRate / containerType / volumeM3 ->
|
||||
* 422 (violations ciblees) ; complet -> 201.
|
||||
*/
|
||||
public function testCharteredRequiresIndexationContainerAndVolume(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$response = $client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => ['name' => 'Affrete Incomplet', 'certificationType' => 'GMP_PLUS', 'isChartered' => true],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'indexationRate');
|
||||
self::assertViolationOnPath($response, 'containerType');
|
||||
self::assertViolationOnPath($response, 'volumeM3');
|
||||
|
||||
$client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => [
|
||||
'name' => 'Affrete Complet',
|
||||
'certificationType' => 'GMP_PLUS',
|
||||
'isChartered' => true,
|
||||
'indexationRate' => '5.00',
|
||||
'containerType' => 'BENNE',
|
||||
'volumeM3' => '90.00',
|
||||
],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.12 : nom deja pris (parmi actifs) -> 409. Le meme nom redevient
|
||||
* disponible apres archivage de l'ancien -> 201.
|
||||
*/
|
||||
public function testDuplicateNameReturns409AndIsFreedAfterArchive(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
$existing = $this->seedCarrier('Doublon Co');
|
||||
|
||||
$client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => $this->validMainPayload('Doublon Co'),
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(409);
|
||||
|
||||
// Archivage de l'ancien -> le nom se libere (index partiel sur actifs).
|
||||
$client->request('PATCH', '/api/carriers/'.$existing->getId(), [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => ['isArchived' => true],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
|
||||
$client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => $this->validMainPayload('Doublon Co'),
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.13 : le nom est persiste en MAJUSCULES (normalisation serveur).
|
||||
*/
|
||||
public function testNameIsUpperCasedOnPersist(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$created = $client->request('POST', '/api/carriers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => $this->validMainPayload('transports x'),
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertSame('TRANSPORTS X', $created['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.14 : PATCH isArchived=true par Admin -> 200 + archivedAt rempli ;
|
||||
* restauration -> archivedAt remis a null.
|
||||
*/
|
||||
public function testAdminArchiveSetsArchivedAtAndRestoreClearsIt(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
$carrier = $this->seedCarrier('A Archiver');
|
||||
|
||||
$archived = $client->request('PATCH', '/api/carriers/'.$carrier->getId(), [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => ['isArchived' => true],
|
||||
])->toArray();
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
self::assertTrue($archived['isArchived']);
|
||||
self::assertNotNull($archived['archivedAt']);
|
||||
|
||||
$restored = $client->request('PATCH', '/api/carriers/'.$carrier->getId(), [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => ['isArchived' => false],
|
||||
])->toArray();
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
self::assertFalse($restored['isArchived']);
|
||||
self::assertNull($restored['archivedAt']);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-4.14 (mode strict) : une requete d'archivage ne peut modifier aucun autre
|
||||
* champ ecrivable -> 422.
|
||||
*/
|
||||
public function testArchiveRequestMixingOtherFieldIsRejected(): void
|
||||
{
|
||||
$client = $this->createAdminClient();
|
||||
$carrier = $this->seedCarrier('Strict Co');
|
||||
|
||||
$client->request('PATCH', '/api/carriers/'.$carrier->getId(), [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => ['isArchived' => true, 'name' => 'Renamed While Archiving'],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifie qu'une violation 422 cible bien la propriete attendue (propertyPath),
|
||||
* gage du mapping inline front (useFormErrors, ERP-101).
|
||||
*/
|
||||
private function assertViolationOnPath(object $response, string $path): void
|
||||
{
|
||||
/** @var \Symfony\Contracts\HttpClient\ResponseInterface $response */
|
||||
$paths = array_column($response->toArray(false)['violations'] ?? [], 'propertyPath');
|
||||
|
||||
self::assertContains(
|
||||
$path,
|
||||
$paths,
|
||||
sprintf('Aucune violation sur "%s" (paths: %s).', $path, implode(', ', $paths)),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user