# 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` رکوردهای پیش‌فرض را برای تگ‌هایی که هنوز ندارند می‌سازد.
