# Import des commandes depuis VitiCore `master`

La commande `viti-core:orders:import-master` migre les checkouts de l'ancien VitiCore `master` sous MySQL/MariaDB vers la base PostgreSQL du VitiCore courant.

Il s'agit d'un import ETL et non d'une copie de tables : les prix, remises, frais de port et informations produit sont figés dans les snapshots attendus par VitiCore actuel.

## Protections

- la base cible est toujours la connexion Doctrine courante ;
- la connexion source est construite à partir des options de la commande, sans DSN ni variable d'environnement ;
- les mots de passe ne sont jamais affichés et peuvent être demandés dans une saisie interactive masquée ;
- un tunnel SSH facultatif est ouvert uniquement pendant l'exécution puis fermé avec la connexion MySQL ;
- la source est lue dans une transaction `REPEATABLE READ` explicitement en lecture seule ;
- sans `--apply`, la commande effectue seulement le préflight ;
- toute l'écriture est réalisée dans une seule transaction DBAL ;
- aucun événement Doctrine, email, paiement, facture ou webhook n'est déclenché ;
- les snapshots de prix, remises et livraison restent figés, y compris pour les anciens checkouts non payés ;
- une source appliquée une première fois est verrouillée ; une reprise exige `--force` ;
- `--force` ignore les commandes déjà importées avec le même checksum et ne crée aucun doublon ;
- une reprise `--force` doit conserver le même `--scope` que le premier import ;
- les UUID cibles dépendent des UUID métier de `master`, pas de l'alias `--source-id` : changer cet alias ne peut donc pas dupliquer les mêmes paniers ;
- une commande source modifiée depuis son import n'est jamais écrasée automatiquement ;
- la présence d'anciennes cartes cadeau liées au périmètre bloque l'import, car le VitiCore cible ne peut pas restaurer leurs jetons et leur cycle de vie ;
- un impact sur le stock courant bloque l'écriture, sauf acceptation explicite avec `--allow-stock-impact` ;
- la migration refuse de supprimer la traçabilité dès que des commandes ont été importées ; l'annulation d'un import passe alors par la restauration d'une sauvegarde cohérente.

Seule la migration Doctrine `Version20260826200000` doit être appliquée avant le préflight. Elle crée les tables de suivi `cart_legacy_order_import_run` et `cart_legacy_order_import_map`, sans modifier les tables métier existantes. L'import n'élargit aucune colonne d'adresse et n'ajoute aucune colonne aux lignes de panier. L'équivalence reste celle du type de la variante cible, selon le fonctionnement habituel de VitiCore ; elle n'est pas dupliquée dans les données importées.

Le runtime PHP qui exécute la commande doit disposer de `pdo_mysql` pour la source et de `pdo_pgsql` pour la cible.

## Correspondances

Les UUID de variantes ne sont pas supposés identiques entre `master` et le VitiCore courant. Le fichier `config/legacy-order-map.json` est donc créé automatiquement au premier lancement de la commande. Cette première exécution s'arrête toujours après la génération du fichier, y compris si `--apply` a été fourni.

Le fichier contient l'inventaire des variantes réellement utilisées par les commandes sources, leurs prix unitaires historiques, puis le catalogue des variantes cibles avec leurs UUID, SKU, libellés et prix public actuel. Les libellés et les prix servent uniquement à la relecture humaine : une cuvée renommée, un nom ressemblant ou un prix identique ne provoque jamais de rapprochement automatique.

Extrait d'un fichier généré :

```json
{
  "schema_version": 1,
  "source": {
    "source_id": "champagne-bandock-master",
    "source_key": "empreinte-stable-generee",
    "scope": "all"
  },
  "variants": {
    "uuid-variante-source-les-lys": "__A_COMPLETER__"
  },
  "payment_methods": {
    "e-transactions": "uuid-moyen-paiement-cible"
  },
  "delivery_methods": {
    "uuid-livraison-source": "__A_COMPLETER__"
  },
  "_source_variants": {
    "uuid-variante-source-les-lys": {
      "label": "Des Lys Extra brut - Bouteille (75 cl)",
      "orders": 42,
      "lines": 42,
      "quantity": 318,
      "prices": [3250, 3400]
    }
  },
  "_target_variants": {
    "uuid-variante-cible-les-lys": {
      "label": "Les Lys Extra brut - Bouteille (75 cl)",
      "sku": "LYS-75",
      "price": 3500,
      "price_discount": null
    }
  }
}
```

