Files
clinicpro/.claude/prompt/sms-verify-lookup-templates.md
hamed 969dc9651f Refactor SMS sending to use KavehNegar VerifyLookup templates
- Removed SmsTextResolver dependency from multiple services and controllers.
- Introduced dispatchTemplate method in SmsService to handle SMS sending with templates.
- Updated existing SMS sending logic across various services (OtpService, PreRegistrationController, ClinicInvitationService, PaymentManager, SecretaryController, RepresentationActionController) to utilize the new dispatchTemplate method.
- Enhanced SmsMessageTemplate entity to include kavenegar_template and token_map fields.
- Created migration to add new fields to the sms_message_templates table and populate them with existing data.
- Updated SeedSmsMessageTemplatesCommand to handle new template structure.
- Added documentation for the new SMS template structure and usage.
2026-07-05 11:20:53 +03:30

17 KiB
Raw Permalink Blame History

تبدیل همه‌ی پیامک‌های سیستمی به VerifyLookup کاوه‌نگار (تمپلت نام‌دار)

پروژه

clinicpro (backend src/Sms + سرویس‌های فرستنده + پنل ادمین assets/admin/pages/SmsPage.tsx)

زمینه

همه‌ی پیامک‌های ما «اطلاع‌رسانی/تراکنشی» هستند. سرویس کاوه‌نگار برای این نوع پیامک‌ها VerifyLookup را الزام می‌کند (نه ارسال متن آزاد sms/send). VerifyLookup فقط با تمپلت‌های از پیش‌ساخته و تأییدشده در پنل کاوه‌نگار کار می‌کند و متغیرها را در جایگاه‌های token, token2, token3, token10, token20 جایگذاری می‌کند.

  • مستند REST: https://kavenegar.com/rest.html#sms-Lookup
  • مستند SDK: https://kavenegar.com/sdk.html#php

وضعیت فعلی کد: KavehNegarProvider از قبل هر دو متد را داردsend() (متن آزاد via sms/send.json) و sendTemplate() (VerifyLookup via verify/lookup.json). اما فقط OTP از lookup استفاده می‌کند؛ بقیه‌ی همه‌ی پیامک‌ها با send() (متن آزاد) می‌روند. همچنین موجودیت SmsMessageTemplate (متن ویرایش‌پذیر هر تگ) نه نام تمپلت کاوه‌نگار دارد نه نگاشت متغیر→token.

مشکل / هدف

۱. همه‌ی پیامک‌های سیستمی (همه‌ی تگ‌ها) باید از طریق VerifyLookup ارسال شوند، نه send() متن‌آزاد. ۲. هر تمپلت سیستمی باید یک نام تمپلت کاوه‌نگار داشته باشد (که کاربر در پنل کاوه‌نگار می‌سازد) + یک نگاشت متغیر→token slot. ۳. مسیر فرستنده متمرکز شود تا هر call-site فقط تگ + متغیرها بدهد و ارسال همیشه lookup باشد.

قید حیاتی VerifyLookup (حتماً رعایت شود)

  • تمپلت باید از قبل در پنل کاوه‌نگار ساخته و تأیید شده باشد؛ متن ثابت پیام در پنل تعریف می‌شود، نه در دیتابیس ما. یعنی بعد از این تغییر، متن واقعی ارسالی = تمپلت کاوه‌نگار؛ فیلد body در DB فقط برای پیش‌نمایش ادمین و متن SmsLog می‌ماند و باید دستی با تمپلت پنل هم‌راستا نگه داشته شود.
  • جایگاه‌ها: token, token2, token3فاصله (space) نمی‌پذیرند (تک‌مقدار بدون space)؛ token10 و token20 → فاصله مجازند. پس هر مقداری که ممکن است space داشته باشد (نام دکتر/بیمار/کلینیک/مالک/نام سایت) باید در token10/token20 برود؛ مقادیر بدون space (کد، تاریخ ۱۴۰۳/۰۵/۱۲، ساعت، username، password، لینک) در token/token2/token3.
  • newline/متن طولانی داخل token مجاز نیست؛ خطوط ثابت و شکست خط باید داخل خودِ تمپلت پنل باشند، فقط مقادیر متغیر token شوند.
  • KavehNegarProvider::sendTemplate() از قبل نگاشت صریح slot را پشتیبانی می‌کند (اگر کلیدهای $vars دقیقاً نام slotها باشند از همان استفاده می‌کند)، پس منطق provider نیاز به تغییر ندارد.

