Skip to content

Commit 08cc4e9

Browse files
committed
Add SmsService with multi-provider support and update VerificationCode
- Add static sendSms() convenience method to CallProSmsService - Create SmsService with automatic provider routing based on phone prefix - Support explicit provider selection (twilio/callpro) - Automatic fallback from CallPro to Twilio if not configured - Smart routing: +976 numbers route to CallPro, others to Twilio - Update VerificationCode model to use SmsService instead of direct Twilio - Maintain backward compatibility with existing Twilio parameters - Add comprehensive logging and error handling
1 parent 4c7eb26 commit 08cc4e9

3 files changed

Lines changed: 360 additions & 4 deletions

File tree

‎src/Models/VerificationCode.php‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Fleetbase\Traits\HasMetaAttributes;
1010
use Fleetbase\Traits\HasSubject;
1111
use Fleetbase\Traits\HasUuid;
12+
use Fleetbase\Services\SmsService;
1213
use Fleetbase\Twilio\Support\Laravel\Facade as Twilio;
1314
use Illuminate\Support\Carbon;
1415
use Illuminate\Support\Facades\Mail;
@@ -180,9 +181,10 @@ public static function generateSmsVerificationFor($subject, $for = 'phone_verifi
180181
$message = $messageCallback($verificationCode);
181182
}
182183

183-
// Twilio params
184-
$twilioParams = [];
184+
// SMS service options
185+
$smsOptions = [];
185186

187+
// Check for company-specific sender ID
186188
$companyUuid = data_get($options, 'company_uuid') ?? session('company') ?? data_get($subject, 'company_uuid');
187189
if ($companyUuid) {
188190
$company = Company::select(['uuid', 'options'])->find($companyUuid);
@@ -192,13 +194,32 @@ public static function generateSmsVerificationFor($subject, $for = 'phone_verifi
192194
$senderId = $company->getOption('alpha_numeric_sender_id');
193195

194196
if ($enabled && !empty($senderId)) {
195-
$twilioParams['from'] = $senderId;
197+
$smsOptions['from'] = $senderId;
198+
$smsOptions['twilioParams']['from'] = $senderId;
196199
}
197200
}
198201
}
199202

203+
// Allow explicit provider selection
204+
$provider = data_get($options, 'provider');
205+
206+
// Send SMS using SmsService with automatic provider routing
200207
if ($subject->phone) {
201-
Twilio::message($subject->phone, $message, [], $twilioParams);
208+
try {
209+
$smsService = new SmsService();
210+
$smsService->send($subject->phone, $message, $smsOptions, $provider);
211+
} catch (\Throwable $e) {
212+
// Log error but don't fail the verification code generation
213+
\Illuminate\Support\Facades\Log::error('Failed to send SMS verification', [
214+
'phone' => $subject->phone,
215+
'error' => $e->getMessage(),
216+
]);
217+
218+
// Optionally rethrow based on configuration
219+
if (config('sms.throw_on_error', false)) {
220+
throw $e;
221+
}
222+
}
202223
}
203224

204225
return $verificationCode;

‎src/Services/CallProSmsService.php‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ public function __construct()
2828
]);
2929
}
3030

