# Sync F → ERPNext — état au 2026-06-11 (session nocturne)

> **TL;DR** — Moteur de sync delta **construit + testé en lecture seule** sur tes vraies données.
> **AUCUNE écriture** n'a été faite (ni F, ni ERPNext). Aucun cron ajouté. Les déploiements
> sont **inertes** (nouveau module `lib/legacy-sync.js` + routes gated, qui ne tournent que sur appel).
> Décision attendue de toi avant toute écriture (voir §6).

## 1. Méthode de sync retenue (rappel)
- **Pas de proxy live** vers F (prod fragile). **Pas de réplication binlog** (F n'a pas `log-bin` → exigerait un redémarrage du MySQL de facturation).
- **Snapshot/delta pull** : le script existant `refresh-report-tables.sh` fait déjà un `mysqldump --single-transaction` (non bloquant, 1 connexion) des tables `account/delivery/service/product` → miroir local `legacy-db`. On lit TOUJOURS le miroir, jamais F live.
- **Détection de changement** confirmée : `account.date_last` est une **vraie date de modification** (86 % des comptes, bouge en temps réel) → watermark possible sans rien changer dans F. `service`/`delivery` = INSERT par `id` (pas de date de modif).
- **Écritures** ERPNext→F = via le **pont PHP** (`ops_reassign.php`, déjà utilisé pour le dispatch), idempotent par `legacy_account_id`. F jamais écrit en double.
- **Propriété par domaine** : F = facturation (tarif/facture/statut billing), ERPNext = ops (dispatch/RDV/notes). Aucun champ disputé.

## 2. Ce qui a été construit
- **`services/targo-hub/lib/legacy-sync.js`** (déployé, inerte) :
  - `previewCustomers()` — lit le miroir + ERPNext, classe chaque écart.
  - `runPreview()` — orchestre customers + locations + services, écrit le rapport complet dans `data/legacy-sync-preview.json`.
  - `applySafeAdds({confirm})` — **dry-run par défaut**, n'écrit QUE si `confirm==='SAFE-ADD'` (non exécuté).
- **Routes** (gated par le token hub, donc OPS-only) :
  - `GET /legacy-sync/preview` → recalcule + renvoie le rapport.
  - `GET /legacy-sync/report` → relit le dernier rapport.
  - `POST /legacy-sync/apply-safe-adds` → **dry-run** sauf `{ "confirm": "SAFE-ADD" }`.

## 3. Politique de sécurité (le cœur de « preview d'abord »)
Chaque écart de champ est classé :
- **ADD** — ERPNext vide + F a une valeur → **sûr** (on remplit).
- **CHANGE** — les deux ont une valeur différente → **révision humaine** (jamais auto-appliqué).
- **NEVER-CLOBBER** — F vide + ERPNext a une valeur → **jamais écrasé** (protège ex. les `stripe_id`).
- **`disabled`** — ERPNext le DÉRIVE des abonnements actifs (≠ `account.status`) → **jamais synchronisé**, juste rapporté comme « divergence de statut ».

## 4. Résultats du preview (réels, 2026-06-11)
**Customers** — F 15 771 / ERPNext 15 303 :
| Bucket | Nombre | Action |
|---|---|---|
| À créer (nouveaux comptes F) | **471** | nouveaux clients depuis la migration (id 15 429+) |
| safe_add (enrichir champs vides) | **8 119** | **sûr** — email 7 073, tél 8 786, cell 2 310, stripe 138 |
| needs_review (valeurs divergentes) | **921** | is_commercial 640, is_bad_payer 161, ppa 47, email 34, nom 21, langue 9 |
| inchangés | 6 260 | — |
| never-clobber (protégés) | 2 | 1 stripe_id + 1 cell (F vide) |

**Divergence de statut** (NON auto-synchronisée) : 713 actifs-F/désactivés-ERP + **3 502 inactifs-F/actifs-ERP** (probables churns qu'ERPNext montre encore actifs → à réviser).

**Locations** delivery→Service Location : 537 à créer (sur 17 645).
**Services ACTIFS** (status=1) service→Service Subscription : **3 580 à créer** (F 41 685 / ERPNext 39 628). *(Avec l'historique inactif c'était 30 469 — désormais filtré sur `status=1`.)*

### Écran de revue OPS (déployé)
Page **`/sync-legacy`** (« Sync F↔ERPNext » dans le menu, `requires: view_settings`) — `src/pages/LegacySyncPage.vue`. **Lecture seule** : appelle `GET /legacy-sync/preview` et affiche les buckets (à créer / à enrichir / à réviser), les ventilations par champ (ADD/CHANGE/never-clobber), la divergence de statut, et des échantillons. Aucun bouton d'écriture (l'apply reste CLI/gated tant que tu n'as pas donné le feu vert).

## 5. Comment l'utiliser
```bash
# Preview (lecture seule) :
docker exec targo-hub node -e "require('/app/lib/legacy-sync').runPreview().then(r=>console.log(JSON.stringify(r.customers.summary)))"
# Rapport complet :
cat /opt/targo-hub/data/legacy-sync-preview.json   # (dans le conteneur : /app/data/…)
# Dry-run de l'apply (n'écrit RIEN) :
docker exec targo-hub node -e "require('/app/lib/legacy-sync').applySafeAdds({}).then(r=>console.log(JSON.stringify(r)))"
```

## 6. Décisions / prochaines étapes (À TON RÉVEIL)
1. **Feu vert pour `safe_add` ?** Remplir les 8 119 champs vides (email/tél) est sûr et réversible (PUT partiel). → lancer `applySafeAdds({confirm:'SAFE-ADD'})`. (Nuance : email/tél canoniques vivent sur le doctype **Contact** ; on remplit ici la copie `Customer.email_id`/`tel_home` pour recherche/affichage — le sync Contact complet est une étape ultérieure.)
2. **needs_review (921)** : surtout `is_commercial` (640) — vérifier le mapping (F.`commercial` vs ERPNext `is_commercial`) ; petit volume, révisable.
3. **Divergence de statut (3 502 inactifs-F/actifs-ERP)** : décider la règle (réconcilier ERPNext.disabled depuis F.status pour les terminés ?). C'est un signal de churn, pas un bug.
4. **Services** : filtrer `status=1` + mapper service→Service Subscription (+Subscription) proprement.
5. **Fraîcheur F→miroir** : planifier le delta (watermark `date_last`/`id`) — toujours pas de cron ajouté (en attente de ta décision de cadence).
6. **Suite du funnel** : Lead (doctype ERPNext) + wizard rep, puis push-to-F (#65), puis onboarding (flow), puis RDV (booking déjà construit).

## 7. Garanties de cette session
- ✅ Zéro écriture dans F. ✅ Zéro écriture dans ERPNext. ✅ Aucun cron/scheduler ajouté.
- ✅ Scheduler ERPNext reste en pause ; facturation F reste autoritaire.
- ✅ Déploiements = nouveau module + routes gated (n'altèrent aucun comportement existant).