فایل‌های مرتبط

فایل نقش
src/Sms/Provider/KavehNegarProvider.php sendTemplate() (VerifyLookup) — آماده است، تغییر نده
src/Sms/Service/SmsService.php dispatchAsync() / sendNow() — افزودن متد متمرکز dispatchTemplate()
src/Sms/Service/SmsTextResolver.php resolve متن body برای log/preview
src/Sms/Entity/SmsMessageTemplate.php افزودن kavenegar_template + token_map به فیلدها و DEFAULTS
src/Sms/Command/SeedSmsMessageTemplatesCommand.php seed از DEFAULTS
src/Sms/Controller/SmsMessageController.php GET/PATCH تمپلت‌های سیستمی (ادمین)
assets/admin/pages/SmsPage.tsx ویرایش تمپلت‌های سیستمی
src/Auth/Service/OtpService.php OTP (تنها جایی که الان lookup می‌کند)
src/Auth/Controller/NotificationMobileController.php تگ notification_mobile
src/Auth/Controller/PreRegistrationController.php تگ pre_registration
src/Secretary/Controller/SecretaryController.php تگ secretary
src/Payment/Service/PaymentManager.php تگ‌های payment و doctor_appointment
src/ClinicInvitation/Service/ClinicInvitationService.php تگ clinic_invitation
src/Representation/Controller/RepresentationActionController.php تگ welcome (الان sprintf inline)
migrations/VersionXXithm.php migration برای دو ستون جدید
docs/api/sms.md مستندسازی

وضعیت فعلی

SmsService::sendNow — انتخاب بین lookup و متن‌آزاد

$success = ($msg->templateCode !== null)
    ? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars)
    : $provider->send($msg->mobile, $msg->message);

الگوی فعلی همه‌ی call-siteها (به‌جز OTP) — متن‌آزاد، بدون templateCode

// PaymentManager.php:318
$message = $this->smsText->resolve(SmsLog::TAG_PAYMENT, ['doctor' => $doctor, 'date' => $date]);
$this->smsService->dispatchAsync($mobile, $message, tag: SmsLog::TAG_PAYMENT);
// ClinicInvitationService.php:116، SecretaryController.php:133، NotificationMobileController.php:64،
// PreRegistrationController.php:158 — همگی همین شکل: resolve(...) سپس dispatchAsync(mobile, message, tag: TAG)

تنها جای درست (OTP) — که باید الگوی بقیه شود

// OtpService.php:80
$this->sms->dispatchAsync(
    $mobile, $message,
    templateCode: $this->otpTemplate,                       // نام تمپلت کاوه‌نگار از env
    templateVars: ['token' => $code, 'token10' => $site],   // نگاشت صریح slot
    tag: SmsLog::TAG_OTP,
);

SmsMessageTemplate::DEFAULTS (فاقد نام تمپلت و نگاشت token)

SmsLog::TAG_PAYMENT => [
    'title'     => 'تأیید پرداخت و نوبت',
    'body'      => 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.',
    'variables' => ['doctor', 'date'],
],
// ... بقیه‌ی تگ‌ها مشابه

RepresentationActionController — welcome به‌صورت inline (بدون تمپلت)

$this->smsService->dispatchAsync(
    $mobile,
    sprintf('دکتر %s عزیز، به %s خوش آمدید. ...', $name, $siteName),
    tag: \App\Sms\Entity\SmsLog::TAG_WELCOME,
);

وظایف

۱. افزودن kavenegar_template و token_map به SmsMessageTemplate + DEFAULTS

در src/Sms/Entity/SmsMessageTemplate.php:

  • دو ستون جدید:
#[ORM\Column(name: 'kavenegar_template', type: 'string', length: 100, nullable: true)]
private ?string $kavenegarTemplate = null;

// نگاشت متغیر منطقی → slot کاوه‌نگار: مثلاً {"code":"token","site":"token10"}
#[ORM\Column(name: 'token_map', type: 'json')]
private array $tokenMap = [];
  • getter/setter، افزودن به constructor و toArray().
  • در هر آیتم DEFAULTS دو کلید اضافه کن: kavenegar_template و token_map. مقادیر پیشنهادی (با رعایت قید space):
