Pont : géoloc camping (fixe) sur les Dispatch Jobs — l'adresse de service ≠ résidence du client

Symptôme : un job de camping (« Lac des pins | Anton Rimerov ») pointait sur la RÉSIDENCE du client
(428 Rue George, Lasalle = 45.58,-73.73) au lieu du camping. Le pont géocodait l'adresse de compte.

- buildJob : détection camping en PRIORITÉ MAX via le registre camping_registry — signal = sujet (label
  explicite, prioritaire) puis ville/adresse de delivery. Garde-fou : le texte doit contenir « camping » OU
  un mot-clé de LIEU spécifique (évite les faux positifs de patronyme, ex. « Daniel Dauphinais »). coord_src='camping'.
  La branche update fait écraser les coords existantes par le camping (comme delivery). 20 jobs ouverts re-coordonnés.
- camping_dispatch_backfill.sql : corrige les jobs DÉJÀ dispatchés (que le sync ne re-traite plus car le ticket
  legacy a quitté le pool ouvert-3301) → 4 Lac des Pins + 2 SandySun. Anton Rimerov/Germaine Thibert → 45.0624,-73.9113 ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-07 17:30:11 -04:00
parent 2b9a863d39
commit f804c2b49d
2 changed files with 49 additions and 3 deletions

View File

@ -0,0 +1,19 @@
-- camping_dispatch_backfill.sql — applique la géoloc FIXE du camping aux Dispatch Jobs DÉJÀ dispatchés.
-- Le pont (legacy-dispatch-sync) ne re-traite que les tickets encore « ouverts + assign_to=3301 » ; les jobs
-- déjà assignés/fermés gardent leurs vieilles coords (résidence). Ce backfill corrige tous les jobs issus du
-- pont dont le SUJET désigne un camping (mots-clés de LIEU sûrs ; pour 'dauphinais', exige aussi « camping »).
-- Idempotent (ne touche que ceux dont la coord diffère). Match via camping_registry.
\timing on
BEGIN;
WITH applied AS (
UPDATE "tabDispatch Job" dj SET latitude = c.latitude, longitude = c.longitude, modified = NOW()
FROM camping_registry c
WHERE dj.legacy_ticket_id <> '' AND c.active
AND lower(unaccent(coalesce(dj.subject, ''))) LIKE '%' || c.keyword || '%'
AND ( c.keyword IN ('lac des pins','lac de pins','sandysun','sandy sun','frontiere','ensoleill')
OR lower(unaccent(coalesce(dj.subject, ''))) LIKE '%camping%' )
AND (dj.latitude IS NULL OR abs(coalesce(dj.latitude,0) - c.latitude) > 1e-4 OR abs(coalesce(dj.longitude,0) - c.longitude) > 1e-4)
RETURNING c.name AS camping
)
SELECT camping, count(*) AS jobs FROM applied GROUP BY camping ORDER BY 2 DESC;
COMMIT;

View File

@ -25,6 +25,28 @@ const erp = require('./erp')
const cfg = require('./config')
const { log, json, httpRequest } = require('./helpers')
const { searchAddressesRpc } = require('./address-search') // recherche trigram RQA (RPC pg_trgm) — celle de l'autocomplete de dispo
const addrdb = require('./address-db') // pool PG local (camping_registry)
// Campings : l'adresse de service est un terrain de camping (≠ résidence du client). On force la géoloc
// FIXE du camping (registre camping_registry). Détection robuste : le texte doit contenir « camping » OU
// un mot-clé de LIEU spécifique (évite les faux positifs de patronyme, ex. « Daniel Dauphinais »).
const CAMP_PLACE_KW = ['lac des pins', 'lac de pins', 'sandysun', 'sandy sun', 'frontiere', 'ensoleill']
let _campings = null; let _campingsAt = 0
async function getCampings () {
if (_campings && (Date.now() - _campingsAt) < 600000) return _campings // cache 10 min
try { const r = await addrdb.pool().query('SELECT keyword, name, latitude, longitude FROM camping_registry WHERE active'); _campings = r.rows; _campingsAt = Date.now() } catch (e) { _campings = _campings || [] }
return _campings
}
// fields en ORDRE DE PRIORITÉ (sujet d'abord = label de service explicite, puis ville/adresse de delivery).
// Le 1er champ qui contient un signal camping décide → évite qu'une ville de delivery (résidence) écrase le sujet.
function campingFor (campings, fields) {
for (const f of (Array.isArray(fields) ? fields : [fields])) {
const t = norm(f || '')
if (!(t.includes('camping') || CAMP_PLACE_KW.some(k => t.includes(k)))) continue
for (const c of campings) if (c.keyword && t.includes(c.keyword)) return c
}
return null
}
let mysql
try { mysql = require('mysql2/promise') } catch { /* dépendance optionnelle */ }
@ -222,7 +244,11 @@ async function buildJob (t) {
const st = startTime(t.due_time); if (st) payload.start_time = st
if (cust) payload.customer = cust.name
let coordSrc = null
if (dc) { payload.latitude = dc.lat; payload.longitude = dc.lon; coordSrc = 'delivery' } // source fiable : point de service legacy
// CAMPING (priorité max) : l'adresse de service est un terrain de camping → géoloc FIXE du camping,
// pas la résidence du client. Signal = sujet/ville/adresse de service du ticket.
const camp = campingFor(await getCampings(), [t.subject, t.dv_city, t.dv_addr])
if (camp) { payload.latitude = camp.latitude; payload.longitude = camp.longitude; coordSrc = 'camping' }
if (!coordSrc && dc) { payload.latitude = dc.lat; payload.longitude = dc.lon; coordSrc = 'delivery' } // point de service legacy
if (sl) {
payload.service_location = sl.name
if (!coordSrc) { const sc = coord(sl.latitude, sl.longitude); if (sc) { payload.latitude = sc.lat; payload.longitude = sc.lon; coordSrc = 'service_location' } } // repli si pas de delivery
@ -281,9 +307,10 @@ async function syncImpl ({ dryRun = false } = {}) {
// existantes (souvent issues du Service Location, moins précises). delivery écrase ; SL/RQA non.
const hasCoord = (v) => v != null && v !== '' && Math.abs(parseFloat(v)) > 0.0001
const exHas = hasCoord(ex.latitude) && hasCoord(ex.longitude)
const isDeliveryUpgrade = b.matched.coord_src === 'delivery' && exHas &&
// delivery (point exact) ET camping (géoloc fixe du camping vs résidence) ÉCRASENT des coords existantes différentes ; SL/RQA/Mapbox non.
const isUpgrade = (b.matched.coord_src === 'delivery' || b.matched.coord_src === 'camping') && exHas &&
(Math.abs(parseFloat(ex.latitude) - b.payload.latitude) > 1e-5 || Math.abs(parseFloat(ex.longitude) - b.payload.longitude) > 1e-5)
if (b.payload.latitude != null && (!exHas || isDeliveryUpgrade)) { patch.latitude = b.payload.latitude; patch.longitude = b.payload.longitude; coordsFilled++ }
if (b.payload.latitude != null && (!exHas || isUpgrade)) { patch.latitude = b.payload.latitude; patch.longitude = b.payload.longitude; coordsFilled++ }
if (!ex.service_location && b.payload.service_location) patch.service_location = b.payload.service_location // backfill lien Service Location
if (ex.status === 'open' && !ex.assigned_tech && b.payload.scheduled_date && b.payload.scheduled_date !== ex.scheduled_date) patch.scheduled_date = b.payload.scheduled_date
if (!dryRun && Object.keys(patch).length) {