diff --git a/services/targo-hub/lib/legacy-dispatch-sync.js b/services/targo-hub/lib/legacy-dispatch-sync.js index af27523..7145b21 100644 --- a/services/targo-hub/lib/legacy-dispatch-sync.js +++ b/services/targo-hub/lib/legacy-dispatch-sync.js @@ -824,17 +824,21 @@ 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) + // MAUVAISE VILLE : le job existant porte une ville ≠ de l'adresse de service recalculée (ex. #255465 stocké + // Sainte-Clotilde vs service réel Saint-Cyprien). On CORRIGE alors adresse + coords + SL + sujet MÊME si le job + // est déjà assigné : le ticket F est encore ouvert (intervention à venir) → le tech doit avoir la BONNE ville. + const cityOfAddr = (s) => { const p = String(s || '').split(','); return p.length >= 2 ? p[p.length - 2].trim() : '' } + const wrongCity = !!(b.payload.address && ex.address && (() => { const a = cityOfAddr(ex.address); const c = cityOfAddr(b.payload.address); return a && c && !cityMatch(a, c) })()) // 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 || 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 - // Adresse de SERVICE : backfill si jamais écrite (cas des 175 jobs historiques) ; et rafraîchit pour les jobs encore au pool (open + non assigné). - if (b.payload.address && (!ex.address || (ex.status === 'open' && !ex.assigned_tech && ex.address !== b.payload.address))) patch.address = b.payload.address + if (b.payload.latitude != null && (!exHas || isUpgrade || wrongCity)) { patch.latitude = b.payload.latitude; patch.longitude = b.payload.longitude; coordsFilled++ } + if (b.payload.service_location && (!ex.service_location || wrongCity)) patch.service_location = b.payload.service_location // backfill / CORRIGE le lien Service Location (mauvaise ville) + // Adresse de SERVICE : backfill si jamais écrite (jobs historiques) ; rafraîchit au pool (open + non assigné) ; et CORRIGE TOUJOURS une mauvaise ville. + if (b.payload.address && (!ex.address || wrongCity || (ex.status === 'open' && !ex.assigned_tech && ex.address !== b.payload.address))) patch.address = b.payload.address 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 - // Rafraîchit le sujet (qui inclut l'adresse de SERVICE) pour les jobs encore au pool (open + non assigné), - // sans surprendre un tech sur un job déjà dispatché. Corrige les sujets anciens basés sur la facturation. - if (ex.status === 'open' && !ex.assigned_tech && b.payload.subject && ex.subject !== b.payload.subject) patch.subject = b.payload.subject + // Rafraîchit le sujet (qui inclut la ville de service) au pool (open + non assigné), OU quand la ville était FAUSSE. + if ((wrongCity || (ex.status === 'open' && !ex.assigned_tech)) && b.payload.subject && ex.subject !== b.payload.subject) patch.subject = b.payload.subject if (!dryRun && Object.keys(patch).length) { const r = await erp.update('Dispatch Job', ex.name, patch) if (r && r.ok) { updated++; details.push({ legacy_id: b.legacy_id, action: 'update', job: ex.name, patch }) } @@ -2793,31 +2797,4 @@ async function provisionIdentities ({ dryRun = true, scope = 'ledger' } = {}) { return res } -// Dry-run CIBLÉ d'UN ticket legacy (0 écriture) → adresse/coords résolues + ville sujet/delivery/facturation. -// Outil de débogage adressage/routage : `require('./lib/legacy-dispatch-sync').previewTicket(255465)`. -async function previewTicket (id) { - const p = pool(); if (!p) return { error: 'legacy DB indisponible' } - const [rows] = await p.query( - `SELECT t.id, t.subject, t.dept_id, dd.name AS dept, t.due_date, t.due_time, t.priority, - COALESCE(NULLIF(t.account_id, 0), NULLIF(pt.account_id, 0)) AS account_id, - COALESCE(NULLIF(t.delivery_id, 0), NULLIF(pt.delivery_id, 0)) AS delivery_id, - t.parent, t.date_create, t.last_update, - a.first_name, a.last_name, a.company, a.email, a.cell, a.tel_home, a.address1, a.address2, a.city, a.state, a.zip, - dv.latitude AS dv_lat, dv.longitude AS dv_lon, dv.placemarks_id AS dv_pmid, dv.address1 AS dv_addr, dv.city AS dv_city, dv.zip AS dv_zip, - (SELECT mm3.msg FROM ticket_msg mm3 WHERE mm3.ticket_id = t.id ORDER BY mm3.id ASC LIMIT 1) AS first_msg - FROM ticket t - LEFT JOIN ticket pt ON pt.id = t.parent - LEFT JOIN ticket_dept dd ON dd.id = t.dept_id - LEFT JOIN account a ON a.id = COALESCE(NULLIF(t.account_id, 0), NULLIF(pt.account_id, 0)) - LEFT JOIN delivery dv ON dv.id = COALESCE(NULLIF(t.delivery_id, 0), NULLIF(pt.delivery_id, 0), - (SELECT d2.id FROM delivery d2 WHERE COALESCE(NULLIF(t.account_id,0),NULLIF(pt.account_id,0))>0 AND d2.account_id=COALESCE(NULLIF(t.account_id,0),NULLIF(pt.account_id,0)) AND d2.latitude IS NOT NULL AND d2.latitude<>0 AND ABS(d2.latitude)>1 ORDER BY d2.id DESC LIMIT 1), - (SELECT d3.id FROM delivery d3 WHERE COALESCE(NULLIF(t.account_id,0),NULLIF(pt.account_id,0))>0 AND d3.account_id=COALESCE(NULLIF(t.account_id,0),NULLIF(pt.account_id,0)) ORDER BY d3.id DESC LIMIT 1)) - WHERE t.id = ? LIMIT 1`, [id]) - if (!rows || !rows[0]) return { error: 'ticket ' + id + ' introuvable' } - const t = rows[0] - const subjCity = await cityFromSubject(t.subject) - const b = await buildJob(t, { dryRun: true }) - return { legacy_id: b.legacy_id, subject: b.payload.subject, subject_city: subjCity, dv_city: t.dv_city, bill_city: t.city, addr_ticket: b.addr, job_address: b.payload.address, latitude: b.payload.latitude, longitude: b.payload.longitude, coord_src: b.matched.coord_src, service_location: b.payload.service_location } -} - -module.exports = { handle, sync, previewTicket, reimportAddresses, fillMissingCoords, fixGeocoding, purgeStaleOrphans, pushAssignments, returnToPool, postTicketLegacy, ticketThread, ticketAssignState, watchLegacy, techSyncReport, techSyncApply, mineDurations, legacyTechTickets, legacyWindowLoad, ingestAssigned, reconcileLegacyJobs, backfillServiceLocations, backfillIssueCustomers, backfillSourceIssue, backfillDependsOn, geolocateJobs, fibreByDelivery, startSync, stopSync, fetchTargoTickets, staleTickets, staleNudge, publishPreview, publishJob, techHistoryBackfill, techHistory, provisionIdentities, coord, prio, startTime, jobType, inTerritory, geocodeRQA, persistGeocodeToSL, reverseNearestFibre } // parseurs purs exposés pour les tests +module.exports = { handle, sync, reimportAddresses, fillMissingCoords, fixGeocoding, purgeStaleOrphans, pushAssignments, returnToPool, postTicketLegacy, ticketThread, ticketAssignState, watchLegacy, techSyncReport, techSyncApply, mineDurations, legacyTechTickets, legacyWindowLoad, ingestAssigned, reconcileLegacyJobs, backfillServiceLocations, backfillIssueCustomers, backfillSourceIssue, backfillDependsOn, geolocateJobs, fibreByDelivery, startSync, stopSync, fetchTargoTickets, staleTickets, staleNudge, publishPreview, publishJob, techHistoryBackfill, techHistory, provisionIdentities, coord, prio, startTime, jobType, inTerritory, geocodeRQA, persistGeocodeToSL, reverseNearestFibre } // parseurs purs exposés pour les tests