Files
Starseed/docs/migration/mixgraine-migration-analysis.md
T
Matthieu ca79b8f8e6
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Failing after 34s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m20s
chore(migration) : outils d'extraction des tiers Mixgraine (WIP)
Boite a outils de migration des tiers (clients / fournisseurs / prestataires)
depuis l'ancien CRM Mixgraine vers Starseed :

- extract_mixgraine.py : extraction + normalisation via l'API Mixgraine (cache
  disque reprenable, debit ~1 req/s, backoff 429/5xx) -> JSON format Starseed
- build_tiers_xlsx.py  : Excel de relecture (1 onglet par type + Synthese,
  colonne 'Site manquant' filtrable)
- run.sh               : enchaine extraction + Excel
- README.md            : prerequis, recuperation du token, lancement
- mixgraine-migration-analysis.md : analyse + mapping des champs Mixgraine -> Starseed

WIP : les commandes d'import Symfony cote Starseed (seed referentiels/sites,
import Client/Supplier/Provider, 2e passe distributeur/courtier) restent a faire.

Le dossier de sortie mixgraine-export/ (IBAN/BIC + PII reelles) est volontairement
.gitignore : reproductible localement via MIXGRAINE_JWT.
2026-06-17 08:38:23 +02:00

22 KiB
Raw Blame History

Migration Mixgraine → Starseed — Analyse des tiers (clients / fournisseurs)

Sources analysées (exports Mixgraine triés par groupe) :

  • client.json.json1023 clients (customer=true)
  • fournisseur.json306 fournisseurs (supplier=true)
  • fournisseur-client.json106 tiers mixtes (customer=true ET supplier=true)

Cible : module Commercial de Starseed — entités Client / Supplier et leurs sous-entités *Contact, *Address, *Rib. Date d'analyse : 2026-06-16. (Remplace l'analyse initiale faite sur customer.json, 737 tiers, où la classification était absente.)

🔑 Source de migration recommandée : l'endpoint unitaire, pas l'export en masse. Les exports CSV/JSON en masse sont incomplets : ni RIB/IBAN, ni N° TVA, ni banque de virement, ni rattachement aux sites. Toutes ces données existent dans l'endpoint détail GET /api/customer/{id} et surtout dans PUT /api/customer/{id} avec {"__data":true} (renvoie le schéma de formulaire complet + les valeurs). Procédure : paginer GET /api/customer/?...&limit=...&page=N pour récupérer les id, puis appeler l'endpoint détail pour chaque id. Cf. § 9.


1. Synthèse

