- 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.
213 lines
17 KiB
Markdown
213 lines
17 KiB
Markdown
# تبدیل همهی پیامکهای سیستمی به 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) — ولی هدف این است که همهی تگهای سیستمی مقدار داشته باشند.
|