Nabeh gateway * OTP_PROVIDER=whatsapp -> legacy self-hosted WhatsApp bots * * Callers pass the code, not a rendered message: Nabeh composes its own message * around the code, while the WhatsApp bots need finished text. Keeping that * difference here means call sites do not care which channel is active. * * With OTP_FALLBACK_ENABLED on, a failure is retried on the other channel * before the login is refused - losing OTP delivery locks out every user. */ class OtpSender { public static function sendOtp(string $phone, string $otp): array { $provider = strtolower(trim((string)env('OTP_PROVIDER', 'whatsapp'))); $fallback = strtolower((string)env('OTP_FALLBACK_ENABLED', 'true')) === 'true'; $result = self::dispatch($provider, $phone, $otp); $result['provider'] = $provider; if ($result['success'] || !$fallback) { return $result; } $alternate = $provider === 'nabeh' ? 'whatsapp' : 'nabeh'; error_log(sprintf( '[OTP] primary channel "%s" failed, falling back to "%s" | error=%s', $provider, $alternate, $result['error'] ?? substr((string)($result['raw_response'] ?? ''), 0, 200) )); $fallbackResult = self::dispatch($alternate, $phone, $otp); $fallbackResult['provider'] = $alternate; $fallbackResult['primary_error'] = $result['error'] ?? null; return $fallbackResult; } private static function dispatch(string $channel, string $phone, string $otp): array { if ($channel === 'nabeh') { return (new NabehOtpService())->sendOtp($phone, $otp); } $appName = (string)env('NABEH_APP_NAME', 'مُصادَق'); $message = "رمز التحقق لتطبيق {$appName}:\n*{$otp}*\n\nصالح لمدة 5 دقائق."; return (new WhatsAppProxyService())->sendMessage($phone, $message); } }