mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
201 lines
6.0 KiB
TypeScript
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 "";
|
|
}
|
|
}
|