Fix phone sanitization and signal group lookup

This commit is contained in:
Darren Clarke 2025-11-20 13:12:56 +01:00
parent d83c1af258
commit 31a3b505af
3 changed files with 81 additions and 17 deletions

View file

@ -100,19 +100,57 @@ export const Zammad = (
};
};
export const getUser = async (zammad: ZammadClient, phoneNumber: string) => {
// Sanitize phone number: only allow digits and + symbol
const mungedNumber = phoneNumber.replace(/[^\d+]/g, "");
/**
* 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, "");
// Validate phone number format (10-15 digits, optional + prefix)
if (!/^\+?\d{10,15}$/.test(mungedNumber)) {
// 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}`);
}
// Remove + for search query
const searchNumber = mungedNumber.replace("+", "");
const results = await zammad.user.search(`phone:${searchNumber}`);
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;
};
@ -123,8 +161,11 @@ export const getOrCreateUser = async (
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: phoneNumber,
phone: sanitized,
note: "User created from incoming voice call",
});
};

View file

@ -1,6 +1,6 @@
import { createLogger } from "@link-stack/logger";
import { db } from "@link-stack/bridge-common";
import { Zammad, getUser } from "../../lib/zammad.js";
import { Zammad, getUser, sanitizePhoneNumber } from "../../lib/zammad.js";
import {
loadFieldMapping,
getFieldValue,
@ -55,12 +55,35 @@ const createTicketFromFormTask = async (
// Extract well-known fields used for special logic (all optional)
const email = getFieldValue(formData, "email", mapping);
const phone = getFieldValue(formData, "phone", mapping);
const signalAccount = getFieldValue(formData, "signalAccount", mapping);
const rawPhone = getFieldValue(formData, "phone", mapping);
const rawSignalAccount = getFieldValue(formData, "signalAccount", mapping);
const organization = getFieldValue(formData, "organization", mapping);
const typeOfSupport = getFieldValue(formData, "typeOfSupport", mapping);
const descriptionOfIssue = getFieldValue(formData, "descriptionOfIssue", mapping);
// Sanitize phone numbers to E.164 format (+15554446666)
let phone: string | undefined;
if (rawPhone) {
try {
phone = sanitizePhoneNumber(rawPhone);
logger.info({ rawPhone, sanitized: phone }, "Sanitized phone number");
} catch (error: any) {
logger.warn({ rawPhone, error: error.message }, "Invalid phone number format, ignoring");
phone = undefined;
}
}
let signalAccount: string | undefined;
if (rawSignalAccount) {
try {
signalAccount = sanitizePhoneNumber(rawSignalAccount);
logger.info({ rawSignalAccount, sanitized: signalAccount }, "Sanitized signal account");
} catch (error: any) {
logger.warn({ rawSignalAccount, error: error.message }, "Invalid signal account format, ignoring");
signalAccount = undefined;
}
}
// Validate that at least one contact method is provided
if (!email && !phone && !signalAccount) {
logger.error(