Files
clinicpro/.claude/prompt/sms-lookup-only.md
T

170 lines
9.6 KiB
Markdown

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