diff --git a/docs/api/sms.md b/docs/api/sms.md index 725cd8c9..3823cc95 100644 --- a/docs/api/sms.md +++ b/docs/api/sms.md @@ -485,7 +485,7 @@ Updated template with `status: "rejected"`. **همه‌ی پیامک‌های سیستمی از طریق Kavenegar VerifyLookup (`verify/lookup.json`) ارسال می‌شوند** (نه متن‌آزاد `sms/send`). هر تمپلت دو فیلد اضافه دارد: - `kavenegar_template`: نام تمپلت مصوب در پنل کاوه‌نگار. **متن واقعیِ ارسالی از همین تمپلت پنل می‌آید، نه از `body` دیتابیس**؛ `body` فقط برای پیش‌نمایش ادمین و رندرِ رکورد `SmsLog` استفاده می‌شود و باید دستی با تمپلت پنل هم‌راستا نگه داشته شود. -- `token_map`: نگاشت متغیر منطقی → جایگاه کاوه‌نگار. قانون فاصله: `token`/`token2`/`token3` **فاصله نمی‌پذیرند**؛ `token10`/`token20` فاصله مجازند. پس مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک/سایت) در `token10`/`token20`. +- `token_map`: نگاشت متغیر منطقی → جایگاه کاوه‌نگار. **قانون کاوه‌نگار:** هر الگو **حتماً باید `%token` داشته باشد** (token1 اجباری است)؛ پس token_map هر تگ یک مقدار به `token` می‌دهد. قانون فاصله: `token`/`token2`/`token3` **فاصله نمی‌پذیرند** — provider (`KavehNegarProvider`) فاصله‌های مقدارِ این جایگاه‌ها را خودکار با نیم‌فاصله (ZWNJ) جایگزین می‌کند؛ `token10`/`token20` فاصله را بدون تغییر می‌پذیرند. پس ترجیحاً مقادیر بدون‌فاصله (کد/تاریخ/ساعت/username/لینک) در `token`/`token2`/`token3` و مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک) در `token10`/`token20`. `SmsService::dispatchTemplate(tag, mobile, vars)` نقطه‌ی واحدِ ارسال است: `kavenegar_template` و `token_map` را از رکورد DB (یا `SmsMessageTemplate::DEFAULTS`) می‌خواند، `vars` را به جایگاه‌ها نگاشت می‌کند و با VerifyLookup می‌فرستد. اگر `kavenegar_template` تعریف نشده باشد → fallback به ارسال متن‌آزاد با `body` رندرشده. @@ -495,12 +495,12 @@ Updated template with `status: "rejected"`. |----|--------------------|-----------| | `otp` | `clinicpro-otp` | code→token, site→token10 | | `notification_mobile` | `clinicpro-notify-code` | code→token | -| `payment` | `clinicpro-payment` | doctor→token10, date→token20 | -| `clinic_invitation` | `clinicpro-clinic-invite` | clinic→token10, link→token | +| `payment` | `clinicpro-payment` | date→token, time→token2, doctor→token10 | +| `clinic_invitation` | `clinicpro-clinic-invite` | link→token, clinic→token10 | | `pre_registration` | `clinicpro-pre-register` | username→token, password→token2, link→token3 | -| `secretary` | `clinicpro-secretary` | owner→token10, username→token, link→token3 | -| `doctor_appointment` | `clinicpro-doctor-appt` | patient→token10, date→token2, time→token3 | -| `welcome` | `clinicpro-welcome` | name→token10, site→token20 | +| `secretary` | `clinicpro-secretary` | username→token, link→token3, owner→token10 | +| `doctor_appointment` | `clinicpro-doctor-appt` | date→token, time→token2, patient→token10 | +| `welcome` | `clinicpro-welcome` | name→token, site→token10 | > **پیش‌نیاز:** این تمپلت‌ها باید در پنل کاوه‌نگار ساخته و تأیید شوند (نیازمند اشتراک advanced). تمپلت‌های دارای `{link}` باید با تأیید لینک ساخته شوند. diff --git a/migrations/Version20260705081716.php b/migrations/Version20260705081716.php new file mode 100644 index 00000000..467f7988 --- /dev/null +++ b/migrations/Version20260705081716.php @@ -0,0 +1,50 @@ + ['{"date":"token","time":"token2","doctor":"token10"}', 'نوبت شما با {doctor} در تاریخ {date} ساعت {time} ثبت و تأیید شد.'], + 'doctor_appointment' => ['{"date":"token","time":"token2","patient":"token10"}', null], + 'welcome' => ['{"name":"token","site":"token10"}', null], + ]; + + public function getDescription(): string + { + return 'token_map پیامک‌ها: تضمین وجود %token در payment/doctor_appointment/welcome'; + } + + public function up(Schema $schema): void + { + foreach (self::FIX as $tag => [$map, $body]) { + if ($body !== null) { + $this->addSql( + 'UPDATE sms_message_templates SET token_map = :map, body = :body WHERE tag = :tag', + ['map' => $map, 'body' => $body, 'tag' => $tag] + ); + } else { + $this->addSql( + 'UPDATE sms_message_templates SET token_map = :map WHERE tag = :tag', + ['map' => $map, 'tag' => $tag] + ); + } + } + } + + public function down(Schema $schema): void + { + $this->addSql("UPDATE sms_message_templates SET token_map = '{\"doctor\":\"token10\",\"date\":\"token20\"}', body = 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.' WHERE tag = 'payment'"); + $this->addSql("UPDATE sms_message_templates SET token_map = '{\"patient\":\"token10\",\"date\":\"token2\",\"time\":\"token3\"}' WHERE tag = 'doctor_appointment'"); + $this->addSql("UPDATE sms_message_templates SET token_map = '{\"name\":\"token10\",\"site\":\"token20\"}' WHERE tag = 'welcome'"); + } +} diff --git a/src/Payment/Service/PaymentManager.php b/src/Payment/Service/PaymentManager.php index 210eaaaa..cc468752 100644 --- a/src/Payment/Service/PaymentManager.php +++ b/src/Payment/Service/PaymentManager.php @@ -312,10 +312,10 @@ final class PaymentManager $mobile = $appointment->getPatientMobile(); if ($mobile) { - $when = $this->jalali->formatDateTime($appointment->getSlotStart()); $this->smsService->dispatchTemplate(SmsLog::TAG_PAYMENT, $mobile, [ 'doctor' => $doctor->getName(), - 'date' => $when, + 'date' => $this->jalali->formatDateTime($appointment->getSlotStart(), false), + 'time' => date('H:i', $appointment->getSlotStart()), ]); } diff --git a/src/Sms/Entity/SmsMessageTemplate.php b/src/Sms/Entity/SmsMessageTemplate.php index 16ab5bd0..7e031b34 100644 --- a/src/Sms/Entity/SmsMessageTemplate.php +++ b/src/Sms/Entity/SmsMessageTemplate.php @@ -29,10 +29,10 @@ class SmsMessageTemplate ], SmsLog::TAG_PAYMENT => [ 'title' => 'تأیید پرداخت و نوبت', - 'body' => 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.', - 'variables' => ['doctor', 'date'], + 'body' => 'نوبت شما با {doctor} در تاریخ {date} ساعت {time} ثبت و تأیید شد.', + 'variables' => ['doctor', 'date', 'time'], 'kavenegar_template' => 'clinicpro-payment', - 'token_map' => ['doctor' => 'token10', 'date' => 'token20'], + 'token_map' => ['date' => 'token', 'time' => 'token2', 'doctor' => 'token10'], ], SmsLog::TAG_CLINIC_INVITATION => [ 'title' => 'دعوت پزشک به کلینیک', @@ -67,14 +67,14 @@ class SmsMessageTemplate 'body' => "نوبت جدید ثبت شد.\nبیمار: {patient}\nتاریخ: {date} ساعت {time}\n\nبرای مدیریت نوبت‌های خود به‌صورت رایگان به سایت clinic-pro.ir مراجعه کنید.\nکلینیک پرو", 'variables' => ['patient', 'date', 'time'], 'kavenegar_template' => 'clinicpro-doctor-appt', - 'token_map' => ['patient' => 'token10', 'date' => 'token2', 'time' => 'token3'], + 'token_map' => ['date' => 'token', 'time' => 'token2', 'patient' => 'token10'], ], SmsLog::TAG_WELCOME => [ 'title' => 'خوش‌آمدگویی', 'body' => '{name} عزیز، به {site} خوش آمدید. پروفایل شما توسط نماینده ثبت شد.', 'variables' => ['name', 'site'], 'kavenegar_template' => 'clinicpro-welcome', - 'token_map' => ['name' => 'token10', 'site' => 'token20'], + 'token_map' => ['name' => 'token', 'site' => 'token10'], ], ]; diff --git a/src/Sms/Provider/KavehNegarProvider.php b/src/Sms/Provider/KavehNegarProvider.php index ee4b7140..1b9ded2d 100644 --- a/src/Sms/Provider/KavehNegarProvider.php +++ b/src/Sms/Provider/KavehNegarProvider.php @@ -43,6 +43,17 @@ class KavehNegarProvider implements SmsProviderInterface } } + // token/token2/token3 مقدارِ دارای فاصله را رد می‌کنند؛ فاصله را با نیم‌فاصله (ZWNJ) جایگزین می‌کنیم. + private const NO_SPACE_SLOTS = ['token', 'token2', 'token3']; + + private function forSlot(string $slot, string $value): string + { + if (!in_array($slot, self::NO_SPACE_SLOTS, true)) { + return $value; + } + return preg_replace('/\s+/u', "\u{200C}", $value) ?? $value; + } + public function sendTemplate(string $mobile, string $templateCode, array $vars): bool { try { @@ -54,11 +65,12 @@ class KavehNegarProvider implements SmsProviderInterface if ($vars !== [] && array_keys($vars) !== range(0, count($vars) - 1) && array_diff(array_keys($vars), $slots) === []) { foreach ($vars as $slot => $v) { - $params[$slot] = $v; + $params[$slot] = $this->forSlot($slot, (string) $v); } } else { foreach (array_values($vars) as $i => $v) { - $params['token' . ($i > 0 ? $i + 1 : '')] = $v; + $slot = 'token' . ($i > 0 ? $i + 1 : ''); + $params[$slot] = $this->forSlot($slot, (string) $v); } } $resp = $this->httpClient->request('POST',