Files
clinicpro/docs/api/sms.md
T
hamed 6ade17a5b4 feat: implement OTP sending via Kavenegar VerifyLookup and add image cropping modal
- Added support for sending OTP messages using Kavenegar's VerifyLookup method, ensuring compliance with specified token formatting and template usage.
- Updated OtpService to handle new template parameters and fallback mechanisms.
- Introduced ImageCropModal component for cropping images with a user-friendly interface.
- Created utility function for cropping images and generating downloadable files.
2026-07-04 21:50:55 +03:30

17 KiB

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)

{
  "mobile": "09123456789",
  "message": "سلام، پیام آزمایشی",
  "provider": "kavenegar"
}
Field Type Required Description
mobile string Recipient mobile (09XXXXXXXXX)
message string Message text
provider string نادیده گرفته می‌شود؛ همیشه kavenegar استفاده می‌شود

Response 200

{
  "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)

{
  "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

{
  "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)

{
  "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

{
  "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)

{
  "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

{ "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

{
  "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)

{
  "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

{
  "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

{
  "success": true,
  "data": {
    "balance_rials": 15000,
    "sms_price_rials": 500,
    "estimated_sms_count": 30
  }
}

POST /api/v1/sms/wallet/charge

شارژ کیف پیامکی از طریق درگاه پرداخت.

Permission: IS_AUTHENTICATED_FULLY

{
  "gateway": "mellat",
  "amount_rials": 50000,
  "frontend_address": "https://example.com/sms-wallet"
}

Response 200:

{
  "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

{
  "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

{
  "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

{
  "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 فراخوانی می‌شود.

{
  "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 — رد متن پیامک.

{ "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، 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 از طریق Kavenegar VerifyLookup (پترن): اگر متغیر محیطی KAVENEGAR_OTP_TEMPLATE (نام پترن مصوب پنل کاوه‌نگار) ست باشد، OTP با verify/lookup.json ارسال می‌شود — متنِ پیام از پترن ثابتِ مصوب کاوه‌نگار می‌آید، نه از body قابل‌ویرایش DB (body صرفاً برای رکورد SmsLog رندر می‌شود). map توکن‌ها طبق قانون فاصله‌ی کاوه‌نگار: token = کد ۵ رقمی (بدون فاصله)، token10 = اسم سایت (تا ۵ فاصله مجاز). اگر پترن مصوب فقط %token داشته باشد، token10 نادیده گرفته می‌شود (اسم سایت نمایش داده نمی‌شود). اگر KAVENEGAR_OTP_TEMPLATE خالی باشد → fallback به ارسال متن‌آزاد send با متنِ قالب DB (placeholderهای {code}، {site}). نیازمند اشتراک advanced کاوه‌نگار.

تگ welcome: پیامک خوش‌آمد که هنگام افزودن پزشک/کلینیک توسط نماینده (POST /api/v1/representation/doctor|clinic) به‌صورت async به موبایل پزشک/مالک ارسال می‌شود. متن فعلاً ثابت است (نام + site_name)، نه از قالب DB.

تگ 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

{
  "success": true,
  "data": {
    "data": [
      {
        "tag": "otp",
        "title": "کد تأیید ورود",
        "body": "کد تأیید شما: {code}",
        "variables": ["code", "site"],
        "updated_at": 1718000000
      }
    ]
  }
}

PATCH /api/v1/admin/sms/messages/{tag}

ویرایش متن یک پیامک سیستمی.

Permission: ROLE_ADMIN

Path Parameters

Param Type Description
tag string یکی از تگ‌های سیستمی

Request Body

{ "body": "کد ورود شما: {code}" }
Field Type Required Description
body string متن جدید؛ فقط placeholderهای مجازِ همان تگ پذیرفته می‌شود

Response 200

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