# تبدیل همه‌ی پیامک‌های سیستمی به VerifyLookup کاوه‌نگار (تمپلت نام‌دار) ## پروژه `clinicpro` (backend `src/Sms` + سرویس‌های فرستنده + پنل ادمین `assets/admin/pages/SmsPage.tsx`) ## زمینه همه‌ی پیامک‌های ما «اطلاع‌رسانی/تراکنشی» هستند. سرویس کاوه‌نگار برای این نوع پیامک‌ها **VerifyLookup** را الزام می‌کند (نه ارسال متن آزاد `sms/send`). VerifyLookup فقط با تمپلت‌های **از پیش‌ساخته و تأییدشده در پنل کاوه‌نگار** کار می‌کند و متغیرها را در جایگاه‌های `token`, `token2`, `token3`, `token10`, `token20` جایگذاری می‌کند. - مستند REST: `https://kavenegar.com/rest.html#sms-Lookup` - مستند SDK: `https://kavenegar.com/sdk.html#php` وضعیت فعلی کد: `KavehNegarProvider` **از قبل هر دو متد را دارد** — `send()` (متن آزاد via `sms/send.json`) و `sendTemplate()` (VerifyLookup via `verify/lookup.json`). اما **فقط OTP** از lookup استفاده می‌کند؛ بقیه‌ی همه‌ی پیامک‌ها با `send()` (متن آزاد) می‌روند. همچنین موجودیت `SmsMessageTemplate` (متن ویرایش‌پذیر هر تگ) **نه نام تمپلت کاوه‌نگار دارد نه نگاشت متغیر→token**. ## مشکل / هدف ۱. همه‌ی پیامک‌های سیستمی (همه‌ی تگ‌ها) باید از طریق **VerifyLookup** ارسال شوند، نه `send()` متن‌آزاد. ۲. هر تمپلت سیستمی باید یک **نام تمپلت کاوه‌نگار** داشته باشد (که کاربر در پنل کاوه‌نگار می‌سازد) + یک **نگاشت متغیر→token slot**. ۳. مسیر فرستنده متمرکز شود تا هر call-site فقط تگ + متغیرها بدهد و ارسال همیشه lookup باشد. ## قید حیاتی VerifyLookup (حتماً رعایت شود) - تمپلت باید از قبل در پنل کاوه‌نگار ساخته و **تأیید** شده باشد؛ متن ثابت پیام در پنل تعریف می‌شود، نه در دیتابیس ما. یعنی بعد از این تغییر، **متن واقعی ارسالی = تمپلت کاوه‌نگار**؛ فیلد `body` در DB فقط برای پیش‌نمایش ادمین و متن `SmsLog` می‌ماند و باید دستی با تمپلت پنل هم‌راستا نگه داشته شود. - جایگاه‌ها: `token`, `token2`, `token3` → **فاصله (space) نمی‌پذیرند** (تک‌مقدار بدون space)؛ `token10` و `token20` → فاصله مجازند. پس هر مقداری که ممکن است space داشته باشد (نام دکتر/بیمار/کلینیک/مالک/نام سایت) باید در `token10`/`token20` برود؛ مقادیر بدون space (کد، تاریخ `۱۴۰۳/۰۵/۱۲`، ساعت، username، password، لینک) در `token`/`token2`/`token3`. - newline/متن طولانی داخل token مجاز نیست؛ خطوط ثابت و شکست خط باید داخل خودِ تمپلت پنل باشند، فقط مقادیر متغیر token شوند. - `KavehNegarProvider::sendTemplate()` از قبل نگاشت صریح slot را پشتیبانی می‌کند (اگر کلیدهای `$vars` دقیقاً نام slotها باشند از همان استفاده می‌کند)، پس منطق provider نیاز به تغییر ندارد. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `src/Sms/Provider/KavehNegarProvider.php` | `sendTemplate()` (VerifyLookup) — **آماده است، تغییر نده** | | `src/Sms/Service/SmsService.php` | `dispatchAsync()` / `sendNow()` — افزودن متد متمرکز `dispatchTemplate()` | | `src/Sms/Service/SmsTextResolver.php` | resolve متن body برای log/preview | | `src/Sms/Entity/SmsMessageTemplate.php` | افزودن `kavenegar_template` + `token_map` به فیلدها و `DEFAULTS` | | `src/Sms/Command/SeedSmsMessageTemplatesCommand.php` | seed از `DEFAULTS` | | `src/Sms/Controller/SmsMessageController.php` | GET/PATCH تمپلت‌های سیستمی (ادمین) | | `assets/admin/pages/SmsPage.tsx` | ویرایش تمپلت‌های سیستمی | | `src/Auth/Service/OtpService.php` | OTP (تنها جایی که الان lookup می‌کند) | | `src/Auth/Controller/NotificationMobileController.php` | تگ `notification_mobile` | | `src/Auth/Controller/PreRegistrationController.php` | تگ `pre_registration` | | `src/Secretary/Controller/SecretaryController.php` | تگ `secretary` | | `src/Payment/Service/PaymentManager.php` | تگ‌های `payment` و `doctor_appointment` | | `src/ClinicInvitation/Service/ClinicInvitationService.php` | تگ `clinic_invitation` | | `src/Representation/Controller/RepresentationActionController.php` | تگ `welcome` (الان `sprintf` inline) | | `migrations/VersionXXithm.php` | migration برای دو ستون جدید | | `docs/api/sms.md` | مستندسازی | ## وضعیت فعلی ### `SmsService::sendNow` — انتخاب بین lookup و متن‌آزاد ```php $success = ($msg->templateCode !== null) ? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars) : $provider->send($msg->mobile, $msg->message); ``` ### الگوی فعلی همه‌ی call-siteها (به‌جز OTP) — متن‌آزاد، بدون `templateCode` ```php // PaymentManager.php:318 $message = $this->smsText->resolve(SmsLog::TAG_PAYMENT, ['doctor' => $doctor, 'date' => $date]); $this->smsService->dispatchAsync($mobile, $message, tag: SmsLog::TAG_PAYMENT); // ClinicInvitationService.php:116، SecretaryController.php:133، NotificationMobileController.php:64، // PreRegistrationController.php:158 — همگی همین شکل: resolve(...) سپس dispatchAsync(mobile, message, tag: TAG) ``` ### تنها جای درست (OTP) — که باید الگوی بقیه شود ```php // OtpService.php:80 $this->sms->dispatchAsync( $mobile, $message, templateCode: $this->otpTemplate, // نام تمپلت کاوه‌نگار از env templateVars: ['token' => $code, 'token10' => $site], // نگاشت صریح slot tag: SmsLog::TAG_OTP, ); ``` ### `SmsMessageTemplate::DEFAULTS` (فاقد نام تمپلت و نگاشت token) ```php SmsLog::TAG_PAYMENT => [ 'title' => 'تأیید پرداخت و نوبت', 'body' => 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.', 'variables' => ['doctor', 'date'], ], // ... بقیه‌ی تگ‌ها مشابه ``` ### `RepresentationActionController` — welcome به‌صورت inline (بدون تمپلت) ```php $this->smsService->dispatchAsync( $mobile, sprintf('دکتر %s عزیز، به %s خوش آمدید. ...', $name, $siteName), tag: \App\Sms\Entity\SmsLog::TAG_WELCOME, ); ``` ## وظایف ### ۱. افزودن `kavenegar_template` و `token_map` به `SmsMessageTemplate` + `DEFAULTS` در `src/Sms/Entity/SmsMessageTemplate.php`: - دو ستون جدید: ```php #[ORM\Column(name: 'kavenegar_template', type: 'string', length: 100, nullable: true)] private ?string $kavenegarTemplate = null; // نگاشت متغیر منطقی → slot کاوه‌نگار: مثلاً {"code":"token","site":"token10"} #[ORM\Column(name: 'token_map', type: 'json')] private array $tokenMap = []; ``` - getter/setter، افزودن به constructor و `toArray()`. - در هر آیتم `DEFAULTS` دو کلید اضافه کن: `kavenegar_template` و `token_map`. مقادیر پیشنهادی (با رعایت قید space): | تگ | نام تمپلت کاوه‌نگار (پیشنهادی) | token_map | |----|-------------------------------|-----------| | `otp` | `clinicpro-otp` | `{ "code": "token", "site": "token10" }` | | `notification_mobile` | `clinicpro-notify-code` | `{ "code": "token" }` | | `payment` | `clinicpro-payment` | `{ "doctor": "token10", "date": "token2" }` | | `clinic_invitation` | `clinicpro-clinic-invite` | `{ "clinic": "token10", "link": "token" }` | | `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" }` | > نام‌ها را نهایی با کاربر چک کن؛ همین نام‌ها باید در پنل کاوه‌نگار ساخته شوند (وظیفه‌ی ۶). - سپس migration بساز (`ddev exec php bin/console doctrine:migrations:diff`) و seed را طوری کن که رکوردهای موجود هم دو ستون جدید را بگیرند (در `SeedSmsMessageTemplatesCommand` علاوه بر ساخت رکورد جدید، اگر رکورد هست ولی `kavenegar_template` خالی است، از `DEFAULTS` پرش کن — یا یک migration داده‌ای `UPDATE` برای پرکردن مقادیر). ### ۲. متد متمرکز `SmsService::dispatchTemplate(tag, mobile, vars)` یک متد واحد که همه‌ی call-siteها از آن استفاده کنند: ```php /** @param array $vars متغیرهای منطقی (مثل ['doctor'=>..,'date'=>..]) */ public function dispatchTemplate(string $tag, string $mobile, array $vars = []): void { $tpl = $this->messageTemplateRepo->findByTag($tag); $kaveTemplate = $tpl?->getKavenegarTemplate() ?? SmsMessageTemplate::DEFAULTS[$tag]['kavenegar_template'] ?? null; $tokenMap = $tpl?->getTokenMap() ?: (SmsMessageTemplate::DEFAULTS[$tag]['token_map'] ?? []); // متن body برای log/preview (تمپلت واقعی سمت کاوه‌نگار است) $message = $this->textResolver->resolve($tag, $vars); if ($kaveTemplate !== null && $tokenMap !== []) { // نگاشت متغیر منطقی → slot کاوه‌نگار $slotVars = []; foreach ($tokenMap as $logicalKey => $slot) { if (array_key_exists($logicalKey, $vars)) { $slotVars[$slot] = (string) $vars[$logicalKey]; } } $this->dispatchAsync($mobile, $message, templateCode: $kaveTemplate, templateVars: $slotVars, tag: $tag); } else { // fallback فقط اگر تمپلت کاوه‌نگار تعریف نشده باشد $this->dispatchAsync($mobile, $message, tag: $tag); } } ``` - `SmsService` باید `SmsMessageTemplateRepository` و `SmsTextResolver` را inject کند. - **قید space را رعایت کن:** اگر مقداری که به `token`/`token2`/`token3` می‌رود شامل space باشد، کاوه‌نگار خطا می‌دهد. token_map در `DEFAULTS` طوری چیده شده که مقادیر دارای space در `token10`/`token20` بروند؛ هنگام افزودن تگ جدید همین را رعایت کن. ### ۳. مهاجرت همه‌ی call-siteها به `dispatchTemplate` هر جای زیر را از الگوی «`resolve()` + `dispatchAsync(..., tag:)`» به یک فراخوانی `dispatchTemplate(tag, mobile, vars)` تبدیل کن (متغیرهای منطقی همان کلیدهای `{...}` بدنه‌اند): - `src/Auth/Service/OtpService.php:80` — `dispatchTemplate(TAG_OTP, $mobile, ['code'=>$code,'site'=>$site])` (شاخه‌ی env `otpTemplate` و fallback حذف شود؛ منبع نام تمپلت حالا DB/DEFAULTS است). - `src/Auth/Controller/NotificationMobileController.php:64` - `src/Auth/Controller/PreRegistrationController.php:158` - `src/Secretary/Controller/SecretaryController.php:133` - `src/Payment/Service/PaymentManager.php:318` (payment) و `:328` (doctor_appointment) - `src/ClinicInvitation/Service/ClinicInvitationService.php:116` ### ۴. تگ `welcome` را از inline به تمپلت تبدیل کن در `src/Representation/Controller/RepresentationActionController.php` (دو جای ~313 و ~388) به‌جای `sprintf(...)` از `dispatchTemplate(TAG_WELCOME, $mobile, ['name'=>$name,'site'=>$siteName])` استفاده کن. مطمئن شو `TAG_WELCOME` در `DEFAULTS` وجود دارد (در وظیفه‌ی ۱ اضافه شد). ### ۵. پنل ادمین: نمایش/ویرایش نام تمپلت کاوه‌نگار - `src/Sms/Controller/SmsMessageController.php`: در پاسخ GET، `kavenegar_template` و `token_map` را هم برگردان (از `toArray()`)؛ در PATCH اجازه‌ی ویرایش `kavenegar_template` (و در صورت لزوم `token_map`) را بده. - `assets/admin/pages/SmsPage.tsx`: فیلد «نام تمپلت کاوه‌نگار» را در فرم ویرایش هر تمپلت سیستمی نشان بده و ذخیره کن. یک راهنمای کوتاه بگذار که این نام باید دقیقاً با تمپلت ساخته‌شده در پنل کاوه‌نگار یکی باشد. - **مستند API** (`docs/api/sms.md`): تغییر response/بدنه‌ی `admin/sms/messages` را ثبت کن. ### ۶. فهرست تمپلت‌هایی که کاربر باید در پنل کاوه‌نگار بسازد در پایان، این جدول را (با نام‌های نهایی) به کاربر بده تا در پنل کاوه‌نگار بسازد؛ متن هر تمپلت باید با `body` همان تگ هم‌راستا باشد و جایگاه‌ها با `%token...` مطابق token_map: | تمپلت | نمونه متن پنل (جایگاه‌ها با token_map) | |-------|----------------------------------------| | `clinicpro-otp` | `کد تأیید شما: %token` (سایت: `%token10`) | | `clinicpro-payment` | `نوبت شما با %token10 در تاریخ %token2 ثبت و تأیید شد.` | | … | (برای هر تگ بر اساس body و token_map) | ## نکات مهم - **متن واقعی ارسالی از پنل کاوه‌نگار می‌آید**، نه از `body` دیتابیس. پس `body` را فقط برای preview/log نگه‌دار و در پنل هم همان متن را بساز؛ اگر ادمین `body` را عوض کند، متن ارسالی عوض نمی‌شود مگر تمپلت پنل هم عوض شود — این را در UI به ادمین گوشزد کن. - **قید space در token/token2/token3** مهم‌ترین علت خطای ۴۳۱/۴۱۸ کاوه‌نگار است؛ مقادیر دارای فاصله را حتماً به `token10`/`token20` بده (token_map پیش‌فرض این را رعایت کرده). - **لینک‌ها**: بعضی تمپلت‌های VerifyLookup لینک را فقط اگر تمپلت با لینک تأیید شده باشد می‌پذیرند؛ برای تگ‌های دارای `{link}` (clinic_invitation, pre_registration, secretary) هنگام ساخت تمپلت در پنل، تأیید لینک را بگیر. - **`SDK` کاوه‌نگار (`kavenegar/php`) لازم نیست**: provider فعلی مستقیم `verify/lookup.json` را با `HttpClient` صدا می‌زند و درست است. اگر کاربر صراحتاً SDK بخواهد، می‌توان `composer require kavenegar/php` کرد و provider را بازنویسی کرد، ولی پیش‌فرض همین HttpClient بماند (بدون وابستگی جدید). - **`TAG_USER_TEMPLATE` (پنل پیامک کلینیک‌ها، `SmsController`)**: این‌ها پیامک‌های متن‌آزادِ کاربرساخته با `SmsTemplate.providerCode` هستند و از قبل مسیر تمپلت‌دار دارند؛ **در دامنه‌ی این تغییر نیستند**. اگر کاربر می‌خواهد آن‌ها هم اجباری lookup شوند، جداگانه بپرس (متن آزاد کاربر با VerifyLookup سازگار نیست مگر هر متن یک تمپلت تأییدشده داشته باشد). - **`env KAVENEGAR_OTP_TEMPLATE`**: بعد از انتقال نام تمپلت به DB، این env و binding `$otpTemplate` در `config/services.yaml:59` را حذف یا به fallback تبدیل کن (تا جای واحدِ حقیقت، DB باشد). - بعد از تغییر Entity: `doctrine:migrations:diff` سپس `migrate`. بعد از تغییر API: `docs/api/sms.md`. تست: `ddev exec php bin/console app:seed-sms-templates` (یا نام واقعی seed) و بررسی ارسال با یک تگ (مثلاً OTP) در محیط تست. - edge case: اگر تگی `token_map` نداشت یا `kavenegar_template` خالی بود، `dispatchTemplate` باید به‌صورت امن fallback کند (نه crash) — ولی هدف این است که همه‌ی تگ‌های سیستمی مقدار داشته باشند.