# اجبار ارسال همه پیامک‌ها از طریق Kavenegar VerifyLookup (حذف مسیر send.json خام)

## پروژه

`clinicpro` (Backend — دامنه `src/Sms`)

## زمینه

کاوه‌نگار در ایران ارسال پیامک خدماتی (سیستمی/غیرشخصی) را فقط از طریق **الگوهای تأییدشده** با
اندپوینت `verify/lookup.json` مجاز می‌داند. ارسال متن آزاد با `sms/send.json` برای این نوع پیام‌ها
**فیلتر/رد** می‌شود (اغلب بدون خطای صریح — پیام در صف می‌رود ولی تحویل نمی‌شود).

هدف: همه‌ی مسیرهای ارسال پیامک باید از `sendTemplate()` (یعنی `verify/lookup.json`) عبور کنند و
هیچ مسیری نباید به `send()` خام (`sms/send.json`) سقوط کند.

## مشکل / هدف

الان اکثر پیام‌های سیستمی درست از الگو استفاده می‌کنند، اما **سه مسیر** هنوز می‌توانند به
`send.json` خام سقوط کنند و پیام‌شان تحویل نشود:

1. **fallback در `SmsService::dispatchTemplate()`** — وقتی تگ، `kavenegar_template` یا `token_map`
   نداشته باشد، به `dispatchAsync()` بدون `templateCode` می‌رود → `send()` خام.
2. **fallback در `SmsService::sendNow()`** — هر `SendSmsMessage` که `templateCode === null` باشد
   با `send()` خام ارسال می‌شود.
3. **ارسال دستی/کاربر** (`SmsController`) — دو اکشن که مستقیم `dispatchAsync()` بدون الگو صدا می‌زنند:
   - `sendCustom` (خط ~75) با `TAG_USER_TEMPLATE` → همیشه `send()` خام.
   - `sendViaTemplate` (خط ~488) اگر `SmsTemplate.providerCode` خالی باشد → `templateCode=null` → `send()` خام.

نکته: همه‌ی تگ‌های سیستمی (`OTP, PAYMENT, CLINIC_INVITATION, PRE_REGISTRATION, NOTIFICATION_MOBILE,
SECRETARY, DOCTOR_APPOINTMENT, WELCOME`) در `SmsMessageTemplate::DEFAULTS` الگو و `token_map` دارند و
درست کار می‌کنند — تمرکز اصلی روی **بستن مسیر سقوط به send.json** است، نه بازنویسی الگوها.

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

| فایل | نقش |
|------|-----|
| `src/Sms/Service/SmsService.php` | نقطه‌ی تصمیم `sendTemplate` در برابر `send` (خطوط ۴۹–۵۲ و ۷۸–۹۱) |
| `src/Sms/Provider/KavehNegarProvider.php` | `send()` = `sms/send.json`، `sendTemplate()` = `verify/lookup.json` |
| `src/Sms/Provider/SmsProviderInterface.php` | قرارداد دو متد `send` / `sendTemplate` |
| `src/Sms/Controller/SmsController.php` | اکشن‌های ارسال دستی (`sendCustom` ~۷۵، `sendViaTemplate` ~۴۸۸) |
| `src/Sms/Entity/SmsMessageTemplate.php` | `DEFAULTS` تگ‌های سیستمی + نگاشت token |
| `src/Sms/Entity/SmsLog.php` | ثابت‌های `TAG_*` |
| `docs/api/sms.md` | مستند API — طبق قانون پروژه باید هم‌زمان به‌روز شود |

## وضعیت فعلی

`src/Sms/Service/SmsService.php` — سقوط به مسیر خام:

```php
public function dispatchTemplate(string $tag, string $mobile, array $vars = [], string $provider = 'kavenegar'): void
{
    $tpl = $this->messageTemplateRepo->findByTag($tag);
    $kaveTemplate = $tpl?->getKavenegarTemplate()
        ?? (SmsMessageTemplate::DEFAULTS[$tag]['kavenegar_template'] ?? null);
    $tokenMap = $tpl?->getTokenMap()
        ?: (SmsMessageTemplate::DEFAULTS[$tag]['token_map'] ?? []);

    $message = $this->textResolver->resolve($tag, $vars);

    if ($kaveTemplate === null || $tokenMap === []) {
        $this->dispatchAsync($mobile, $message, $provider, tag: $tag);   // ← send.json خام
        return;
    }
    // ...
}

public function sendNow(SendSmsMessage $msg): bool
{
    $provider = $this->resolveProvider($msg->provider);

    $success = ($msg->templateCode !== null)
        ? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars)
        : $provider->send($msg->mobile, $msg->message);                  // ← send.json خام
    // ...
}
```

`src/Sms/Controller/SmsController.php` — ارسال دستی بدون الگو (خط ~۷۵):

```php
$this->smsService->dispatchAsync($mobile, $message, $provider, tag: \App\Sms\Entity\SmsLog::TAG_USER_TEMPLATE);
```

## وظایف

### ۱. بستن سقوط در `dispatchTemplate()` — نبود الگو باید خطای صریح باشد، نه ارسال خام

