Files

9.6 KiB
Raw Permalink Blame History

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

نکته: همه‌ی تگ‌های سیستمی (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 طبق سیاست جدید مجاز نیست. دو گزینه (در پرامپت اجرا یکی را انتخاب کن):
    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.