gigafibre-fsm/scripts/targo-sync/README.md
louispaulb 004c3f8dee sync-legacy: scoreboard de réconciliation + fix des abonnements fantômes à la source
Scoreboard (console de réconciliation F ↔ ERPNext) :
- hub: legacy-sync.js scoreboard() + GET /legacy-sync/scoreboard — comptes bruts
  par module (clients/lieux/abos/appareils/tickets/factures/paiements) + métrique
  ghost_active_subscriptions. Devices = tabService Equipment (pas tabDevice).
  Count F « abos actifs » exclut les comptes résiliés (comparaison pomme-à-pomme).
- ops: carte « Réconciliation par module » sur LegacySyncPage.vue.

Fix fantômes à la SOURCE (scripts/targo-sync/, jusqu'ici hors repo) :
- La vraie sync des services = scripts Python hôte (/opt/targo-sync/, cron horaire),
  PAS le hub. svc_status() ignorait le statut du COMPTE → un compte résilié F
  (status 3/4/5) dont les services restent status=1 ressortait « Actif » (F ne
  cascade pas). Résultat : ~3855 abonnements fantômes recréés à chaque heure.
- sync_services_incremental.py: svc_status() rendu conscient du compte (force
  'Annulé' si compte résilié) sur Phase B (création) + Phase D (rafraîchissement).
  Rollout: dry-run {Actif→Annulé: 3855} → APPLY=1 → ghost=0 stable.
- Versionne aussi run.sh + sync_invoices_incremental.py + README (snapshot fidèle
  de la prod ; ces scripts écrivent en base chaque heure et n'étaient pas suivis).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:03:36 -04:00

60 lines
3.2 KiB
Markdown

# 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@<prod>:/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
| Fichier | Rôle |
|---|---|
| `run.sh` | Orchestrateur cron (4 étapes DAG) |
| `sync_services_incremental.py` | Service Location (delivery) + Service Subscription (service) : création + prix + **statut (fix fantômes)** |
| `sync_invoices_incremental.py` | Sales Invoice (miroir des factures F ; statut reflétant F) |