Teren-app/app/Services/Sms/SmsApiSiClient.php
Simon Pocrnjič 930ac83604 SMS service
2025-10-24 21:39:10 +02:00

203 lines
7.5 KiB
PHP

<?php
namespace App\Services\Sms;
use App\Models\SmsProfile;
use Illuminate\Support\Facades\Http;
class SmsApiSiClient implements SmsClient
{
public function send(SmsProfile $profile, SmsMessage $message): SmsResult
{
$baseUrl = config('services.sms.providers.smsapi_si.base_url');
$endpoint = config('services.sms.providers.smsapi_si.send_endpoint', '/poslji-sms');
$timeout = (int) config('services.sms.providers.smsapi_si.timeout', 10);
$url = rtrim($baseUrl, '/').$endpoint;
$payload = [
'un' => $profile->api_username,
'ps' => $profile->decryptApiPassword() ?? '',
// provider requires phone number in `from`
'from' => isset($message->senderPhone) && $message->senderPhone !== '' ? urlencode($message->senderPhone) : '',
'to' => $message->to,
'm' => $message->content,
];
// Log payload safely (mask password) for debugging; uses context so it will appear on supported channels
$logPayload = $payload;
if (isset($logPayload['ps'])) {
$logPayload['ps'] = '***';
}
\Log::info('sms.send payload', ['payload' => $logPayload]);
/*if (! empty($message->sender)) {
$payload['sname'] = $message->sender;
}*/
// Default country code when not provided
$payload['cc'] = urlencode(! empty($message->countryCode) ? $message->countryCode : '386');
if ($message->deliveryReport) {
$payload['dr'] = 1;
}
$response = Http::asForm()
->timeout($timeout)
->post($url, $payload);
$body = trim((string) $response->body());
$statusCode = $response->status();
// Parse according to provider docs: ID##SMS_PRICE##FROM##TO
// Success: 123##0.03##040123456##040654321
// Failure: -1##ERROR_ID##FROM##TO
$parts = array_values(array_filter(array_map(static fn ($s) => trim((string) $s), explode('##', $body)), static fn ($s) => $s !== ''));
// Known error codes (Slovenian descriptions from docs)
$errorMap = [
1 => 'Napaka v validaciji.',
2 => 'Sporočilo je predolgo ali prazno.',
3 => 'Številka pošiljatelja ni pravilno tvorjena ali pa je prazna.',
4 => 'Številka prejemnika ni pravilno tvorjena ali pa je prazna.',
5 => 'Uporabnik nima dovolj kreditov.',
6 => 'Napaka na serverju.',
7 => 'Številka pošiljatelja ni registrirana.',
8 => 'Referenca uporabnika ni veljavna.',
9 => 'Koda države ni veljavna.',
10 => 'ID pošiljatelja ni potrjen.',
11 => 'Koda države ni podprta',
12 => 'Številka pošiljatelja ni potrjena',
13 => 'Številka oz. država pošiljatelja ne podpira MMS vsebin.',
14 => 'MMS tip oblike (mime type) ni podprta',
15 => 'MMS url je nedosegljiv ali datoteka ne obstaja.',
16 => 'MMS vsebina je predolga',
];
if ($response->successful() && ! empty($parts)) {
// Failure shape
if ($parts[0] === '-1') {
$errorId = isset($parts[1]) ? (int) $parts[1] : null;
$from = $parts[2] ?? null;
$to = $parts[3] ?? null;
return new SmsResult(
status: 'failed',
providerMessageId: null,
cost: null,
currency: null,
meta: [
'status_code' => $statusCode,
'body' => $body,
'parts' => $parts,
'error_id' => $errorId,
'error_message' => $errorId ? ($errorMap[$errorId] ?? 'Neznana napaka') : 'Neznana napaka',
'from' => $from,
'to' => $to,
]
);
}
// Success shape
if (preg_match('/^\d+$/', $parts[0])) {
$providerId = $parts[0];
$price = null;
if (isset($parts[1])) {
$priceStr = str_replace(',', '.', $parts[1]);
$price = is_numeric($priceStr) ? (float) $priceStr : null;
}
$from = $parts[2] ?? null;
$to = $parts[3] ?? null;
return new SmsResult(
status: 'sent',
providerMessageId: $providerId,
cost: $price,
currency: null,
meta: [
'status_code' => $statusCode,
'body' => $body,
'parts' => $parts,
'from' => $from,
'to' => $to,
]
);
}
}
// HTTP error or unexpected body
return new SmsResult(
status: 'failed',
providerMessageId: null,
cost: null,
currency: null,
meta: [
'status_code' => $statusCode,
'body' => $body,
'parts' => $parts,
]
);
}
public function getCreditBalance(SmsProfile $profile): int
{
$baseUrl = config('services.sms.providers.smsapi_si.base_url');
$endpoint = config('services.sms.providers.smsapi_si.credits_endpoint', '/preveri-stanje-kreditov');
$timeout = (int) config('services.sms.providers.smsapi_si.timeout', 10);
$url = rtrim($baseUrl, '/').$endpoint;
$response = Http::asForm()
->timeout($timeout)
->post($url, [
'un' => $profile->api_username,
'ps' => $profile->decryptApiPassword() ?? '',
]);
if (! $response->successful()) {
\Log::warning('SMS credits request failed', [
'status' => $response->status(),
'url' => $url,
'username' => $profile->api_username,
'body' => (string) $response->body(),
]);
throw new \RuntimeException('Credits endpoint returned HTTP '.$response->status());
}
$body = trim((string) $response->body());
if ($body === '') {
return 0;
}
// Per provider docs, response is '##' separated: first token contains the balance
$parts = explode('##', $body);
$first = trim($parts[0] ?? $body);
// Fallback to the raw first token if no number found
return intval($first);
}
public function getPriceQuotes(SmsProfile $profile): array
{
$baseUrl = config('services.sms.providers.smsapi_si.base_url');
$endpoint = config('services.sms.providers.smsapi_si.price_endpoint', '/dobi-ceno');
$timeout = (int) config('services.sms.providers.smsapi_si.timeout', 10);
$url = rtrim($baseUrl, '/').$endpoint;
$response = Http::asForm()
->timeout($timeout)
->post($url, [
'un' => $profile->api_username,
'ps' => $profile->decryptApiPassword() ?? '',
]);
$body = trim((string) $response->body());
if ($body === '') {
return [];
}
// Provider returns '##' separated values; trim and drop empty tokens
$parts = explode('##', $body);
$parts = array_map(static fn ($s) => trim((string) $s), $parts);
$parts = array_values(array_filter($parts, static fn ($s) => $s !== ''));
return $parts;
}
}