diff --git a/migrations/Version20260617120000.php b/migrations/Version20260617120000.php new file mode 100644 index 0000000..edf6d5b --- /dev/null +++ b/migrations/Version20260617120000.php @@ -0,0 +1,49 @@ +addSql('ALTER TABLE carrier_contact DROP CONSTRAINT chk_carrier_contact_filled'); + $this->addSql('ALTER TABLE carrier_contact ADD CONSTRAINT chk_carrier_contact_name CHECK (first_name IS NOT NULL OR last_name IS NOT NULL)'); + + $this->addSql('COMMENT ON TABLE carrier_contact IS $_$Contacts d un transporteur (1:n) — onglet Contact (M4). Au moins le prenom OU le nom rempli (RG-4.08, chk_carrier_contact_name), max 2 telephones.$_$'); + $this->addSql('COMMENT ON COLUMN carrier_contact.first_name IS $_$Prenom du contact (capitalise serveur). Prenom OU nom obligatoire (RG-4.08, chk_carrier_contact_name).$_$'); + $this->addSql('COMMENT ON COLUMN carrier_contact.last_name IS $_$Nom du contact (capitalise serveur). Prenom OU nom obligatoire (RG-4.08, chk_carrier_contact_name).$_$'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE carrier_contact DROP CONSTRAINT chk_carrier_contact_name'); + $this->addSql('ALTER TABLE carrier_contact ADD CONSTRAINT chk_carrier_contact_filled CHECK (first_name IS NOT NULL OR last_name IS NOT NULL OR job_title IS NOT NULL OR phone_primary IS NOT NULL OR email IS NOT NULL)'); + + $this->addSql('COMMENT ON TABLE carrier_contact IS $_$Contacts d un transporteur (1:n) — onglet Contact (M4). Au moins un champ rempli (RG-4.08, chk_carrier_contact_filled), max 2 telephones.$_$'); + $this->addSql('COMMENT ON COLUMN carrier_contact.first_name IS $_$Prenom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).$_$'); + $this->addSql('COMMENT ON COLUMN carrier_contact.last_name IS $_$Nom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).$_$'); + } +} diff --git a/src/Module/Transport/Domain/Entity/CarrierContact.php b/src/Module/Transport/Domain/Entity/CarrierContact.php index 93a507a..ddb7924 100644 --- a/src/Module/Transport/Domain/Entity/CarrierContact.php +++ b/src/Module/Transport/Domain/Entity/CarrierContact.php @@ -21,8 +21,8 @@ use Symfony\Component\Validator\Constraints as Assert; /** * Contact d'un transporteur (1:n) — onglet Contact (M4). Jumeau de - * SupplierContact (M2) : au moins un champ rempli (RG-4.08, garanti par le - * CHECK chk_carrier_contact_filled + le Processor), max 2 telephones. + * SupplierContact (M2) : au moins le prenom OU le nom (RG-4.08, garanti par le + * CHECK chk_carrier_contact_name + le Processor), max 2 telephones. * * Lecture : proprietes en `carrier:item:read` (embarquees au detail du * transporteur). Ecriture : groupe `carrier:write:contacts`. diff --git a/src/Module/Transport/Infrastructure/ApiPlatform/State/Processor/CarrierContactProcessor.php b/src/Module/Transport/Infrastructure/ApiPlatform/State/Processor/CarrierContactProcessor.php index 6291feb..1ca6c7b 100644 --- a/src/Module/Transport/Infrastructure/ApiPlatform/State/Processor/CarrierContactProcessor.php +++ b/src/Module/Transport/Infrastructure/ApiPlatform/State/Processor/CarrierContactProcessor.php @@ -23,21 +23,21 @@ use function is_string; /** * Processor d'ecriture de la sous-ressource Contact d'un transporteur (M4, * spec-back § 4.5). Jumeau du SupplierContactProcessor (M2), recentre sur le - * perimetre ERP-160, AVEC deux specificites M4 : RG-4.08 (≥ 1 champ rempli, max - * 2 telephones) portee a la fois par le CHECK BDD chk_carrier_contact_filled et - * par ce Processor. + * perimetre ERP-160. RG-4.08 (correctif, alignement M1/M2/M3) : un contact exige + * au moins le PRENOM OU le NOM (la fonction / le telephone / l'email seuls ne + * suffisent pas), porte a la fois par le CHECK BDD chk_carrier_contact_name et par + * ce Processor ; le « max 2 telephones » reste une specificite M4. * * Sequence : * - POST / PATCH : rattachement au transporteur parent (linkParent), * normalisation serveur RG-4.13 (prenom/nom Title Case, email lowercase), * mapping du tableau d'ecriture `phones` -> phonePrimary/phoneSecondary - * (max 2, chiffres uniquement), puis garde RG-4.08 (≥ 1 champ) avant - * persistance. + * (max 2, chiffres uniquement), puis garde « prenom OU nom » avant persistance. * - DELETE : aucune regle metier specifique (suppression physique directe). * - * RG-4.08 vit ICI (double du CHECK BDD) pour transformer une violation SQL (500 - * generique) en 422 propre rattachee au champ `firstName` (mapping inline - * ERP-101). Le « max 2 telephones » est rattache au champ `phones` : seul + * La garde « prenom OU nom » vit ICI (double du CHECK BDD) pour transformer une + * violation SQL (500 generique) en 422 propre rattachee au champ `firstName` + * (mapping inline ERP-101). Le « max 2 telephones » est rattache au champ `phones` : seul * point de saisie des numeros (les colonnes phonePrimary/phoneSecondary sont en * lecture seule). * @@ -77,7 +77,7 @@ final class CarrierContactProcessor implements ProcessorInterface $this->linkParent($data, $uriVariables); $this->normalize($data); $this->applyPhones($data); - $this->validateAtLeastOneField($data); + $this->validateName($data); return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } @@ -187,25 +187,18 @@ final class CarrierContactProcessor implements ProcessorInterface } /** - * RG-4.08 : un bloc Contact est valide des qu'au moins 1 champ est rempli - * (firstName, lastName, jobTitle, phonePrimary ou email — meme perimetre que - * le CHECK BDD chk_carrier_contact_filled, qui exclut phoneSecondary). Double - * garde : leve une 422 propre rattachee a `firstName` plutot qu'une 500 SQL. - * Joue apres normalisation + mapping telephones, donc les chaines vides sont - * deja ramenees a null. + * RG-4.08 (alignement M1/M2/M3) : un bloc Contact exige au moins le PRENOM OU le + * NOM — un contact se materialise par son nom ; fonction / telephone / email + * seuls ne suffisent pas. Double garde avec le CHECK BDD chk_carrier_contact_name + * — leve une 422 propre rattachee a `firstName` plutot qu'une 500 SQL. Joue apres + * normalisation + mapping telephones, donc les chaines vides sont deja null. */ - private function validateAtLeastOneField(CarrierContact $contact): void + private function validateName(CarrierContact $contact): void { - if ( - null === $contact->getFirstName() - && null === $contact->getLastName() - && null === $contact->getJobTitle() - && null === $contact->getPhonePrimary() - && null === $contact->getEmail() - ) { + if (null === $contact->getFirstName() && null === $contact->getLastName()) { $violations = new ConstraintViolationList(); $violations->add(new ConstraintViolation( - 'Renseignez au moins un champ pour le contact.', + 'Le prénom ou le nom du contact est obligatoire.', null, [], $contact, @@ -219,8 +212,8 @@ final class CarrierContactProcessor implements ProcessorInterface /** * Trim + chaine vide -> null (la fonction n'est pas normalisee en casse, - * contrairement aux noms de personne). Garantit que RG-4.08 detecte un champ - * « non rempli » meme si le client envoie une chaine vide. + * contrairement aux noms de personne). Evite de persister une chaine vide + * (« » devient null) cote fonction du contact. */ private function blankToNull(?string $value): ?string { diff --git a/src/Module/Transport/Infrastructure/DataFixtures/CarrierFixtures.php b/src/Module/Transport/Infrastructure/DataFixtures/CarrierFixtures.php index 0cce512..1cd7aeb 100644 --- a/src/Module/Transport/Infrastructure/DataFixtures/CarrierFixtures.php +++ b/src/Module/Transport/Infrastructure/DataFixtures/CarrierFixtures.php @@ -194,7 +194,7 @@ class CarrierFixtures extends Fixture implements DependentFixtureInterface /** * Ajoute un contact normalise au transporteur (cascade persist via - * Carrier.contacts). Au moins un champ est toujours fourni (RG-4.08). + * Carrier.contacts). Prenom OU nom toujours fourni (RG-4.08, chk_carrier_contact_name). */ private function addContact( Carrier $carrier, diff --git a/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php b/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php index 6cf7e2d..0c476d3 100644 --- a/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php +++ b/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php @@ -509,11 +509,11 @@ final class ColumnCommentsCatalog ] + self::timestampableBlamableComments(), 'carrier_contact' => [ - '_table' => 'Contacts d un transporteur (1:n) — onglet Contact (M4). Au moins un champ rempli (RG-4.08, chk_carrier_contact_filled), max 2 telephones.', + '_table' => 'Contacts d un transporteur (1:n) — onglet Contact (M4). Au moins le prenom OU le nom rempli (RG-4.08, chk_carrier_contact_name), max 2 telephones.', 'id' => 'Identifiant interne auto-incremente.', 'carrier_id' => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire du contact.', - 'first_name' => 'Prenom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).', - 'last_name' => 'Nom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).', + 'first_name' => 'Prenom du contact (capitalise serveur). Prenom OU nom obligatoire (RG-4.08, chk_carrier_contact_name).', + 'last_name' => 'Nom du contact (capitalise serveur). Prenom OU nom obligatoire (RG-4.08, chk_carrier_contact_name).', 'job_title' => 'Fonction / intitule de poste du contact (≤ 120 caracteres).', 'phone_primary' => 'Telephone principal — chiffres uniquement (normalisation serveur).', 'phone_secondary' => 'Telephone secondaire — chiffres uniquement (max 2 telephones, RG-4.08).', diff --git a/tests/Module/Transport/Api/CarrierContactApiTest.php b/tests/Module/Transport/Api/CarrierContactApiTest.php index 68f1189..aebbcfd 100644 --- a/tests/Module/Transport/Api/CarrierContactApiTest.php +++ b/tests/Module/Transport/Api/CarrierContactApiTest.php @@ -15,8 +15,8 @@ use Symfony\Component\Console\Output\NullOutput; * POST /api/carriers/{id}/contacts, PATCH/DELETE /api/carrier_contacts/{id}. * * Contrat verifie : - * - RG-4.08 : contact totalement vide -> 422 (au moins 1 champ requis) ; - * - RG-4.08 : 1 seul champ rempli -> 201 ; + * - RG-4.08 : contact sans prenom ni nom -> 422 (alignement M1/M2/M3) ; + * - RG-4.08 : un nom (ou prenom) 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). @@ -51,7 +51,8 @@ final class CarrierContactApiTest extends AbstractCarrierApiTestCase public function testEmptyContactReturns422(): void { - // RG-4.08 : aucun champ rempli -> 422 (garde Processor, double du CHECK BDD). + // RG-4.08 (alignement M1/M2/M3) : sans prenom ni nom -> 422 (garde Processor, + // double du CHECK BDD chk_carrier_contact_name). $carrier = $this->seedCarrier('Contact Vide'); $client = $this->createAdminClient(); @@ -60,13 +61,13 @@ final class CarrierContactApiTest extends AbstractCarrierApiTestCase 'json' => [], ]); self::assertResponseStatusCodeSame(422); - // RG-4.08 : la violation est rattachee a `firstName` (mapping inline ERP-101). + // La violation est rattachee a `firstName` (mapping inline ERP-101). self::assertViolationOnPath($response, 'firstName'); } public function testSingleFieldContactIsCreated(): void { - // RG-4.08 : un seul champ suffit a valider le bloc. + // RG-4.08 : un nom (ou prenom) suffit a valider le bloc. $carrier = $this->seedCarrier('Contact Mono'); $client = $this->createAdminClient();