Files
clinicpro/.claude/prompt/otp-via-kavenegar-lookup.md
T
hamed 6ade17a5b4 feat: implement OTP sending via Kavenegar VerifyLookup and add image cropping modal
- Added support for sending OTP messages using Kavenegar's VerifyLookup method, ensuring compliance with specified token formatting and template usage.
- Updated OtpService to handle new template parameters and fallback mechanisms.
- Introduced ImageCropModal component for cropping images with a user-friendly interface.
- Created utility function for cropping images and generating downloadable files.
2026-07-04 21:50:55 +03:30

192 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ارسال پیامک OTP فقط از طریق Kavenegar VerifyLookup (پترن)
## پروژه
`clinicpro` (Backend — SMS/Auth)
## زمینه
پیامک کد تأیید ورود (OTP) الان با متد **متن‌آزاد** `send()` کاوه‌نگار (`/sms/send.json`) ارسال می‌شود. کاوه‌نگار برای ارسال کد تأیید، متد اختصاصی **VerifyLookup** (`/verify/lookup.json`) دارد که با **پترن مصوب** کار می‌کند، روی خطوط اشتراکی هم تحویل مطمئن‌تری دارد و برای OTP توصیه/لازم است. هدف: **فقط OTP** از این به بعد از طریق Lookup ارسال شود (بقیه پیامک‌ها — welcome، secretary، doctor_appointment، user_template — دست‌نخورده بمانند).
## اسپک Kavenegar VerifyLookup (از داکیومنت رسمی)
- **Endpoint:** `https://api.kavenegar.com/v1/{API-KEY}/verify/lookup.json` (GET/POST)
- **پارامترهای اجباری:** `receptor` (موبایل)، `token` (مقدار کد؛ max 100؛ **بدون فاصله**؛ بدون آندرلاین/خط جدید)، `template` (نام پترن مصوب در پنل)
- **پارامترهای اختیاری توکن و قانون فاصله (بحرانی):**
| توکن | فاصله |
|------|-------|
| `token`, `token2`, `token3` | **بدون فاصله** (رد می‌شود) |
| `token10` | حداکثر **۵ فاصله** مجاز |
| `token20` | حداکثر **۸ فاصله** مجاز |
- `type`: `sms` (پیش‌فرض) یا `call`.
- **پترن باید از قبل در پنل کاوه‌نگار تأیید شده باشد**؛ متنِ پیام روی پنل ثابت است (نه از DB).
- نیازمند اشتراک advanced.
**پیامد:** کد ۵ رقمی (بدون فاصله) → `token`. اسم سایت مثل «یاسوج نوبت» (۱ فاصله) → باید در **`token10`** برود (نه `token`/`token2`/`token3` که فاصله را رد می‌کنند).
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Auth/Service/OtpService.php` | ساخت و dispatch پیامک OTP — باید به Lookup سوییچ شود |
| `src/Sms/Service/SmsService.php` | `dispatchAsync(...templateCode, templateVars...)` → اگر `templateCode` باشد `sendTemplate()` صدا می‌زند |
| `src/Sms/Provider/KavehNegarProvider.php` | `sendTemplate()``/verify/lookup.json`؛ map توکن باید نام‌دار شود (برای `token10`) |
| `.env` + `config/services.yaml` | افزودن `KAVENEGAR_OTP_TEMPLATE` و bind به OtpService |
| `docs/api/sms.md` | مستندسازی سوییچ OTP به Lookup |
## وضعیت فعلی
**OtpService::sendCode** (متن‌آزاد، بدون templateCode):
```php
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);
}
```
Constructor فعلی:
```php
public function __construct(
private readonly CacheInterface $cache,
private readonly SmsService $sms,
private readonly SmsTextResolver $smsText,
private readonly CityRepository $cityRepo,
private readonly int $otpTtl = 1200,
private readonly string $appEnv = 'dev',
) {}
```
**SmsService::dispatchAsync / sendNow** (مسیر انتخاب send vs lookup):
```php
public function dispatchAsync(
string $mobile, string $message, string $provider = 'kavenegar',
?string $templateUuid = null, array $templateVars = [],
?string $templateCode = null, string $tag = SmsLog::TAG_GLOBAL,
): void { /* dispatch SendSmsMessage */ }
// sendNow:
$success = ($msg->templateCode !== null)
? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars)
: $provider->send($msg->mobile, $msg->message);
```
**KavehNegarProvider::sendTemplate** (map ترتیبی فعلی — فاصله را در token2/3 می‌گذارد که رد می‌شود):
```php
public function sendTemplate(string $mobile, string $templateCode, array $vars): bool
{
try {
$params = ['receptor' => $mobile, 'template' => $templateCode];
foreach (array_values($vars) as $i => $v) {
$params['token' . ($i > 0 ? $i + 1 : '')] = $v;
}
$resp = $this->httpClient->request('POST',
self::BASE . '/' . $this->key() . '/verify/lookup.json', [
'body' => http_build_query($params), 'timeout' => 10,
]);
$data = $resp->toArray();
return ($data['return']['status'] ?? 0) === 200;
} catch (\Throwable $e) { /* log; return false */ }
}
```
**caller دیگر `sendTemplate`:** فقط `POST /api/v1/sms/send-template` در `src/Sms/Controller/SmsController.php:488` که `$vars` را با **کلیدهای معنایی** (مثل `name`,`date`) و وابسته به **ترتیب** پاس می‌دهد. پس map ترتیبی نباید برای این caller بشکند.
**.env فعلی:**
```
OTP_TTL=1200
KAVENEGAR_API_KEY=change_me
```
## وظایف
### ۱. `KavehNegarProvider::sendTemplate` — پشتیبانی توکنِ نام‌دار (backward-safe)
اگر همه‌ی کلیدهای `$vars` نام slot معتبر کاوه‌نگار باشند (`token`,`token2`,`token3`,`token10`,`token20`) → همان‌ها را مستقیم استفاده کن (تا اسم سایتِ فاصله‌دار در `token10` برود). در غیر این صورت (کلیدهای معنایی/لیستی) → همان map ترتیبی فعلی (برای caller `send-template`).
```php
$params = ['receptor' => $mobile, 'template' => $templateCode];
$slots = ['token', 'token2', 'token3', 'token10', 'token20'];
if ($vars !== [] && array_keys($vars) !== range(0, count($vars) - 1)
&& array_diff(array_keys($vars), $slots) === []) {
foreach ($vars as $slot => $v) { $params[$slot] = $v; }
} else {
foreach (array_values($vars) as $i => $v) {
$params['token' . ($i > 0 ? $i + 1 : '')] = $v;
}
}
```
### ۲. `.env` + `config/services.yaml` — نام پترن OTP
`.env` (کاربر مقدار واقعی پترن مصوب را می‌گذارد):
```
KAVENEGAR_OTP_TEMPLATE=
```
`config/services.yaml` روی سرویس `OtpService`:
```yaml
App\Auth\Service\OtpService:
arguments:
$otpTtl: '%env(int:OTP_TTL)%'
$appEnv: '%kernel.environment%'
$otpTemplate: '%env(default::KAVENEGAR_OTP_TEMPLATE)%'
```
### ۳. `OtpService` — dispatch از طریق Lookup
- پارامتر constructor جدید `?string $otpTemplate = null` **بعد از** پارامترهای بدون‌دیفالت و کنار `$appEnv` (ترتیب معتبر PHP؛ همه دیفالت‌دار در انتها).
- در `sendCode`، وقتی پترن ست است → با `templateCode` و توکن‌های نام‌دار dispatch کن:
```php
if ($this->appEnv !== 'dev') {
$site = $this->resolveSiteName($domain);
$message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]); // فقط برای لاگ SmsLog
if ($this->otpTemplate) {
$this->sms->dispatchAsync(
$mobile, $message,
templateCode: $this->otpTemplate,
templateVars: ['token' => $code, 'token10' => $site],
tag: SmsLog::TAG_OTP,
);
} else {
// fallback متن‌آزاد فقط وقتی پترن ست نشده (مثلاً محیط توسعه)
$this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
}
}
```
- `token` = کد ۵ رقمی (بدون فاصله ✓). `token10` = اسم سایت (تا ۵ فاصله مجاز ✓).
- اگر پترن مصوب فقط `%token` داشته باشد، `token10` اضافی توسط کاوه‌نگار نادیده گرفته می‌شود (بی‌خطر) — اسم سایت فقط وقتی نمایش داده می‌شود که پترن `%token10` هم داشته باشد.
### ۴. مستندسازی `docs/api/sms.md`
- در بخش «متن ویرایش‌پذیر پیامک‌های سیستمی»، تگ `otp`: تصریح کن که **ارسال OTP از طریق Kavenegar VerifyLookup** انجام می‌شود (نه `send` متن‌آزاد) وقتی `KAVENEGAR_OTP_TEMPLATE` ست باشد؛ متنِ پیام از **پترن مصوب کاوه‌نگار** می‌آید نه از body قابل‌ویرایش DB (body صرفاً برای لاگ `SmsLog`).
- map توکن‌ها را مستند کن: `token` = کد، `token10` = اسم سایت (فاصله‌دار).
- الزام `.env`: `KAVENEGAR_OTP_TEMPLATE` باید نام پترن مصوب باشد؛ بدون آن، fallback به متن‌آزاد.
## نکات مهم
- **فقط OTP** به Lookup می‌رود؛ مسیر `send()` متن‌آزاد برای بقیه‌ی تگ‌ها و caller `send-template` دست‌نخورده بماند.
- **قانون فاصله** رعایت شود: کد → `token`؛ هر مقدار فاصله‌دار (اسم سایت) → `token10`. هرگز اسم فاصله‌دار در `token`/`token2`/`token3` نگذار.
- **پترن باید در پنل کاوه‌نگار مصوب باشد** و ساختار توکنش با map کد بخواند (`%token` برای کد، در صورت نیاز `%token10` برای سایت). این خارج از کد است و باید توسط صاحب حساب انجام شود.
- نیازمند **اشتراک advanced** کاوه‌نگار.
- **backward-safe**: تغییر `sendTemplate` نباید caller `send-template` (کلیدهای معنایی/ترتیبی) را بشکند — با شرط «همه کلیدها slot معتبرند» تضمین شود.
- **cache**: بعد از تغییر constructor `OtpService` و bind جدید، `cache:clear` برای **هر دو محیط** `dev` و `test` لازم است (وگرنه کانتینر کامپایل‌شده‌ی قدیمی TypeError می‌دهد). worker پیامک (`messenger:consume async`) هم باید ری‌استارت شود.
- **تست**:
```bash
ddev exec php -l src/Auth/Service/OtpService.php
ddev exec php -l src/Sms/Provider/KavehNegarProvider.php
ddev exec php bin/console cache:clear
ddev exec php bin/console cache:clear --env=test
ddev exec php bin/console lint:container
ddev exec php bin/phpunit tests/Auth/SendCodeMobileRateLimitTest.php
```
(تست واقعی ارسال Lookup نیاز به API key و پترن مصوب دارد؛ در `dev`/`test` پیامک ارسال نمی‌شود.)
- بعد از تغییر، `docs/api/sms.md` را در همان session به‌روز کن (قانون استاندینگ پروژه).