9.6 KiB
اجبار ارسال همه پیامکها از طریق Kavenegar VerifyLookup (حذف مسیر send.json خام)
پروژه
clinicpro (Backend — دامنه src/Sms)
زمینه
کاوهنگار در ایران ارسال پیامک خدماتی (سیستمی/غیرشخصی) را فقط از طریق الگوهای تأییدشده با
اندپوینت verify/lookup.json مجاز میداند. ارسال متن آزاد با sms/send.json برای این نوع پیامها
فیلتر/رد میشود (اغلب بدون خطای صریح — پیام در صف میرود ولی تحویل نمیشود).
هدف: همهی مسیرهای ارسال پیامک باید از sendTemplate() (یعنی verify/lookup.json) عبور کنند و
هیچ مسیری نباید به send() خام (sms/send.json) سقوط کند.
مشکل / هدف
الان اکثر پیامهای سیستمی درست از الگو استفاده میکنند، اما سه مسیر هنوز میتوانند به
send.json خام سقوط کنند و پیامشان تحویل نشود:
- fallback در
SmsService::dispatchTemplate()— وقتی تگ،kavenegar_templateیاtoken_mapنداشته باشد، بهdispatchAsync()بدونtemplateCodeمیرود →send()خام. - fallback در
SmsService::sendNow()— هرSendSmsMessageکهtemplateCode === nullباشد باsend()خام ارسال میشود. - ارسال دستی/کاربر (
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 — سقوط به مسیر خام:
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 — ارسال دستی بدون الگو (خط ~۷۵):
$this->smsService->dispatchAsync($mobile, $message, $provider, tag: \App\Sms\Entity\SmsLog::TAG_USER_TEMPLATE);
وظایف
۱. بستن سقوط در dispatchTemplate() — نبود الگو باید خطای صریح باشد، نه ارسال خام
وقتی تگی kavenegar_template/token_map ندارد، بهجای ارسال با send.json، باید:
- یک خطای لاگشدهی واضح ثبت شود (کدام تگ الگو ندارد) و
- پیام ارسال نشود (یا در صورت نیاز، یک
SmsLogناموفق با دلیل ثبت شود تا در پنل دیده شود).
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 ثبت شود تا در گزارشها دیده شود:
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به صف نرود.
if (!$template->getProviderCode()) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این تمپلیت کد VerifyLookup کاوهنگار ندارد و قابل ارسال نیست', 422);
}
sendCustom(~خط ۷۵،TAG_USER_TEMPLATE): ارسال متن آزاد باsend.jsonطبق سیاست جدید مجاز نیست. دو گزینه (در پرامپت اجرا یکی را انتخاب کن):- این اکشن را به الگو-محور تبدیل کن (فقط از
sendViaTemplateبا الگوی تأییدشده استفاده شود) و مسیر متن آزاد را حذف/۴۲۲ کن. - اگر کسبوکار به متن آزاد نیاز دارد، آن را پشت یک الگوی «آزاد» تأییدشدهی کاوهنگار ببر
(نیازمند الگوی مصوب) — نه
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.