# ارسال پیامک OTP فقط از طریق Kavenegar VerifyLookup (پترن)

## پروژه

`clinicpro` (Backend — SMS/Auth)

## زمینه

پیامک کد تأیید ورود (OTP) الان با متد **متن‌آزاد** `send()` کاوه‌نگار (`/sms/send.json`) ارسال می‌شود. کاوه‌نگار برای ارسال کد تأیید، متد اختصاصی **VerifyLookup** (`/verify/lookup.json`) دارد که با **پترن مصوب** کار می‌کند، روی خطوط اشتراکی هم تحویل مطمئن‌تری دارد و برای OTP توصیه/لازم است. هدف: **فقط OTP** از این به بعد از طریق Lookup ارسال شود (بقیه پیامک‌ها — welcome، secretary، doctor_appointment، user_template — دست‌نخورده بمانند).

## اسپک Kavenegar VerifyLookup (از داکیومنت رسمی)

- **Endpoint:** `https://api.kavenegar.com/v1/{API-KEY}/verify/lookup.json` (GET/POST)
- **پارامترهای اجباری:** `receptor` (موبایل)، `token` (مقدار کد؛ max 100؛ **بدون فاصله**؛ بدون آندرلاین/خط جدید)، `template` (نام پترن مصوب در پنل)
- **پارامترهای اختیاری توکن و قانون فاصله (بحرانی):**
  | توکن | فاصله |
  |------|-------|
  | `token`, `token2`, `token3` | **بدون فاصله** (رد می‌شود) |
  | `token10` | حداکثر **۵ فاصله** مجاز |
  | `token20` | حداکثر **۸ فاصله** مجاز |
- `type`: `sms` (پیش‌فرض) یا `call`.
- **پترن باید از قبل در پنل کاوه‌نگار تأیید شده باشد**؛ متنِ پیام روی پنل ثابت است (نه از DB).
- نیازمند اشتراک advanced.

**پیامد:** کد ۵ رقمی (بدون فاصله) → `token`. اسم سایت مثل «یاسوج نوبت» (۱ فاصله) → باید در **`token10`** برود (نه `token`/`token2`/`token3` که فاصله را رد می‌کنند).

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `src/Auth/Service/OtpService.php` | ساخت و dispatch پیامک OTP — باید به Lookup سوییچ شود |
| `src/Sms/Service/SmsService.php` | `dispatchAsync(...templateCode, templateVars...)` → اگر `templateCode` باشد `sendTemplate()` صدا می‌زند |
| `src/Sms/Provider/KavehNegarProvider.php` | `sendTemplate()` → `/verify/lookup.json`؛ map توکن باید نام‌دار شود (برای `token10`) |
| `.env` + `config/services.yaml` | افزودن `KAVENEGAR_OTP_TEMPLATE` و bind به OtpService |
| `docs/api/sms.md` | مستندسازی سوییچ OTP به Lookup |

## وضعیت فعلی

**OtpService::sendCode** (متن‌آزاد، بدون templateCode):

```php
if ($this->appEnv !== 'dev') {
    $site    = $this->resolveSiteName($domain);
    $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]);
    $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
}
```

Constructor فعلی:

```php
public function __construct(
    private readonly CacheInterface  $cache,
    private readonly SmsService      $sms,
    private readonly SmsTextResolver $smsText,
    private readonly CityRepository  $cityRepo,
    private readonly int             $otpTtl = 1200,
    private readonly string          $appEnv = 'dev',
) {}
```

**SmsService::dispatchAsync / sendNow** (مسیر انتخاب send vs lookup):

```php
public function dispatchAsync(
    string $mobile, string $message, string $provider = 'kavenegar',
    ?string $templateUuid = null, array $templateVars = [],
    ?string $templateCode = null, string $tag = SmsLog::TAG_GLOBAL,
): void { /* dispatch SendSmsMessage */ }

// sendNow:
$success = ($msg->templateCode !== null)
    ? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars)
    : $provider->send($msg->mobile, $msg->message);
```

**KavehNegarProvider::sendTemplate** (map ترتیبی فعلی — فاصله را در token2/3 می‌گذارد که رد می‌شود):

```php
public function sendTemplate(string $mobile, string $templateCode, array $vars): bool
{
    try {
        $params = ['receptor' => $mobile, 'template' => $templateCode];
        foreach (array_values($vars) as $i => $v) {
            $params['token' . ($i > 0 ? $i + 1 : '')] = $v;
        }
        $resp = $this->httpClient->request('POST',
            self::BASE . '/' . $this->key() . '/verify/lookup.json', [
                'body' => http_build_query($params), 'timeout' => 10,
            ]);
        $data = $resp->toArray();
        return ($data['return']['status'] ?? 0) === 200;
    } catch (\Throwable $e) { /* log; return false */ }
}
```

**caller دیگر `sendTemplate`:** فقط `POST /api/v1/sms/send-template` در `src/Sms/Controller/SmsController.php:488` که `$vars` را با **کلیدهای معنایی** (مثل `name`,`date`) و وابسته به **ترتیب** پاس می‌دهد. پس map ترتیبی نباید برای این caller بشکند.

**.env فعلی:**
```
OTP_TTL=1200
KAVENEGAR_API_KEY=change_me
```

## وظایف

### ۱. `KavehNegarProvider::sendTemplate` — پشتیبانی توکنِ نام‌دار (backward-safe)

