Files
clinicpro/docs/api/sms.md
T
hamed fa332f7fa1 feat: Add tagging system for SMS logs and templates
- Introduced a `tag` field in the `SmsLog` entity to categorize SMS messages.
- Updated the `SmsService` to handle the new `tag` parameter during SMS dispatch.
- Implemented a `SmsTextResolver` service to resolve SMS message templates based on tags.
- Created a new `SmsMessageTemplate` entity for editable SMS templates with placeholders.
- Added endpoints for managing SMS message templates in the admin panel.
- Enhanced existing SMS dispatching methods across various controllers to utilize the tagging system.
- Migrated the database to include the new `tag` field and created a seeding command for default SMS templates.
- Updated admin API to filter SMS logs by tag and include tag information in responses.
2026-06-19 20:42:04 +03:30

526 lines
12 KiB
Markdown

# SMS API
> **Prefix:** `/api/v1/sms`
> **Providers:** `kavenegar` (default) | `rangineh`
> All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue.
---
## 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"` or `"rangineh"` (default from env) |
### 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": "...",
"redirect_url": "https://gateway...",
"order_id": "ORD-..."
}
}
```
پس از پرداخت موفق، موجودی کیف خودکار شارژ می‌شود.
### 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 با وضعیت `pending`
```json
{
"success": true,
"data": {
"data": [
{
"id": 3,
"entity_type": "doctor",
"entity_id": 7,
"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)
---
## متن ویرایش‌پذیر پیامک‌های سیستمی
متن پیامک‌های سیستمی (OTP، پرداخت، دعوت کلینیک، پیش‌ثبت‌نام، تأیید موبایل) از پنل قابل ویرایش است و بر اساس **تگ** کلیددار می‌شود. هر متن placeholderهای مجاز خود را دارد (مثل `{code}`، `{doctor}`، `{date}`). هنگام ارسال، `SmsTextResolver` متنِ ویرایش‌شده‌ی DB را می‌گیرد و placeholderها را جایگزین می‌کند؛ اگر رکوردی نبود به متن پیش‌فرض fallback می‌شود.
> تگ‌ها: `otp`، `payment`، `clinic_invitation`، `pre_registration`، `notification_mobile`. (پیامک قالبیِ کاربر با تگ `user_template` جداگانه از طریق `POST /api/v1/sms/template` مدیریت می‌شود.)
### GET `/api/v1/admin/sms/messages`
لیست همه‌ی متن‌های سیستمی (تگ‌هایی که هنوز رکورد ندارند با مقدار پیش‌فرض برگردانده می‌شوند).
**Permission:** `ROLE_ADMIN`
#### Response `200`
```json
{
"success": true,
"data": {
"data": [
{
"tag": "otp",
"title": "کد تأیید ورود",
"body": "کد تأیید شما: {code}",
"variables": ["code"],
"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}" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `body` | string | ✅ | متن جدید؛ فقط placeholderهای مجازِ همان تگ پذیرفته می‌شود |
#### 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` رکوردهای پیش‌فرض را برای تگ‌هایی که هنوز ندارند می‌سازد.