- Removed the "دکتر" prefix from doctor names in various components and API responses to ensure consistency and clarity. - Updated the AppointmentDetailPage, CommentsPage, DashboardPage, RatingsPage, SecretariesPage, and other relevant files to reflect the changes in doctor name formatting. - Adjusted API documentation to align with the new naming conventions. - Implemented validation to prevent the creation of clinics without a name and restricted users to a single clinic. - Added tests to verify that doctor names are stored without titles and that clinic creation adheres to the new validation rules.
577 lines
21 KiB
Markdown
577 lines
21 KiB
Markdown
# SMS API
|
|
|
|
> **Prefix:** `/api/v1/sms`
|
|
> **Provider:** همیشه `kavenegar` (پیشفرض و تنها گزینه فعال).
|
|
> All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue.
|
|
|
|
> **سیاست lookup-only (مهم):** همهی پیامکها فقط از طریق الگوی تأییدشدهی کاوهنگار
|
|
> (`verify/lookup.json`) ارسال میشوند. ارسال متن آزاد با `sms/send.json` **غیرفعال** است
|
|
> (در ایران برای پیام خدماتی فیلتر میشود). هر پیامی که الگو/کد VerifyLookup نداشته باشد
|
|
> ارسال **نمیشود**؛ در `SmsLog` با `success=false` ثبت و در لاگ برنامه خطا میخورد.
|
|
> هر رکورد `SmsLog` نام الگوی کاوهنگارِ استفادهشده را در فیلد `template_code` نگه میدارد
|
|
> (در پاسخ `GET /api/v1/admin/sms/logs` با کلید `template`)؛ برای پیامکهای ردشده `null` است.
|
|
|
|
## 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` حذف شدهاند).
|
|
- **ترابری (transport) کاوهنگار:** همهی فراخوانیهای `KavehNegarProvider` به کاوهنگار بهصورت **GET با query string** ارسال میشوند (مطابق مستند رسمی) — هیچ درخواست `POST`/`body` ساخته نمیشود. GET بدنه ندارد پس curl هدر `Expect: 100-continue` نمیفرستد؛ این جلوی خطاهای `431 Request Header Fields Too Large` و `Idle timeout` را که در prod دیده میشد میگیرد. هر درخواست `timeout=15s`، `max_duration=30s`، بایپس proxy محیطی (`proxy=null`) و تا **۳ بار retry** با backoff برای خطاهای گذرای شبکه دارد.
|
|
- **قیمت هر پیامک** از کلید تنظیمات `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` — ⛔ غیرفعال (Deprecated)
|
|
|
|
ارسال متن آزاد **دیگر مجاز نیست** (سیاست lookup-only). این endpoint اکنون همیشه `422`
|
|
برمیگرداند و هیچ پیامکی ارسال نمیکند. برای ارسال دستی از
|
|
[`POST /api/v1/sms/send-template`](#post-apiv1smssend-template) با یک تمپلیت تأییدشده
|
|
که کد VerifyLookup کاوهنگار دارد استفاده کنید.
|
|
|
|
**Permission:** `ROLE_ADMIN`
|
|
|
|
### Response `422` (همیشه)
|
|
```json
|
|
{
|
|
"success": false,
|
|
"errors": [
|
|
{ "code": "ERR_VALIDATION_001", "message": "ارسال متن آزاد مجاز نیست؛ از تمپلیت تأییدشده (VerifyLookup) استفاده کنید" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Errors
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `ERR_AUTH_001` | 401 | Missing token |
|
|
| `ERR_AUTH_006` | 403 | Not admin |
|
|
| `ERR_VALIDATION_001` | 422 | ارسال متن آزاد غیرفعال است (همیشه) |
|
|
|
|
---
|
|
|
|
## 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 |
|
|
| `ERR_VALIDATION_001` | 422 | تمپلیت کد VerifyLookup کاوهنگار ندارد و قابل ارسال نیست (`provider_code` خالی) |
|
|
|
|
---
|
|
|
|
## 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` داشته باشد** (token1 اجباری است)؛ پس token_map هر تگ یک مقدار به `token` میدهد. قانون فاصله: `token`/`token2`/`token3` **فاصله نمیپذیرند** — provider (`KavehNegarProvider`) فاصلههای مقدارِ این جایگاهها را خودکار با نیمفاصله (ZWNJ) جایگزین میکند؛ `token10`/`token20` فاصله را بدون تغییر میپذیرند. پس ترجیحاً مقادیر بدونفاصله (کد/تاریخ/ساعت/username/لینک) در `token`/`token2`/`token3` و مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک) در `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` | date→token, time→token2, doctor→token10 |
|
|
| `clinic_invitation` | `clinicpro-clinic-invite` | link→token, clinic→token10 |
|
|
| `pre_registration` | `clinicpro-pre-register` | username→token, password→token2, link→token3 |
|
|
| `secretary` | `clinicpro-secretary` | username→token, link→token3, owner→token10 |
|
|
| `doctor_appointment` | `clinicpro-doctor-appt` | date→token, time→token2, patient→token10 |
|
|
| `welcome` | `clinicpro-welcome` | name→token, site→token10 |
|
|
|
|
> **پیشنیاز:** این تمپلتها باید در پنل کاوهنگار ساخته و تأیید شوند (نیازمند اشتراک 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` رکوردهای پیشفرض را برای تگهایی که هنوز ندارند میسازد.
|