gigafibre-fsm/docs/features/legacy-dispatch-bridge.md
louispaulb b6831a1e48 Pont legacy : géocodage RQA via la recherche TRIGRAM (RPC search_addresses) + garde-fou anti-faux-positif
- address-search.js : expose searchAddressesRpc() → RPC Postgres `search_addresses` (pg_trgm), la MÊME
  recherche que l'autocomplete de disponibilité fibre. Trouve les rues que l'ilike manquait (générique géré
  par la colonne odonyme_recompose_long + phase 2 trigram), priorise les CP J0L/J0S (territoire).
- geocodeRQA() (bridge) bascule de l'ilike vers la RPC. Garde-fou : la phase 2 trigram dérive quand le
  civique est absent du RQA (« 2245 René-Vinet » → « Rue Grenet, Montréal »). On n'accepte un résultat que si
  le civique concorde + un token de nom de rue correspond + (territoire J0L/J0S OU CP/ville legacy concordants).
  Vérifié sur les données réelles : accepte 494 Av Curry / 3055 Routhier / 228 Principale / 61 Jean-François ;
  rejette René-Vinet→Grenet/Panet, chemin Ridge→Ferme, rue West→Perras (bons faux positifs écartés).
- Le faible compte RQA (8) = haute précision (l'ilike comptait 17 dont des faux positifs). Mapbox couvre le
  reste (rues neuves/civiques absents) ; ~109/125 (87 %) coordonnés ; les « aucune » = campings/villes mal écrites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:05:14 -04:00

113 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Pont legacy → Dispatch (osTicket → Dispatch Job) — Handoff dev
Tire **régulièrement** les tickets ouverts assignés au compte « Tech Targo » dans la
DB legacy (osTicket/MariaDB `gestionclient`) et crée/maj un **Dispatch Job** ERPNext
pour les répartir (grille Planification / tableau Dispatch).
## Pourquoi « Tech Targo » = staff id 3301
Dans le legacy, le travail terrain à dispatcher est assigné au compte générique
**« Tech Targo »** (`staff.id = 3301`, username `tech`) — c'est le `default_staff` des
départements techniciens (Installation/Réparation/Fibre). Filtre du pont :
`ticket.status='open' AND ticket.assign_to=3301`. (~70 tickets au démarrage.)
Override possible via `LEGACY_TARGO_STAFF_ID`.
## Surfaces
| Quoi | Où |
|---|---|
| Module | `services/targo-hub/lib/legacy-dispatch-sync.js` |
| Routage + scheduler | `services/targo-hub/server.js` (`/dispatch/legacy-sync`, `startSync()` au boot) |
| Champ idempotence | Custom Field `Dispatch Job.legacy_ticket_id` (`dispatch-app/frappe-setup/setup_dispatch_custom_fields.py`) |
| Conso côté UI | Pool « à assigner » du tableau Dispatch + panneau « Jobs à assigner » de la Planification |
## Mapping ticket legacy → Dispatch Job
| Dispatch Job | Source legacy | Notes |
|---|---|---|
| `legacy_ticket_id` | `ticket.id` | **clé d'idempotence** (lookup avant create) ; affiché dans le panneau détail |
| `legacy_dept` | `ticket_dept.name` | département granulaire → **coloriage des cartes « comme legacy »** |
| `ticket_id` | `'LEG-' + ticket.id` | nom lisible du DJ |
| `subject` | `ticket.subject` | + adresse ajoutée si pas de Service Location |
| `customer` | `Customer``legacy_account_id = ticket.account_id` | 61/70 matchés ; sinon laissé vide |
| `service_location` + `latitude`/`longitude` | `Service Location` du customer (ville qui matche) | → pin carte |
| `job_type` | `ticket.dept_id` → {Installation, Réparation, Retrait, Autre} | valeurs valides du Select |
| `scheduled_date` | `ticket.due_date` (epoch) | converti **America/Toronto** (anti-décalage UTC) |
| `start_time` | `ticket.due_time` | `HH:MM` tel quel · `am`→08:00 · `pm`→13:00 · `day`→aucune |
| `priority` | `ticket.priority` (1/2/≥3) | → low / medium / high |
| `duration_h` | défaut par type | Install 2h · Répar 1.5h · autre 1h (le legacy n'en a pas) |
| `status` | — | toujours `open` (pool ; PAS auto-assigné à un tech précis) |
## Comportement
- **Idempotent** : 1 ticket legacy = 1 Dispatch Job (clé `legacy_ticket_id`). Re-run ⇒ 0 doublon.
- **Ne clobbe PAS le répartiteur** : un DJ déjà assigné/déplacé n'est plus touché ; maj de `scheduled_date`
seulement tant qu'il est encore `open` + non assigné.
- **SÉQUENTIEL** (frappe_pg ne supporte pas la concurrence) — pas de `Promise.all`.
## Coloriage des cartes (comme legacy)
`jobColor()` (`apps/ops/src/composables/useHelpers.js` → `legacyDeptColor`) colore par `legacyDept` :
Installation(Fibre)/Monteur/Fusionneur → **vert** `#46992f` · Réparation(Fibre) → **or** `#f1c84b` ·
Télé (install/réparation) → **rose** `#ec5fb0` · Téléphonie → **vert pâle** `#8fce93` ·
Désinstallation → **rouge foncé** `#c0392b`. (Tech en pause → rouge vif, priorité.) Le store
(`_mapJob`) expose `legacyDept`/`legacyTicketId` ; le pool « non-assigné » est déjà trié par `scheduledDate`.
## Endpoints
- `GET /dispatch/legacy-sync/preview`**dry-run, 0 écriture** : ce qui serait créé + matching client/SL + non-matchés.
- `POST /dispatch/legacy-sync/run` — exécute la synchro (création/maj). Retourne `{tickets, created, updated, skipped, errors, unmatched_customer}`.
## Récurrence
`startSync()` (server.js, au boot) — **opt-in** via env :
```
LEGACY_DISPATCH_SYNC=on # active la récurrence (sinon preview/run manuels seulement)
LEGACY_DISPATCH_SYNC_MIN=15 # période en minutes (défaut 15)
```
Posé dans `/opt/targo-hub/.env`. ⚠️ Après modif de `.env`, **recréer** le conteneur
(`cd /opt/targo-hub && docker compose up -d`) — `docker restart` ne relit pas l'env_file.
1er passage différé de 90 s après le boot, puis toutes les `MIN` minutes.
## État (mise en service 2026-06-06)
70 tickets importés (0 erreur, 9 clients non matchés = comptes post-migration + 2 tickets internes
« FORMATION EN HAUTEUR »). Récurrence active (15 min).
## Coordonnées GPS & routage routier réel (2026-06-06 f)
Le pont importe des **coordonnées fiables** par job (pour le routage routier réel dans l'éditeur de
tournée). Cascade de sources, de la plus précise à la plus large :
1. **`delivery` legacy** (point de service exact, via `ticket.delivery_id → delivery.latitude/longitude`)
— JOIN ajouté à `fetchTargoTickets`. Source de référence ; on préfère aussi l'**adresse de service**
(`delivery.address1/city/zip`) à l'adresse de facturation du compte.
2. **Service Location ERPNext** (coords du client matché) — repli.
3. **Géocodage RQA via recherche TRIGRAM** (`address-search.searchAddressesRpc` → RPC Postgres `search_addresses`,
`pg_trgm`) — **la même recherche que l'autocomplete de disponibilité fibre** (phase 1 = numéro civique + mots
de rue sur odonyme normal/court/**long**[avec générique]/municipalité/CP ; phase 2 = trigram complet ; priorise
les CP J0L/J0S = territoire). Bien plus robuste que l'ancien ilike (qui manquait « René-Vinet », générique absent
de `odonyme_recompose_normal`). **Garde-fou anti-faux-positif** (la phase 2 trigram dérive quand le civique est
absent du RQA, ex. « 2245 René-Vinet » → « Rue Grenet, Montréal ») : on n'accepte un résultat que si le **civique
concorde** + au moins **un token de nom de rue** correspond + (**territoire J0L/J0S** OU CP/ville legacy concordants).
4. **Géocodage Mapbox** (`MAPBOX_TOKEN`, clé publique) — couvre ce que le RQA n'a pas (rues neuves, civiques absents).
Contraint au Québec (`country=ca` + proximity + bornes `coord()`).
Validation `coord()` : bornes Québec (lat 44→63, lon 80→57) → rejette 0/0 et placeholders. Backfill
**+ UPGRADE** : sur un job existant, on remplit les coords absentes ET on **écrase** des coords Service
Location moins précises par les coords `delivery` (point exact) — jamais l'inverse. Caches géocodage au
niveau module (1 appel par adresse / vie du hub ; échecs mémorisés). Couverture : **~109/125 tickets (87 %)**
`coord_src` (run courant) : delivery 26 · SL 38 · RQA-trigram 8 · Mapbox 37 · aucune 16. Le faible compte
RQA = **haute précision** (l'ancien ilike comptait 17 mais avec des faux positifs hors-rue/hors-ville) ; les
« aucune » = adresses réellement absentes (campings « Lac des Pins », villes mal orthographiées « Franlkin »).
**Routage routier réel (Ops → Planification → éditeur de journée)** : `loadDayRoute()` appelle l'**API
Mapbox Matrix** une fois à l'ouverture (toutes les durées routières d'un coup) → `travelBetween()` retourne
le temps RÉEL ; le réordonnancement réutilise la matrice **sans nouvelle requête**. Repli haversine
(40 km/h) si Mapbox indispo. Indicateur 🚗 (réel) vs 📏 (vol d'oiseau).
## Robustesse (2026-06-06 f)
- **Comptabilité honnête** : `erp.create/update` ne *throw* pas (renvoient `{ok:false,error}`) → le pont
vérifie `r.ok` (sinon `errors++` + `error_samples` dans le résumé). Avant : creates échoués comptés réussis.
- **Verrou de sérialisation** sur `sync()` : tick récurrent + runs manuels ne se chevauchent JAMAIS
(frappe_pg sans concurrence → sinon « socket hang up » + écritures perdues dans un rollback).
- **Subject tronqué à 140** (champ `Data` Frappe) : les jobs sans Service Location ajoutaient l'adresse au
sujet → `CharacterLengthExceededError`. Détail complet conservé dans `legacy_detail`/coords.
- Env : `MAPBOX_TOKEN` ajouté à `/opt/targo-hub/.env` (clé publique `pk.`) → recréer le conteneur.
## TODO / idées
- Matcher les clients non matchés (créer le Customer / enrichir `legacy_account_id`) → réduit les 15 « aucune coord ».
- Géoloc live du tech (Capacitor `transistorsoft/capacitor-background-geolocation`) → 1er point de la tournée.
- Filtrer les départements non-terrain (ToDo, Support Informatique, Conception…) si bruit.
- Écrire en retour le tech assigné / la date vers le legacy (bidirectionnel) — non fait (lecture seule legacy).