Skip to content

Commit e17f095

Browse files
committed
Update CallPro SMS send integration
1 parent fe2ca71 commit e17f095

3 files changed

Lines changed: 226 additions & 40 deletions

File tree

src/Services/CallProSmsService.php

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,38 +31,46 @@ public function __construct()
3131
/**
3232
* Send an SMS message (static convenience method).
3333
*
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
34+
* @param string $to Recipient phone number
35+
* @param string $text Message text
36+
* @param string|null $from Optional sender number, defaults to config
37+
* @param array $options Optional CallPro parameters
3738
*
3839
* @return array Response containing status and message ID
3940
*
4041
* @throws \Exception If API request fails
4142
*/
42-
public static function sendSms(string $to, string $text, ?string $from = null): array
43+
public static function sendSms(string $to, string $text, ?string $from = null, array $options = []): array
4344
{
4445
$instance = new static();
4546

46-
return $instance->send($to, $text, $from);
47+
return $instance->send($to, $text, $from, $options);
4748
}
4849

4950
/**
5051
* Send an SMS message.
5152
*
52-
* @param string $to Recipient phone number (8 digits)
53-
* @param string $text Message text (max 160 characters)
54-
* @param string|null $from Optional sender ID (8 characters), defaults to config
53+
* @param string $to Recipient phone number
54+
* @param string $text Message text
55+
* @param string|null $from Optional sender number, defaults to config
56+
* @param array $options Optional CallPro parameters
5557
*
5658
* @return array Response containing status and message ID
5759
*
5860
* @throws \Exception If API request fails
5961
*/
60-
public function send(string $to, string $text, ?string $from = null): array
62+
public function send(string $to, string $text, ?string $from = null, array $options = []): array
6163
{
6264
$from = $from ?? $this->from;
6365

64-
// Validate parameters
6566
$this->validateParameters($to, $text, $from);
67+
$payload = array_filter([
68+
'from' => $from,
69+
'to' => $to,
70+
'text' => $text,
71+
'brand' => data_get($options, 'brand'),
72+
'unique_id' => data_get($options, 'unique_id'),
73+
], static fn ($value) => $value !== null && $value !== '');
6674

6775
try {
6876
Log::info('Sending SMS via CallPro', [
@@ -73,31 +81,26 @@ public function send(string $to, string $text, ?string $from = null): array
7381

7482
$response = Http::withHeaders([
7583
'x-api-key' => $this->apiKey,
76-
])->get("{$this->baseUrl}/send", [
77-
'from' => $from,
78-
'to' => $to,
79-
'text' => $text,
80-
]);
84+
])->post("{$this->baseUrl}/send", $payload);
8185

8286
$statusCode = $response->status();
8387
$body = $response->json();
8488

85-
// Handle response based on status code
86-
if ($statusCode === 200) {
89+
if ($statusCode === 200 && is_array($body) && isset($body['message_id'])) {
8790
Log::info('SMS sent successfully', [
88-
'message_id' => $body['Message ID'] ?? null,
89-
'result' => $body['Result'] ?? null,
91+
'message_id' => $body['message_id'],
92+
'status' => $body['status'] ?? null,
9093
]);
9194

9295
return [
9396
'success' => true,
94-
'message_id' => $body['Message ID'] ?? null,
95-
'result' => $body['Result'] ?? 'SUCCESS',
97+
'message_id' => $body['message_id'],
98+
'result' => $body['status'] ?? 'queued',
99+
'status' => $body['status'] ?? 'queued',
96100
];
97101
}
98102

99-
// Handle error responses
100-
$errorMessage = $this->getErrorMessage($statusCode);
103+
$errorMessage = $this->getErrorMessage($statusCode, $body);
101104

102105
Log::error('SMS sending failed', [
103106
'status_code' => $statusCode,
@@ -131,16 +134,12 @@ protected function validateParameters(string $to, string $text, string $from): v
131134
throw new \InvalidArgumentException('CallPro API key is not configured');
132135
}
133136

134-
if (strlen($from) !== 8 || !ctype_digit($from)) {
135-
throw new \InvalidArgumentException('Sender ID (from) must be exactly 8 digits');
136-
}
137-
138-
if (strlen($to) !== 8) {
139-
throw new \InvalidArgumentException('Recipient phone number (to) must be exactly 8 characters');
137+
if (!preg_match('/^\d{8}$/', $from)) {
138+
throw new \InvalidArgumentException('Sender number (from) must be exactly 8 digits');
140139
}
141140

142-
if (strlen($text) > 160) {
143-
throw new \InvalidArgumentException('Message text cannot exceed 160 characters');
141+
if (!$this->isValidRecipientNumber($to)) {
142+
throw new \InvalidArgumentException('Recipient phone number (to) must be an 8-digit, 976-prefixed, +976-prefixed, or international number');
144143
}
145144

146145
if (empty($text)) {
@@ -151,17 +150,40 @@ protected function validateParameters(string $to, string $text, string $from): v
151150
/**
152151
* Get error message based on status code.
153152
*/
154-
protected function getErrorMessage(int $statusCode): string
153+
protected function getErrorMessage(int $statusCode, ?array $body = null): string
155154
{
155+
if (is_array($body)) {
156+
$error = data_get($body, 'error') ?? data_get($body, 'reason');
157+
if (is_string($error) && !empty($error)) {
158+
return $error;
159+
}
160+
161+
$issues = data_get($body, 'issues');
162+
if (is_array($issues) && !empty($issues)) {
163+
return json_encode($issues);
164+
}
165+
}
166+
156167
return match ($statusCode) {
157-
402 => 'Invalid request parameters',
158-
403 => 'Invalid API key (x-api-key)',
159-
404 => 'Invalid sender ID or recipient phone number format',
160-
503 => 'API rate limit exceeded (max 5 requests per second)',
168+
400 => 'Invalid request parameters',
169+
401 => 'Invalid or missing API key',
170+
402 => 'Payment not paid',
171+
403 => 'Blocked number',
172+
404 => 'Tenant or phone number not found',
173+
422 => 'Validation error',
174+
500 => 'CallPro server error',
161175
default => "API request failed with status code: {$statusCode}",
162176
};
163177
}
164178

179+
/**
180+
* Validate recipient number formats accepted by CallPro.
181+
*/
182+
protected function isValidRecipientNumber(string $to): bool
183+
{
184+
return (bool) preg_match('/^(?:\d{8}|976\d{8}|\+976\d{8}|\d{9,15})$/', $to);
185+
}
186+
165187
/**
166188
* Check if the service is configured.
167189
*/

src/Services/SmsService.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ protected function sendViaCallPro(string $to, string $text, array $options = [])
115115
return $this->sendViaTwilio($to, $text, $options);
116116
}
117117

118-
// Extract the last 8 digits for CallPro (Mongolia format)
118+
// Keep Mongolia routing backward compatible while allowing documented international format.
119119
$toNumber = $this->extractCallProNumber($to);
120120

121121
// CallPro does NOT support alphanumeric sender IDs (Twilio-specific)
@@ -128,7 +128,12 @@ protected function sendViaCallPro(string $to, string $text, array $options = [])
128128
$from = null; // Let CallPro use its configured default
129129
}
130130

131-
return $callProService->send($toNumber, $text, $from);
131+
$callProOptions = array_filter([
132+
'brand' => data_get($options, 'brand'),
133+
'unique_id' => data_get($options, 'unique_id'),
134+
], static fn ($value) => $value !== null && $value !== '');
135+
136+
return $callProService->send($toNumber, $text, $from, $callProOptions);
132137
}
133138

134139
/**
@@ -223,10 +228,13 @@ protected function normalizePhoneNumber(string $phoneNumber): string
223228
*/
224229
protected function extractCallProNumber(string $phoneNumber): string
225230
{
226-
// Remove + and country code, get last 8 digits
227231
$digits = preg_replace('/[^0-9]/', '', $phoneNumber);
228232

229-
return substr($digits, -8);
233+
if (strlen($digits) === 11 && str_starts_with($digits, '976')) {
234+
return substr($digits, -8);
235+
}
236+
237+
return $digits;
230238
}
231239

232240
/**
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
use Fleetbase\Services\CallProSmsService;
4+
use Fleetbase\Services\SmsService;
5+
use Illuminate\Config\Repository;
6+
use Illuminate\Container\Container;
7+
use Illuminate\Http\Client\Factory;
8+
use Illuminate\Support\Facades\Facade;
9+
use Illuminate\Support\Facades\Http;
10+
use Psr\Log\NullLogger;
11+
12+
if (!function_exists('config')) {
13+
function config($key = null, $default = null)
14+
{
15+
$config = Container::getInstance()->make('config');
16+
17+
if ($key === null) {
18+
return $config;
19+
}
20+
21+
return $config->get($key, $default);
22+
}
23+
}
24+
25+
beforeEach(function () {
26+
$app = new Container();
27+
28+
$app->instance('config', new Repository([
29+
'services' => [
30+
'callpromn' => [
31+
'api_key' => 'callpro-api-key',
32+
'from' => '72001234',
33+
'base_url' => 'https://api-text.callpro.mn/v1/sms',
34+
],
35+
],
36+
'sms' => [
37+
'default_provider' => SmsService::PROVIDER_TWILIO,
38+
'routing_rules' => [
39+
'+976' => SmsService::PROVIDER_CALLPRO,
40+
],
41+
],
42+
]));
43+
$app->instance('log', new NullLogger());
44+
$app->instance(Factory::class, new Factory());
45+
46+
Container::setInstance($app);
47+
Facade::setFacadeApplication($app);
48+
Facade::clearResolvedInstances();
49+
});
50+
51+
test('callpro sms service sends renewed post payload with api key header', function () {
52+
Http::fake([
53+
'https://api-text.callpro.mn/v1/sms/send' => Http::response([
54+
'status' => 'queued',
55+
'message_id' => '0195c03b-7f96-7f9f-8b71-4d7a930adf2f_1',
56+
], 200),
57+
]);
58+
59+
$result = (new CallProSmsService())->send('99112233', 'Hello', null, [
60+
'brand' => '42',
61+
'unique_id' => 'custom-prefix',
62+
]);
63+
64+
expect($result)->toMatchArray([
65+
'success' => true,
66+
'message_id' => '0195c03b-7f96-7f9f-8b71-4d7a930adf2f_1',
67+
'result' => 'queued',
68+
'status' => 'queued',
69+
]);
70+
71+
Http::assertSent(function ($request) {
72+
return $request->method() === 'POST'
73+
&& $request->url() === 'https://api-text.callpro.mn/v1/sms/send'
74+
&& $request->hasHeader('x-api-key', 'callpro-api-key')
75+
&& $request['from'] === '72001234'
76+
&& $request['to'] === '99112233'
77+
&& $request['text'] === 'Hello'
78+
&& $request['brand'] === '42'
79+
&& $request['unique_id'] === 'custom-prefix';
80+
});
81+
});
82+
83+
test('callpro sms service allows long segmented messages', function () {
84+
Http::fake([
85+
'https://api-text.callpro.mn/v1/sms/send' => Http::response([
86+
'status' => 'queued',
87+
'message_id' => 'long-message-id',
88+
], 200),
89+
]);
90+
91+
$result = (new CallProSmsService())->send('99112233', str_repeat('A', 320));
92+
93+
expect($result['success'])->toBeTrue()
94+
->and($result['message_id'])->toBe('long-message-id');
95+
});
96+
97+
test('callpro sms service accepts documented recipient number formats', function (string $phone) {
98+
Http::fake([
99+
'https://api-text.callpro.mn/v1/sms/send' => Http::response([
100+
'status' => 'queued',
101+
'message_id' => 'message-id',
102+
], 200),
103+
]);
104+
105+
$result = (new CallProSmsService())->send($phone, 'Hello');
106+
107+
expect($result['success'])->toBeTrue();
108+
})->with([
109+
'99112233',
110+
'97699112233',
111+
'+97699112233',
112+
'15612767156',
113+
]);
114+
115+
test('callpro sms service returns renewed api error details', function () {
116+
Http::fake([
117+
'https://api-text.callpro.mn/v1/sms/send' => Http::response([
118+
'error' => 'Unauthorized',
119+
], 401),
120+
]);
121+
122+
$result = (new CallProSmsService())->send('99112233', 'Hello');
123+
124+
expect($result)->toMatchArray([
125+
'success' => false,
126+
'error' => 'Unauthorized',
127+
'code' => 401,
128+
]);
129+
});
130+
131+
test('sms service passes callpro options and ignores twilio sender ids', function () {
132+
Http::fake([
133+
'https://api-text.callpro.mn/v1/sms/send' => Http::response([
134+
'status' => 'queued',
135+
'message_id' => 'message-id',
136+
], 200),
137+
]);
138+
139+
$result = (new SmsService())->send('+97699112233', 'Hello', [
140+
'from' => 'FLEETBASE',
141+
'brand' => '42',
142+
'unique_id' => 'verification-123',
143+
], SmsService::PROVIDER_CALLPRO);
144+
145+
expect($result)->toMatchArray([
146+
'success' => true,
147+
'provider' => SmsService::PROVIDER_CALLPRO,
148+
]);
149+
150+
Http::assertSent(function ($request) {
151+
return $request['from'] === '72001234'
152+
&& $request['to'] === '99112233'
153+
&& $request['brand'] === '42'
154+
&& $request['unique_id'] === 'verification-123';
155+
});
156+
});

0 commit comments

Comments
 (0)