170 lines
9.6 KiB
Markdown
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.
|