Files
oneuptime/Common/Types/Phone.ts

201 lines
6.0 KiB
TypeScript

import DatabaseProperty from "./Database/DatabaseProperty";
import BadDataException from "./Exception/BadDataException";
import { JSONObject, ObjectType } from "./JSON";
import { FindOperator } from "typeorm";
import Zod, { ZodSchema } from "../Utils/Schema/Zod";
export default class Phone extends DatabaseProperty {
private _phone: string = "";
public get phone(): string {
return this._phone;
}
public static pickPhoneNumberToSendSMSOrCallFrom(data: {
to: Phone | string;
primaryPhoneNumberToPickFrom: Phone | string;
secondaryPhoneNumbersToPickFrom: Phone[] | string[];
}): Phone {
/*
* convert all to string, so that we can compare them
* if the country code matches in secondary phone numbers, then pick that number
* if no country code matches, then pick the primary phone number and return it.
*/
const to: string =
typeof data.to === "string" ? data.to : data.to.toString();
const primaryPhoneNumberToPickFrom: string =
typeof data.primaryPhoneNumberToPickFrom === "string"
? data.primaryPhoneNumberToPickFrom
: data.primaryPhoneNumberToPickFrom.toString();
const secondaryPhoneNumbersToPickFrom: string[] =
data.secondaryPhoneNumbersToPickFrom.map((phone: Phone | string) => {
return typeof phone === "string" ? phone : phone.toString();
});
type NormalizePhoneNumberFunction = (phone: string) => string;
const normalizePhoneNumber: NormalizePhoneNumberFunction = (
phone: string,
): string => {
phone = phone.trim();
return phone.startsWith("+") ? phone.substring(1) : phone;
};
const toCountryCode: string = normalizePhoneNumber(to).substring(0, 2);
const primaryPhoneNumberToPickFromCountryCode: string =
normalizePhoneNumber(primaryPhoneNumberToPickFrom).substring(0, 2);
if (toCountryCode === primaryPhoneNumberToPickFromCountryCode) {
return new Phone(primaryPhoneNumberToPickFrom);
}
for (const secondaryPhoneNumber of secondaryPhoneNumbersToPickFrom) {
const secondaryPhoneNumberCountryCode: string = normalizePhoneNumber(
secondaryPhoneNumber,
).substring(0, 2);
if (toCountryCode === secondaryPhoneNumberCountryCode) {
return new Phone(secondaryPhoneNumber);
}
}
return new Phone(primaryPhoneNumberToPickFrom);
}
public set phone(v: string) {
/*
* TODO: Have a valid regex for phone.
* const re: RegExp =
* /^(([^<>()[\].,;:\s@"]+(.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+.)+[^<>()[\].,;:\s@"]{2,})$/i;
* const isValid: boolean = re.test(v);
* if (!isValid) {
* throw new BadDataException('Phone is not in valid format.');
* }
*/
v = v.trim();
const re: RegExp = /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,7}$/; // regex for international phone numbers format based on (ITU-T E.123)
const isValid: boolean = re.test(v);
if (!isValid) {
throw new BadDataException(`Phone is not in valid format: ${v}`);
}
this._phone = v;
}
public constructor(phone: string | Phone) {
super();
if (phone instanceof Phone) {
phone = phone.phone;
}
this.phone = phone;
}
public override toString(): string {
return this.phone;
}
public override toJSON(): JSONObject {
return {
_type: ObjectType.Phone,
value: (this as Phone).toString(),
};
}
public static override fromJSON(json: JSONObject): Phone {
if (json["_type"] === ObjectType.Phone) {
return new Phone((json["value"] as string) || "");
}
throw new BadDataException("Invalid JSON: " + JSON.stringify(json));
}
protected static override toDatabase(
value: Phone | FindOperator<Phone>,
): string | null {
if (value) {
if (typeof value === "string") {
value = new Phone(value);
}
return value.toString();
}
return null;
}
protected static override fromDatabase(_value: string): Phone | null {
if (_value) {
return new Phone(_value);
}
return null;
}
public static override getSchema(): ZodSchema {
return Zod.object({
_type: Zod.literal(ObjectType.Phone),
value: Zod.string().openapi({
type: "string",
example: "+1-555-123-4567",
}),
}).openapi({
type: "object",
description: "Phone object",
example: { _type: ObjectType.Phone, value: "+1-555-123-4567" },
});
}
// Map of calling code prefixes to ISO country codes
private static readonly CALLING_CODE_TO_COUNTRY_MAP: Record<string, string> =
{
"+1": "US", // US and Canada
"+44": "GB", // United Kingdom
"+61": "AU", // Australia
"+49": "DE", // Germany
"+33": "FR", // France
"+91": "IN", // India
"+81": "JP", // Japan
"+86": "CN", // China
"+55": "BR", // Brazil
"+52": "MX", // Mexico
};
/**
* Extract ISO country code from a phone number
* @param phoneNumber - The phone number (e.g., "+15551234567")
* @returns ISO country code (e.g., "US") or "US" as default
*/
public static getCountryCodeFromPhoneNumber(phoneNumber: string): string {
for (const [prefix, countryCode] of Object.entries(
Phone.CALLING_CODE_TO_COUNTRY_MAP,
)) {
if (phoneNumber.startsWith(prefix)) {
return countryCode;
}
}
return "US"; // Default to US if unknown
}
/**
* Extract area code from a phone number
* Currently only supports US/Canada (+1) numbers
* @param phoneNumber - The phone number (e.g., "+15551234567")
* @returns Area code (e.g., "555") or empty string for non-US/CA numbers
*/
public static getAreaCodeFromPhoneNumber(phoneNumber: string): string {
// For US/Canada numbers (+1), extract the next 3 digits
if (phoneNumber.startsWith("+1")) {
const number: string = phoneNumber.substring(2);
if (number.length >= 3) {
return number.substring(0, 3);
}
}
// For other countries, area code extraction is more complex
return "";
}
}