5e15c1f69f
Auto Tag Develop / tag (push) Successful in 11s
Lot de retours métier **ERP-193** (« Fix tous les retours starseed »), transverse aux 4 répertoires (clients, fournisseurs, prestataires, transporteurs).
## Contenu
- **Pagination** : défaut à 25 items/page sur les 4 répertoires.
- **Libellé** : colonne « Dernière activité » → « Dernière modification ».
- **Consultation** : masquage des onglets vides (coquilles « à venir » + onglets de données sans donnée).
- **Chiffre d'affaires** : plafonné à 999 999 999 999,99 (clamp front + `Assert\LessThanOrEqual` back).
- **Date de création** : interdiction des dates futures (`:max` MalioDate + `Assert\LessThanOrEqual('today')` back).
- **Caractères spéciaux** : blocage des caractères parasites (`²³§~#|…`) dans les champs texte via une allow-list par profil (nom de personne / texte libre / adresse / code alphanumérique) — filtrage front à la frappe + `Assert\Regex` back autoritaire. Email/IBAN/BIC/TVA conservent leurs validateurs de format.
- **UI** : champs en consultation et onglets validés grisés (`readonly` → `disabled`).
- **UI** : boutons « Archiver » en rouge (variant `danger`).
## Tests
- Back : nouveaux tests RG (plafond CA, dates futures, caractères spéciaux) + garde-fou contraintes — suite complète verte (813 tests).
- Front : nouveaux tests unitaires (sanitizers, helpers date/montant) — 615 tests verts, eslint clean.
---------
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #139
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
211 lines
7.9 KiB
PHP
211 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Transport\Api;
|
|
|
|
use App\Module\Core\Infrastructure\DataFixtures\RbacDemoFixtures;
|
|
use App\Module\Transport\Domain\Entity\CarrierContact;
|
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
|
use Symfony\Component\Console\Input\ArrayInput;
|
|
use Symfony\Component\Console\Output\NullOutput;
|
|
|
|
/**
|
|
* Sous-ressource Contact d'un transporteur (spec-back M4 § 4.5, ERP-160).
|
|
* POST /api/carriers/{id}/contacts, PATCH/DELETE /api/carrier_contacts/{id}.
|
|
*
|
|
* Contrat verifie :
|
|
* - ERP-193 : contact sans prenom ni nom -> 201 (bloc Contact optionnel) ;
|
|
* - un nom (ou prenom) seul suffit -> 201 ;
|
|
* - RG-4.08 : 3 telephones (tableau `phones`) -> 422 (max 2) ;
|
|
* - mapping `phones[]` -> phonePrimary / phoneSecondary + normalisation (RG-4.13) ;
|
|
* - PATCH / DELETE OK avec transport.carriers.manage, 403 sans (view seul).
|
|
*
|
|
* @internal
|
|
*/
|
|
final class CarrierContactApiTest 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 testContactWithoutNameIsCreated(): void
|
|
{
|
|
// ERP-193 (retour metier) : l'onglet Contact n'est plus obligatoire et la
|
|
// garde « prenom OU nom » (ex RG-4.08) est retiree -> un contact sans nom
|
|
// (ici un simple telephone) est desormais accepte (201).
|
|
$carrier = $this->seedCarrier('Contact Sans Nom');
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['phones' => ['0611111111']],
|
|
]);
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertJsonContains(['phonePrimary' => '0611111111']);
|
|
}
|
|
|
|
public function testSingleFieldContactIsCreated(): void
|
|
{
|
|
// Un nom (ou prenom) seul suffit a creer un contact.
|
|
$carrier = $this->seedCarrier('Contact Mono');
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['lastName' => 'martin'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(201);
|
|
|
|
// RG-4.13 : nom capitalise serveur.
|
|
self::assertJsonContains(['lastName' => 'Martin']);
|
|
}
|
|
|
|
public function testThirdPhoneReturns422(): void
|
|
{
|
|
// RG-4.08 : max 2 telephones. Le contrat d'ecriture accepte un tableau
|
|
// `phones` (liste dynamique cote front « x1, +1 possible, max 2 ») ; un 3e
|
|
// numero -> 422 rattachee au champ `phones`.
|
|
$carrier = $this->seedCarrier('Contact Trois Tel');
|
|
$client = $this->createAdminClient();
|
|
|
|
$response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'firstName' => 'Jean',
|
|
'phones' => ['0611111111', '0622222222', '0633333333'],
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(422);
|
|
// Le max-2 cible le champ virtuel `phones` (mapping inline ERP-101).
|
|
self::assertViolationOnPath($response, 'phones');
|
|
}
|
|
|
|
public function testInvalidEmailReturns422(): void
|
|
{
|
|
// L'email du contact porte un Assert\Email (nouvelle contrainte M4) : une
|
|
// adresse mal formee -> 422 ciblee sur `email`.
|
|
$carrier = $this->seedCarrier('Contact Email Invalide');
|
|
$client = $this->createAdminClient();
|
|
|
|
$response = $client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['lastName' => 'Durand', 'email' => 'pas-un-email'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(422);
|
|
self::assertViolationOnPath($response, 'email');
|
|
}
|
|
|
|
public function testPostContactOnUnknownCarrierReturns404(): void
|
|
{
|
|
// Parent introuvable (read:false) -> 404 explicite du processor.
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/carriers/999999/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['lastName' => 'Martin'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(404);
|
|
}
|
|
|
|
public function testPhonesAreMappedAndNormalized(): void
|
|
{
|
|
// Mapping `phones[0]` -> phonePrimary, `phones[1]` -> phoneSecondary +
|
|
// normalisation RG-4.13 (chiffres uniquement).
|
|
$carrier = $this->seedCarrier('Contact Deux Tel');
|
|
$client = $this->createAdminClient();
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => [
|
|
'lastName' => 'Dupont',
|
|
'phones' => ['06.11.11.11.11', '06 22 22 22 22'],
|
|
],
|
|
]);
|
|
self::assertResponseStatusCodeSame(201);
|
|
self::assertJsonContains([
|
|
'phonePrimary' => '0611111111',
|
|
'phoneSecondary' => '0622222222',
|
|
]);
|
|
}
|
|
|
|
public function testPatchAndDeleteSucceedWithManage(): void
|
|
{
|
|
$contact = $this->seedContact('Patch Delete');
|
|
$client = $this->authenticatedClient('bureau', self::PWD); // manage (matrice § 5.2)
|
|
|
|
// PATCH (manage) -> 200
|
|
$client->request('PATCH', '/api/carrier_contacts/'.$contact->getId(), [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['jobTitle' => 'Directeur'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(200);
|
|
|
|
// DELETE (manage) -> 204
|
|
$client->request('DELETE', '/api/carrier_contacts/'.$contact->getId());
|
|
self::assertResponseStatusCodeSame(204);
|
|
}
|
|
|
|
public function testWriteForbiddenWithoutManage(): void
|
|
{
|
|
$contact = $this->seedContact('Forbidden');
|
|
$carrier = $contact->getCarrier();
|
|
self::assertNotNull($carrier);
|
|
$client = $this->authenticatedClient('commerciale', self::PWD); // view seul
|
|
|
|
$client->request('POST', '/api/carriers/'.$carrier->getId().'/contacts', [
|
|
'headers' => ['Content-Type' => self::LD],
|
|
'json' => ['lastName' => 'Bernard'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
$client->request('PATCH', '/api/carrier_contacts/'.$contact->getId(), [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['jobTitle' => 'Chef'],
|
|
]);
|
|
self::assertResponseStatusCodeSame(403);
|
|
|
|
$client->request('DELETE', '/api/carrier_contacts/'.$contact->getId());
|
|
self::assertResponseStatusCodeSame(403);
|
|
}
|
|
|
|
/**
|
|
* Seede un transporteur + un contact rattache (pour les tests PATCH/DELETE).
|
|
*/
|
|
private function seedContact(string $name): CarrierContact
|
|
{
|
|
$em = $this->getEm();
|
|
$carrier = $this->seedCarrier($name);
|
|
|
|
$contact = new CarrierContact();
|
|
$contact->setCarrier($carrier);
|
|
$contact->setLastName('Martin');
|
|
$contact->setPhonePrimary('0612345678');
|
|
$carrier->addContact($contact);
|
|
$em->persist($contact);
|
|
$em->flush();
|
|
|
|
return $contact;
|
|
}
|
|
}
|