feat(legacy-sync): adresse depuis le CORPS du ticket + exclut hors-sujet (pneus/stage)

- Adresse de service absente (delivery/facturation vides) → on extrait « Adresse : … » du corps du
  ticket (ex. #143107 « 2081 route 202 Hinchinbrooke ») : affichée + géocodée (RQA > OSM, ville
  détectée via muni). Le dispatch n'est plus aveugle quand l'adresse est tapée dans les notes.
- NEG_RE : exclut aussi « pneu / demande de stage / stagiaire » (hors-sujet, option D).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-24 15:04:41 -04:00
parent 176184f55f
commit b71add4dac

View File

@ -558,7 +558,23 @@ async function buildJob (t, { dryRun = false } = {}) {
const dc = coord(t.dv_lat, t.dv_lon)
const svcAddr = [t.dv_addr, t.dv_city, t.dv_zip].filter(Boolean).join(', ')
const billAddr = [t.address1, t.address2, t.city, t.state, t.zip].filter(Boolean).join(', ')
const addr = svcAddr || billAddr
let addr = svcAddr || billAddr
let bodyGeo = null
// FALLBACK adresse : ni delivery ni facturation → extraire « Adresse : … » du CORPS du ticket (ex. #143107 :
// « Adresse : 2081 route 202 Hinchinbrooke »). Sert l'affichage ET le géocodage (ville détectée via muni sur la chaîne).
if (!addr && t.first_msg) {
const body = stripHtml(t.first_msg)
const m = body.match(/adresse\s*:?\s*([0-9][^\n·|]{5,88})/i)
if (m) {
const raw = m[1].trim().replace(/\s+/g, ' ').replace(/\s+(voir|svp|s'il|merci|t[eé]l|cell|contact|pour)\b.*$/i, '').trim()
if (raw.length >= 6) {
addr = raw
let cityG = ''
try { await muni.load(); for (const w of raw.split(/[\s,]+/).filter(x => x.length >= 4 && !/^\d/.test(x)).reverse()) { const mm = muni.resolveSync(w); if (mm.matched && !mm.ambiguous) { cityG = String(mm.canonical || '').split(/\s+/).map(x => x === 'ST' ? 'Saint' : x === 'STE' ? 'Sainte' : x).join('-'); break } } } catch (e) {}
bodyGeo = { line: raw, city: cityG }
}
}
}
// Le client existe (ou vient d'être créé) mais n'a AUCUNE Service Location → on la crée depuis l'adresse de service legacy
// (delivery de préférence, sinon facturation) pour que la fiche montre l'adresse et que le job pointe vers un lieu réel.
if (cust && !sl && CREATE_MISSING && !dryRun) {
@ -642,6 +658,10 @@ async function buildJob (t, { dryRun = false } = {}) {
else if (ctr) { payload.latitude = ctr.lat; payload.longitude = ctr.lon; coordSrc = 'postal_centroid' } // 3) repli : jamais la mauvaise ville
}
}
if (!coordSrc && bodyGeo) { // coords depuis l'adresse extraite du CORPS : RQA autoritaire, sinon OSM
const g = (await geocodeRQA(bodyGeo.line, '', bodyGeo.city)) || (await geocodeNominatim(bodyGeo.line, bodyGeo.city, ''))
if (g) { payload.latitude = g.lat; payload.longitude = g.lon; coordSrc = 'body_addr_geocode' }
}
if (!coordSrc) { // sans compte : retrouver le client par le TÉLÉPHONE du sujet (compte UNIQUE + 1 seule adresse) → adresse PRÉCISE + lien client
const pm = await accountByPhone(pool(), t.subject)
if (pm) {
@ -786,7 +806,7 @@ async function ingestAssignedImpl ({ dryRun = false, loDays = -7, hiDays = 30, i
const FIELD_RE = /install|r[eé]paration|t[eé]l[eé]|monteur|fusion|d[eé]sinstall|coupure|bris|signal|modem|\bonu\b|\bont\b|fibre|ramassage|connect|d[eé]viation|antenne|prise|internet|d[eé]m[eé]nagement|changement|activation|\bstb\b|d[eé]codeur/i
// + « nouveau message » / « message vocal » : notifications de messages ENTRANTS clients (pas des interventions terrain) —
// elles n'ont ni client ni adresse et encombraient le dispatch. Exclues de l'import terrain (option 1, retour terrain #255465).
const NEG_RE = /cong[eé]|absence|vacances|\bf[eé]ri[eé]|r[eé]union|formation|nouveau message|message vocal/i
const NEG_RE = /cong[eé]|absence|vacances|\bf[eé]ri[eé]|r[eé]union|formation|nouveau message|message vocal|\bpneu|demande de stage|stagiaire/i
let created = 0, updated = 0, skipped = 0, errors = 0
const details = [], errSamples = []
for (const t of (rows || [])) {