6b1e2c2a80
POST /api/carriers/{id}/prices + PATCH/DELETE /api/carrier_prices/{id}
(security transport.carriers.manage) via CarrierPriceProcessor.
RG-4.09->4.11 : coherence de branche CLIENT/FOURNISSEUR (champs requis +
appartenance de l'adresse de livraison au client / de l'adresse d'appro au
fournisseur, sinon 422), nettoyage de la branche opposee (CHECK BDD). Champs
communs obligatoires via Assert\NotBlank + Assert\Choice.
Les contrats Shared ClientAddressInterface / SupplierAddressInterface exposent
desormais getClient() / getSupplier() (canal cross-module, regle n°1) pour la
verification d'appartenance. Colonnes enum du prix whitelistees dans le miroir
Assert\Length (deja bornees par Choice).
277 lines
11 KiB
PHP
277 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Transport\Api;
|
|
|
|
use App\Module\Commercial\Domain\Entity\ClientAddress;
|
|
use App\Module\Core\Infrastructure\DataFixtures\RbacDemoFixtures;
|
|
use App\Module\Sites\Domain\Entity\Site;
|
|
use App\Module\Transport\Domain\Entity\CarrierPrice;
|
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
|
use Symfony\Component\Console\Input\ArrayInput;
|
|
use Symfony\Component\Console\Output\NullOutput;
|
|
|
|
/**
|
|
* Sous-ressource Prix d'un transporteur (spec-back M4 § 4.5, ERP-161).
|
|
* POST /api/carriers/{id}/prices, PATCH/DELETE /api/carrier_prices/{id}.
|
|
*
|
|
* Contrat verifie (RG-4.09→4.11) :
|
|
* - branche CLIENT incomplete -> 422 ;
|
|
* - branche FOURNISSEUR incomplete -> 422 ;
|
|
* - adresse de livraison etrangere au client -> 422 ;
|
|
* - adresse d'appro etrangere au fournisseur -> 422 ;
|
|
* - prix CLIENT / FOURNISSEUR complets -> 201 ;
|
|
* - PATCH / DELETE OK avec transport.carriers.manage, 403 sans (view seul).
|
|
*
|
|
* @internal
|
|
*/
|
|
final class CarrierPriceApiTest extends AbstractCarrierApiTestCase
|
|
{
|
|
private const string PWD = RbacDemoFixtures::DEMO_PASSWORD;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Seed idempotent des roles + matrice § 5.2 + comptes demo (meme chemin
|
|
// qu'en recette), requis pour les tests de permission (bureau/commerciale).
|
|
self::bootKernel();
|
|
$application = new Application(self::$kernel);
|
|
$application->setAutoExit(false);
|
|
$exit = $application->run(
|
|
new ArrayInput([
|
|
'command' => 'app:seed-rbac',
|
|
'--with-demo-users' => true,
|
|
'--password' => self::PWD,
|
|
]),
|
|
new NullOutput(),
|
|
);
|
|
self::assertSame(0, $exit, 'app:seed-rbac a echoue (permissions transport.carriers.* synchronisees ?).');
|
|
|
|
self::ensureKernelShutdown();
|
|
}
|
|
|
|
public function testIncompleteClientBranchReturns422(): void
|
|
{
|
|
// RG-4.10 : direction CLIENT sans client / adresse / site de depart -> 422.
|
|
$carrier = $this->seedCarrier('Prix Client Incomplet');
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'direction' => 'CLIENT',
|
|
'containerType' => 'BENNE',
|
|
'pricingUnit' => 'TONNE',
|
|
'price' => '42.50',
|
|
'priceState' => 'VALIDE',
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
public function testIncompleteSupplierBranchReturns422(): void
|
|
{
|
|
// RG-4.11 : direction FOURNISSEUR sans fournisseur / adresse / site -> 422.
|
|
$carrier = $this->seedCarrier('Prix Fournisseur Incomplet');
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'direction' => 'FOURNISSEUR',
|
|
'containerType' => 'FOND_MOUVANT',
|
|
'pricingUnit' => 'FORFAIT',
|
|
'price' => '320.00',
|
|
'priceState' => 'EN_COURS',
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
public function testForeignClientAddressReturns422(): void
|
|
{
|
|
// RG-4.10 : l'adresse de livraison doit appartenir au client choisi.
|
|
$carrier = $this->seedCarrier('Prix Adresse Etrangere Client');
|
|
$addrA = $this->seedClientWithAddress('Client A');
|
|
$addrB = $this->seedClientWithAddress('Client B');
|
|
$this->getEm()->flush();
|
|
$siteId = $this->aSiteId();
|
|
|
|
$client = $this->createAdminClient();
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'direction' => 'CLIENT',
|
|
'client' => '/api/clients/'.$addrA->getClient()?->getId(),
|
|
'clientDeliveryAddress' => '/api/client_addresses/'.$addrB->getId(), // adresse du client B
|
|
'departureSite' => '/api/sites/'.$siteId,
|
|
'containerType' => 'BENNE',
|
|
'pricingUnit' => 'TONNE',
|
|
'price' => '42.50',
|
|
'priceState' => 'VALIDE',
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
public function testForeignSupplierAddressReturns422(): void
|
|
{
|
|
// RG-4.11 : l'adresse d'appro doit appartenir au fournisseur choisi.
|
|
$carrier = $this->seedCarrier('Prix Adresse Etrangere Fournisseur');
|
|
$addrA = $this->seedSupplierWithAddress('Fournisseur A');
|
|
$addrB = $this->seedSupplierWithAddress('Fournisseur B');
|
|
$this->getEm()->flush();
|
|
$siteId = $this->aSiteId();
|
|
|
|
$client = $this->createAdminClient();
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'direction' => 'FOURNISSEUR',
|
|
'supplier' => '/api/suppliers/'.$addrA->getSupplier()?->getId(),
|
|
'supplierSupplyAddress' => '/api/supplier_addresses/'.$addrB->getId(), // adresse du fournisseur B
|
|
'deliverySite' => '/api/sites/'.$siteId,
|
|
'containerType' => 'FOND_MOUVANT',
|
|
'pricingUnit' => 'FORFAIT',
|
|
'price' => '320.00',
|
|
'priceState' => 'EN_COURS',
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
public function testValidClientPriceIsCreated(): void
|
|
{
|
|
$carrier = $this->seedCarrier('Prix Client Valide');
|
|
$addr = $this->seedClientWithAddress('Client OK');
|
|
$this->getEm()->flush();
|
|
$siteId = $this->aSiteId();
|
|
|
|
$client = $this->createAdminClient();
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'direction' => 'CLIENT',
|
|
'client' => '/api/clients/'.$addr->getClient()?->getId(),
|
|
'clientDeliveryAddress' => '/api/client_addresses/'.$addr->getId(),
|
|
'departureSite' => '/api/sites/'.$siteId,
|
|
'containerType' => 'BENNE',
|
|
'pricingUnit' => 'TONNE',
|
|
'price' => '42.50',
|
|
'priceState' => 'VALIDE',
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertJsonContains(['direction' => 'CLIENT', 'priceState' => 'VALIDE']);
|
|
}
|
|
|
|
public function testValidSupplierPriceIsCreated(): void
|
|
{
|
|
$carrier = $this->seedCarrier('Prix Fournisseur Valide');
|
|
$addr = $this->seedSupplierWithAddress('Fournisseur OK');
|
|
$this->getEm()->flush();
|
|
$siteId = $this->aSiteId();
|
|
|
|
$client = $this->createAdminClient();
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'direction' => 'FOURNISSEUR',
|
|
'supplier' => '/api/suppliers/'.$addr->getSupplier()?->getId(),
|
|
'supplierSupplyAddress' => '/api/supplier_addresses/'.$addr->getId(),
|
|
'deliverySite' => '/api/sites/'.$siteId,
|
|
'containerType' => 'FOND_MOUVANT',
|
|
'pricingUnit' => 'FORFAIT',
|
|
'price' => '320.00',
|
|
'priceState' => 'EN_COURS',
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertJsonContains(['direction' => 'FOURNISSEUR', 'priceState' => 'EN_COURS']);
|
|
}
|
|
|
|
public function testPatchAndDeleteSucceedWithManage(): void
|
|
{
|
|
$price = $this->seedClientPrice('Patch Delete');
|
|
$client = $this->authenticatedClient('bureau', self::PWD); // manage (matrice § 5.2)
|
|
|
|
// PATCH (manage) -> 200
|
|
$client->request('PATCH', '/api/carrier_prices/'.$price->getId(), [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['priceState' => 'NON_VALIDE'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(200);
|
|
self::assertJsonContains(['priceState' => 'NON_VALIDE']);
|
|
|
|
// DELETE (manage) -> 204
|
|
$client->request('DELETE', '/api/carrier_prices/'.$price->getId());
|
|
self::assertResponseStatusCodeSame(204);
|
|
}
|
|
|
|
public function testWriteForbiddenWithoutManage(): void
|
|
{
|
|
$price = $this->seedClientPrice('Forbidden');
|
|
$carrier = $price->getCarrier();
|
|
self::assertNotNull($carrier);
|
|
$client = $this->authenticatedClient('commerciale', self::PWD); // view seul
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/prices', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['direction' => 'CLIENT'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
$client->request('PATCH', '/api/carrier_prices/'.$price->getId(), [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['priceState' => 'VALIDE'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
$client->request('DELETE', '/api/carrier_prices/'.$price->getId());
|
|
self::assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
/** Id d'un site fixture (adresse de depart / livraison des prix). */
|
|
private function aSiteId(): int
|
|
{
|
|
$site = $this->getEm()->getRepository(Site::class)->findOneBy([]);
|
|
self::assertNotNull($site, 'Un site fixture est requis (SitesFixtures).');
|
|
$id = $site->getId();
|
|
self::assertNotNull($id);
|
|
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* Seede un transporteur + un prix CLIENT complet rattache (pour les tests
|
|
* PATCH / DELETE). Passe par l'EM directement (le flux d'ecriture est teste
|
|
* via l'API ailleurs).
|
|
*/
|
|
private function seedClientPrice(string $name): CarrierPrice
|
|
{
|
|
$em = $this->getEm();
|
|
$carrier = $this->seedCarrier($name);
|
|
|
|
/** @var ClientAddress $addr */
|
|
$addr = $this->seedClientWithAddress($name);
|
|
|
|
$price = new CarrierPrice();
|
|
$price->setCarrier($carrier);
|
|
$price->setDirection('CLIENT');
|
|
$price->setClient($addr->getClient());
|
|
$price->setClientDeliveryAddress($addr);
|
|
$price->setDepartureSite($em->getRepository(Site::class)->findOneBy([]));
|
|
$price->setContainerType('BENNE');
|
|
$price->setPricingUnit('TONNE');
|
|
$price->setPrice('42.50');
|
|
$price->setPriceState('VALIDE');
|
|
$carrier->addPrice($price);
|
|
$em->persist($price);
|
|
$em->flush();
|
|
|
|
return $price;
|
|
}
|
|
}
|