Files
boursecu_server/app/Services/Automation/AutomationSender.php
T
mohmdsdgandCursor 9bb0fa3a1d fix(automations): always append app name footer to SMS with fallbacks
Resolve display name from name_fa/name/name_en, skip duplicate lines, and support %نام_اپ placeholder in templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 21:57:20 +03:30

189 lines
6.4 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Automation;
use App\Models\Automation;
use App\Models\AutomationSend;
use App\Models\Customer;
use App\Models\Message;
use App\Services\ConfigService;
use App\Services\CustomerAppNotifyService;
use App\Services\SmsBilling\SmsGatewayService;
use App\Exceptions\SmsBlockedException;
use Illuminate\Support\Facades\Log;
class AutomationSender
{
public function __construct(
private readonly SmsGatewayService $sms,
private readonly CustomerAppNotifyService $notify,
private readonly ConfigService $config,
) {
}
public function send(Automation $auto, Customer $customer, string $channel, bool $force = false): bool
{
if (! $this->maySend($auto, $customer, $channel, $force)) {
return false;
}
$title = $this->render((string) $auto->title, $customer);
$body = $this->render((string) $auto->body, $customer);
try {
if ($channel === AutomationCatalog::CHANNEL_SMS) {
$mobile = trim((string) $customer->mobile);
if ($mobile === '') {
throw new \RuntimeException('موبایل خالی است');
}
$text = $this->ensureAppNameFooter(trim($title."\n".$body));
$result = $this->sms->sendOrThrow($mobile, $text, 'automation');
if (! $result->sent) {
throw new \RuntimeException($result->errorMessage ?: 'ارسال پیامک ناموفق');
}
Message::create([
'sms' => $text,
'receiver' => $mobile,
'platforms' => ($result->provider ?: 'sms').':'.$mobile,
]);
} else {
$row = $this->notify->notifyCustomer((int) $customer->id, $title, $body, 'info');
if (! $row) {
throw new \RuntimeException('اعلان ساخته نشد');
}
}
$this->log($auto, $customer, $channel, 'sent');
$auto->increment('sent_count');
return true;
} catch (SmsBlockedException $e) {
$this->log($auto, $customer, $channel, 'failed', 'اعتبار پیامک کافی نیست یا مسدود است');
$auto->increment('failed_count');
Log::warning('Automation SMS blocked', ['automation_id' => $auto->id]);
return false;
} catch (\Throwable $e) {
$this->log($auto, $customer, $channel, 'failed', $e->getMessage());
$auto->increment('failed_count');
return false;
}
}
public function maySend(Automation $auto, Customer $customer, string $channel, bool $force = false): bool
{
if (! $auto->usesChannel($channel)) {
return false;
}
if ($force) {
return true;
}
$max = $auto->max_sends;
if ($max !== null && (int) $max > 0) {
$count = AutomationSend::query()
->where('automation_id', $auto->id)
->where('customer_id', $customer->id)
->where('channel', $channel)
->where('status', 'sent')
->count();
if ($count >= (int) $max) {
return false;
}
}
$cooldown = max(0, (int) $auto->cooldown_hours);
if ($cooldown > 0) {
$recent = AutomationSend::query()
->where('automation_id', $auto->id)
->where('customer_id', $customer->id)
->where('channel', $channel)
->where('status', 'sent')
->where('created_at', '>=', now()->subHours($cooldown))
->exists();
if ($recent) {
return false;
}
}
return true;
}
private function ensureAppNameFooter(string $message): string
{
$appName = $this->resolveAppDisplayName();
$trimmed = rtrim($message);
if ($appName === '') {
return $trimmed;
}
if ($this->messageContainsAppName($trimmed, $appName)) {
return $trimmed;
}
return $trimmed === '' ? $appName : ($trimmed."\n".$appName);
}
private function resolveAppDisplayName(): string
{
foreach (['name_fa', 'name', 'name_en'] as $key) {
$value = trim($this->config->getString($key));
if ($value !== '') {
return $value;
}
}
return '';
}
private function messageContainsAppName(string $message, string $appName): bool
{
$lines = preg_split('/\R/u', $message) ?: [];
foreach ($lines as $line) {
if (trim((string) $line) === $appName) {
return true;
}
}
return false;
}
private function render(string $text, Customer $customer): string
{
$name = trim((string) ($customer->name ?: trim(($customer->fname ?? '').' '.($customer->lname ?? ''))));
$fname = (string) ($customer->fname ?? '');
$mobile = (string) ($customer->mobile ?? '');
$appName = $this->resolveAppDisplayName();
// سازگاری با قالب قدیمی {{name}}
$out = strtr($text, [
'{{name}}' => $name,
'{{fname}}' => $fname,
'{{mobile}}' => $mobile,
'{{app_name}}' => $appName,
]);
// الگوی پیامک/تنظیمات: %نام ، ٪نام ، % name ، %name%
// نام_کوچک قبل از نام — وگرنه %نام داخل %نام_کوچک زودتر match می‌شود
$out = preg_replace('/[%٪]\s*(?:fname|نام_کوچک|نام‌کوچک)[%٪]?/iu', $fname, $out) ?? $out;
$out = preg_replace('/[%٪]\s*(?:app_name|نام_اپ|نام‌اپ)[%٪]?/iu', $appName, $out) ?? $out;
$out = preg_replace('/[%٪]\s*(?:name|نام)(?![_‌]?کوچک)(?![_‌]?اپ)[%٪]?/iu', $name, $out) ?? $out;
$out = preg_replace('/[%٪]\s*(?:mobile|موبایل)[%٪]?/iu', $mobile, $out) ?? $out;
return $out;
}
private function log(Automation $auto, Customer $customer, string $channel, string $status, ?string $error = null): void
{
AutomationSend::create([
'automation_id' => $auto->id,
'customer_id' => $customer->id,
'channel' => $channel,
'status' => $status,
'error' => $error,
]);
}
}