L'export est désormais trié par groupe : la distinction client / fournisseur est nette, ce qui lève le principal point bloquant de l'analyse précédente. 1435 tiers distincts (aucun recoupement d'id entre les trois fichiers).

Groupe source Tiers Devient dans Starseed
client.json.json (customer=true) 1023 Client
fournisseur.json (supplier=true) 306 Supplier
fournisseur-client.json (customer=true + supplier=true) 106 Client ET Supplier (cf. § 4)
prestataires (prestataire=true, vue séparée Mixgraine) à extraire via l'API (filtre prestataire:true) Provider (module Technique, M3)

Les prestataires sont un 4e type de tiers, géré dans une vue dédiée de Mixgraine mais sur le même endpoint /api/customer/ (filtre {"prestataire":true}). Côté Starseed, ils ont une entité dédiée : Provider (module Technique, src/Module/Technique/Domain/Entity/Provider.php), jumelle du Supplier, avec ProviderContact / ProviderAddress / ProviderRib. Ils ne deviennent donc PAS des Supplier. Particularités vs Supplier : pas d'onglet Information (entité minimale nom + comptabilité) et les sites sont rattachés directement au prestataire (M2M provider_site, min 1, RG-3.03) — le script agrège donc les sites des adresses au niveau du prestataire. Catégories de type PRESTATAIRE exigées (RG-3.09). Le script collecte les prestataires via leur liste dédiée et produit providers.json (§ 9).

Volumétrie migrable (tous groupes confondus) :

Objet source Quantité Cible Starseed
Tiers racine 1435 Client (1129) + Supplier (412) = 1541 entités créées (les 106 mixtes comptent double)
Adresses 2282 ClientAddress / SupplierAddress
Contacts 2704 ClientContact / SupplierContact
RIB 0 rien à migrer (ni IBAN ni BIC dans l'export)

Détail par groupe :

Groupe Tiers Adresses Contacts Avec tél Avec catégorie Avec type paiement
Clients 1023 1125 1279 1023 (100 %) 648 (63 %) 599
Fournisseurs 306 585 758 306 (100 %) 92 (30 %) 283
Mixtes 106 572 667 106 (100 %) 37 (35 %) 95

Points de décision restants (détaillés plus bas) :

  1. Les 106 tiers mixtes doivent être dédoublés en un Client + un Supplier (le modèle Starveed sépare les deux entités). § 4.
  2. Référentiels à compléter : paymentDelay (« 20 jours » absent du seed), tvaMode (Intracom/Export/achats), mapping des ~100 catégories. § 5.
  3. Site obligatoire par adresse (min 1) — notion toujours absente de l'export. § 6.

2. Structure de l'export

Chaque ligne = un tiers. Les champs addresses.* et contacts.* sont des listes parallèles (un tiers peut avoir plusieurs adresses / contacts).

Qualité des données (tous groupes) :

  • Téléphone racine : rempli sur 100 % des tiers (1435/1435).
  • Emails contacts : 1257 présents, 1256 valides (1 invalide).
  • Codes postaux : 2282 présents, 10 invalides à nettoyer : . (×3), B.5300, ?, 684, 854300, etc. (sinon 422 sur la regex 4-5 chiffres RG-1.09/2.05).
  • Catégories : 55 % des tiers en ont au moins une (45 % sans) ; 92 % des adresses ont une catégorie (2104/2282).
  • Noms de contact : format « CIVILITÉ NOM PRÉNOM » (M , Mme, M.) → à parser pour lastName / firstName.

Pays des adresses : FR (2266), ES (11), BE (2), NL (2), PT (1).


3. Mapping champ par champ

Légende : champ existant · ⚠️ migrable avec transformation · pas de cible (donnée perdue) · 🔒 contrainte de validation à gérer.

3.1 Niveau Tiers → Client / Supplier

Champ Mixgraine Cible Starseed Statut Note de transformation
name / reference companyName (≤180) Vérifier l'unicité (index partiel LOWER(company_name)). Risque : un même nom présent en client ET fournisseur reste OK (tables distinctes) ; doublons intra-table à contrôler.
phone (tél de l'objet de base) *Contact.phonePrimary ⚠️ Migré dans la liste de contacts (cf. § 3.3bis), jamais au niveau racine. Rempli à 100 %.
categoriesStr / category.name categories (M2M Category) ⚠️🔒 Min 1 obligatoire. ~100 valeurs distinctes à mapper vers le référentiel Category (§ 5.1). Attention Courtier/Distributeur (cf. broker/distributor).
liability.name tvaMode ⚠️ France (ventes)FRANCE_VENTES (1426), Intracom (ventes)INTRACOM_VENTES (6), Export (ventes)EXPORT_VENTES (2), France (achats)pas de mode « achats » au seed (1 cas).
paymentDelay.label paymentDelay ⚠️🔒 15 joursJ15 (1376), 30 joursJ30 (43), A réceptionA_RECEPTION (1), 20 jours (15)→ code J20 à créer (absent du seed).
paymentType.label paymentType ⚠️🔒 LCR non soumiseà trancher (LCR impose ≥1 RIB, or 0 RIB → préférer NON_SOUMISE), VirementVIREMENT (95, exige bank absente de la source), ChèqueCHEQUE (28).
billingAccount accountNumber (≤40) Peu rempli.
distributor.name distributor (FK auto-réf. Client) ⚠️ Suppose que le distributeur existe comme Client. Pas d'équivalent côté Supplier.
courtier.id / courtier.name broker (FK auto-réf.) ⚠️ À vérifier sur les nouveaux exports (vide dans l'ancien). Mutuellement exclusif avec distributor (RG-1.03).
customer / supplier choix de l'entité cible (Client vs Supplier) ⚠️ Sert au routage, pas stocké tel quel.
tiersIndexable, prestataire, coreLocked, isOgm, maxOwed, autoGenerateInvoice Flags Mixgraine sans équivalent métier.
organization1..3 / organizationsStr Notion d'organisation/agence absente du modèle.
articlesSold.* Relève du catalogue, hors périmètre Client/Supplier.
siren, nTva, bank siren, nTva, bank Non présents dans l'export → resteront vides.

3.2 Niveau Adresse → ClientAddress / SupplierAddress

Champ Mixgraine Cible Starseed Statut Note
addresses.street1 street (≤255) Obligatoire.
addresses.street2 streetComplement (≤255)
addresses.postcode postalCode (≤20, regex 4-5 chiffres) ⚠️ 10 valeurs à nettoyer.
addresses.city city (≤120) Obligatoire.
addresses.country country (≤80, défaut « France ») ⚠️ Convertir code ISO → libellé (FR→France, ES→Espagne, BE→Belgique, NL→Pays-Bas, PT→Portugal).
addresses.lat / addresses.lng Coordonnées GPS perdues (pas de champ géo).
addresses.billing (true) isBilling (Client) ⚠️🔒 Si true, billingEmail obligatoire (RG-1.11), absent de la source → soit ne pas marquer facturation, soit collecter l'email.
addresses.sales isDelivery (Client) ⚠️
addresses.purchase type adresse (Supplier : DEPART/RENDU) ⚠️ Côté fournisseur, l'adresse a un enum PROSPECT/DEPART/RENDU.
addresses.salesTrip isProspect (?) ⚠️ Sémantique « tournée commerciale » à confirmer.
addresses.benneCount bennes (SupplierAddress) ⚠️ Souvent vide.
addresses.carrierTypeStr, addresses.name, addresses.distances Sans cible.
addresses.categories.name categories (M2M adresse) ⚠️🔒 92 % des adresses en ont une. Min 1 obligatoire côté ClientAddress. ⚠️ Codes DISTRIBUTEUR/COURTIER interdits au niveau adresse (RG-1.29).
— (aucun) sites (M2M, min 1) 🔒 Notion absente de l'export — bloquant majeur (§ 6).

3.3 Niveau Contact → ClientContact / SupplierContact

Champ Mixgraine Cible Starseed Statut Note
contacts.name lastName / firstName (≤120) ⚠️ Parser « CIVILITÉ NOM PRÉNOM » : retirer civilité, 1er token = lastName, reste = firstName. Au moins l'un des deux requis (RG-1.05/2.04).
contacts.function jobTitle (≤120)
contacts.email email (≤180) 99,9 % valides.
contacts.phone phonePrimary (≤20)
contacts.mobile phoneSecondary (≤20)
contacts.fax Pas de champ fax dans les contacts Starseed.
contacts.siege Notion « contact siège » non modélisée.
addresses.contacts.* *Address.contacts (M2M) ⚠️ Rattachement contact↔adresse via les ids addresses.contacts.id.

3.3bis — Règle : le contact porté par l'objet de base va dans la liste de contacts

Starseed n'a aucun champ contact au niveau racine de Client/Supplier. Tout doit atterrir dans la collection *Contact :

  • Le bloc contacts.* (nom, fonction, email, tél, mobile) → une entrée *Contact par contact.
  • Le phone de l'objet de base (rempli à 100 %) → reporté sur un contact :
    • si le numéro figure déjà sur un contact → ne rien créer (déduplication) ;
    • s'il diffère → l'ajouter comme phoneSecondary d'un contact existant ;
    • si le tiers n'a aucun contactcréer un contact « Standard » (lastName = "Standard" pour respecter RG-1.05/2.04) portant ce phonePrimary.

Conséquence : aucun tiers ne perd son téléphone, et rien n'est stocké au niveau racine (conforme au modèle Starseed).

3.4 Niveau RIB → ClientRib / SupplierRib

Disponible via l'endpoint unitaire (champ banks[]), absent de l'export en masse. Mapping direct :

Champ Mixgraine (banks[]) Cible Starseed (*Rib) Statut
banks[].label label (≤120)
banks[].iban iban (≤34) (validé Assert\Iban)
banks[].bic bic (≤20) (validé Assert\Bic)

⚠️ Toujours mapper paymentType="LCR non soumise" vers NON_SOUMISE (et pas LCR) : les tiers sans banks[] violeraient sinon RG-1.13/2.08 (min 1 RIB).

3.5 Champs supplémentaires de l'endpoint unitaire (absents de l'export en masse)

Champ Mixgraine (détail / __data) Cible Starseed Statut Note
vatNumber nTva (≤40) N° TVA.
accountingBank (1=CIC, 2=SOCIETE GENERALE, 3=CREDIT AGRICOLE) bank (FK) Correspond au seed Starseed (CIC/SG/CA). Banque de virement.
courtier (FK select, 18 valeurs) broker (FK auto-réf.) Courtier réel (≠ catégorie « Courtier »). Exclusif avec distributor (RG-1.03).
distributor (FK select, 14 valeurs) distributor (FK auto-réf.) Distributeur réel.
mails[] {mail, invoice} ClientAddress.billingEmail / secondaire ⚠️ Emails d'envoi de documents ; invoice=true → email de facturation (RG-1.11).
address carrierType (1=Rendu, 2=Départ) SupplierAddress.addressType (RENDU/DEPART) ⚠️ Radio exclusif côté fournisseur (RG-2.09).
address organization_1/2/3 *Address.sites (M2M) Les sites (cf. § 6).
address hasBenne / benneCount SupplierAddress.bennes ⚠️ Nombre de bennes.
address validated « Adresse vérifiée », pas de cible.
isDistributor, directBilling, distributorDirectOrder, newGuarantees, commentMail, zoneChartering(2), emailDocument* Champs Mixgraine sans équivalent métier.

4. Tiers mixtes (client + fournisseur) — 106 cas

Les 106 tiers du fichier fournisseur-client.json sont à la fois client et fournisseur. Le modèle Starseed sépare strictement Client et Supplier (deux tables, deux jeux de sous-entités). Deux options :

  • (Recommandé) Créer deux entités par tiers mixte : un Client et un Supplier, chacun avec sa copie des adresses/contacts. C'est cohérent avec le modèle, au prix d'une duplication (à reconcilier plus tard si un lien inter-entités est ajouté).
  • Choisir une seule face (client OU fournisseur) selon l'usage dominant — risque de perte d'information.

Impact volumétrie : 1023 + 106 = 1129 Client, 306 + 106 = 412 Supplier.


5. Référentiels à préparer avant import

5.1 Catégories (Category)

Situation nettement meilleure que l'export initial : 55 % des tiers et 92 % des adresses portent une catégorie. L'export contient ~100 valeurs distinctes (top : Eleveur 392, Semencier 61, Coopérative 53, Courtier 38, Négociant 30, …, puis une longue traîne de catégories « fournisseur » métier : Transports, Informatique, Garagistes, Banques…).

À faire :

  • Construire le référentiel Category depuis ces valeurs (dédoublonner casse/accents, ex. Courtier/COURTIERS).
  • Politique pour les 45 % sans catégorie : attribuer une catégorie par défaut (ex. À QUALIFIER) pour satisfaire la contrainte min 1.
  • Niveau adresse : exclure DISTRIBUTEUR/COURTIER (interdits RG-1.29) — les router vers les FK distributor/broker ou la catégorie tiers.
  • Distinguer catégories type CLIENT vs type FOURNISSEUR (RG-2.10 : une catégorie fournisseur est exigée sur un Supplier).

5.2 Référentiels comptables

Référentiel Valeurs export Action
paymentDelay 15 jours, 30 jours, 20 jours, A réception Le seed n'a que J15/J30/A_RECEPTIONcréer J20.
paymentType LCR non soumise, Virement, Chèque Mapper LCR non soumiseNON_SOUMISE (éviter la contrainte RIB) ; VirementVIREMENT (mais bank absente → soit la laisser vide en assouplissant RG-1.12, soit collecter).
tvaMode France (ventes), Intracom (ventes), Export (ventes), France (achats) Mapper sur FRANCE_VENTES/INTRACOM_VENTES/EXPORT_VENTES ; pas de mode « achats » au seed (1 cas) → décision.

6. Sites — résolu via les « organisations » Mixgraine

Chaque ClientAddress/SupplierAddress doit référencer au moins un Site (RG-1.10/2.06). Cette notion existe dans Mixgraine sous le nom d'organisations, visible dans l'endpoint unitaire :

  • au niveau tiers : organizationsStr (ex. "Châtellerault, Saint-Jean, Pommevic") ;
  • au niveau adresse : trois booléens organization_1 / organization_2 / organization_3 étiquetés Châtellerault / Saint-Jean / Pommevic ;
  • le bloc addresses.distances donne même la distance de l'adresse vers chacun de ces 3 sites.

Les 3 sites à créer dans Starseed : Châtellerault, Saint-Jean, Pommevic. Le rattachement adresse↔site se reconstruit depuis les booléens organization_n=true. Pour une adresse sans aucune organisation cochée, prévoir un site par défaut (à décider).

⚠️ Ce rattachement n'est lisible que via l'endpoint unitaire — raison de plus pour migrer depuis le détail (cf. § 9).


7. Synthèse des pertes de données

Perdus si non traités en amont : coordonnées GPS (lat/lng), fax des contacts, contact siège, articles vendus, « adresse vérifiée », et divers flags Mixgraine (isOgm, maxOwed, directBilling, zoneChartering, commentMail).

Champs cibles qui resteront vides (vraiment absents de la source, même au détail) : siren (le vatNumber alimente nTva, mais pas de SIREN distinct).

Note : nTva, bank et les *Rib ne sont plus des pertes — ils sont disponibles via l'endpoint unitaire (§ 3.4 / § 3.5). Ils n'étaient absents que de l'export en masse.


7bis. Excel de relecture (un onglet par type)

Beaucoup d'adresses n'ont aucun site rattaché (les organisations ne sont pas cochées dans Mixgraine), et d'autres trous existent (CP invalide, facturation sans email…). Avant tout import, on produit un classeur via build_tiers_xlsx.py : mixgraine-tiers.xlsx.

  • Un onglet Synthèse (compteurs) + un onglet par type : Clients, Fournisseurs, Prestataires.
  • Chaque onglet liste toutes les données, une ligne par adresse (colonnes du tiers répétées).
  • Colonne Site manquant (OUI/vide) + filtre automatique → un clic pour isoler les adresses sans site. Colonne Problèmes pour le reste. Lignes à problème surlignées.

Chiffres réels (extraction 2026-06-16, 1442 tiers) :

Type Tiers Lignes (adresses) Adresses sans site Sans catégorie
Clients 1129 1697 1111 444 (39 %)
Fournisseurs 412 1157 612 283 (68 %)
Prestataires 300 333 0 8 (2 %)
Total 1841* 3187 1723 735

* mixtes comptés en Client + Fournisseur. RIB extraits : 90. Tiers totalement bloqués : 90.

Motifs de blocage détectés :

Niveau Motif bloquant
Adresse aucun site rattaché (RG-1.10/2.06/3.03), code postal absent/invalide, ville absente, rue absente, facturation sans email (client)
Tiers nom absent, VIREMENT sans banque, LCR sans RIB, prestataire sans aucun site (RG-3.03), catégorie À QUALIFIER

Filtrer « Site manquant = OUI » dans chaque onglet permet de corriger la donnée à la source (re-cocher les organisations dans Mixgraine, compléter les emails) avant de relancer l'import (le cache accélère).


8. Séquence d'import recommandée

  1. Extraire la donnée complète depuis l'endpoint unitaire (§ 9) — pas depuis l'export en masse.
  2. Relecture : build_tiers_xlsx.pymixgraine-tiers.xlsx (un onglet par type, filtre « Site manquant »), faire corriger à la source (§ 7bis).
  3. Sites : créer les 3 sites (Châtellerault, Saint-Jean, Pommevic) + un site par défaut éventuel (§ 6).
  4. Référentiels : créer/compléter Category (~100 valeurs + défaut À QUALIFIER), paymentDelay (ajouter J20), tvaMode, vérifier paymentType, bank (CIC/SG/CA déjà OK) (§ 5).
  5. Nettoyer : 10 codes postaux invalides, parser les noms de contacts (civilité), convertir codes pays ISO → libellés (table fournie par le schéma country).
  6. Importer dans l'ordre : référentiels + sites → tiers → adresses (+ rattachement site via organization_n) → contacts (+ rattachement adresse + tél de l'objet de base) → RIB (banks[]).
  7. Tiers mixtes (106) : créer Client + Supplier (§ 4).
  8. Gérer les 422 attendus : adresses isBilling=true sans billingEmail ; paymentType=VIREMENT sans bank.

9. Stratégie d'extraction via l'API Mixgraine

L'export en masse est insuffisant (§ entête). La donnée complète s'obtient en deux temps depuis https://liot.mixsuite.fr (auth : Authorization: Bearer <JWT>) :

  1. Lister les ids par groupe, page par page : GET /api/customer/?fields=["name"]&filters={...}&limit=200&order=name&page=N La réponse porte count (total) et data[].id. On pagine trois listes filtrées puis on fait l'union dédupliquée :

    • {"customer":true} → clients (Client),
    • {"supplier":true} → fournisseurs (Supplier),
    • {"prestataire":true} → prestataires (Provider, module Technique).

    La classification se fait par appartenance à ces listes (plus fiable que les flags du formulaire __data) : un id dans customer ET supplier = mixte (Client + Supplier) ; un id dans prestataire = Provider. Le script produit clients.json, suppliers.json, providers.json + referentials.json.

  2. Récupérer le détail complet de chaque tiers : GET /api/customer/{id} (objet riche : contacts, addresses, banks, liability, paymentType…) ou PUT /api/customer/{id} avec corps {"__data":true} qui renvoie en plus le schéma de formulaire (libellés des organisations/sites, choix des référentiels, FK distributor/courtier) et les valeurs sous __data.

Champs clés présents uniquement au détail : banks[] (RIB), vatNumber, accountingBank, courtier, distributor, mails[], addresses[].organization_1/2/3 (sites), addresses[].carrierType, details.geo (lat/lng par adresse).

⚠️ Le JWT fourni est un secret de session : ne pas le committer dans le repo. À passer en variable d'environnement au script d'extraction.


📦 Tickets Lesstime générés

TaskGroup Lesstime : #32 — Migration tiers Mixgraine → Starseed (projet STARSEED / ERP)

# Ticket Réf. Effort Tag
1.1 Extraire et normaliser les tiers Mixgraine en JSON ERP-174 M Backend
1.2 Seeder les référentiels et les 3 sites ERP-175 M Backend
1.3 Importer les clients (Commercial) + adresses/contacts/RIB ERP-176 L Backend
1.4 Importer les fournisseurs (Commercial) + adresses/contacts/RIB ERP-177 M Backend
1.5 Importer les prestataires (Technique/Provider) ERP-178 M Backend
1.6 Relier distributeurs/courtiers, traiter les mixtes et vérifier ERP-179 M Backend

Tous au statut Prêt à dev. Prochaine action : lancer le 1.1 (le script extract_mixgraine.py est déjà écrit, reste à le tester avec un vrai token).


Document basé sur l'analyse des exports en masse client.json.json / fournisseur.json / fournisseur-client.json et sur les réponses des endpoints /api/customer/ (liste), /api/customer/{id} (détail) et PUT /api/customer/{id} (__data). Aucune donnée n'a été importée — rapport d'analyse préalable.