c1fcd9a7c8
Repond aux retours de review (rigueur d'assertion transversale) :
- mutualise assertViolationOnPath dans AbstractCarrierApiTestCase (au lieu d'un
duplicata local a CarrierWriteApiTest) ;
- asserte le propertyPath des 422 des sous-ressources (adresses city/street/postalCode,
contacts firstName/phones/email, prix clientDeliveryAddress/supplierSupplyAddress/price)
-> evite les faux-verts du mapping inline (ERP-101) ;
- 404 parent (POST sur /carriers/999999/{addresses,contacts,prices}) ;
- 401 anonyme + filtre ?certificationType= sur la collection (trous releves sur le
contrat de lecture).
283 lines
11 KiB
PHP
283 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Transport\Api;
|
|
|
|
use ApiPlatform\Symfony\Bundle\Test\Client;
|
|
use App\Module\Commercial\Domain\Entity\Client as ClientEntity;
|
|
use App\Module\Commercial\Domain\Entity\ClientAddress;
|
|
use App\Module\Commercial\Domain\Entity\Supplier;
|
|
use App\Module\Commercial\Domain\Entity\SupplierAddress;
|
|
use App\Module\Sites\Domain\Entity\Site;
|
|
use App\Module\Transport\Domain\Entity\Carrier;
|
|
use App\Module\Transport\Domain\Entity\CarrierAddress;
|
|
use App\Module\Transport\Domain\Entity\CarrierContact;
|
|
use App\Module\Transport\Domain\Entity\CarrierPrice;
|
|
use App\Module\Transport\Domain\Entity\QualimatCarrier;
|
|
use App\Tests\Module\Core\Api\AbstractApiTestCase;
|
|
use DateTimeImmutable;
|
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
|
|
|
/**
|
|
* Base des tests fonctionnels du repertoire transporteurs (M4). Apporte les
|
|
* factories de seed direct (sans passer par l'API : le flux d'ecriture arrive
|
|
* au WT4) pour les tests de lecture / serialisation / contrat (DoD § 4.0.bis).
|
|
*
|
|
* Donnees (RETEX M1) : chaque test seede ses transporteurs ; le tearDown les
|
|
* purge (cascade BDD sur les sous-collections) ainsi que les lignes
|
|
* qualimat_carrier de test (prefixe SIRET dedie).
|
|
*
|
|
* @internal
|
|
*/
|
|
abstract class AbstractCarrierApiTestCase extends AbstractApiTestCase
|
|
{
|
|
protected const string LD = 'application/ld+json';
|
|
protected const string MERGE = 'application/merge-patch+json';
|
|
|
|
/** Prefixe SIRET des lignes qualimat_carrier seedees par les tests (purge ciblee). */
|
|
private const string TEST_SIRET_PREFIX = 'TESTQ';
|
|
|
|
/** Prefixe des Client/Supplier de test (cross-module Prix) — purge ciblee. */
|
|
private const string TEST_REF_PREFIX = 'TESTCARRIERREF';
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$em = $this->getEm();
|
|
// Carrier d'abord : ON DELETE CASCADE purge carrier_price (FK RESTRICT vers
|
|
// client/supplier), liberant les Client/Supplier de test pour leur purge.
|
|
$em->createQuery('DELETE FROM '.Carrier::class)->execute();
|
|
$em->createQuery('DELETE FROM '.ClientEntity::class.' c WHERE c.companyName LIKE :p')
|
|
->setParameter('p', self::TEST_REF_PREFIX.'%')->execute()
|
|
;
|
|
$em->createQuery('DELETE FROM '.Supplier::class.' s WHERE s.companyName LIKE :p')
|
|
->setParameter('p', self::TEST_REF_PREFIX.'%')->execute()
|
|
;
|
|
// qualimat_carrier : insere en DBAL brut (entite lecture seule) -> purge DBAL.
|
|
$em->getConnection()->executeStatement(
|
|
'DELETE FROM qualimat_carrier WHERE siret LIKE :p',
|
|
['p' => self::TEST_SIRET_PREFIX.'%'],
|
|
);
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
protected function createAdminClient(): Client
|
|
{
|
|
return $this->authenticatedClient('admin', 'admin');
|
|
}
|
|
|
|
/**
|
|
* Garde-fou ERP-101 : verifie qu'une reponse 422 porte une violation sur le
|
|
* `propertyPath` attendu (et pas seulement le bon code HTTP). Sans cette
|
|
* assertion, une 422 venue d'une AUTRE cause (autre champ manquant, IRI 404)
|
|
* ferait passer le test au vert sans prouver le mapping inline par champ.
|
|
*
|
|
* Mutualise dans la base (au lieu d'un duplicata par fichier) pour que toute
|
|
* la stack d'ecriture (formulaire principal + sous-ressources) l'utilise.
|
|
*/
|
|
protected static function assertViolationOnPath(object $response, string $path): void
|
|
{
|
|
/** @var ResponseInterface $response */
|
|
$paths = array_column($response->toArray(false)['violations'] ?? [], 'propertyPath');
|
|
|
|
self::assertContains(
|
|
$path,
|
|
$paths,
|
|
sprintf('Aucune violation sur "%s" (paths: %s).', $path, implode(', ', $paths)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Payload minimal valide du formulaire principal (transporteur non-QUALIMAT,
|
|
* non affrete) : nom + certification GMP_PLUS. Sert de base aux tests
|
|
* d'ecriture / RBAC.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function validMainPayload(string $name): array
|
|
{
|
|
return [
|
|
'name' => $name,
|
|
'certificationType' => 'GMP_PLUS',
|
|
'isChartered' => false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Seede un transporteur minimal (nom en MAJUSCULES, comme le ferait le
|
|
* futur Processor). Sert aux tests de liste / archivage.
|
|
*/
|
|
protected function seedCarrier(string $name, bool $isArchived = false): Carrier
|
|
{
|
|
$em = $this->getEm();
|
|
$carrier = new Carrier();
|
|
$carrier->setName(mb_strtoupper($name, 'UTF-8'));
|
|
$carrier->setCertificationType('GMP_PLUS');
|
|
$carrier->setIsArchived($isArchived);
|
|
if ($isArchived) {
|
|
$carrier->setArchivedAt(new DateTimeImmutable());
|
|
}
|
|
$em->persist($carrier);
|
|
$em->flush();
|
|
|
|
return $carrier;
|
|
}
|
|
|
|
/**
|
|
* Seede un transporteur COMPLET (sans passer par l'API) : lien QUALIMAT,
|
|
* 1 adresse, 1 contact, et 2 prix couvrant les deux branches (CLIENT avec
|
|
* client + adresse de livraison + site de depart ; FOURNISSEUR avec
|
|
* fournisseur + adresse d'appro + site de livraison). Socle du contrat de
|
|
* serialisation et de la DoD (§ 4.0.bis).
|
|
*/
|
|
protected function seedCompleteCarrier(string $name): Carrier
|
|
{
|
|
$em = $this->getEm();
|
|
$suffix = substr(bin2hex(random_bytes(3)), 0, 6);
|
|
|
|
$qualimat = $this->seedQualimatCarrier($name);
|
|
|
|
$carrier = new Carrier();
|
|
$carrier->setName(mb_strtoupper($name.' '.$suffix, 'UTF-8'));
|
|
$carrier->setQualimatCarrier($qualimat);
|
|
$carrier->setCertificationType('QUALIMAT');
|
|
$em->persist($carrier);
|
|
|
|
$address = new CarrierAddress();
|
|
$address->setCarrier($carrier);
|
|
$address->setPostalCode('86000');
|
|
$address->setCity('Poitiers');
|
|
$address->setStreet('12 rue des Acacias');
|
|
$carrier->addAddress($address);
|
|
$em->persist($address);
|
|
|
|
$contact = new CarrierContact();
|
|
$contact->setCarrier($carrier);
|
|
$contact->setFirstName('Marie');
|
|
$contact->setLastName('Martin');
|
|
$contact->setPhonePrimary('0612345678');
|
|
$contact->setEmail('marie.martin@seed.test');
|
|
$carrier->addContact($contact);
|
|
$em->persist($contact);
|
|
|
|
// Refs cross-module : seedees localement (en test, les fixtures M1/M2 ne
|
|
// sont pas chargees — seuls les sites le sont). Prouve l'embed via les
|
|
// contrats Shared + resolve_target_entities (regle n°1).
|
|
$site = $em->getRepository(Site::class)->findOneBy([]);
|
|
self::assertNotNull($site, 'Un site fixture est requis (SitesFixtures).');
|
|
|
|
$clientAddress = $this->seedClientWithAddress($name.' '.$suffix);
|
|
$supplierAddress = $this->seedSupplierWithAddress($name.' '.$suffix);
|
|
|
|
// Branche CLIENT (RG-4.10).
|
|
$clientPrice = new CarrierPrice();
|
|
$clientPrice->setCarrier($carrier);
|
|
$clientPrice->setDirection('CLIENT');
|
|
$clientPrice->setClient($clientAddress->getClient());
|
|
$clientPrice->setClientDeliveryAddress($clientAddress);
|
|
$clientPrice->setDepartureSite($site);
|
|
$clientPrice->setContainerType('BENNE');
|
|
$clientPrice->setPricingUnit('TONNE');
|
|
$clientPrice->setPrice('42.50');
|
|
$clientPrice->setPriceState('VALIDE');
|
|
$carrier->addPrice($clientPrice);
|
|
$em->persist($clientPrice);
|
|
|
|
// Branche FOURNISSEUR (RG-4.11).
|
|
$supplierPrice = new CarrierPrice();
|
|
$supplierPrice->setCarrier($carrier);
|
|
$supplierPrice->setDirection('FOURNISSEUR');
|
|
$supplierPrice->setSupplier($supplierAddress->getSupplier());
|
|
$supplierPrice->setSupplierSupplyAddress($supplierAddress);
|
|
$supplierPrice->setDeliverySite($site);
|
|
$supplierPrice->setContainerType('FOND_MOUVANT');
|
|
$supplierPrice->setPricingUnit('FORFAIT');
|
|
$supplierPrice->setPrice('320.00');
|
|
$supplierPrice->setPriceState('EN_COURS');
|
|
$carrier->addPrice($supplierPrice);
|
|
$em->persist($supplierPrice);
|
|
|
|
$em->flush();
|
|
|
|
return $carrier;
|
|
}
|
|
|
|
/**
|
|
* Seede un Client minimal (companyName prefixe pour la purge) + une adresse
|
|
* de livraison valide (CHECKs client_address respectes). Retourne l'adresse.
|
|
*/
|
|
protected function seedClientWithAddress(string $label): ClientAddress
|
|
{
|
|
$em = $this->getEm();
|
|
$suffix = substr(bin2hex(random_bytes(3)), 0, 6);
|
|
|
|
$client = new ClientEntity();
|
|
$client->setCompanyName(mb_strtoupper(self::TEST_REF_PREFIX.' CLI '.$label.' '.$suffix, 'UTF-8'));
|
|
$em->persist($client);
|
|
|
|
$address = new ClientAddress();
|
|
$address->setClient($client);
|
|
// Adresse de livraison : is_delivery=true, is_prospect=false, is_billing=false
|
|
// -> satisfait chk_client_address_prospect_exclusive + chk_client_address_billing_email.
|
|
$address->setIsDelivery(true);
|
|
$address->setPostalCode('86000');
|
|
$address->setCity('Poitiers');
|
|
$address->setStreet('1 rue de la Livraison');
|
|
$em->persist($address);
|
|
|
|
return $address;
|
|
}
|
|
|
|
/**
|
|
* Seede un Supplier minimal (companyName prefixe pour la purge) + une adresse
|
|
* d'approvisionnement valide (address_type DEPART). Retourne l'adresse.
|
|
*/
|
|
protected function seedSupplierWithAddress(string $label): SupplierAddress
|
|
{
|
|
$em = $this->getEm();
|
|
$suffix = substr(bin2hex(random_bytes(3)), 0, 6);
|
|
|
|
$supplier = new Supplier();
|
|
$supplier->setCompanyName(mb_strtoupper(self::TEST_REF_PREFIX.' FRN '.$label.' '.$suffix, 'UTF-8'));
|
|
$em->persist($supplier);
|
|
|
|
$address = new SupplierAddress();
|
|
$address->setSupplier($supplier);
|
|
$address->setAddressType('DEPART');
|
|
$address->setPostalCode('17000');
|
|
$address->setCity('La Rochelle');
|
|
$address->setStreet('2 quai de l Appro');
|
|
$em->persist($address);
|
|
|
|
return $address;
|
|
}
|
|
|
|
/**
|
|
* Insere une ligne qualimat_carrier de test en DBAL brut (l'entite mappee est
|
|
* en lecture seule) et retourne l'entite rechargee. SIRET prefixe pour la purge.
|
|
*/
|
|
protected function seedQualimatCarrier(string $name): QualimatCarrier
|
|
{
|
|
$em = $this->getEm();
|
|
$siret = self::TEST_SIRET_PREFIX.substr(bin2hex(random_bytes(6)), 0, 9);
|
|
|
|
$em->getConnection()->insert('qualimat_carrier', [
|
|
'siret' => $siret,
|
|
'name' => mb_strtoupper($name, 'UTF-8'),
|
|
'address' => '12 rue des Acacias',
|
|
'postal_code' => '86000',
|
|
'city' => 'Poitiers',
|
|
'status' => 'Valide',
|
|
'validity_date' => '2027-12-31',
|
|
'is_active' => 'true',
|
|
'last_synced_at' => new DateTimeImmutable()->format('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
$qualimat = $em->getRepository(QualimatCarrier::class)->findOneBy(['siret' => $siret]);
|
|
self::assertNotNull($qualimat, 'La ligne qualimat_carrier de test doit etre rechargeable.');
|
|
|
|
return $qualimat;
|
|
}
|
|
}
|