fix(otp): fall back to email for foreign phone numbers

The SMS gateway is domestic-only, but OTP sends fanned out to any phone
on the account - a foreign number meant a code queued into the void
while the response claimed success. isDomesticPhone (+2519/+2517 E.164)
now gates SMS: dual-channel sends with a foreign phone go email-only
(the phone stays on the row so verify still matches it), and a
phone-only foreign target still tries SMS as the only route. The
staff-triggered reset exposes phoneIsDomestic so the backoffice disables
the SMS channel with an explanation, and the API refuses the channel
directly for foreign numbers.

EDRFREIGHT-186
This commit is contained in:
Nathnael
2026-07-21 09:09:45 +00:00
parent 4afffca3d2
commit 649316070d
5 changed files with 91 additions and 5 deletions

View File

@@ -61,8 +61,11 @@ export default function ResetPasswordAction({
if (!allowed) return null;
// SMS is domestic-only: a foreign number counts as unavailable, same as a
// missing one, so staff can't send a link that will never arrive.
const phoneUsable = !!target?.phone && target.phoneIsDomestic !== false;
const channelMissing =
!!target && (channel === "email" ? !target.email : !target.phone);
!!target && (channel === "email" ? !target.email : !phoneUsable);
return (
<>
@@ -106,9 +109,13 @@ export default function ResetPasswordAction({
<Radio
value="phone"
label="SMS"
disabled={!target.phone}
disabled={!phoneUsable}
description={
target.phone ?? "No phone number on this account"
!target.phone
? "No phone number on this account"
: target.phoneIsDomestic === false
? `${target.phone} — foreign number, SMS unavailable; use email`
: target.phone
}
/>
<Radio

View File

@@ -131,6 +131,8 @@ export interface CustomerResetTarget {
name: string;
email: string | null;
phone: string | null;
/** SMS gateway is domestic-only; `false` means SMS can't reach this phone. `null` = no phone. */
phoneIsDomestic: boolean | null;
}
/** Mirrors backend `Company` (+ its `companyProfiles`). */