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