/** * POST /api/telephony/match-contacts { numbers: string[] } * * Resolves phone numbers (as they appear in the PBX CDR, e.g. "01743479603") * to ERP contacts via the Elasticsearch `ad_user` index (phone/phone2 are * keyword fields, so a regexp on the last digits tolerates the usual * "+49 174 …" / "0174 …" / "0174-…" spellings). Called ASYNCHRONOUSLY by the * dashboard card after it rendered — never on the critical path. Fail-soft: * numbers without a match (or an ES error) simply come back unmatched. */ import elasticHelper from '../../utils/elasticHelper' import { isAdminVerified } from '../../utils/mobileReleases' const digitsOf = (n: any) => String(n ?? '').replace(/\D+/g, '') /** Regexp over a keyword phone field: the significant digits (last 8), any separators in between. */ const phoneRegexp = (digits: string) => { const tail = digits.slice(-8).split('').join('[ /().\\-]?') return `.*${tail}` } export default defineEventHandler(async (event) => { const token = await getTokenHelper(event) if (!token) return { status: 401, matches: {} } if (!(await isAdminVerified(event, token))) return { status: 403, matches: {} } const body: any = await readBody(event).catch(() => ({})) const numbers = [...new Set((Array.isArray(body?.numbers) ? body.numbers : []).map((n: any) => String(n || '').trim()).filter((n: string) => digitsOf(n).length >= 6))].slice(0, 25) const matches: Record = {} await Promise.all(numbers.map(async (n) => { const d = digitsOf(n) try { const res: any = await elasticHelper('ad_user/_search', 'POST', { size: 3, _source: ['ad_user_id', 'name', 'c_bpartner_id', 'phone', 'phone2'], query: { bool: { filter: [{ term: { isactive: true } }], should: [{ regexp: { phone: phoneRegexp(d) } }, { regexp: { phone2: phoneRegexp(d) } }], minimum_should_match: 1 } } }) const hits: any[] = res?.hits?.hits || [] // Prefer an exact digit match over a suffix match. const pick = hits.find((h) => [h._source?.phone, h._source?.phone2].some((p) => digitsOf(p).endsWith(d.slice(-9)))) || hits[0] if (pick?._source?.ad_user_id) matches[n] = { userId: Number(pick._source.ad_user_id), name: String(pick._source.name || ''), partnerId: pick._source.c_bpartner_id ? Number(pick._source.c_bpartner_id) : null } } catch (err: any) { console.warn('[dial] contact match failed for', n, '-', err?.message || err) } })) // Enrich (fail-soft, still off the critical path): lead flag + partner name from // iDempiere so the card can offer "open as lead / user / partner" links. await Promise.all(Object.values(matches).map(async (m) => { try { const u: any = await fetchHelper(event, `models/ad_user/${m.userId}?$select=Name,IsSalesLead,IsVendorLead,C_BPartner_ID`, 'GET', token, null) m.isLead = u?.IsSalesLead === true || u?.IsVendorLead === true if (u?.C_BPartner_ID?.id) { m.partnerId = Number(u.C_BPartner_ID.id); m.partnerName = String(u.C_BPartner_ID.identifier || '') } } catch { /* keep the ES match as is */ } })) return { status: 200, matches } })