# targo-sync — miroir opérationnel F (legacy) → ERPNext Snapshot versionné des scripts qui tournent sur **l'hôte de prod** dans `/opt/targo-sync/` (hors conteneur). Avant 2026-07-06 ils n'étaient dans aucun repo — c'est le vrai moteur de synchronisation « legacy F → ERPNext » pour la couche **opérationnelle** (ce que les équipes voient : clients, adresses, services/abonnements, factures, paiements). **F reste autoritaire** pour la facturation (scheduler ERPNext en pause) ; ERPNext = miroir + grand livre à la bascule. ## Cadence Cron horaire sur l'hôte (`15 * * * *`) → `run.sh` → 4 étapes, dans l'ordre du DAG de dépendances : 1. **Comptes manquants** — hub `POST /legacy-payments/ensure-customers` (crée les Customer absents). 2. **Adresses + services** — `sync_services_incremental.py` (Python, exécuté **dans** `erpnext-backend-1`). 3. **Factures** — `sync_invoices_incremental.py` (idem). 4. **Paiements + soldes** — hub `POST /legacy-payments/sync-cycle`. Les scripts Python sont idempotents (`ON CONFLICT DO NOTHING` pour les créations, `UPDATE` conditionnel pour les rafraîchissements). **`APPLY=0` = dry-run** (n'écrit rien) ; `run.sh` lance en `APPLY=1`. Autres env : `PG_HOST` (défaut `db`), `LIMIT` (taille de lot), `LEGACY_HOST`. ## Déploiement (host, hors repo) ```sh scp scripts/targo-sync/*.py scripts/targo-sync/run.sh root@:/opt/targo-sync/ # le cron docker cp le .py dans erpnext-backend-1 puis l'exécute — pas de restart requis ``` Dry-run manuel d'un script avant bascule : ```sh docker cp /opt/targo-sync/sync_services_incremental.py erpnext-backend-1:/tmp/svc.py docker exec -e APPLY=0 -e PG_HOST=db erpnext-backend-1 \ /home/frappe/frappe-bench/env/bin/python /tmp/svc.py ``` ## ⚠️ FIX 2026-07-06 — abonnements fantômes (compte résilié affiché « Actif ») En F, résilier un **compte** met `account.status ∈ {3,4,5}` mais **laisse les services à `service.status=1`** (F ne cascade pas). `svc_status()` dérivait le statut du seul `service.status` → un compte résilié ressortait « Actif » dans ERPNext. Comme le statut client est dérivé des abonnements, ~2 663 ex-clients apparaissaient actifs et ~3 855 abonnements étaient des fantômes (MRR gonflé, audiences polluées). Correctif dans `sync_services_incremental.py` (marqueur `FIX 2026-07-06`) : `svc_status()` charge une fois `SELECT id FROM account WHERE status IN (3,4,5)` et **force `Annulé` dès que le compte est résilié**, sur les deux chemins (Phase B création + Phase D rafraîchissement). Sans ce garde-fou, le cron ré-activait les fantômes à chaque heure. Voir la mémoire projet `feedback_ghost_active_subscriptions`. Réconciliation visible dans OPS : page **/sync-legacy** → carte « Réconciliation par module » (endpoint hub `GET /legacy-sync/scoreboard`, métrique `ghost_active_subscriptions` = doit rester 0). ## Fichiers ## Catch-up QUOTIDIEN — tickets + appareils (cron séparé) `tickets-daily.sh` (cron `30 4 * * *`) lance deux imports idempotents (skip par `legacy_*_id`, n'importent que les nouveaux) : - **`migrate_tickets.py`** → `tabIssue` (skip par `legacy_ticket_id`). Messages des tickets *closed* non importés (deferred ; `import_ticket_msgs.py` au besoin). - **`import_devices_and_enrich.py`** (avec `SKIP_ENRICH=1` = devices seulement) → `tabService Equipment` (skip par `legacy_device_id`). Sans `SKIP_ENRICH`, enrichit aussi les Service Location (fibre/OLT/VLAN). Séparé de `run.sh` (horaire) car les scans complets (252k tickets / 11k devices) sont trop lourds pour l'horaire. Canoniques dans `scripts/migration/` ; en déployer une copie dans `/opt/targo-sync/`. ⚠️ **Bug d'import historique corrigé 2026-07-06** : `rename_all_doctypes.py` a renommé les `Service Equipment` (`EQ-{md5}` → `EQP-{seq}`) SANS mettre à jour `Service Subscription.device` → 6980 liens orphelins (`Could not find Device: EQ-…`, bloquait les saves pleine-doc Frappe). 5241 remappés (via `legacy_device_id`+md5) ; 1739 résiduels pointaient un device supprimé côté F (à trancher : NULL vs relink au device courant du lieu). Voir mémoire `feedback_device_hierarchy`. ## 🔐 Secrets — JAMAIS dans le repo Le mot de passe F (`LEGACY_PW`) se charge depuis `/opt/targo-sync/secrets.env` (hôte, `chmod 600`, **non versionné** — voir `.gitignore`). Les `.py` du repo ont un défaut VIDE (`os.environ.get("LEGACY_PW","")`). Ne **jamais** committer une copie prod d'un script qui aurait le mot de passe en dur. ## Fichiers | Fichier | Rôle | |---|---| | `run.sh` | Orchestrateur cron horaire (4 étapes DAG) | | `sync_services_incremental.py` | Service Location (delivery) + Service Subscription (service) : création + prix + **statut (fix fantômes raffiné)** | | `sync_invoices_incremental.py` | Sales Invoice (miroir des factures F ; statut reflétant F) | | `tickets-daily.sh` | Wrapper cron quotidien → `migrate_tickets.py` + `import_devices_and_enrich.py` (secret via `secrets.env`) | | `migrate_tickets.py` | (déployé depuis `scripts/migration/`) tickets F → Issue | | `import_devices_and_enrich.py` | (déployé depuis `scripts/migration/`) devices F → Service Equipment | | `secrets.env` | **Hôte seulement, non versionné** : `LEGACY_PW=…` |