Files
clinicpro/.claude/prompt/sms-verify-lookup-templates.md
T
hamed 969dc9651f Refactor SMS sending to use KavehNegar VerifyLookup templates
- Removed SmsTextResolver dependency from multiple services and controllers.
- Introduced dispatchTemplate method in SmsService to handle SMS sending with templates.
- Updated existing SMS sending logic across various services (OtpService, PreRegistrationController, ClinicInvitationService, PaymentManager, SecretaryController, RepresentationActionController) to utilize the new dispatchTemplate method.
- Enhanced SmsMessageTemplate entity to include kavenegar_template and token_map fields.
- Created migration to add new fields to the sms_message_templates table and populate them with existing data.
- Updated SeedSmsMessageTemplatesCommand to handle new template structure.
- Added documentation for the new SMS template structure and usage.
2026-07-05 11:20:53 +03:30

213 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# تبدیل همه‌ی پیامک‌های سیستمی به 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<string,string|int> $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) — ولی هدف این است که همه‌ی تگ‌های سیستمی مقدار داشته باشند.