/* eslint-disable camelcase,@typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any */ import querystring from "querystring"; import Wreck from "@hapi/wreck"; export interface User { id: number; firstname?: string; lastname?: string; email?: string; phone?: string; } export interface Ticket { id: number; title?: string; group_id?: number; customer_id?: number; } export interface ZammadClient { ticket: { create: (data: any) => Promise; update: (id: number, data: any) => Promise; }; user: { search: (data: any) => Promise; create: (data: any) => Promise; }; get: (path: string) => Promise; } export type ZammadCredentials = | { username: string; password: string } | { token: string }; export interface ZammadClientOpts { headers?: Record; } const formatAuth = (credentials: any) => { if (credentials.username) { return ( "Basic " + Buffer.from(`${credentials.username}:${credentials.password}`).toString( "base64", ) ); } if (credentials.token) { return `Token ${credentials.token}`; } throw new Error("invalid zammad credentials type"); }; export const Zammad = ( credentials: ZammadCredentials, host: string, opts?: ZammadClientOpts, ): ZammadClient => { const extraHeaders = (opts && opts.headers) || {}; const wreck = Wreck.defaults({ baseUrl: `${host}/api/v1/`, headers: { authorization: formatAuth(credentials), ...extraHeaders, }, json: true, }); return { ticket: { create: async (payload) => { const { payload: result } = await wreck.post("tickets", { payload }); return result as Ticket; }, update: async (id, payload) => { const { payload: result } = await wreck.put(`tickets/${id}`, { payload, }); return result as Ticket; }, }, user: { search: async (query) => { const qp = querystring.stringify({ query }); const { payload: result } = await wreck.get(`users/search?${qp}`); return result as User[]; }, create: async (payload) => { const { payload: result } = await wreck.post("users", { payload }); return result as User; }, }, get: async (path) => { const { payload: result } = await wreck.get(path); return result; }, }; }; /** * Sanitizes phone number to E.164 format: +15554446666 * Strips all non-digit characters except +, ensures + prefix * @param phoneNumber - Raw phone number (e.g., "(555) 444-6666", "5554446666", "+1 555 444 6666") * @returns E.164 formatted phone number (e.g., "+15554446666") * @throws Error if phone number is invalid */ export const sanitizePhoneNumber = (phoneNumber: string): string => { // Remove all characters except digits and + let cleaned = phoneNumber.replace(/[^\d+]/g, ""); // Ensure it starts with + if (!cleaned.startsWith("+")) { // Assume US/Canada if no country code (11 digits starting with 1, or 10 digits) if (cleaned.length === 10) { cleaned = "+1" + cleaned; } else if (cleaned.length === 11 && cleaned.startsWith("1")) { cleaned = "+" + cleaned; } else if (cleaned.length >= 10) { // International number without +, add it cleaned = "+" + cleaned; } } // Validate E.164 format: + followed by 10-15 digits if (!/^\+\d{10,15}$/.test(cleaned)) { throw new Error(`Invalid phone number format: ${phoneNumber}`); } return cleaned; }; export const getUser = async (zammad: ZammadClient, phoneNumber: string) => { // Sanitize to E.164 format const sanitized = sanitizePhoneNumber(phoneNumber); // Remove + for Zammad search query const searchNumber = sanitized.replace("+", ""); // Try sanitized format first (e.g., "6464229653" for "+16464229653") let results = await zammad.user.search(`phone:${searchNumber}`); if (results.length > 0) return results[0]; // Fall back to searching for original input (handles legacy formatted numbers) // This ensures we can find users with "(646) 422-9653" format in database const originalCleaned = phoneNumber.replace(/[^\d+]/g, "").replace("+", ""); if (originalCleaned !== searchNumber) { results = await zammad.user.search(`phone:${originalCleaned}`); if (results.length > 0) return results[0]; } return undefined; }; export const getOrCreateUser = async ( zammad: ZammadClient, phoneNumber: string, ) => { const customer = await getUser(zammad, phoneNumber); if (customer) return customer; // Sanitize phone number to E.164 format before storing const sanitized = sanitizePhoneNumber(phoneNumber); return zammad.user.create({ phone: sanitized, note: "User created from incoming voice call", }); };