Règles :

- compléter uniquement les valeurs à droite dans `variants`, `payment_methods` et `delivery_methods` ;
- remplacer `__A_COMPLETER__` par l'UUID cible, par `sku:CODE` pour une variante, ou par `null` pour un détachement historique volontaire ;
- tant qu'une valeur `__A_COMPLETER__` subsiste, le préflight et l'import sont refusés ;
- les entrées générées ne doivent pas être supprimées ;
- une variante portant encore exactement le même UUID cible est la seule correspondance que le générateur peut préremplir sans intervention humaine ;
- une valeur `null` est une décision explicite : la ligne conserve son nom, format, prix et UUID dans le snapshot historique, mais son `CartItem` n'est rattaché à aucune variante actuelle ;
- une variante peut être ciblée par UUID ou par `sku:CODE` ; un SKU ambigu est refusé ;
- les moyens de paiement générés sont rattachés par UUID cible ; l'ancien format manuel par code métier reste compris, mais un code ambigu est refusé ;
- un moyen de paiement absent reste autorisé pour un checkout non payé et est conservé dans les métadonnées historiques ;
- le mode de livraison peut rester détaché, car son nom et son prix sont conservés dans le snapshot.

Le bloc `source` lie le fichier à l'identifiant et au périmètre qui ont servi à le générer. Le modifier ou réutiliser le fichier avec un autre `--source-id` ou `--scope` bloque le préflight. La génération n'écrase jamais un fichier existant. À l'ouverture de l'assistant, les données informatives des variantes et des prix sont rafraîchies depuis les deux bases sans modifier les correspondances déjà choisies ; chaque nouveau choix est ensuite enregistré immédiatement.

Ne placez aucun mot de passe dans ce fichier.

## Exécution

La connexion source est entièrement décrite par les options de la commande :

- `--database` : nom de la base MySQL/MariaDB ;
- `--user` et `--password` : identifiants MySQL/MariaDB ;
- `--host` : hôte MySQL, facultatif, `127.0.0.1` par défaut ;
- `--port` : port MySQL, facultatif, `3306` par défaut ;
- `--ssh-host` : active le tunnel SSH quand cette option est renseignée ;
- `--ssh-user`, `--ssh-key` et `--ssh-port` : utilisateur, clé privée et port SSH, avec le port `22` par défaut.

En mode interactif, `--database`, `--user` et `--password` peuvent être omis : la commande les demande, en masquant le mot de passe. En mode SSH, elle demande aussi l'utilisateur et la clé SSH s'ils manquent. Elle ne demande pas l'hôte MySQL : il reste automatiquement à `127.0.0.1`, sauf surcharge volontaire avec `--host`.

Pour une connexion MySQL directe :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --host=mysql.example.test \
  --port=3306
```

Pour atteindre le MySQL local de `server2.cpcreation.fr` par SSH :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --ssh-host=server2.cpcreation.fr \
  --ssh-user=utilisateur-ssh \
  --ssh-key=~/.ssh/cle-server2
```

Cette seconde forme ouvre un port local éphémère vers `127.0.0.1:3306` sur le serveur SSH. La commande demande séparément l'utilisateur et le mot de passe MySQL. La clé doit être lisible par l'utilisateur qui exécute PHP ; si elle est chiffrée, elle doit déjà être déverrouillée dans `ssh-agent`.

Pour une automatisation non interactive, toutes les valeurs obligatoires peuvent être passées en options. Le mot de passe apparaît alors potentiellement dans l'historique du shell et la liste des processus ; la saisie interactive masquée reste recommandée.

Les options de connexion doivent être répétées à chaque lancement. Les exemples suivants montrent le cas d'un MySQL local avec seulement `--database`; pour `server2`, ajoutez à chaque fois le même bloc `--ssh-host`, `--ssh-user` et `--ssh-key` que ci-dessus.

