# اجبار ارسال همه پیامک‌ها از طریق 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.