وقتی تگی `kavenegar_template`/`token_map` ندارد، به‌جای ارسال با `send.json`، باید:
- یک خطای لاگ‌شده‌ی واضح ثبت شود (کدام تگ الگو ندارد) و
- پیام ارسال **نشود** (یا در صورت نیاز، یک `SmsLog` ناموفق با دلیل ثبت شود تا در پنل دیده شود).

```php
if ($kaveTemplate === null || $tokenMap === []) {
    $this->logger->error(sprintf('SMS tag "%s" has no Kavenegar template/token_map; refusing raw send', $tag), [
        'tag' => $tag, 'mobile' => $mobile,
    ]);
    // اختیاری: ثبت SmsLog ناموفق برای مشاهده در پنل به‌جای سکوت کامل
    return;
}
```

> `LoggerInterface` را به `SmsService` تزریق کن (constructor) اگر موجود نیست.

### ۲. بستن سقوط در `sendNow()` — `templateCode` نال یعنی خطا، نه send خام

پیامی که بدون `templateCode` به صف رسیده نباید با `send.json` برود. رفتار پیشنهادی: اگر
`templateCode === null` بود، ارسال را ناموفق در نظر بگیر و لاگ کن (به‌جای `send()`), و `SmsLog`
با `success=false` ثبت شود تا در گزارش‌ها دیده شود:

```php
public function sendNow(SendSmsMessage $msg): bool
{
    $provider = $this->resolveProvider($msg->provider);

    if ($msg->templateCode === null) {
        $this->logger->error('SMS refused: no templateCode (lookup-only policy)', [
            'mobile' => $msg->mobile, 'tag' => $msg->tag,
        ]);
        $success = false;
    } else {
        $success = $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars);
    }

    $log = new SmsLog($msg->mobile, $msg->message, $provider->getName(), $success, $msg->tag);
    if ($msg->templateUuid) $log->setTemplateUuid($msg->templateUuid);
    $this->logRepo->save($log);

    return $success;
}
```

> تصمیم معماری: اگر بخواهی «تک مسیر»، می‌توانی `send()` را از `SmsProviderInterface` و
> `KavehNegarProvider` کلاً حذف کنی. اگر می‌خواهی احتیاطی نگه‌ داری، حداقل هیچ فراخوان‌کننده‌ای
> نباید به آن برسد. در پرامپت اجرا تصمیم را صریح ثبت کن.

### ۳. اکشن‌های ارسال دستی `SmsController` — فقط با الگوی تأییدشده

- **`sendViaTemplate` (~خط ۴۸۸):** پیش از `dispatchAsync`، اگر `$template->getProviderCode()` خالی بود،
  خطای ۴۲۲ برگردان (الگو کد کاوه‌نگار ندارد) و ارسال نکن — تا `templateCode=null` به صف نرود.

```php
if (!$template->getProviderCode()) {
    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این تمپلیت کد VerifyLookup کاوه‌نگار ندارد و قابل ارسال نیست', 422);
}
```

- **`sendCustom` (~خط ۷۵، `TAG_USER_TEMPLATE`):** ارسال متن آزاد با `send.json` طبق سیاست جدید مجاز نیست.
  دو گزینه (در پرامپت اجرا یکی را انتخاب کن):
  1. این اکشن را به الگو-محور تبدیل کن (فقط از `sendViaTemplate` با الگوی تأییدشده استفاده شود) و
     مسیر متن آزاد را حذف/۴۲۲ کن.
  2. اگر کسب‌وکار به متن آزاد نیاز دارد، آن را پشت یک الگوی «آزاد» تأییدشده‌ی کاوه‌نگار ببر
     (نیازمند الگوی مصوب) — نه `send.json`.

### ۴. تست‌ها + مستندات

- تست‌های `tests/` مرتبط با SMS را اجرا/به‌روز کن؛ یک تست اضافه کن که ثابت کند:
  «تگ بدون الگو → هیچ فراخوان `send()` انجام نمی‌شود و `SmsLog` ناموفق ثبت می‌شود».
- `docs/api/sms.md` را به‌روز کن: قانون «فقط VerifyLookup»، رفتار جدید `sendCustom`/`sendViaTemplate`
  (شرط `providerCode`)، و کد خطای ۴۲۲ جدید.

## نکات مهم

- الگوهای سیستمی موجود دست‌نخورده‌اند؛ فقط **مسیر سقوط** بسته می‌شود. رگرسیون OTP/پرداخت/دعوت را چک کن
  (این‌ها الگو دارند و باید هنوز کار کنند).
- `token/token2/token3` فاصله را رد می‌کنند؛ مقدار دارای فاصله باید در `token10/token20` بنشیند —
  این منطق در `KavehNegarProvider::forSlot()` هست، تغییرش نده.
- همه‌ی خروجی‌ها باید در `SmsLog` ثبت شوند (چه موفق چه ناموفق) تا در پنل SMS قابل رصد باشد؛ سکوت کامل ممنوع.
- `KAVENEGAR_API_KEY` فقط از env خوانده می‌شود؛ در محیط لوکال ممکن است خالی باشد — تست‌ها نباید به شبکه‌ی واقعی وابسته باشند (provider را mock کن).
- طبق الگوی پروژه: پاسخ‌ها با `$this->error()/success()`، کدهای خطا از `ErrorCodes`، تاریخ‌ها Unix timestamp.
