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:
parent
2b9a863d39
commit
f804c2b49d
19
scripts/migration/camping_dispatch_backfill.sql
Normal file
19
scripts/migration/camping_dispatch_backfill.sql
Normal 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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user