# افزودن اسم سایت شهر به پیامک کد تأیید (OTP) — به‌صورت اختیاری ## پروژه `clinicpro` (Backend) — **cross-repo**؛ پرامپت همتای frontend: `nobat724_front/.claude/prompt/otp-sms-site-name.md` ## زمینه سایت عمومی چند-دامنه‌ای است (هر شهر یک دامنه: `yasuj-nobat.ir`، `ahvaz-nobat.ir`، ...). هر شهر در جدول `cities` فیلد `site_name` دارد (مثل «یاسوج نوبت»). الان پیامک کد تأیید ورود ثابت است: `کد تأیید شما: {code}` و اسم سایت شهر ندارد. هدف: بتوان **اختیاری** اسم سایتِ شهرِ درخواست‌کننده را در پیامک OTP نمایش داد — مثلاً «کد تأیید شما در یاسوج نوبت: ۱۲۳۴۵». اختیاری‌بودن از طریق **قالب قابل‌ویرایش پنل SMS** کنترل می‌شود: اگر ادمین `{site}` را در متن قالب بگذارد نمایش داده می‌شود، نگذارد نمی‌شود. ## مشکل / هدف - درخواست `send-code` از دامنه‌ی شهر به API (`api.clinic-pro.ir`) می‌رسد → `Host` = دامنه‌ی API، نه شهر. پس backend خودش شهر را نمی‌فهمد. - **راه‌حل:** frontend دامنه‌ی خودش را در body می‌فرستد؛ backend از روی `domain` در جدول `cities` → `site_name` را پیدا می‌کند و به‌عنوان متغیر `{site}` به قالب پیامک تزریق می‌کند. اگر شهری پیدا نشد یا `domain` نیامد → fallback به برند پیش‌فرض «نوبت ۷۲۴». - متغیر `{site}` همیشه مقداردهی می‌شود؛ استفاده یا عدم‌استفاده‌اش به قالب بستگی دارد (اختیاری). ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `src/Auth/Controller/AuthController.php` | endpoint `POST /api/v1/user/send-code` — خواندن `domain` اختیاری از body | | `src/Auth/Service/OtpService.php` | ساخت و ارسال پیامک OTP — resolve اسم سایت و تزریق `{site}` | | `src/Location/Repository/CityRepository.php` | lookup شهر بر اساس `domain` | | `src/Location/Entity/City.php` | دارای `getDomain()` و `getSiteName()` (آماده، تغییر نمی‌خواهد) | | `src/Sms/Entity/SmsMessageTemplate.php` | ثبت `{site}` به‌عنوان متغیر مجاز قالب OTP | | `docs/api/auth.md`, `docs/api/sms.md` | مستندسازی | ## وضعیت فعلی `OtpService::sendCode` (src/Auth/Service/OtpService.php): ```php public function sendCode(string $mobile): string { $uuid = Uuid::v4()->toRfc4122(); $code = $this->appEnv === 'dev' ? '12345' : str_pad((string) random_int(10000, 99999), 5, '0', STR_PAD_LEFT); // ... cache set ... if ($this->appEnv !== 'dev') { $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code]); $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP); } return $uuid; } ``` `AuthController::sendCode` (src/Auth/Controller/AuthController.php:136): ```php $data = json_decode($request->getContent(), true) ?? []; $mobile = trim($data['mobile'] ?? ''); // ... validation + rate limit ... $uuid = $this->otpService->sendCode($mobile); ``` قالب پیش‌فرض OTP (src/Sms/Entity/SmsMessageTemplate.php): ```php SmsLog::TAG_OTP => [ 'title' => 'کد تأیید ورود', 'body' => 'کد تأیید شما: {code}', 'variables' => ['code'], ], ``` resolver (src/Sms/Service/SmsTextResolver.php) هر `{key}` را با str_replace جایگزین می‌کند — پس `{site}` بدون تغییر resolver کار می‌کند. ## وظایف ### ۱. `OtpService` — resolve اسم سایت و تزریق `{site}` - `CityRepository` را به constructor تزریق کن. - امضای متد را به `sendCode(string $mobile, ?string $domain = null)` تغییر بده (پارامتر اختیاری → سازگاری عقب‌رو). - اسم سایت را resolve کن و به متغیرهای قالب اضافه کن: ```php public function sendCode(string $mobile, ?string $domain = null): string { // ... code + cache بدون تغییر ... if ($this->appEnv !== 'dev') { $site = $this->resolveSiteName($domain); $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]); $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP); } return $uuid; } private function resolveSiteName(?string $domain): string { $default = 'نوبت ۷۲۴'; if (!$domain) { return $default; } $domain = strtolower(trim(preg_replace('#^https?://#', '', $domain))); $domain = preg_replace('#/.*$#', '', $domain); // strip path $city = $this->cityRepo->findOneBy(['domain' => $domain]); return $city?->getSiteName() ?: $default; } ``` - **نکته:** `{site}` همیشه مقدار دارد؛ اگر قالب فعلی `{site}` نداشته باشد، این متغیر بی‌اثر است (اختیاری‌بودن حفظ می‌شود). ### ۲. `AuthController::sendCode` — خواندن `domain` اختیاری از body ```php $mobile = trim($data['mobile'] ?? ''); $domain = isset($data['domain']) ? trim((string) $data['domain']) : null; // ... validation + rate limit بدون تغییر ... $uuid = $this->otpService->sendCode($mobile, $domain ?: null); ``` - `domain` اختیاری است؛ اگر نیامد، رفتار قبلی حفظ می‌شود. - روی `domain` **validation سخت‌گیرانه لازم نیست** (فقط برای lookup استفاده می‌شود، نه اجرا)، اما trim و طول محدود (مثلاً حداکثر ۲۵۳ کاراکتر) اعمال کن. ### ۳. `SmsMessageTemplate` — ثبت `{site}` به‌عنوان متغیر مجاز OTP قالب پیش‌فرض را **بدون** `{site}` نگه دار (opt-in)، اما `site` را به لیست `variables` اضافه کن تا در پنل ویرایش قالب به‌عنوان متغیر قابل‌استفاده نمایش داده شود: ```php SmsLog::TAG_OTP => [ 'title' => 'کد تأیید ورود', 'body' => 'کد تأیید شما: {code}', // پیش‌فرض بدون site 'variables' => ['code', 'site'], // site به‌عنوان متغیر اختیاری در پنل ], ``` - این‌طوری ادمین در پنل SMS می‌تواند body را به `کد تأیید شما در {site}: {code}` تغییر دهد تا اسم سایت نمایش داده شود — یا ندهد. ### ۴. مستندسازی - `docs/api/auth.md`: در `POST /api/v1/user/send-code`، فیلد **اختیاری** `domain` را به request body اضافه کن (نوع string، توضیح: دامنه‌ی شهرِ درخواست‌کننده برای شخصی‌سازی متن پیامک؛ اگر نیامد از برند پیش‌فرض استفاده می‌شود). نمونه request/response را به‌روز کن. - `docs/api/sms.md`: متغیر جدید `{site}` را برای قالب `OTP` مستند کن (مقدار = `site_name` شهرِ دامنه یا «نوبت ۷۲۴»). ## نکات مهم - **backward-compatible**: هر دو `domain` (body) و پارامتر `sendCode` اختیاری‌اند؛ کلاینت‌های فعلی (پنل ادمین clinicpro، لاگین بدون domain) بدون تغییر کار می‌کنند و برند پیش‌فرض را می‌گیرند. - محیط `dev`: پیامک ارسال نمی‌شود (کد ثابت `12345`) — این مسیر دست‌نخورده بماند. - `{site}` هرگز خالی نباشد (fallback «نوبت ۷۲۴») تا اگر ادمین `{site}` را در قالب گذاشت، متن ناقص «... در : ...» تولید نشود. - lookup دامنه: مقدار ارسالی frontend ممکن است `yasuj-nobat.ir` یا با `www.`/`https://` باشد — در `resolveSiteName` normalize کن (حذف پروتکل و path). اگر لازم شد `www.` را هم strip کن، اما مقادیر `cities.domain` بدون `www` ذخیره شده‌اند (مثل `yasuj-nobat.ir`). - **پیامک OTP با پترن اپراتور:** provider پترن `{site}` را پشتیبانی می‌کند (تأییدشده). مطمئن شو ترتیب/نام متغیرها با پترن مصوب سامانه‌ی پیامکی هماهنگ است. - تست: ```bash ddev exec php -l src/Auth/Service/OtpService.php ddev exec php -l src/Auth/Controller/AuthController.php ddev exec php bin/console cache:clear # ارسال با domain و بدون domain را در محیط staging بررسی کن (dev پیامک نمی‌فرستد) ``` - بعد از تغییر API، `docs/api/auth.md` و `docs/api/sms.md` را در همین session به‌روز کن (قانون استاندینگ پروژه).