31+
/**
32+
* Send an SMS message (static convenience method).
33+
*
34+
* @param string $to Recipient phone number (8 digits)
35+
* @param string $text Message text (max 160 characters)
36+
* @param string|null $from Optional sender ID (8 characters), defaults to config
37+
*
38+
* @return array Response containing status and message ID
39+
*
40+
* @throws \Exception If API request fails
41+
*/
42+
public static function sendSms(string $to, string $text, ?string $from = null): array
43+
{
44+
$instance = new static();
45+
46+
return $instance->send($to, $text, $from);
47+
}
48+
3149
/**
3250
* Send an SMS message.
3351
*

‎src/Services/SmsService.php‎

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
<?php
2+
3+
namespace Fleetbase\Services;
4+
5+
use Fleetbase\Twilio\Support\Laravel\Facade as Twilio;
6+
use Illuminate\Support\Facades\Log;
7+
use Illuminate\Support\Str;
8+
9+
class SmsService
10+
{
11+
/**
12+
* Available SMS providers.
13+
*/
14+
public const PROVIDER_TWILIO = 'twilio';
15+
public const PROVIDER_CALLPRO = 'callpro';
16+
17+
/**
18+
* Default SMS provider.
19+
*/
20+
protected string $defaultProvider;
21+
22+
/**
23+
* Provider routing rules based on phone number prefixes.
24+
*
25+
* @var array
26+
*/
27+
protected array $routingRules = [];
28+
29+
/**
30+
* Create a new SmsService instance.
31+
*/
32+
public function __construct()
33+
{
34+
$this->defaultProvider = config('sms.default_provider', self::PROVIDER_TWILIO);
35+
36+
// Load routing rules from config or use defaults
37+
$this->routingRules = config('sms.routing_rules', [
38+
'+976' => self::PROVIDER_CALLPRO, // Mongolia numbers route to CallPro
39+
]);
40+
41+
Log::info('SmsService initialized', [
42+
'default_provider' => $this->defaultProvider,
43+
'routing_rules' => $this->routingRules,
44+
]);
45+
}
46+
47+
/**
48+
* Send an SMS message with automatic provider selection.
49+
*
50+
* @param string $to Recipient phone number
51+
* @param string $text Message text
52+
* @param array $options Additional options (from, provider, twilioParams, etc.)
53+
* @param string|null $provider Explicitly specify provider (overrides auto-routing)
54+
*
55+
* @return array Response containing status and provider information
56+
*
57+
* @throws \Exception If SMS sending fails
58+
*/
59+
public function send(string $to, string $text, array $options = [], ?string $provider = null): array
60+
{
61+
// Normalize phone number
62+
$normalizedPhone = $this->normalizePhoneNumber($to);
63+
64+
// Determine provider
65+
$selectedProvider = $provider ?? $this->determineProvider($normalizedPhone);
66+
67+
Log::info('Sending SMS', [
68+
'to' => $to,
69+
'provider' => $selectedProvider,
70+
'text' => substr($text, 0, 50) . (strlen($text) > 50 ? '...' : ''),
71+
]);
72+
73+
try {
74+
$result = match ($selectedProvider) {
75+
self::PROVIDER_CALLPRO => $this->sendViaCallPro($normalizedPhone, $text, $options),
76+
self::PROVIDER_TWILIO => $this->sendViaTwilio($normalizedPhone, $text, $options),
77+
default => throw new \InvalidArgumentException("Unsupported SMS provider: {$selectedProvider}"),
78+
};
79+
80+
$result['provider'] = $selectedProvider;
81+
82+
Log::info('SMS sent successfully', [
83+
'provider' => $selectedProvider,
84+
'to' => $to,
85+
]);
86+
87+
return $result;
88+
} catch (\Throwable $e) {
89+
Log::error('SMS sending failed', [
90+
'provider' => $selectedProvider,
91+
'to' => $to,
92+
'error' => $e->getMessage(),
93+
]);
94+
95+
throw $e;
96+
}
97+
}
98+
99+
/**
100+
* Send SMS via CallPro/MessagePro.mn.
101+
*
102+
* @param string $to Recipient phone number
103+
* @param string $text Message text
104+
* @param array $options Additional options
105+
*
106+
* @return array Response from CallPro service
107+
*
108+
* @throws \Exception If CallPro is not configured or sending fails
109+
*/
110+
protected function sendViaCallPro(string $to, string $text, array $options = []): array
111+
{
112+
$callProService = new CallProSmsService();
113+
114+
if (!$callProService->isConfigured()) {
115+
Log::warning('CallPro not configured, falling back to Twilio');
116+
117+
return $this->sendViaTwilio($to, $text, $options);
118+
}
119+
120+
// Extract the last 8 digits for CallPro (Mongolia format)
121+
$toNumber = $this->extractCallProNumber($to);
122+
123+
// Get from number from options or use default
124+
$from = data_get($options, 'from');
125+
126+
return $callProService->send($toNumber, $text, $from);
127+
}
128+
129+
/**
130+
* Send SMS via Twilio.
131+
*
132+
* @param string $to Recipient phone number
133+
* @param string $text Message text
134+
* @param array $options Additional options (twilioParams, from, etc.)
135+
*
136+
* @return array Response from Twilio service
137+
*
138+
* @throws \Exception If Twilio sending fails
139+
*/
140+
protected function sendViaTwilio(string $to, string $text, array $options = []): array
141+
{
142+
$twilioParams = data_get($options, 'twilioParams', []);
143+
144+
// Support 'from' in options
145+
if (isset($options['from']) && !isset($twilioParams['from'])) {
146+
$twilioParams['from'] = $options['from'];
147+
}
148+
149+
try {
150+
$response = Twilio::message($to, $text, [], $twilioParams);
151+
152+
return [
153+
'success' => true,
154+
'message_id' => $response->sid ?? null,
155+
'result' => 'SUCCESS',
156+
'response' => $response,
157+
];
158+
} catch (\Throwable $e) {
159+
return [
160+
'success' => false,
161+
'error' => $e->getMessage(),
162+
'code' => $e->getCode(),
163+
];
164+
}
165+
}
166+
167+
/**
168+
* Determine which provider to use based on phone number.
169+
*
170+
* @param string $phoneNumber Normalized phone number
171+
*
172+
* @return string Provider identifier
173+
*/
174+
protected function determineProvider(string $phoneNumber): string
175+
{
176+
// Check routing rules
177+
foreach ($this->routingRules as $prefix => $provider) {
178+
if (Str::startsWith($phoneNumber, $prefix)) {
179+
Log::debug('Phone number matched routing rule', [
180+
'phone' => $phoneNumber,
181+
'prefix' => $prefix,
182+
'provider' => $provider,
183+
]);
184+
185+
return $provider;
186+
}
187+
}
188+
189+
// Return default provider
190+
return $this->defaultProvider;
191+
}
192+
193+
/**
194+
* Normalize phone number to E.164 format if possible.
195+
*
196+
* @param string $phoneNumber Raw phone number
197+
*
198+
* @return string Normalized phone number
199+
*/
200+
protected function normalizePhoneNumber(string $phoneNumber): string
201+
{
202+
// Remove all non-numeric characters except +
203+
$normalized = preg_replace('/[^0-9+]/', '', $phoneNumber);
204+
205+
// Ensure + prefix for international numbers
206+
if (!Str::startsWith($normalized, '+') && strlen($normalized) > 10) {
207+
$normalized = '+' . $normalized;
208+
}
209+
210+
return $normalized;
211+
}
212+
213+
/**
214+
* Extract 8-digit number for CallPro (Mongolia format).
215+
*
216+
* @param string $phoneNumber Full phone number
217+
*
218+
* @return string 8-digit number
219+
*/
220+
protected function extractCallProNumber(string $phoneNumber): string
221+
{
222+
// Remove + and country code, get last 8 digits
223+
$digits = preg_replace('/[^0-9]/', '', $phoneNumber);
224+
225+
return substr($digits, -8);
226+
}
227+
228+
/**
229+
* Get available providers.
230+
*
231+
* @return array List of available providers
232+
*/
233+
public function getAvailableProviders(): array
234+
{
235+
$providers = [
236+
self::PROVIDER_TWILIO => [
237+
'name' => 'Twilio',
238+
'available' => true, // Twilio is always available if configured
239+
],
240+
];
241+
242+
$callProService = new CallProSmsService();
243+
$providers[self::PROVIDER_CALLPRO] = [
244+
'name' => 'CallPro/MessagePro.mn',
245+
'available' => $callProService->isConfigured(),
246+
];
247+
248+
return $providers;
249+
}
250+
251+
/**
252+
* Add a routing rule for phone number prefix.
253+
*
254+
* @param string $prefix Phone number prefix (e.g., '+976')
255+
* @param string $provider Provider identifier
256+
*/
257+
public function addRoutingRule(string $prefix, string $provider): void
258+
{
259+
$this->routingRules[$prefix] = $provider;
260+
261+
Log::info('Routing rule added', [
262+
'prefix' => $prefix,
263+
'provider' => $provider,
264+
]);
265+
}
266+
267+
/**
268+
* Get current routing rules.
269+
*
270+
* @return array Routing rules
271+
*/
272+
public function getRoutingRules(): array
273+
{
274+
return $this->routingRules;
275+
}
276+
277+
/**
278+
* Set default provider.
279+
*
280+
* @param string $provider Provider identifier
281+
*/
282+
public function setDefaultProvider(string $provider): void
283+
{
284+
$this->defaultProvider = $provider;
285+
286+
Log::info('Default provider changed', ['provider' => $provider]);
287+
}
288+
289+
/**
290+
* Get default provider.
291+
*
292+
* @return string Provider identifier
293+
*/
294+
public function getDefaultProvider(): string
295+
{
296+
return $this->defaultProvider;
297+
}
298+
299+
/**
300+
* Static convenience method to send SMS.
301+
*
302+
* @param string $to Recipient phone number
303+
* @param string $text Message text
304+
* @param array $options Additional options
305+
* @param string|null $provider Explicitly specify provider
306+
*
307+
* @return array Response containing status and provider information
308+
*
309+
* @throws \Exception If SMS sending fails
310+
*/
311+
public static function sendSms(string $to, string $text, array $options = [], ?string $provider = null): array
312+
{
313+
$instance = new static();
314+
315+
return $instance->send($to, $text, $options, $provider);
316+
}
317+
}

0 commit comments

Comments
 (0)