# SMS API > **Prefix:** `/api/v1/sms` > **Provider:** همیشه `kavenegar` (پیش‌فرض و تنها گزینه فعال). > All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue. ## Configuration - **کلید API کاوه‌نگار فقط از متغیر محیطی `KAVENEGAR_API_KEY` خوانده می‌شود** — نه از دیتابیس و نه از پنل. در پنل ادمین فقط وضعیت read-only «تنظیم‌شده/نشده» نمایش داده می‌شود. - شماره فرستنده تنظیم نمی‌شود؛ کاوه‌نگار از خط پیش‌فرض حساب استفاده می‌کند. - endpoint `GET /api/v1/admin/settings` یک فیلد read-only به نام `sms_api_key_configured` (boolean) برمی‌گرداند. - `PATCH /api/v1/admin/settings` کلیدهای `sms_provider`، `kavenegar_api_key`، `kavenegar_sender`، `rangineh_api_key`، `rangineh_sender` را نمی‌پذیرد (از `ALLOWED_KEYS` حذف شده‌اند). - **قیمت هر پیامک** از کلید تنظیمات `sms_price_rials` خوانده می‌شود (قابل ویرایش در `/admin/settings` → بخش پیامک، و از طریق `PATCH /api/v1/admin/settings`). اگر تنظیم نشده باشد، مقدار پیش‌فرض `SmsWalletController::SMS_PRICE_RIALS = 500` ریال به‌عنوان fallback استفاده می‌شود. `GET /api/v1/sms/wallet/balance` این مقدار را در `sms_price_rials` و تعداد تخمینی پیامک را در `estimated_sms_count` برمی‌گرداند. --- ## POST `/api/v1/sms/send` Send a direct SMS message (free text). **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "mobile": "09123456789", "message": "سلام، پیام آزمایشی", "provider": "kavenegar" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `mobile` | string | ✅ | Recipient mobile (`09XXXXXXXXX`) | | `message` | string | ✅ | Message text | | `provider` | string | ❌ | نادیده گرفته می‌شود؛ همیشه `kavenegar` استفاده می‌شود | ### Response `200` ```json { "success": true, "data": { "message": "پیامک با موفقیت ارسال شد" } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_VALIDATION_001` | 422 | Invalid mobile format | --- ## POST `/api/v1/sms/send-template` Send an SMS using an approved template. **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "mobile": "09123456789", "template_uuid": "tmpl-uuid-...", "vars": { "name": "دکتر علی احمدی", "date": "۱۵ خرداد ۱۴۰۴" }, "provider": "kavenegar" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `mobile` | string | ✅ | Recipient mobile | | `template_uuid` | string (UUID) | ✅ | UUID of an approved template | | `vars` | object | ❌ | Key-value substitutions for template placeholders | | `provider` | string | ❌ | Override provider | ### Response `200` ```json { "success": true, "data": { "message": "پیامک با موفقیت ارسال شد" } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Template not found | | `ERR_VALIDATION_001` | 422 | Template not approved | --- ## POST `/api/v1/sms/template` Create a new SMS template. **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "name": "تأیید نوبت", "body": "دکتر گرامی ${name}، نوبت شما در تاریخ ${date} تأیید شد.", "variables": ["name", "date"] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | ✅ | Template display name | | `body` | string | ✅ | Template text with `${variable}` placeholders | | `variables` | string[] | ❌ | List of expected variable names | ### Response `201` ```json { "success": true, "data": { "uuid": "tmpl-uuid-...", "name": "تأیید نوبت", "body": "دکتر گرامی ${name}...", "variables": ["name", "date"], "status": "draft", "created_at": 1717000000 } } ``` **Template Status Values:** | Value | Description | |-------|-------------| | `draft` | Created, not submitted | | `pending` | Submitted for review | | `approved` | Ready to use | | `rejected` | Rejected | ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_VALIDATION_002` | 422 | Missing required field | --- ## GET `/api/v1/sms/template/{uuid}` Get template detail. **Permission:** `AUTH` ### Response `200` Template object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_NOT_FOUND_001` | 404 | Template not found | --- ## PATCH `/api/v1/sms/template/{uuid}` Update a template (only allowed in `draft` or `rejected` status). **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "name": "تأیید نوبت - ویرایش", "body": "نوبت شما در ${date} تأیید شد.", "variables": ["date"] } ``` All fields optional. ### Response `200` Updated template object. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Template not found | | `ERR_SMS_003` | 422 | Template already submitted/approved | --- ## POST `/api/v1/sms/template/{uuid}/submit` Submit template for admin review (moves status from `draft` to `pending`). **Permission:** `ROLE_ADMIN` ### Response `200` Updated template with `status: "pending"`. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Template not found | | `ERR_SMS_003` | 422 | Already submitted | --- ## DELETE `/api/v1/sms/template/{uuid}` Delete a template. **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": { "message": "قالب پیامک حذف شد" } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Template not found | --- ## GET `/api/v1/admin/sms/templates` List all templates (admin view with all statuses). **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": [ { "uuid": "...", "name": "تأیید نوبت", "status": "approved", "created_at": 1717000000 } ] } ``` --- ## POST `/api/v1/admin/sms/template/{uuid}/approve` Approve a pending template. **Permission:** `ROLE_ADMIN` ### Request Body (`application/json`) ```json { "note": "تأیید شد", "provider_code": "verify_appointment" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `note` | string | ❌ | Admin note | | `provider_code` | string | ❌ | Provider-side template code | ### Response `200` Updated template with `status: "approved"`. --- ## POST `/api/v1/admin/sms/template/{uuid}/reject` Reject a pending template. **Permission:** `ROLE_ADMIN` ### Request Body ```json { "note": "متن قالب نامناسب است" } ``` | Field | Type | Required | |-------|------|----------| | `note` | string | ✅ | ### Response `200` Updated template with `status: "rejected"`. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_VALIDATION_002` | 422 | Missing note | --- ## SMS Wallet کیف پیامکی — جدا از کیف مالی، فقط برای ارسال پیامک. ### GET /api/v1/sms/wallet/balance **Permission:** `IS_AUTHENTICATED_FULLY` ```json { "success": true, "data": { "balance_rials": 15000, "sms_price_rials": 500, "estimated_sms_count": 30 } } ``` ### POST /api/v1/sms/wallet/charge شارژ کیف پیامکی از طریق درگاه پرداخت. **Permission:** `IS_AUTHENTICATED_FULLY` ```json { "gateway": "mellat", "amount_rials": 50000, "frontend_address": "https://example.com/sms-wallet" } ``` **Response 200:** ```json { "success": true, "data": { "payment_uuid": "...", "pay_url": "{APP_BASE_URL}/api/v1/payment/pay/ORD-...", "order_id": "ORD-..." } } ``` > این endpoint فقط `Payment` (type=`sms_wallet`) می‌سازد و `pay_url` می‌دهد؛ **ارتباط با بانک اینجا انجام نمی‌شود** و از flow واحد پرداخت (`GET /payment/pay/{orderId}` → callback → `PaymentManager`) عبور می‌کند. کلاینت باید مرورگر را به `pay_url` هدایت کند. پس از پرداخت موفق، `PaymentManager` موجودی کیف را خودکار شارژ می‌کند. ### GET /api/v1/sms/wallet/logs تراکنش‌های کیف پیامک (paginated). **Query params:** `page`, `limit` ```json { "success": true, "data": [ { "uuid": "...", "type": "credit", "amount_rials": 50000, "description": "شارژ کیف پیامک", "created_at": 1718000000 } ], "meta": { "totalRecords": 5, "totalPages": 1, "currentPage": 1 } } ``` --- ## SMS Settings ### GET /api/v1/sms/settings تنظیمات پیامک entity جاری. **Permission:** `IS_AUTHENTICATED_FULLY` ```json { "success": true, "data": { "entity_type": "clinic", "entity_id": 5, "reminder_enabled": true, "reminder_hours_before": 2, "post_visit_enabled": false, "post_visit_text": null, "post_visit_text_pending": null, "post_visit_text_status": "none", "post_visit_text_reject_reason": null, "updated_at": 1718000000 } } ``` ### PATCH /api/v1/sms/settings **Permission:** `IS_AUTHENTICATED_FULLY` ```json { "reminder_enabled": true, "reminder_hours_before": 3, "post_visit_enabled": true, "post_visit_text": "از مراجعه شما سپاسگزاریم" } ``` **تغییر رفتار `post_visit_text`:** متن ارسال‌شده مستقیماً اعمال نمی‌شود — در فیلد `post_visit_text_pending` ذخیره می‌شود و وضعیت `post_visit_text_status` به `pending` تغییر می‌کند. پس از تأیید ادمین، به `post_visit_text` منتقل می‌شود. **مقادیر `post_visit_text_status`:** `none` | `pending` | `approved` | `rejected` --- ## Admin Endpoints ### GET /api/v1/admin/sms/settings/review **Permission:** `ROLE_ADMIN` — لیست تنظیمات SMS بر اساس وضعیت متن ویزیت. **Query params:** | پارامتر | مقدار | پیش‌فرض | توضیح | |---|---|---|---| | `status` | `pending` \| `approved` | `pending` | فیلتر بر اساس `post_visit_text_status`. مقدار نامعتبر → `pending`. | برای تب «در انتظار تأیید» با `status=pending` و برای تب «پیامک‌های تأییدشده» با `status=approved` فراخوانی می‌شود. ```json { "success": true, "data": { "data": [ { "id": 3, "entity_type": "doctor", "entity_id": 7, "entity_name": "دکتر محمد محمدی", "post_visit_text_pending": "متن در انتظار تأیید", "post_visit_text_status": "pending", ... } ] } } ``` ### POST /api/v1/admin/sms/settings/{id}/approve **Permission:** `ROLE_ADMIN` — تأیید متن پیامک. `post_visit_text_pending` به `post_visit_text` منتقل می‌شود. ### POST /api/v1/admin/sms/settings/{id}/reject **Permission:** `ROLE_ADMIN` — رد متن پیامک. ```json { "reason": "متن نامناسب است" } ``` --- ### GET /api/v1/admin/sms/wallet-report **Permission:** `ROLE_ADMIN` — لیست همه کیف‌های پیامکی (paginated) --- ## متن ویرایش‌پذیر پیامک‌های سیستمی متن پیامک‌های سیستمی از پنل قابل ویرایش است و بر اساس **تگ** کلیددار می‌شود. هر متن placeholderهای مجاز خود را دارد (مثل `{code}`، `{doctor}`، `{date}`). **همه‌ی پیامک‌های سیستمی از طریق Kavenegar VerifyLookup (`verify/lookup.json`) ارسال می‌شوند** (نه متن‌آزاد `sms/send`). هر تمپلت دو فیلد اضافه دارد: - `kavenegar_template`: نام تمپلت مصوب در پنل کاوه‌نگار. **متن واقعیِ ارسالی از همین تمپلت پنل می‌آید، نه از `body` دیتابیس**؛ `body` فقط برای پیش‌نمایش ادمین و رندرِ رکورد `SmsLog` استفاده می‌شود و باید دستی با تمپلت پنل هم‌راستا نگه داشته شود. - `token_map`: نگاشت متغیر منطقی → جایگاه کاوه‌نگار. قانون فاصله: `token`/`token2`/`token3` **فاصله نمی‌پذیرند**؛ `token10`/`token20` فاصله مجازند. پس مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک/سایت) در `token10`/`token20`. `SmsService::dispatchTemplate(tag, mobile, vars)` نقطه‌ی واحدِ ارسال است: `kavenegar_template` و `token_map` را از رکورد DB (یا `SmsMessageTemplate::DEFAULTS`) می‌خواند، `vars` را به جایگاه‌ها نگاشت می‌کند و با VerifyLookup می‌فرستد. اگر `kavenegar_template` تعریف نشده باشد → fallback به ارسال متن‌آزاد با `body` رندرشده. نگاشت پیش‌فرض هر تگ (نام تمپلت پنل + token_map): | تگ | kavenegar_template | token_map | |----|--------------------|-----------| | `otp` | `clinicpro-otp` | code→token, site→token10 | | `notification_mobile` | `clinicpro-notify-code` | code→token | | `payment` | `clinicpro-payment` | doctor→token10, date→token20 | | `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 | > **پیش‌نیاز:** این تمپلت‌ها باید در پنل کاوه‌نگار ساخته و تأیید شوند (نیازمند اشتراک advanced). تمپلت‌های دارای `{link}` باید با تأیید لینک ساخته شوند. > تگ‌ها: `otp`، `payment`، `clinic_invitation`، `pre_registration`، `notification_mobile`، `welcome`، `secretary`، `doctor_appointment`. (پیامک قالبیِ کاربر با تگ `user_template` جداگانه از طریق `POST /api/v1/sms/template` مدیریت می‌شود.) > > تگ `otp`: کد تأیید ورود. مقدار `site` از فیلد `domain` در `POST /api/v1/user/send-code` گرفته می‌شود: `site_name` شهرِ متناظر در جدول `cities`؛ اگر `domain` نیامد یا شهر پیدا نشد → «کلینیک پرو». > > تگ `otp`: کد تأیید ورود از طریق تمپلت `clinicpro-otp` (VerifyLookup). نام تمپلت از رکورد DB خوانده می‌شود (متغیر محیطی `KAVENEGAR_OTP_TEMPLATE` **حذف شده**). token_map: `code→token`، `site→token10`. > > تگ `welcome`: پیامک خوش‌آمد که هنگام افزودن پزشک/کلینیک توسط نماینده (`POST /api/v1/representation/doctor|clinic`) به‌صورت async به موبایل پزشک/مالک ارسال می‌شود. از تمپلت `clinicpro-welcome` (`name→token10`, `site→token20`). > > تگ `secretary`: پیامک خوش‌آمد که هنگام تعریف منشی جدید (`POST /api/v1/secretary`) به‌صورت async به موبایل منشی ارسال می‌شود. placeholderها: `{owner}` (نام دکتر یا کلینیک)، `{username}` (موبایل منشی)، `{link}` (لینک ورود). متن از قالب DB می‌آید (fallback به پیش‌فرض `SmsMessageTemplate::DEFAULTS`). > > تگ `doctor_appointment`: اعلانِ «نوبت جدید» به **شمارهٔ اعلان دکتر** (`Doctor.notificationMobile` که در `/admin/profile` ست می‌شود). **فقط برای نوبت‌های پرداخت‌شدهٔ سایت** ارسال می‌شود — در `PaymentManager::handleAppointmentConfirmation` که تنها پس از verify موفقِ پرداخت اجرا می‌شود؛ نوبت‌های ثبت‌شده توسط منشی (بدون پرداخت) این پیامک را نمی‌گیرند. placeholderها: `{patient}` (نام بیمار)، `{date}` (تاریخ شمسی)، `{time}` (ساعت `HH:MM`). اگر `notificationMobile` خالی باشد ارسال نمی‌شود. متن از قالب DB (fallback به `DEFAULTS`). ### GET `/api/v1/admin/sms/messages` لیست همه‌ی متن‌های سیستمی (تگ‌هایی که هنوز رکورد ندارند با مقدار پیش‌فرض برگردانده می‌شوند). **Permission:** `ROLE_ADMIN` #### Response `200` ```json { "success": true, "data": { "data": [ { "tag": "otp", "title": "کد تأیید ورود", "body": "کد تأیید شما: {code}", "variables": ["code", "site"], "kavenegar_template": "clinicpro-otp", "token_map": { "code": "token", "site": "token10" }, "updated_at": 1718000000 } ] } } ``` ### PATCH `/api/v1/admin/sms/messages/{tag}` ویرایش متن یک پیامک سیستمی. **Permission:** `ROLE_ADMIN` #### Path Parameters | Param | Type | Description | |-------|------|-------------| | `tag` | string | یکی از تگ‌های سیستمی | #### Request Body ```json { "body": "کد ورود شما: {code}", "kavenegar_template": "clinicpro-otp" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `body` | string | ✅ | متن جدید (پیش‌نمایش/لاگ)؛ فقط placeholderهای مجازِ همان تگ پذیرفته می‌شود | | `kavenegar_template` | string | ❌ | نام تمپلت مصوب پنل کاوه‌نگار؛ رشته‌ی خالی → `null` | #### Response `200` ```json { "success": true, "data": { "data": { "tag": "otp", "title": "...", "body": "...", "variables": ["code"], "updated_at": 1718000123 } } } ``` #### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | تگ ناشناخته | | `ERR_VALIDATION_002` | 422 | متن خالی | | `ERR_VALIDATION_001` | 422 | placeholder نامعتبر (خارج از متغیرهای مجاز تگ) | > **Command:** `php bin/console app:seed-sms-message-templates` رکوردهای پیش‌فرض را برای تگ‌هایی که هنوز ندارند می‌سازد.