From c8377a208af861d2a8ab2d3386ac780af9024cbc Mon Sep 17 00:00:00 2001 From: louispaulb Date: Sat, 6 Jun 2026 13:03:10 -0400 Subject: [PATCH] =?UTF-8?q?fix(dispatch):=20pont=20lisait=20un=20r=C3=A9pl?= =?UTF-8?q?ica=20fig=C3=A9=20(avril)=20+=20auto-fermeture=20des=20DJ=20don?= =?UTF-8?q?t=20le=20ticket=20legacy=20est=20closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ROOT CAUSE : LEGACY_DB_HOST='legacy-db' = réplica figé au 2026-04-07 → tickets fermés/réassignés depuis paraissaient encore ouverts (ex. 244659). Fix infra : .env LEGACY_DB_HOST=10.100.80.100 (DB live, SELECT-only). - closeResolved() : tout DJ issu du pont (open/assigned/On Hold) dont le ticket legacy est 'closed' → status 'Completed'. Appelé à chaque sync + route POST /dispatch/legacy-sync/close-resolved. Résultat 1er run live : 125 ouverts réels, 102 créés, 44 fermés (dont LEG-244659). NB In Progress non touché. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../targo-hub/lib/legacy-dispatch-sync.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/services/targo-hub/lib/legacy-dispatch-sync.js b/services/targo-hub/lib/legacy-dispatch-sync.js index b3ad19f..57ad947 100644 --- a/services/targo-hub/lib/legacy-dispatch-sync.js +++ b/services/targo-hub/lib/legacy-dispatch-sync.js @@ -197,7 +197,9 @@ async function sync ({ dryRun = false } = {}) { errors++; details.push({ legacy_id: String(t.id), error: String((e && e.message) || e) }) } } - const summary = { ok: true, dryRun, tech_staff_id: TARGO_TECH_STAFF_ID, tickets: tickets.length, created, updated, skipped, errors, unmatched_customer: unmatched } + let closedResolved = 0 + if (!dryRun) { try { const cr = await closeResolved(); closedResolved = cr.closed } catch (e) { log('closeResolved error:', e.message) } } // retire les DJ dont le ticket legacy est fermé + const summary = { ok: true, dryRun, tech_staff_id: TARGO_TECH_STAFF_ID, tickets: tickets.length, created, updated, skipped, errors, unmatched_customer: unmatched, closed: closedResolved } if (!dryRun) { _lastRun = { at: new Date().toISOString(), ...summary }; log(`legacy-dispatch-sync: ${JSON.stringify(summary)}`) } // heartbeat return { ...summary, details } } @@ -217,6 +219,24 @@ async function reconcile () { return { ok: true, legacy_open_3301: legacyIds.size, erpnext_bridged: erpIds.size, missing_count: missing.length, missing, orphan_count: orphan.length, orphan, last_sync: _lastRun } } +// Auto-fermeture : un Dispatch Job issu du pont dont le ticket legacy est passé `closed` → on le marque « Completed » +// (sort du pool / des listes ouvertes). NE touche PAS « In Progress » (tech en action). SÉQUENTIEL. +async function closeResolved () { + const p = pool(); if (!p) return { checked: 0, closed: 0 } + const djs = await erp.list('Dispatch Job', { filters: [['legacy_ticket_id', '!=', ''], ['status', 'in', ['open', 'assigned', 'On Hold']]], fields: ['name', 'legacy_ticket_id', 'status'], limit: 5000 }) + if (!djs.length) return { checked: 0, closed: 0 } + const ids = [...new Set(djs.map(j => parseInt(j.legacy_ticket_id)).filter(Boolean))] + const [rows] = await p.query('SELECT id, status FROM ticket WHERE id IN (?)', [ids]) + const st = {}; for (const r of rows) st[String(r.id)] = r.status + let closed = 0; const details = [] + for (const j of djs) { + if (st[j.legacy_ticket_id] === 'closed') { // fermé côté legacy → on retire d'ERPNext + try { await erp.update('Dispatch Job', j.name, { status: 'Completed' }); closed++; details.push({ job: j.name, legacy_ticket_id: j.legacy_ticket_id }) } catch (e) {} + } + } + return { checked: djs.length, closed, details } +} + // Fil COMPLET d'un ticket legacy (description + commentaires/réponses des collaborateurs) — read-only. async function ticketThread (legacyId) { const p = pool(); if (!p) throw new Error('mysql2 indisponible sur le hub') @@ -256,6 +276,7 @@ async function handle (req, res, method, path) { if (path === '/dispatch/legacy-sync/preview' && method === 'GET') return json(res, 200, await sync({ dryRun: true })) if (path === '/dispatch/legacy-sync/run' && method === 'POST') return json(res, 200, await sync({ dryRun: false })) if (path === '/dispatch/legacy-sync/reconcile' && method === 'GET') return json(res, 200, await reconcile()) + if (path === '/dispatch/legacy-sync/close-resolved' && method === 'POST') return json(res, 200, await closeResolved()) if (path === '/dispatch/legacy-sync/ticket-thread' && method === 'GET') { const id = new URL(req.url, 'http://localhost').searchParams.get('id'); return json(res, 200, await ticketThread(id)) } if (path === '/dispatch/legacy-sync/status' && method === 'GET') { // heartbeat pour Uptime-Kuma (keyword "stale":false) const ageMin = _lastRun ? Math.round((Date.now() - Date.parse(_lastRun.at)) / 60000) : null