Le premier lancement couvre par défaut les checkouts visibles dans l'ancien back-office, c'est-à-dire ceux dont `deleted_at` est nul, payés ou non. Il crée `config/legacy-order-map.json`, puis s'arrête sans importer :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --source-id=identifiant-stable-du-site-master
```

En mode interactif normal, la commande propose immédiatement de compléter le JSON. Pour chaque variante, moyen de paiement et mode de livraison, elle affiche la donnée source et au maximum 20 cibles numérotées par page. Pour les variantes, le prix unitaire enregistré dans les anciennes commandes apparaît comme « Prix historique » et le tarif public actuel de chaque variante cible apparaît dans une colonne « Prix actuel ». Si plusieurs prix historiques existent, ils sont tous affichés ; si le tarif cible est remisé, le prix remisé et le prix initial sont indiqués. Chaque sélection est enregistrée atomiquement dans le fichier dès qu'elle est validée. Il reste aussi possible de compléter manuellement les seules valeurs de droite `__A_COMPLETER__`.

Commandes disponibles dans l'assistant :

- `1` à `20` : sélectionner la cible affichée ;
- `n` / `p` : page de cibles suivante / précédente ;
- `r` / `s` : correspondance précédente / suivante sans modifier la valeur ;
- `d` : enregistrer un détachement historique explicite avec `null` ;
- `q` : enregistrer les choix déjà effectués et quitter.

Le détachement d'un moyen de paiement reste interdit par le préflight pour une commande payée.

L'assistant peut être rouvert, y compris pour modifier des choix déjà résolus :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --source-id=identifiant-stable-du-site-master \
  --configure-mapping
```

`--configure-mapping` est incompatible avec `--no-interaction`. À l'inverse, ajouter `--no-interaction` au premier lancement crée seulement le fichier sans poser de question.

La génération ou une modification interactive s'arrête toujours avant le préflight et l'import. Il faut relancer explicitement la commande après la relecture du JSON. Si `--apply` avait été fourni par erreur pendant cette étape, il est ignoré et la commande termine avec un code d'échec pour qu'aucune automatisation ne puisse croire que l'import a eu lieu.

Pour limiter l'import aux commandes payées :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --source-id=identifiant-stable-du-site-master \
  --scope=paid
```

Dans ce cas, le fichier est lié au périmètre `paid` et les lancements suivants doivent conserver `--scope=paid`.

Après un préflight sans erreur, appliquer :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --source-id=identifiant-stable-du-site-master \
  --apply
```

Une reprise volontaire utilise strictement le même `--source-id` :

```bash
php bin/console viti-core:orders:import-master \
  --database=base_legacy \
  --source-id=identifiant-stable-du-site-master \
  --apply --force
```

L'option `--mapping=/autre/chemin.json` reste disponible si le fichier ne doit pas être placé dans `config/`.

Si `--source-id` est omis, le nom de la base est utilisé. Il est préférable de le renseigner pour qu'un changement de tunnel, d'hôte ou de mot de passe ne modifie jamais l'identité fonctionnelle de la source.

## Données conservées

- dates du panier, du checkout et du paiement ;
- statut payé/non payé et statut d'expédition historique ;
- lignes, quantités et prix historiques sans recalcul depuis le catalogue actuel ;
- TVA, frais de port, remise en montant, remise en pourcentage et dégressivité avec les arrondis de `master` ;
- adresses de livraison et de facturation ;
- moyen de paiement, réponse technique existante et informations de transporteur ;
- token, commentaire, locale, CGV et adresse IP historique.

L'import ne crée ni compte client ni facture. Quand l'ancienne adresse de livraison était déclarée identique, l'adresse de facturation source devient l'adresse de livraison du nouveau modèle. Une adresse de facturation séparée n'est créée que si l'ancien checkout indiquait réellement une adresse différente.

Les téléphones sont normalisés quand une notation internationale exploitable peut être déduite, notamment `00` vers `+` et `+33 (0)` vers `+33`. Les valeurs sont enregistrées uniquement dans les champs d'adresse habituels et tronquées à 12 caractères pour le téléphone et à 16 caractères pour le numéro de TVA.

## Contrôles après import

Vérifier au minimum :

- le nombre de lignes dans `cart_legacy_order_import_map` ;
- les compteurs payés/non payés et le montant TTC indiqués par le rapport de préflight ;
- plusieurs commandes dans l'administration, dont une avec remise, une avec livraison et une ligne historique détachée ;
- une reprise avec `--apply --force`, qui doit annoncer zéro ajout et uniquement des commandes déjà présentes ;
- l'absence de nouvelle facture, d'email et de paiement déclenché.
