$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; } }