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.
This commit is contained in:
hamed
2026-07-05 11:20:53 +03:30
parent 828e3552c0
commit 969dc9651f
17 changed files with 480 additions and 102 deletions
+30 -5
View File
@@ -480,15 +480,37 @@ Updated template with `status: "rejected"`.
## متن ویرایش‌پذیر پیامک‌های سیستمی
متن پیامک‌های سیستمی (OTP، پرداخت، دعوت کلینیک، پیش‌ثبت‌نام، تأیید موبایل) از پنل قابل ویرایش است و بر اساس **تگ** کلیددار می‌شود. هر متن placeholderهای مجاز خود را دارد (مثل `{code}`، `{doctor}`، `{date}`). هنگام ارسال، `SmsTextResolver` متنِ ویرایش‌شده‌ی DB را می‌گیرد و placeholderها را جایگزین می‌کند؛ اگر رکوردی نبود به متن پیش‌فرض fallback می‌شود.
متن پیامک‌های سیستمی از پنل قابل ویرایش است و بر اساس **تگ** کلیددار می‌شود. هر متن placeholderهای مجاز خود را دارد (مثل `{code}`، `{doctor}`، `{date}`).
**همه‌ی پیامک‌های سیستمی از طریق Kavenegar VerifyLookup (`verify/lookup.json`) ارسال می‌شوند** (نه متن‌آزاد `sms/send`). هر تمپلت دو فیلد اضافه دارد:
- `kavenegar_template`: نام تمپلت مصوب در پنل کاوه‌نگار. **متن واقعیِ ارسالی از همین تمپلت پنل می‌آید، نه از `body` دیتابیس**؛ `body` فقط برای پیش‌نمایش ادمین و رندرِ رکورد `SmsLog` استفاده می‌شود و باید دستی با تمپلت پنل هم‌راستا نگه داشته شود.
- `token_map`: نگاشت متغیر منطقی → جایگاه کاوه‌نگار. قانون فاصله: `token`/`token2`/`token3` **فاصله نمی‌پذیرند**؛ `token10`/`token20` فاصله مجازند. پس مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک/سایت) در `token10`/`token20`.
`SmsService::dispatchTemplate(tag, mobile, vars)` نقطه‌ی واحدِ ارسال است: `kavenegar_template` و `token_map` را از رکورد DB (یا `SmsMessageTemplate::DEFAULTS`) می‌خواند، `vars` را به جایگاه‌ها نگاشت می‌کند و با VerifyLookup می‌فرستد. اگر `kavenegar_template` تعریف نشده باشد → fallback به ارسال متن‌آزاد با `body` رندرشده.
نگاشت پیش‌فرض هر تگ (نام تمپلت پنل + token_map):
| تگ | kavenegar_template | token_map |
|----|--------------------|-----------|
| `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 |
| `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 |
> **پیش‌نیاز:** این تمپلت‌ها باید در پنل کاوه‌نگار ساخته و تأیید شوند (نیازمند اشتراک advanced). تمپلت‌های دارای `{link}` باید با تأیید لینک ساخته شوند.
> تگ‌ها: `otp`، `payment`، `clinic_invitation`، `pre_registration`، `notification_mobile`، `welcome`، `secretary`، `doctor_appointment`. (پیامک قالبیِ کاربر با تگ `user_template` جداگانه از طریق `POST /api/v1/sms/template` مدیریت می‌شود.)
>
> تگ `otp`: کد تأیید ورود. مقدار `site` از فیلد `domain` در `POST /api/v1/user/send-code` گرفته می‌شود: `site_name` شهرِ متناظر در جدول `cities`؛ اگر `domain` نیامد یا شهر پیدا نشد → «کلینیک پرو».
>
> **ارسال OTP از طریق Kavenegar VerifyLookup (پترن):** اگر متغیر محیطی `KAVENEGAR_OTP_TEMPLATE` (نام پترن مصوب پنل کاوه‌نگار) ست باشد، OTP با `verify/lookup.json` ارسال می‌شود — **متنِ پیام از پترن ثابتِ مصوب کاوه‌نگار می‌آید، نه از body قابل‌ویرایش DB** (body صرفاً برای رکورد `SmsLog` رندر می‌شود). map توکن‌ها طبق قانون فاصله‌ی کاوه‌نگار: `token` = کد ۵ رقمی (بدون فاصله)، `token10` = اسم سایت (تا ۵ فاصله مجاز). اگر پترن مصوب فقط `%token` داشته باشد، `token10` نادیده گرفته می‌شود (اسم سایت نمایش داده نمی‌شود). اگر `KAVENEGAR_OTP_TEMPLATE` خالی باشد → fallback به ارسال متن‌آزاد `send` با متنِ قالب DB (placeholderهای `{code}`، `{site}`). نیازمند اشتراک advanced کاوه‌نگار.
> تگ `otp`: کد تأیید ورود از طریق تمپلت `clinicpro-otp` (VerifyLookup). نام تمپلت از رکورد DB خوانده می‌شود (متغیر محیطی `KAVENEGAR_OTP_TEMPLATE` **حذف شده**). token_map: `code→token`، `site→token10`.
>
> تگ `welcome`: پیامک خوش‌آمد که هنگام افزودن پزشک/کلینیک توسط نماینده (`POST /api/v1/representation/doctor|clinic`) به‌صورت async به موبایل پزشک/مالک ارسال می‌شود. متن فعلاً ثابت است (نام + `site_name`)، نه از قالب DB.
> تگ `welcome`: پیامک خوش‌آمد که هنگام افزودن پزشک/کلینیک توسط نماینده (`POST /api/v1/representation/doctor|clinic`) به‌صورت async به موبایل پزشک/مالک ارسال می‌شود. از تمپلت `clinicpro-welcome` (`name→token10`, `site→token20`).
>
> تگ `secretary`: پیامک خوش‌آمد که هنگام تعریف منشی جدید (`POST /api/v1/secretary`) به‌صورت async به موبایل منشی ارسال می‌شود. placeholderها: `{owner}` (نام دکتر یا کلینیک)، `{username}` (موبایل منشی)، `{link}` (لینک ورود). متن از قالب DB می‌آید (fallback به پیش‌فرض `SmsMessageTemplate::DEFAULTS`).
>
@@ -511,6 +533,8 @@ Updated template with `status: "rejected"`.
"title": "کد تأیید ورود",
"body": "کد تأیید شما: {code}",
"variables": ["code", "site"],
"kavenegar_template": "clinicpro-otp",
"token_map": { "code": "token", "site": "token10" },
"updated_at": 1718000000
}
]
@@ -531,11 +555,12 @@ Updated template with `status: "rejected"`.
#### Request Body
```json
{ "body": "کد ورود شما: {code}" }
{ "body": "کد ورود شما: {code}", "kavenegar_template": "clinicpro-otp" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `body` | string | ✅ | متن جدید؛ فقط placeholderهای مجازِ همان تگ پذیرفته می‌شود |
| `body` | string | ✅ | متن جدید (پیش‌نمایش/لاگ)؛ فقط placeholderهای مجازِ همان تگ پذیرفته می‌شود |
| `kavenegar_template` | string | ❌ | نام تمپلت مصوب پنل کاوه‌نگار؛ رشته‌ی خالی → `null` |
#### Response `200`
```json