Files
edr-platform/apps/edr-passenger-api/src/common/utils/phone.utils.ts

78 lines
3.3 KiB
TypeScript

/**
* Phone normalisation shared by any lookup that has to match a number a customer typed
* against one already stored. Ethiopian numbers reach us in three interchangeable shapes
* (+2519…, 2519…, 09…) depending on whether they came from IAM, a guest booking form or a
* saved profile, so an exact-string match silently misses.
*/
/**
* Returns all plausible normalised variants of a raw phone string so that the
* DB query matches regardless of how the number was stored (local 09… vs international +251…).
* Returns an empty array when the input is clearly invalid (< 7 digits).
*/
export function normalizePhoneVariants(raw: string): string[] {
// Strip whitespace, dashes, dots, parentheses — keep digits and a leading +
const stripped = raw.replace(/[^\d+]/g, '');
const digits = stripped.replace(/^\+/, '');
if (digits.length < 7) return [];
const variants = new Set<string>([stripped]);
if (stripped.startsWith('+251') && digits.length === 12) {
// +251 9XXXXXXXX → 251 9XXXXXXXX (no +) → 09XXXXXXXX
variants.add(digits); // 251XXXXXXXXX
variants.add('0' + digits.slice(3)); // 09XXXXXXXXX
} else if (stripped.startsWith('251') && digits.length === 12) {
// 251 9XXXXXXXX → +251 9XXXXXXXX → 09XXXXXXXX
variants.add('+' + stripped); // +251XXXXXXXXX
variants.add('0' + digits.slice(3)); // 09XXXXXXXXX
} else if (stripped.startsWith('0') && digits.length === 10) {
// 09XXXXXXXX → +251 9XXXXXXXX → 251 9XXXXXXXX (no +)
variants.add('+251' + digits.slice(1)); // +251XXXXXXXXX
variants.add('251' + digits.slice(1)); // 251XXXXXXXXX
} else if (!stripped.startsWith('+') && digits.length >= 9) {
// bare international digits without +
variants.add('+' + digits);
}
return [...variants];
}
/**
* A sign-in identifier is a single free-text field: the passenger types either an email
* address or a phone number and the server works out which. Phone is the default reading —
* an email must contain an `@` with something either side of it, everything else is treated
* as a number so that malformed emails don't silently fall through to a phone lookup that
* can never match.
*/
export type ResolvedIdentifier = {
kind: 'email' | 'phone';
/** Lower-cased email, or null when the input is a phone number. */
email: string | null;
/** Every stored shape the number could have, or [] when the input is an email. */
phoneVariants: string[];
};
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function resolveIdentifier(raw: string): ResolvedIdentifier {
const trimmed = raw.trim();
if (EMAIL_RE.test(trimmed)) {
return { kind: 'email', email: trimmed.toLowerCase(), phoneVariants: [] };
}
return { kind: 'phone', email: null, phoneVariants: normalizePhoneVariants(trimmed) };
}
/**
* `+251912345678` → `+2519****678`. Shown on the OTP screen so the passenger can tell which
* number the code went to without the server handing back the full number to an unauthenticated
* caller.
*/
export function maskPhone(phone: string): string {
const stripped = phone.replace(/[^\d+]/g, '');
if (stripped.length <= 7) return stripped;
const head = stripped.slice(0, stripped.startsWith('+') ? 5 : 4);
const tail = stripped.slice(-3);
return `${head}${'*'.repeat(4)}${tail}`;
}