اگر همه‌ی کلیدهای `$vars` نام slot معتبر کاوه‌نگار باشند (`token`,`token2`,`token3`,`token10`,`token20`) → همان‌ها را مستقیم استفاده کن (تا اسم سایتِ فاصله‌دار در `token10` برود). در غیر این صورت (کلیدهای معنایی/لیستی) → همان map ترتیبی فعلی (برای caller `send-template`).

```php
$params = ['receptor' => $mobile, 'template' => $templateCode];
$slots = ['token', 'token2', 'token3', 'token10', 'token20'];
if ($vars !== [] && array_keys($vars) !== range(0, count($vars) - 1)
    && array_diff(array_keys($vars), $slots) === []) {
    foreach ($vars as $slot => $v) { $params[$slot] = $v; }
} else {
    foreach (array_values($vars) as $i => $v) {
        $params['token' . ($i > 0 ? $i + 1 : '')] = $v;
    }
}
```

### ۲. `.env` + `config/services.yaml` — نام پترن OTP

`.env` (کاربر مقدار واقعی پترن مصوب را می‌گذارد):
```
KAVENEGAR_OTP_TEMPLATE=
```

`config/services.yaml` روی سرویس `OtpService`:
```yaml
App\Auth\Service\OtpService:
    arguments:
        $otpTtl: '%env(int:OTP_TTL)%'
        $appEnv: '%kernel.environment%'
        $otpTemplate: '%env(default::KAVENEGAR_OTP_TEMPLATE)%'
```

### ۳. `OtpService` — dispatch از طریق Lookup

- پارامتر constructor جدید `?string $otpTemplate = null` **بعد از** پارامترهای بدون‌دیفالت و کنار `$appEnv` (ترتیب معتبر PHP؛ همه دیفالت‌دار در انتها).
- در `sendCode`، وقتی پترن ست است → با `templateCode` و توکن‌های نام‌دار dispatch کن:

```php
if ($this->appEnv !== 'dev') {
    $site    = $this->resolveSiteName($domain);
    $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]); // فقط برای لاگ SmsLog

    if ($this->otpTemplate) {
        $this->sms->dispatchAsync(
            $mobile, $message,
            templateCode: $this->otpTemplate,
            templateVars: ['token' => $code, 'token10' => $site],
            tag: SmsLog::TAG_OTP,
        );
    } else {
        // fallback متن‌آزاد فقط وقتی پترن ست نشده (مثلاً محیط توسعه)
        $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
    }
}
```

- `token` = کد ۵ رقمی (بدون فاصله ✓). `token10` = اسم سایت (تا ۵ فاصله مجاز ✓).
- اگر پترن مصوب فقط `%token` داشته باشد، `token10` اضافی توسط کاوه‌نگار نادیده گرفته می‌شود (بی‌خطر) — اسم سایت فقط وقتی نمایش داده می‌شود که پترن `%token10` هم داشته باشد.

### ۴. مستندسازی `docs/api/sms.md`

- در بخش «متن ویرایش‌پذیر پیامک‌های سیستمی»، تگ `otp`: تصریح کن که **ارسال OTP از طریق Kavenegar VerifyLookup** انجام می‌شود (نه `send` متن‌آزاد) وقتی `KAVENEGAR_OTP_TEMPLATE` ست باشد؛ متنِ پیام از **پترن مصوب کاوه‌نگار** می‌آید نه از body قابل‌ویرایش DB (body صرفاً برای لاگ `SmsLog`).
- map توکن‌ها را مستند کن: `token` = کد، `token10` = اسم سایت (فاصله‌دار).
- الزام `.env`: `KAVENEGAR_OTP_TEMPLATE` باید نام پترن مصوب باشد؛ بدون آن، fallback به متن‌آزاد.

## نکات مهم

- **فقط OTP** به Lookup می‌رود؛ مسیر `send()` متن‌آزاد برای بقیه‌ی تگ‌ها و caller `send-template` دست‌نخورده بماند.
- **قانون فاصله** رعایت شود: کد → `token`؛ هر مقدار فاصله‌دار (اسم سایت) → `token10`. هرگز اسم فاصله‌دار در `token`/`token2`/`token3` نگذار.
- **پترن باید در پنل کاوه‌نگار مصوب باشد** و ساختار توکنش با map کد بخواند (`%token` برای کد، در صورت نیاز `%token10` برای سایت). این خارج از کد است و باید توسط صاحب حساب انجام شود.
- نیازمند **اشتراک advanced** کاوه‌نگار.
- **backward-safe**: تغییر `sendTemplate` نباید caller `send-template` (کلیدهای معنایی/ترتیبی) را بشکند — با شرط «همه کلیدها slot معتبرند» تضمین شود.
- **cache**: بعد از تغییر constructor `OtpService` و bind جدید، `cache:clear` برای **هر دو محیط** `dev` و `test` لازم است (وگرنه کانتینر کامپایل‌شده‌ی قدیمی TypeError می‌دهد). worker پیامک (`messenger:consume async`) هم باید ری‌استارت شود.
- **تست**:
  ```bash
  ddev exec php -l src/Auth/Service/OtpService.php
  ddev exec php -l src/Sms/Provider/KavehNegarProvider.php
  ddev exec php bin/console cache:clear
  ddev exec php bin/console cache:clear --env=test
  ddev exec php bin/console lint:container
  ddev exec php bin/phpunit tests/Auth/SendCodeMobileRateLimitTest.php
  ```
  (تست واقعی ارسال Lookup نیاز به API key و پترن مصوب دارد؛ در `dev`/`test` پیامک ارسال نمی‌شود.)
- بعد از تغییر، `docs/api/sms.md` را در همان session به‌روز کن (قانون استاندینگ پروژه).
