- Updated SubscriptionPeriod interface to include tax-related fields: tax_percent, tax_rials, and payable_rials. - Modified payment API documentation to reflect changes in tax handling for subscriptions and SMS wallet charges. - Adjusted PaymentController to calculate payment amounts based on subscription period details instead of client input. - Enhanced PaymentManager to handle net amounts for SMS wallet charges, ensuring tax is not credited to the wallet. - Created PaymentTaxCalculator and SubscriptionTaxCalculator services to manage tax calculations consistently across payment types. - Added tests for tax calculations in both subscription and SMS wallet contexts, ensuring correct behavior with and without tax enabled. - Updated frontend components to display tax information appropriately during payment processes.
602 lines
24 KiB
Markdown
602 lines
24 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` نمیفرستد؛ این جلوی `Idle timeout` را میگیرد. هر درخواست `timeout=15s`، `max_duration=30s`، بایپس proxy محیطی (`proxy=null`) و تا **۳ بار retry** با backoff فقط برای خطاهای **گذرای شبکه** (`TransportException`) دارد؛ خطاهای HTTP کاوهنگار (`4xx`/`5xx`، مثل `431`) دائماند و retry نمیشوند.
|
||
- **سقف طول توکن (`431` guard):** مقدار طولانی در توکنها (مثل نام کلینیک فارسی که هر کاراکترش ۹ بایت URL-encode میشود) query string را بزرگ میکند و کاوهنگار `431 Request Header Fields Too Large` برمیگرداند. provider هر مقدار توکن را به `MAX_TOKEN_LEN = 60` کاراکتر cap میکند. اگر با این حال کاوهنگار خطای HTTP بدهد، بهجای `error` پرسروصدا فقط یک `warning` کوتاه با کد وضعیت لاگ میشود (چون خطای دائم است، نه transient).
|
||
- **قیمت هر پیامک** از کلید تنظیمات `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-...",
|
||
"net_rials": 50000,
|
||
"tax_percent": 10,
|
||
"tax_rials": 5000,
|
||
"payable_rials": 55000
|
||
}
|
||
}
|
||
```
|
||
|
||
> این endpoint فقط `Payment` (type=`sms_wallet`) میسازد و `pay_url` میدهد؛ **ارتباط با بانک اینجا انجام نمیشود** و از flow واحد پرداخت (`GET /payment/pay/{orderId}` → callback → `PaymentManager`) عبور میکند. کلاینت باید مرورگر را به `pay_url` هدایت کند. پس از پرداخت موفق، `PaymentManager` موجودی کیف را خودکار شارژ میکند.
|
||
|
||
#### مالیات
|
||
|
||
`amount_rials` ورودی **خالص** است — همان اعتباری که به کیف پول مینشیند. مالیات رویش
|
||
**اضافه** میشود و مبلغی که به بانک میرود `payable_rials` است.
|
||
|
||
| فیلد | معنی |
|
||
|------|------|
|
||
| `net_rials` | اعتباری که بعد از پرداخت موفق به کیف پول اضافه میشود |
|
||
| `tax_percent` | درصد مؤثر؛ با `tax_enabled=0` برابر `0` |
|
||
| `tax_rials` | `round(net_rials × tax_percent / 100)` |
|
||
| `payable_rials` | `net_rials + tax_rials` — مبلغ رکورد `Payment` و مبلغ درگاه |
|
||
|
||
نرخ از همان کلیدهای سراسری `tax_enabled` / `tax_percent` میآید؛ محاسبه در
|
||
`App\Payment\Service\PaymentTaxCalculator`.
|
||
|
||
**اعتبار کیف پول هرگز شامل مالیات نیست.** مقدار خالص در `metadata.net_rials` رکورد پرداخت
|
||
ذخیره میشود و `PaymentManager::handleSmsWalletCharge` همان را شارژ میکند — نه
|
||
`amount_rials` را. استرداد هم قرینهٔ همین است. پرداختهای قدیمی که `net_rials` ندارند به
|
||
مبلغ کلشان fallback میکنند.
|
||
|
||
### 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`. مقدار هر جایگاه به `MAX_TOKEN_LEN = 60` کاراکتر cap میشود تا از خطای `431` جلوگیری شود.
|
||
|
||
`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` رکوردهای پیشفرض را برای تگهایی که هنوز ندارند میسازد.
|