تگ نام تمپلت کاوه‌نگار (پیشنهادی) token_map
otp clinicpro-otp { "code": "token", "site": "token10" }
notification_mobile clinicpro-notify-code { "code": "token" }
payment clinicpro-payment { "doctor": "token10", "date": "token2" }
clinic_invitation clinicpro-clinic-invite { "clinic": "token10", "link": "token" }
pre_registration clinicpro-pre-register { "username": "token", "password": "token2", "link": "token3" }
secretary clinicpro-secretary { "owner": "token10", "username": "token", "link": "token3" }
doctor_appointment clinicpro-doctor-appt { "patient": "token10", "date": "token2", "time": "token3" }
welcome clinicpro-welcome { "name": "token10", "site": "token20" }

نام‌ها را نهایی با کاربر چک کن؛ همین نام‌ها باید در پنل کاوه‌نگار ساخته شوند (وظیفه‌ی ۶).

  • سپس migration بساز (ddev exec php bin/console doctrine:migrations:diff) و seed را طوری کن که رکوردهای موجود هم دو ستون جدید را بگیرند (در SeedSmsMessageTemplatesCommand علاوه بر ساخت رکورد جدید، اگر رکورد هست ولی kavenegar_template خالی است، از DEFAULTS پرش کن — یا یک migration داده‌ای UPDATE برای پرکردن مقادیر).

۲. متد متمرکز SmsService::dispatchTemplate(tag, mobile, vars)

یک متد واحد که همه‌ی call-siteها از آن استفاده کنند:

/** @param array<string,string|int> $vars متغیرهای منطقی (مثل ['doctor'=>..,'date'=>..]) */
public function dispatchTemplate(string $tag, string $mobile, array $vars = []): void
{
    $tpl = $this->messageTemplateRepo->findByTag($tag);
    $kaveTemplate = $tpl?->getKavenegarTemplate()
        ?? SmsMessageTemplate::DEFAULTS[$tag]['kavenegar_template'] ?? null;
    $tokenMap = $tpl?->getTokenMap()
        ?: (SmsMessageTemplate::DEFAULTS[$tag]['token_map'] ?? []);

    // متن body برای log/preview (تمپلت واقعی سمت کاوه‌نگار است)
    $message = $this->textResolver->resolve($tag, $vars);

    if ($kaveTemplate !== null && $tokenMap !== []) {
        // نگاشت متغیر منطقی → slot کاوه‌نگار
        $slotVars = [];
        foreach ($tokenMap as $logicalKey => $slot) {
            if (array_key_exists($logicalKey, $vars)) {
                $slotVars[$slot] = (string) $vars[$logicalKey];
            }
        }
        $this->dispatchAsync($mobile, $message, templateCode: $kaveTemplate, templateVars: $slotVars, tag: $tag);
    } else {
        // fallback فقط اگر تمپلت کاوه‌نگار تعریف نشده باشد
        $this->dispatchAsync($mobile, $message, tag: $tag);
    }
}
  • SmsService باید SmsMessageTemplateRepository و SmsTextResolver را inject کند.
  • قید space را رعایت کن: اگر مقداری که به token/token2/token3 می‌رود شامل space باشد، کاوه‌نگار خطا می‌دهد. token_map در DEFAULTS طوری چیده شده که مقادیر دارای space در token10/token20 بروند؛ هنگام افزودن تگ جدید همین را رعایت کن.

۳. مهاجرت همه‌ی call-siteها به dispatchTemplate

هر جای زیر را از الگوی «resolve() + dispatchAsync(..., tag:)» به یک فراخوانی dispatchTemplate(tag, mobile, vars) تبدیل کن (متغیرهای منطقی همان کلیدهای {...} بدنه‌اند):

  • src/Auth/Service/OtpService.php:80dispatchTemplate(TAG_OTP, $mobile, ['code'=>$code,'site'=>$site]) (شاخه‌ی env otpTemplate و fallback حذف شود؛ منبع نام تمپلت حالا DB/DEFAULTS است).
  • src/Auth/Controller/NotificationMobileController.php:64
  • src/Auth/Controller/PreRegistrationController.php:158
  • src/Secretary/Controller/SecretaryController.php:133
  • src/Payment/Service/PaymentManager.php:318 (payment) و :328 (doctor_appointment)
  • src/ClinicInvitation/Service/ClinicInvitationService.php:116

۴. تگ welcome را از inline به تمپلت تبدیل کن

در src/Representation/Controller/RepresentationActionController.php (دو جای ~313 و ~388) به‌جای sprintf(...) از dispatchTemplate(TAG_WELCOME, $mobile, ['name'=>$name,'site'=>$siteName]) استفاده کن. مطمئن شو TAG_WELCOME در DEFAULTS وجود دارد (در وظیفه‌ی ۱ اضافه شد).

