# M1 · Ticket 1/3 (Backend) — Supprimer le contact inline du `Client` ## 1. Objectif Retirer de l'entité `Client` (et de la table `client`) les **5 champs du contact principal inline** : `firstName`, `lastName`, `phonePrimary`, `phoneSecondary`, `email`. La gestion des contacts passe désormais **exclusivement** par la sous-entité `ClientContact` (onglet « Contacts »), déjà en place et déjà porteuse des mêmes champs. Le code M1 est **déjà livré en prod** : ce ticket inclut donc une **migration de données** (backfill) pour ne perdre aucune information de contact existante avant de supprimer les colonnes. Contexte et justification : voir `README.md` du dossier `refonte-contact`. ## 2. Périmètre ### IN - Migration Doctrine : **backfill puis suppression** des 5 colonnes de `client`. - `Client` (entité) : supprimer les 5 propriétés, getters/setters, annotations ORM / `Assert` / `Groups`. - `ClientProcessor` : retirer les 5 champs de `MAIN_FIELDS`, `changedBusinessFields()`, `normalize()` ; supprimer `validateMainContact()` (RG-1.01 — n'a plus d'objet). - `DoctrineClientRepository::applySearch()` : trancher D1 (recherche) et l'appliquer. - `ClientExportController` : trancher D2 (colonnes export) et l'appliquer. - `ClientFixtures` : retirer les 5 paramètres inline de `ensureClient()` ; garantir que chaque client seedé possède au moins 1 `ClientContact` (déjà géré par `addContact()`). - Tests PHPUnit : mettre à jour / retirer les cas qui exercent ces 5 champs sur `Client`. ### OUT - Toute modification de `ClientContact` / `ClientContactProcessor` : **inchangés** (c'est la cible, les champs y restent). `ClientFieldNormalizer` reste tel quel (toujours appelé par `ClientContactProcessor`). - Le front (formulaires, vues, types, i18n) → **ticket 2/3**. - Les specs (`spec-back.md`, `spec-front.md`, cahier de test) → **ticket 3/3**. ## 3. Fichiers à modifier | Fichier | Action | |---|---| | `src/Module/Commercial/Domain/Entity/Client.php` | Supprimer props `firstName` (~l.158), `lastName` (~l.163), `phonePrimary` (~l.168), `phoneSecondary` (~l.172), `email` (~l.178) + leurs getters/setters (~l.329-382) + groupes `client:read`/`client:write:main` + `Assert\*`. | | `src/Module/Commercial/Infrastructure/ApiPlatform/State/Processor/ClientProcessor.php` | Retirer les 5 clés de `MAIN_FIELDS` (~l.63) ; de `changedBusinessFields()` (~l.277-281) ; les 6 lignes de `normalize()` qui touchent email/phone/first/last/secondary (~l.433-441) ; supprimer `validateMainContact()` (~l.447-456) et son appel. | | `src/Module/Commercial/Infrastructure/Doctrine/DoctrineClientRepository.php` | `applySearch()` (~l.110-124) : appliquer **D1**. | | `src/Module/Commercial/Infrastructure/Controller/ClientExportController.php` | `buildHeaders()` (~l.94-114) + `buildRows()` (~l.121-143) : appliquer **D2**. | | `src/Module/Commercial/Infrastructure/DataFixtures/ClientFixtures.php` | `ensureClient()` (~l.357-395) : retirer firstName/lastName/phonePrimary/phoneSecondary/email ; conserver `addContact()`. | | `migrations/Version.php` (NOUVELLE) | Backfill + `DROP COLUMN` (cf. § 4). | | `tests/Module/Commercial/**` | Voir § 5. | ## 4. Migration Doctrine — backfill puis suppression > Migration **modulaire** (`src/Module/Commercial/Infrastructure/Doctrine/Migrations/`) : ce > n'est PAS une migration d'initialisation, le schéma `client` / `client_contact` existe > déjà (règle ABSOLUE n°11). ### `up()` 1. **Backfill — ne créer un contact que pour les clients qui n'en ont aucun**, afin de ne pas dupliquer le contact déjà recopié à la création (`prefillFirstContact`) : ```sql INSERT INTO client_contact (client_id, first_name, last_name, phone_primary, phone_secondary, email, position, created_at, updated_at) SELECT c.id, c.first_name, c.last_name, c.phone_primary, c.phone_secondary, c.email, 0, NOW(), NOW() FROM client c WHERE NOT EXISTS (SELECT 1 FROM client_contact cc WHERE cc.client_id = c.id) AND (c.first_name IS NOT NULL OR c.last_name IS NOT NULL); ``` > Le `WHERE ... first_name OU last_name IS NOT NULL` respecte le CHECK > `chk_client_contact_name`. Les rares clients sans nom de contact ET sans contact > existant ne reçoivent pas de ligne (cas théorique : `phone_primary`/`email` étaient > `NOT NULL` mais les noms nullables). 2. **Supprimer les 5 colonnes** : ```sql ALTER TABLE client DROP COLUMN first_name, DROP COLUMN last_name, DROP COLUMN phone_primary, DROP COLUMN phone_secondary, DROP COLUMN email; ``` > Pas de `COMMENT ON COLUMN` à poser (on supprime). Vérifier qu'aucun index ne portait > sur `email` (l'index unique `uq_client_email_active` a déjà été supprimé — décision Q4 / > RG-1.17, cf. `ClientMigrationTest`). ### `down()` (best-effort) 1. Recréer les 5 colonnes (`phone_primary`/`email` en `NOT NULL` impose un défaut transitoire ou un re-remplissage depuis le contact `position = 0`). 2. Re-remplir depuis `client_contact` (`position = 0`) si possible. 3. Reposer les `COMMENT ON COLUMN` d'origine (textes RG-1.19/1.20/1.21/1.01/1.17 — cf. `migrations/Version20260601000000.php` l.251-255). > `down()` ne peut pas restaurer parfaitement les données (ambiguïté si plusieurs contacts). > Documenter cette limite dans le docblock de la migration. ## 5. Tests à mettre à jour | Fichier | Action | |---|---| | `tests/Module/Commercial/Api/ClientApiTest.php` | Retirer firstName/lastName/phone/email des payloads POST/PATCH `client` et des assertions JSON. | | `tests/.../ClientFormulaireMainTest.php` | Supprimer les tests RG-1.01 (firstName/lastName) et RG-1.02 (téléphones) **côté Client** — ils basculent côté `ClientContact` (couverts ailleurs). | | `tests/.../ClientExportControllerTest.php` | Aligner les en-têtes/lignes attendus sur **D2**. | | `tests/.../ClientMigrationTest.php` | Asserter que les 5 colonnes **n'existent plus** sur `client` ; vérifier le backfill (un client sans contact obtient bien 1 `client_contact`). | | `tests/.../ClientFieldNormalizerTest.php` | Conserver les tests du normalizer (toujours utilisé par `ClientContact`) ; retirer les cas spécifiques aux champs `Client` s'il y en a. | | RG-1.01/1.02 (matrice) | Ne plus tester sur `Client` ; vérifier qu'ils restent couverts sur `ClientContact` (RG-1.05). | ## 6. Décisions à trancher (cf. README § 3) - **D1 — recherche** : recommandé = `LEFT JOIN client_contact` (fuzzy sur `companyName` + contact `first_name`/`last_name`/`email`). Attention au `DISTINCT` / risque de doublons de lignes si plusieurs contacts matchent (grouper par `client.id`). - **D2 — export** : recommandé = alimenter les colonnes contact depuis le contact de plus petit `position` (fetch-join `contacts` pour éviter le N+1). ## 7. Critères d'acceptation (DoD) - [ ] Les colonnes `first_name`, `last_name`, `phone_primary`, `phone_secondary`, `email` n'existent plus sur la table `client`. - [ ] La migration est jouable sur une base seedée sans perte de contact (backfill vérifié) et `down()` documenté comme best-effort. - [ ] `Client`, `ClientProcessor`, `DoctrineClientRepository`, `ClientExportController`, `ClientFixtures` ne référencent plus les 5 champs. - [ ] D1 et D2 implémentées conformément à la décision validée. - [ ] `ClientContact` / `ClientContactProcessor` / `ClientFieldNormalizer` inchangés. - [ ] `make test` vert (notamment `tests/Architecture/ColumnsHaveSqlCommentTest` et `EntitiesAreTimestampableBlamableTest`). - [ ] `make php-cs-fixer-allow-risky` ne signale rien sur les fichiers touchés. - [ ] Aucune régression du contrat de sérialisation : capturer le JSON réel de `GET /api/clients/{id}` et vérifier l'absence des 5 champs (réflexe RETEX M1).