# 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<timestamp>.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).
