(string) Str::uuid(), 'profile_id' => $profile->id, 'to_number' => $to, 'sender' => $sender?->sname, 'message' => $content, 'status' => 'queued', 'queued_at' => now(), ]); $log->save(); $result = $this->client->send($profile, new SmsMessage( to: $to, content: $content, sender: $sender?->sname, senderPhone: $sender?->phone_number, countryCode: $countryCode, deliveryReport: $deliveryReport, clientReference: $clientReference, )); if ($result->status === 'sent') { $log->status = 'sent'; $log->sent_at = now(); } else { $log->status = 'failed'; $log->failed_at = now(); } $log->provider_message_id = $result->providerMessageId; $log->cost = $result->cost; $log->currency = $result->currency; $log->meta = $result->meta; $log->save(); return $log; }); } /** * Render an SMS from template and send. */ public function sendFromTemplate(SmsTemplate $template, string $to, array $variables = [], ?SmsProfile $profile = null, ?SmsSender $sender = null, ?string $countryCode = null, bool $deliveryReport = false, ?string $clientReference = null): SmsLog { $profile = $profile ?: $template->defaultProfile; if (! $profile) { throw new \InvalidArgumentException('SMS profile is required to send a message.'); } $sender = $sender ?: $template->defaultSender; $content = $this->renderContent($template->content, $variables); $log = $this->sendRaw($profile, $to, $content, $sender, $countryCode, $deliveryReport, $clientReference); $log->template_id = $template->id; $log->save(); return $log; } public function renderContent(string $content, array $vars): string { // Support {token} and {nested.keys} using dot-notation lookup $resolver = function (array $arr, string $path) { if (array_key_exists($path, $arr)) { return $arr[$path]; } $segments = explode('.', $path); $cur = $arr; foreach ($segments as $seg) { if (is_array($cur) && array_key_exists($seg, $cur)) { $cur = $cur[$seg]; } else { return null; } } return $cur; }; return preg_replace_callback('/\{([a-zA-Z0-9_\.]+)\}/', function ($m) use ($vars, $resolver) { $key = $m[1]; $val = $resolver($vars, $key); return $val !== null ? (string) $val : $m[0]; }, $content); } /** * Format a number to EU style: thousands separated by '.', decimals by ','. */ public function formatAmountEu(mixed $value, int $decimals = 2): string { if ($value === null || $value === '') { return number_format(0, $decimals, ',', '.'); } $str = (string) $value; // Normalize possible EU-style input like "1.234,56" to standard for float casting if (str_contains($str, ',')) { $str = str_replace(['.', ','], ['', '.'], $str); } $num = (float) $str; return number_format($num, $decimals, ',', '.'); } /** * Get current credit balance from provider. */ public function getCreditBalance(SmsProfile $profile): string { return $this->client->getCreditBalance($profile); } /** * Get price quote(s) from provider. * Returns array of strings as provided by the API. */ public function getPriceQuotes(SmsProfile $profile): array { return $this->client->getPriceQuotes($profile); } }