36b5e84053
Assert\Bic avec ibanPropertyPath sur ClientRib et SupplierRib : le pays du BIC (positions 5-6) doit correspondre au pays de l'IBAN (positions 1-2). Un BIC et un IBAN valides isolement mais de pays differents -> 422, violation portee par le champ bic avec message FR (ibanMessage), mappee inline cote front. Tests fonctionnels du mismatch (BIC DE + IBAN FR -> 422 sur propertyPath=bic) cote client et fournisseur.
383 lines
15 KiB
PHP
383 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Commercial\Api;
|
|
|
|
use App\Module\Commercial\Domain\Entity\Supplier;
|
|
use App\Module\Sites\Domain\Entity\Site;
|
|
|
|
/**
|
|
* Tests fonctionnels des sous-ressources Contacts / Adresses / RIB du fournisseur
|
|
* (M2, spec § 4.5). Couvrent : normalisation contact (RG-2.12), RG-2.04 (prenom
|
|
* OU nom), RG-2.05 (code postal), RG-2.06 (>= 1 site), RG-2.09 (enum addressType),
|
|
* RG-2.10 (categorie FOURNISSEUR sur adresse), RG-2.08 (DELETE dernier RIB sous
|
|
* LCR -> 409), DELETE contact libre au M2 (pas de garde « dernier contact ») et le
|
|
* gating comptable des RIB (manage seul -> 403). Jumeau de ClientSubResourceApiTest.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class SupplierSubResourceApiTest extends AbstractSupplierApiTestCase
|
|
{
|
|
// === Contacts ===
|
|
|
|
public function testPostContactNormalizesFields(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Contact Host');
|
|
|
|
$data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'firstName' => 'JEAN',
|
|
'lastName' => 'dupont',
|
|
'phonePrimary' => '06.12.34.56.78',
|
|
'email' => 'Jean.DUPONT@ACME.FR',
|
|
],
|
|
])->toArray();
|
|
|
|
self::assertResponseStatusCodeSame(201);
|
|
// RG-2.12 : prenom/nom Title Case, telephone chiffres seuls, email lowercase.
|
|
self::assertSame('Jean', $data['firstName']);
|
|
self::assertSame('Dupont', $data['lastName']);
|
|
self::assertSame('0612345678', $data['phonePrimary']);
|
|
self::assertSame('jean.dupont@acme.fr', $data['email']);
|
|
}
|
|
|
|
public function testPostContactWithoutNameReturns422OnFirstNamePath(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Contact No Name');
|
|
|
|
$response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
|
'json' => ['jobTitle' => 'Directeur'],
|
|
]);
|
|
|
|
// RG-2.04 (prenom OU nom obligatoire) -> 422 rattachee a firstName.
|
|
self::assertResponseStatusCodeSame(422);
|
|
$byPath = $this->violationsByPath($response->toArray(false));
|
|
self::assertArrayHasKey('firstName', $byPath);
|
|
}
|
|
|
|
public function testPostContactOnMissingSupplierReturns404(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/suppliers/999999/contacts', [
|
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
|
'json' => ['firstName' => 'Orphan'],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(404);
|
|
}
|
|
|
|
public function testDeleteLastContactReturns204(): void
|
|
{
|
|
// M2 : pas de garde « dernier contact » (RG-2.13 front-driven) — la
|
|
// suppression du dernier contact est libre (204), contrairement au M1.
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Contact Solo');
|
|
$contact = $this->addContact($seed, 'Unique', 'Contact');
|
|
|
|
$client->request('DELETE', '/api/supplier_contacts/'.$contact->getId());
|
|
|
|
self::assertResponseStatusCodeSame(204);
|
|
}
|
|
|
|
public function testContactWriteWithoutManageReturns403(): void
|
|
{
|
|
// Un user sans aucune permission suppliers -> 403 sur la sous-ressource.
|
|
$seed = $this->seedSupplier('Contact Forbidden');
|
|
$creds = $this->createUserWithPermission('core.users.view');
|
|
$http = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
$http->request('POST', '/api/suppliers/'.$seed->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['firstName' => 'Nope'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
// === Adresses ===
|
|
|
|
public function testPostAddressWithValidPayloadReturns201(): void
|
|
{
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address Host');
|
|
$category = $this->supplierCategory('NEGOCIANT');
|
|
|
|
$data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'addressType' => 'DEPART',
|
|
'postalCode' => '86100',
|
|
'city' => 'Châtellerault',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [$this->firstSiteIri()],
|
|
'categories' => ['/api/categories/'.$category->getId()],
|
|
],
|
|
])->toArray();
|
|
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertSame('DEPART', $data['addressType']);
|
|
}
|
|
|
|
public function testPostAddressWithoutSiteReturns422(): void
|
|
{
|
|
// Sans cette garde, un module Sites desactive renverrait 404 (route
|
|
// /addresses indisponible) et le test passerait pour la MAUVAISE raison
|
|
// au lieu de prouver RG-2.06 (Assert\Count min 1 sur sites).
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address No Site');
|
|
|
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'addressType' => 'DEPART',
|
|
'postalCode' => '86100',
|
|
'city' => 'Châtellerault',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [],
|
|
],
|
|
]);
|
|
|
|
// RG-2.06 (Assert\Count min 1 sur sites).
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
public function testPostAddressWithInvalidPostalCodeReturns422(): void
|
|
{
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address Bad CP');
|
|
|
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'addressType' => 'DEPART',
|
|
'postalCode' => '123',
|
|
'city' => 'Châtellerault',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [$this->firstSiteIri()],
|
|
],
|
|
]);
|
|
|
|
// RG-2.05 (Assert\Regex ^[0-9]{4,5}$).
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
public function testPostAddressWithIncoherentCityAndPostalCodeReturns201(): void
|
|
{
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address Incoherent');
|
|
|
|
// RG-2.05 : pas de controle strict de coherence CP/ville cote serveur.
|
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'addressType' => 'DEPART',
|
|
'postalCode' => '86100',
|
|
'city' => 'Marseille',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [$this->firstSiteIri()],
|
|
],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(201);
|
|
}
|
|
|
|
public function testPostAddressWithInvalidTypeReturns422(): void
|
|
{
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address Bad Type');
|
|
|
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'addressType' => 'INVALID',
|
|
'postalCode' => '86100',
|
|
'city' => 'Châtellerault',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [$this->firstSiteIri()],
|
|
],
|
|
]);
|
|
|
|
// RG-2.09 (Assert\Choice PROSPECT|DEPART|RENDU).
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
/**
|
|
* RG-2.09 : les 3 valeurs valides de addressType sont acceptees.
|
|
*/
|
|
public function testPostAddressWithEachValidTypeReturns201(): void
|
|
{
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address Types');
|
|
$siteIri = $this->firstSiteIri();
|
|
|
|
foreach (['PROSPECT', 'DEPART', 'RENDU'] as $type) {
|
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'addressType' => $type,
|
|
'postalCode' => '86100',
|
|
'city' => 'Châtellerault',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [$siteIri],
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(201, sprintf('addressType=%s doit etre accepte.', $type));
|
|
}
|
|
}
|
|
|
|
public function testPostAddressWithNonFournisseurCategoryReturns422(): void
|
|
{
|
|
$this->skipIfSitesModuleDisabled();
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Address Bad Cat');
|
|
// categorie de type CLIENT -> interdite sur une adresse fournisseur.
|
|
$clientTypedCategory = $this->createCategory('SECTEUR');
|
|
|
|
$response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/addresses', [
|
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
|
'json' => [
|
|
'addressType' => 'DEPART',
|
|
'postalCode' => '86100',
|
|
'city' => 'Châtellerault',
|
|
'street' => '1 rue du Test',
|
|
'sites' => [$this->firstSiteIri()],
|
|
'categories' => ['/api/categories/'.$clientTypedCategory->getId()],
|
|
],
|
|
]);
|
|
|
|
// RG-2.10 -> 422 rattachee a categories.
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertArrayHasKey('categories', $this->violationsByPath($response->toArray(false)));
|
|
}
|
|
|
|
// === RIBs ===
|
|
|
|
public function testPostRibByAdminReturns201(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Rib Host');
|
|
|
|
$data = $client->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'label' => 'Compte principal',
|
|
'bic' => self::VALID_BIC,
|
|
'iban' => self::VALID_IBAN,
|
|
],
|
|
])->toArray();
|
|
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertSame('Compte principal', $data['label']);
|
|
}
|
|
|
|
public function testPostRibWithInvalidIbanReturns422(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Rib Bad Iban');
|
|
|
|
$client->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['label' => 'Compte invalide', 'bic' => self::VALID_BIC, 'iban' => 'INVALID-IBAN'],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(422);
|
|
}
|
|
|
|
/**
|
|
* Controle croise pays BIC/IBAN (Assert\Bic ibanPropertyPath) : un BIC (DE) et
|
|
* un IBAN (FR) valides isolement mais de pays differents -> 422. La violation
|
|
* porte propertyPath=bic et le message FR `ibanMessage` (mapping inline front).
|
|
*/
|
|
public function testPostRibWithBicIbanCountryMismatchReturns422WithFrenchMessageOnBic(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Rib Pays Mismatch');
|
|
|
|
$response = $client->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
|
|
'headers' => ['Content-Type' => self::LD, 'Accept' => self::LD],
|
|
'json' => ['label' => 'Compte incoherent', 'bic' => self::FOREIGN_BIC, 'iban' => self::VALID_IBAN],
|
|
]);
|
|
|
|
self::assertResponseStatusCodeSame(422);
|
|
$byPath = $this->violationsByPath($response->toArray(false));
|
|
self::assertArrayHasKey('bic', $byPath, 'Le mismatch pays BIC/IBAN doit porter propertyPath=bic (mapping front).');
|
|
self::assertSame('Le BIC ne correspond pas au pays de l\'IBAN.', $byPath['bic']);
|
|
}
|
|
|
|
public function testDeleteRibNonLcrReturns204(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Rib Non LCR');
|
|
$rib = $this->addRib($seed);
|
|
|
|
$client->request('DELETE', '/api/supplier_ribs/'.$rib->getId());
|
|
|
|
self::assertResponseStatusCodeSame(204);
|
|
}
|
|
|
|
public function testDeleteLastRibUnderLcrReturns409(): void
|
|
{
|
|
$client = $this->createAdminClient();
|
|
$seed = $this->seedSupplier('Rib LCR Solo');
|
|
$rib = $this->addRib($seed);
|
|
// Passe le fournisseur en LCR (seed direct).
|
|
$em = $this->getEm();
|
|
$managed = $em->getRepository(Supplier::class)->find($seed->getId());
|
|
$managed->setPaymentType($this->paymentType('LCR'));
|
|
$em->flush();
|
|
|
|
$client->request('DELETE', '/api/supplier_ribs/'.$rib->getId());
|
|
|
|
// RG-2.08 : LCR exige >= 1 RIB -> suppression du dernier refusee.
|
|
self::assertResponseStatusCodeSame(409);
|
|
}
|
|
|
|
public function testRibWriteWithoutAccountingManageReturns403(): void
|
|
{
|
|
// Un user portant seulement suppliers.manage (sans accounting.manage) ne
|
|
// peut ni creer, ni modifier, ni supprimer un RIB (gating renforce § 4.5).
|
|
$seed = $this->seedSupplier('Rib Forbidden');
|
|
$rib = $this->addRib($seed);
|
|
$creds = $this->createUserWithPermission('commercial.suppliers.manage');
|
|
$http = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
$http->request('POST', '/api/suppliers/'.$seed->getId().'/ribs', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['label' => 'X', 'bic' => self::VALID_BIC, 'iban' => self::VALID_IBAN],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
$http->request('PATCH', '/api/supplier_ribs/'.$rib->getId(), [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['label' => 'Y'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
$http->request('DELETE', '/api/supplier_ribs/'.$rib->getId());
|
|
self::assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
// === Helpers ===
|
|
|
|
// violationsByPath() : helper mutualise dans AbstractSupplierApiTestCase.
|
|
|
|
private function firstSiteIri(): string
|
|
{
|
|
$site = $this->getEm()->getRepository(Site::class)->findOneBy([]);
|
|
self::assertNotNull($site, 'Aucun site seede : impossible de tester les adresses.');
|
|
|
|
return '/api/sites/'.$site->getId();
|
|
}
|
|
}
|