Files
Starseed/migrations/Version20260603120000.php
T
matthieu 96ddd15c86
Auto Tag Develop / tag (push) Successful in 9s
refactor(commercial) : suppression du contact principal inline du Client (M1 · back 1/3) (#55)
## Contexte

M1 · Ticket 1/3 (Backend) de la refonte contact. Le contact principal inline du `Client` (firstName, lastName, phonePrimary, phoneSecondary, email) faisait doublon avec la sous-entité `ClientContact` (onglet Contact). Il est supprimé : les contacts vivent désormais **uniquement** dans `client_contact`.

RG-1.01 (firstName OU lastName sur Client) et RG-1.02 (max 2 téléphones sur Client) sont **supprimées** du Client — leur équivalent vit déjà sur `ClientContact` (RG-1.05 / RG-1.14).

## Changements

- **Migration** `Version20260603120000` (namespace racine `DoctrineMigrations` — tri par timestamp, cf. AlphabeticalComparator) : **backfill** des clients sans contact vers `client_contact` (position 0) **avant** le `DROP` des 5 colonnes. `down()` best-effort documenté.
- **Entité `Client`** : retrait des 5 propriétés + getters/setters + groupes de sérialisation.
- **`ClientProcessor`** : `MAIN_FIELDS` / `changedBusinessFields()` / `normalize()` allégés ; `validateMainContact()` (RG-1.01) supprimée.
- **Recherche répertoire (D1)** : sur `companyName` seul (les anciens critères lastName/email vivaient sur les colonnes supprimées).
- **Export XLSX (D2)** : colonnes de contact retirées (Nom entreprise / Catégories / Sites / [SIREN] / Date).
- **Fixtures** + **catalogue de commentaires SQL** (`ColumnCommentsCatalog`) alignés.
- **Tests** fonctionnels et unitaires mis à jour.

## Décisions actées

- **Migration** au namespace racine (et non modulaire Commercial) : une migration `App\Module\Commercial\…` trierait avant le `CREATE TABLE client` sur base fraîche → casse. Conforme à la règle ABSOLUE n°11.
- **D1** = recherche `companyName` seul. **D2** = retrait des colonnes contact de l'export.

## Vérifications

-  `make db-reset && make migration-migrate` : migration rejouable sur base fraîche (backfill no-op si contacts déjà présents).
-  `make test` : 466 tests verts.
-  `make php-cs-fixer-allow-risky` : clean.
-  Contrat réel `GET /api/clients/{id}` : les 5 champs ont disparu de la racine, `contacts[]` porte l'info.

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #55
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-03 13:52:25 +00:00

132 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* M1 — Suppression du contact principal inline du `Client` (refonte contact).
*
* Modele AVANT : le `Client` portait 5 colonnes de contact principal
* (first_name, last_name, phone_primary, phone_secondary, email) en doublon
* conceptuel de la sous-entite `ClientContact` (onglet Contact).
*
* Modele APRES (decision produit, README refonte-contact) : les contacts vivent
* UNIQUEMENT dans `client_contact`. Les 5 colonnes inline disparaissent du
* `client`. RG-1.01 (firstName OU lastName sur Client) et RG-1.02 (max 2
* telephones sur Client) sont supprimees : leur equivalent vit deja sur
* `client_contact` (RG-1.05 / RG-1.14).
*
* Le code etant deja en prod, la suppression est precedee d'un BACKFILL : pour
* tout client n'ayant encore AUCUN contact, on materialise son contact principal
* inline en une ligne `client_contact` (position 0) avant le DROP, afin de ne
* perdre aucune donnee.
*
* Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire
* Commercial : avec plusieurs migrations_paths, Doctrine Migrations 3.x trie par
* FQCN alphabetique (AlphabeticalComparator). Une migration
* `App\Module\Commercial\...` trierait AVANT toutes les `DoctrineMigrations\...`
* sur base vide -> ce DROP s'executerait avant le CREATE TABLE client
* (Version20260601000000). Le namespace racine garantit l'ordre par timestamp.
*/
final class Version20260603120000 extends AbstractMigration
{
/** Colonnes de contact inline supprimees du `client`. */
private const array INLINE_CONTACT_COLUMNS = [
'first_name', 'last_name', 'phone_primary', 'phone_secondary', 'email',
];
public function getDescription(): string
{
return 'M1 : suppression du contact inline du Client (backfill vers client_contact puis DROP des 5 colonnes).';
}
public function up(Schema $schema): void
{
// 1. Backfill : tout client SANS contact recoit une ligne client_contact
// (position 0) reprenant ses champs inline. phone_primary / email du
// client sont NOT NULL -> toujours une donnee a reporter. Le CHECK
// chk_client_contact_name (first_name OU last_name) est garanti par le
// fallback company_name si jamais les deux noms etaient null (cas qui ne
// devrait pas exister, RG-1.01 ayant ete appliquee a l'ecriture).
// created_at/updated_at NOT NULL -> NOW() ; created_by/updated_by null
// (backfill hors contexte HTTP, libelle « Systeme » cote front).
$this->addSql(<<<'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,
CASE
WHEN c.first_name IS NULL AND c.last_name IS NULL THEN c.company_name
ELSE c.last_name
END,
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
)
SQL);
// 2. DROP des 5 colonnes inline (rien a documenter : suppression).
$this->addSql(<<<'SQL'
ALTER TABLE client
DROP COLUMN first_name,
DROP COLUMN last_name,
DROP COLUMN phone_primary,
DROP COLUMN phone_secondary,
DROP COLUMN email
SQL);
}
public function down(Schema $schema): void
{
// Best-effort : on RECREE les 5 colonnes (en NULLABLE — l'etat NOT NULL
// d'origine de phone_primary/email ne peut etre restaure sur une table
// peuplee sans risque) et on retro-alimente depuis le contact principal
// (position minimale) de chaque client. La donnee n'est pas garantie
// identique a l'origine : ce down() sert au rollback technique, pas a une
// restauration fidele.
$this->addSql('ALTER TABLE client ADD COLUMN first_name VARCHAR(120) DEFAULT NULL');
$this->addSql('ALTER TABLE client ADD COLUMN last_name VARCHAR(120) DEFAULT NULL');
$this->addSql('ALTER TABLE client ADD COLUMN phone_primary VARCHAR(20) DEFAULT NULL');
$this->addSql('ALTER TABLE client ADD COLUMN phone_secondary VARCHAR(20) DEFAULT NULL');
$this->addSql('ALTER TABLE client ADD COLUMN email VARCHAR(180) DEFAULT NULL');
// Retro-alimentation depuis le contact de position la plus basse.
$this->addSql(<<<'SQL'
UPDATE client c SET
first_name = cc.first_name,
last_name = cc.last_name,
phone_primary = cc.phone_primary,
phone_secondary = cc.phone_secondary,
email = cc.email
FROM (
SELECT DISTINCT ON (client_id)
client_id, first_name, last_name, phone_primary, phone_secondary, email
FROM client_contact
ORDER BY client_id, position ASC, id ASC
) cc
WHERE cc.client_id = c.id
SQL);
// Re-pose des commentaires d'origine (regle ABSOLUE n°12) — dollar-quoting
// Postgres pour eviter tout echappement d apostrophe.
$this->addSql('COMMENT ON COLUMN client.first_name IS $_$Prenom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).$_$');
$this->addSql('COMMENT ON COLUMN client.last_name IS $_$Nom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).$_$');
$this->addSql('COMMENT ON COLUMN client.phone_primary IS $_$Telephone principal — stocke en chiffres uniquement (RG-1.20). Obligatoire.$_$');
$this->addSql('COMMENT ON COLUMN client.phone_secondary IS $_$Telephone secondaire optionnel — chiffres uniquement (RG-1.20).$_$');
$this->addSql('COMMENT ON COLUMN client.email IS $_$Email principal (lowercase serveur, RG-1.21). NON unique (RG-1.17 supprimee, Q4).$_$');
}
}