mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 00:47:37 +00:00
78 lines
3.3 KiB
TypeScript
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}`;
|
|
}
|