۵. پنل ادمین: نمایش/ویرایش نام تمپلت کاوه‌نگار

  • src/Sms/Controller/SmsMessageController.php: در پاسخ GET، kavenegar_template و token_map را هم برگردان (از toArray())؛ در PATCH اجازه‌ی ویرایش kavenegar_template (و در صورت لزوم token_map) را بده.
  • assets/admin/pages/SmsPage.tsx: فیلد «نام تمپلت کاوه‌نگار» را در فرم ویرایش هر تمپلت سیستمی نشان بده و ذخیره کن. یک راهنمای کوتاه بگذار که این نام باید دقیقاً با تمپلت ساخته‌شده در پنل کاوه‌نگار یکی باشد.
  • مستند API (docs/api/sms.md): تغییر response/بدنه‌ی admin/sms/messages را ثبت کن.

۶. فهرست تمپلت‌هایی که کاربر باید در پنل کاوه‌نگار بسازد

در پایان، این جدول را (با نام‌های نهایی) به کاربر بده تا در پنل کاوه‌نگار بسازد؛ متن هر تمپلت باید با body همان تگ هم‌راستا باشد و جایگاه‌ها با %token... مطابق token_map:

تمپلت نمونه متن پنل (جایگاه‌ها با token_map)
clinicpro-otp کد تأیید شما: %token (سایت: %token10)
clinicpro-payment نوبت شما با %token10 در تاریخ %token2 ثبت و تأیید شد.
(برای هر تگ بر اساس body و token_map)

نکات مهم

  • متن واقعی ارسالی از پنل کاوه‌نگار می‌آید، نه از body دیتابیس. پس body را فقط برای preview/log نگه‌دار و در پنل هم همان متن را بساز؛ اگر ادمین body را عوض کند، متن ارسالی عوض نمی‌شود مگر تمپلت پنل هم عوض شود — این را در UI به ادمین گوشزد کن.
  • قید space در token/token2/token3 مهم‌ترین علت خطای ۴۳۱/۴۱۸ کاوه‌نگار است؛ مقادیر دارای فاصله را حتماً به token10/token20 بده (token_map پیش‌فرض این را رعایت کرده).
  • لینک‌ها: بعضی تمپلت‌های VerifyLookup لینک را فقط اگر تمپلت با لینک تأیید شده باشد می‌پذیرند؛ برای تگ‌های دارای {link} (clinic_invitation, pre_registration, secretary) هنگام ساخت تمپلت در پنل، تأیید لینک را بگیر.
  • SDK کاوه‌نگار (kavenegar/php) لازم نیست: provider فعلی مستقیم verify/lookup.json را با HttpClient صدا می‌زند و درست است. اگر کاربر صراحتاً SDK بخواهد، می‌توان composer require kavenegar/php کرد و provider را بازنویسی کرد، ولی پیش‌فرض همین HttpClient بماند (بدون وابستگی جدید).
  • TAG_USER_TEMPLATE (پنل پیامک کلینیک‌ها، SmsController): این‌ها پیامک‌های متن‌آزادِ کاربرساخته با SmsTemplate.providerCode هستند و از قبل مسیر تمپلت‌دار دارند؛ در دامنه‌ی این تغییر نیستند. اگر کاربر می‌خواهد آن‌ها هم اجباری lookup شوند، جداگانه بپرس (متن آزاد کاربر با VerifyLookup سازگار نیست مگر هر متن یک تمپلت تأییدشده داشته باشد).
  • env KAVENEGAR_OTP_TEMPLATE: بعد از انتقال نام تمپلت به DB، این env و binding $otpTemplate در config/services.yaml:59 را حذف یا به fallback تبدیل کن (تا جای واحدِ حقیقت، DB باشد).
  • بعد از تغییر Entity: doctrine:migrations:diff سپس migrate. بعد از تغییر API: docs/api/sms.md. تست: ddev exec php bin/console app:seed-sms-templates (یا نام واقعی seed) و بررسی ارسال با یک تگ (مثلاً OTP) در محیط تست.
  • edge case: اگر تگی token_map نداشت یا kavenegar_template خالی بود، dispatchTemplate باید به‌صورت امن fallback کند (نه crash) — ولی هدف این است که همه‌ی تگ‌های سیستمی مقدار داشته باشند.