feat(dispatch): provisioning identites depuis legacy staff (convergence ledger)
provisionIdentities({dryRun,scope}) : pour chaque staff du ledger, garantit une
identite canonique portant legacy_staff_id (+ tech_id si Dispatch Technician, donc
les admins restent non-tech). LINK (identite existante par courriel/nom -> remplit
legacy_staff_id) ou CREATE (depuis staff si courriel). Idempotent. Route
GET|POST /dispatch/legacy-sync/tech-history/provision-identities?scope=ledger|active.
Applique en prod: +33 identites liees a leur legacy_staff_id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
60da7a01da
commit
e98b21ba0a
|
|
@ -1994,6 +1994,9 @@ async function handle (req, res, method, path) {
|
|||
const u = new URL(req.url, 'http://localhost')
|
||||
return json(res, 200, await techHistoryBackfill({ dryRun: method === 'GET', reset: u.searchParams.get('reset') === '1', maxBatches: Number(u.searchParams.get('max')) || 40, batch: Number(u.searchParams.get('batch')) || 5000 }))
|
||||
}
|
||||
if (path === '/dispatch/legacy-sync/tech-history/provision-identities' && (method === 'GET' || method === 'POST')) { // GET=dry-run · POST=applique. Lie legacy_staff_id aux identités (crée si absente) pour faire converger le ledger. ?scope=ledger|active
|
||||
const u = new URL(req.url, 'http://localhost'); return json(res, 200, await provisionIdentities({ dryRun: method === 'GET', scope: u.searchParams.get('scope') || 'ledger' }))
|
||||
}
|
||||
if (path === '/dispatch/legacy-sync/tech-sync' && method === 'GET') return json(res, 200, await techSyncReport()) // rapport réconciliation techs (3 systèmes)
|
||||
if (path === '/dispatch/legacy-sync/mine-durations' && method === 'GET') return json(res, 200, await mineDurations()) // baseline durées apprises (proxy réponses staff)
|
||||
if (path === '/dispatch/legacy-sync/tech-sync/apply' && method === 'POST') { // crée les fiches Dispatch Technician choisies (ERPNext only)
|
||||
|
|
@ -2617,4 +2620,34 @@ async function techHistory ({ tech = '', limit = 200 } = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = { handle, sync, reimportAddresses, fillMissingCoords, fixGeocoding, purgeStaleOrphans, pushAssignments, returnToPool, postTicketLegacy, ticketThread, watchLegacy, techSyncReport, techSyncApply, mineDurations, legacyTechTickets, legacyWindowLoad, ingestAssigned, reconcileLegacyJobs, backfillServiceLocations, backfillIssueCustomers, backfillSourceIssue, backfillDependsOn, startSync, stopSync, fetchTargoTickets, staleTickets, staleNudge, publishPreview, publishJob, techHistoryBackfill, techHistory, coord, prio, startTime, jobType, inTerritory, geocodeRQA, persistGeocodeToSL, reverseNearestFibre } // parseurs purs exposés pour les tests
|
||||
// PROVISIONING IDENTITÉS depuis le legacy staff → fait CONVERGER le ledger. Pour chaque staff apparaissant dans les
|
||||
// assignations : garantit une identité canonique portant son legacy_staff_id (+ tech_id SI Dispatch Technician → les
|
||||
// admins restent SANS tech_id). LINK (identité trouvée par courriel/nom → renseigne legacy_staff_id) ou CREATE (aucune →
|
||||
// crée depuis le staff). Idempotent. dryRun => 0 écriture. scope 'ledger' (défaut : staff du ledger) | 'active' (tout staff actif).
|
||||
async function provisionIdentities ({ dryRun = true, scope = 'ledger' } = {}) {
|
||||
const p = pool(); const pg = techHistPg(); const idn = require('./identity')
|
||||
let staffIds = []
|
||||
if (scope === 'active') { const [r] = await p.query('SELECT id FROM staff WHERE status=1'); staffIds = r.map(x => x.id) } else { const r = await pg.query('SELECT DISTINCT to_staff_id FROM ops_tech_ticket_history WHERE to_staff_id IS NOT NULL'); staffIds = r.rows.map(x => x.to_staff_id) }
|
||||
if (!staffIds.length) return { ok: true, scope, targets: 0, linked: 0, created: 0, already: 0, no_email: [], samples: [] }
|
||||
const [staff] = await p.query('SELECT id, first_name, last_name, email, status FROM staff WHERE id IN (?)', [staffIds])
|
||||
const byId = new Map(staff.map(s => [String(s.id), s]))
|
||||
const techs = await erp.list('Dispatch Technician', { fields: ['name', 'technician_id'], limit: 1000 })
|
||||
const techByStaff = {}; for (const t of (techs || [])) { const m = String(t.technician_id || '').match(/(\d+)$/); if (m) techByStaff[m[1]] = t.technician_id || t.name }
|
||||
const res = { ok: true, scope, dryRun, targets: staffIds.length, linked: 0, created: 0, already: 0, no_email: [], samples: [] }
|
||||
for (const sid of staffIds) {
|
||||
const s = byId.get(String(sid)); if (!s) continue
|
||||
const full = [s.first_name, s.last_name].filter(Boolean).join(' ').trim(); const email = norm(s.email); const techId = techByStaff[String(sid)] || ''
|
||||
const existing = idn.resolveIdentity(String(sid))
|
||||
if (existing && String(existing.legacy_staff_id) === String(sid)) {
|
||||
if (techId && !existing.tech_id) { if (!dryRun) idn.upsert({ key: existing.key, label: existing.label, primary_email: existing.primary_email, legacy_staff_id: sid, tech_id: techId }); res.linked++; if (res.samples.length < 30) res.samples.push({ staff: sid, action: 'fill-tech', key: existing.key }) } else res.already++
|
||||
continue
|
||||
}
|
||||
const cand = (email && idn.resolveIdentity(email)) || (full && idn.resolveIdentity(full)) || null
|
||||
if (cand) { if (!dryRun) idn.upsert({ key: cand.key, label: cand.label, primary_email: cand.primary_email, legacy_staff_id: sid, tech_id: techId || cand.tech_id }); res.linked++; if (res.samples.length < 30) res.samples.push({ staff: sid, name: full, action: 'link', key: cand.key, tech: !!techId }); continue }
|
||||
if (email) { if (!dryRun) idn.upsert({ label: full || email, primary_email: email, legacy_staff_id: sid, tech_id: techId, kind: 'employee', active: Number(s.status) === 1 }); res.created++; if (res.samples.length < 30) res.samples.push({ staff: sid, name: full, action: 'create', email, tech: !!techId }); continue }
|
||||
res.no_email.push({ staff: sid, name: full })
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
module.exports = { handle, sync, reimportAddresses, fillMissingCoords, fixGeocoding, purgeStaleOrphans, pushAssignments, returnToPool, postTicketLegacy, ticketThread, watchLegacy, techSyncReport, techSyncApply, mineDurations, legacyTechTickets, legacyWindowLoad, ingestAssigned, reconcileLegacyJobs, backfillServiceLocations, backfillIssueCustomers, backfillSourceIssue, backfillDependsOn, 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user