feat(sms): add doctor appointment notification SMS for paid appointments
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# پیامک اعلان نوبت جدید به «شمارهٔ اعلان» دکتر (فقط نوبتهای پرداختشدهٔ سایت)
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (backend / Payment + Sms)
|
||||
|
||||
## زمینه
|
||||
|
||||
هر دکتر در `/admin/profile` یک «شمارهٔ اعلان نوبت» (`Doctor::notificationMobile`) ست میکند («پیامک نوبت جدید به این شماره ارسال میشود»). این فیلد و تگ `SmsLog::TAG_NOTIFICATION_MOBILE` وجود دارند، ولی تگ فعلی فقط برای **تأیید ثبت شماره** (کد تأیید در `NotificationMobileController`) استفاده میشود — به رویداد «نوبت جدید» وصل نیست.
|
||||
|
||||
الان هنگام موفقیت پرداخت نوبت، فقط به **بیمار** پیامک میرود (`PaymentManager::handleAppointmentConfirmation` با تگ `TAG_PAYMENT`). هدف: علاوه بر آن، به **شمارهٔ اعلانِ دکتر** هم یک پیامک با قالب مشخص (شامل نام بیمار و ساعت نوبت) ارسال شود.
|
||||
|
||||
**فقط نوبتهای پرداختشده از سایت** باید این پیامک را بدهند — نه نوبتهایی که منشی ثبت میکند. چون `handleAppointmentConfirmation` **فقط** در مسیر موفقیت پرداخت (`PaymentManager::processCallback` → `runPostAction`) اجرا میشود و نوبتهای ثبتشده توسط منشی از این مسیر عبور نمیکنند، افزودن کد در همینجا بهطور طبیعی شرط «فقط سایت/پرداخت» را رعایت میکند.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
هنگام تأیید پرداختِ یک نوبت، اگر دکترِ آن نوبت `notificationMobile` ست کرده باشد، یک پیامک با تگ/قالب اختصاصی (`TAG_DOCTOR_APPOINTMENT`) شامل `{patient}` و `{date}` و `{time}` به آن شماره ارسال شود.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `clinicpro/src/Sms/Entity/SmsLog.php` | افزودن تگ `TAG_DOCTOR_APPOINTMENT` + به آرایهٔ `TAGS` |
|
||||
| `clinicpro/src/Sms/Entity/SmsMessageTemplate.php` | افزودن قالب پیشفرض این تگ به `DEFAULTS` |
|
||||
| `clinicpro/src/Payment/Service/PaymentManager.php` | ارسال پیامک به `notificationMobile` دکتر در `handleAppointmentConfirmation` |
|
||||
| `clinicpro/docs/api/sms.md` | مستندسازی تگ/قالب جدید |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
### `PaymentManager::handleAppointmentConfirmation` (فقط پیامک بیمار)
|
||||
|
||||
```php
|
||||
private function handleAppointmentConfirmation(Payment $payment): void
|
||||
{
|
||||
$appointment = $payment->getAppointment();
|
||||
if ($appointment === null || !$appointment->canTransitionTo(Appointment::STATUS_CONFIRMED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
||||
$this->em->persist($appointment);
|
||||
|
||||
$doctor = $appointment->getDoctor();
|
||||
$this->commissionService->processAppointment(
|
||||
$payment,
|
||||
$doctor->getRepresentationId(),
|
||||
$appointment->getBookingRepresentationId(),
|
||||
$doctor->getId(),
|
||||
);
|
||||
|
||||
$mobile = $appointment->getPatientMobile();
|
||||
if ($mobile) {
|
||||
$when = $this->jalali->formatDateTime($appointment->getSlotStart());
|
||||
$message = $this->smsText->resolve(SmsLog::TAG_PAYMENT, [
|
||||
'doctor' => $doctor->getName(),
|
||||
'date' => $when,
|
||||
]);
|
||||
$this->smsService->dispatchAsync($mobile, $message, tag: SmsLog::TAG_PAYMENT);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### مقادیر در دسترس
|
||||
|
||||
- `Doctor::getNotificationMobile(): ?string` (فیلد `notification_mobile`, nullable)
|
||||
- `Doctor::getName()`
|
||||
- `Appointment::getPatientName(): ?string`، `getPatientMobile()`، `getSlotStart(): int` (unix)
|
||||
- `JalaliDateService::formatDateTime(int $ts, bool $withTime = true): string` — `false` = فقط تاریخ شمسی. متد جدا برای «ساعت» ندارد؛ ساعت را با `date('H:i', $slotStart)` بگیر.
|
||||
|
||||
### الگوی تگها (SmsLog)
|
||||
|
||||
```php
|
||||
public const TAG_NOTIFICATION_MOBILE = 'notification_mobile';
|
||||
public const TAGS = [ self::TAG_GLOBAL, self::TAG_OTP, self::TAG_PAYMENT, /* ... */ self::TAG_SECRETARY ];
|
||||
```
|
||||
|
||||
### الگوی قالبها (SmsMessageTemplate::DEFAULTS)
|
||||
|
||||
```php
|
||||
SmsLog::TAG_PAYMENT => [
|
||||
'title' => 'تأیید پرداخت و نوبت',
|
||||
'body' => 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.',
|
||||
'variables' => ['doctor', 'date'],
|
||||
],
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. تگ جدید در `SmsLog`
|
||||
|
||||
```php
|
||||
public const TAG_DOCTOR_APPOINTMENT = 'doctor_appointment';
|
||||
```
|
||||
و آن را به آرایهٔ `TAGS` اضافه کن (تا در پنل مدیریت قالبها و seed شناخته شود).
|
||||
|
||||
### ۲. قالب پیشفرض در `SmsMessageTemplate::DEFAULTS`
|
||||
|
||||
```php
|
||||
SmsLog::TAG_DOCTOR_APPOINTMENT => [
|
||||
'title' => 'نوبت جدید (اعلان به پزشک)',
|
||||
'body' => "نوبت جدید ثبت شد.\nبیمار: {patient}\nتاریخ: {date} ساعت {time}",
|
||||
'variables' => ['patient', 'date', 'time'],
|
||||
],
|
||||
```
|
||||
|
||||
### ۳. ارسال پیامک به شمارهٔ اعلان دکتر در `handleAppointmentConfirmation`
|
||||
|
||||
بعد از بلوک پیامک بیمار (داخل همان متد)، اضافه کن:
|
||||
|
||||
```php
|
||||
$notify = $doctor->getNotificationMobile();
|
||||
if ($notify) {
|
||||
$docMessage = $this->smsText->resolve(SmsLog::TAG_DOCTOR_APPOINTMENT, [
|
||||
'patient' => $appointment->getPatientName() ?? '—',
|
||||
'date' => $this->jalali->formatDateTime($appointment->getSlotStart(), false),
|
||||
'time' => date('H:i', $appointment->getSlotStart()),
|
||||
]);
|
||||
$this->smsService->dispatchAsync($notify, $docMessage, tag: SmsLog::TAG_DOCTOR_APPOINTMENT);
|
||||
}
|
||||
```
|
||||
|
||||
### ۴. seed قالب جدید
|
||||
|
||||
بعد از افزودن به `DEFAULTS`، دستور موجود را اجرا کن تا رکورد قالب برای تگ جدید ساخته شود (فقط تگهای بدون رکورد را میسازد):
|
||||
```bash
|
||||
ddev exec php bin/console app:seed-sms-message-templates
|
||||
```
|
||||
|
||||
### ۵. مستندسازی
|
||||
|
||||
`docs/api/sms.md`: تگ `doctor_appointment`، قالب پیشفرض، placeholderها (`patient`/`date`/`time`)، و اینکه فقط برای نوبتهای **پرداختشدهٔ سایت** (در مسیر verify پرداخت) ارسال میشود و به `Doctor.notificationMobile` میرود.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **فقط مسیر پرداخت:** کد داخل `handleAppointmentConfirmation` است که تنها از `runPostAction` بعد از verify موفق صدا زده میشود؛ نوبتهای منشی (بدون پرداخت) اینجا نمیآیند — پس شرط «نه نوبت منشی» خودکار برقرار است. کد را جای دیگری (مثل ساخت نوبت) نگذار.
|
||||
- اگر `notificationMobile` خالی بود، هیچ پیامکی نرود (شرط `if ($notify)`).
|
||||
- `dispatchAsync` مثل پیامک بیمار استفاده شود (صف async، شکست پیامک نباید جریان پرداخت را بشکند — الگوی موجود).
|
||||
- ارسال پیامک دکتر **مستقل از** پیامک بیمار است (حتی اگر `patientMobile` خالی باشد، پیامک دکتر باید برود).
|
||||
- `title`/`body` قالب فارسی و قابل ویرایش از پنل قالبهاست؛ فقط مقدار پیشفرض را در `DEFAULTS` بگذار.
|
||||
- `Doctor` تغییر Entity ندارد (فیلد موجود است) → migration لازم نیست.
|
||||
- هزینهٔ پیامک: این پیامک هم مثل بقیه از کیفپول/سهمیهٔ پیامک همان entity کسر میشود (اگر `SmsService`/wallet چنین منطقی دارد، خودکار اعمال میشود؛ چیزی اضافه نکن مگر لازم شود).
|
||||
+3
-1
@@ -482,11 +482,13 @@ Updated template with `status: "rejected"`.
|
||||
|
||||
متن پیامکهای سیستمی (OTP، پرداخت، دعوت کلینیک، پیشثبتنام، تأیید موبایل) از پنل قابل ویرایش است و بر اساس **تگ** کلیددار میشود. هر متن placeholderهای مجاز خود را دارد (مثل `{code}`، `{doctor}`، `{date}`). هنگام ارسال، `SmsTextResolver` متنِ ویرایششدهی DB را میگیرد و placeholderها را جایگزین میکند؛ اگر رکوردی نبود به متن پیشفرض fallback میشود.
|
||||
|
||||
> تگها: `otp`، `payment`، `clinic_invitation`، `pre_registration`، `notification_mobile`، `welcome`، `secretary`. (پیامک قالبیِ کاربر با تگ `user_template` جداگانه از طریق `POST /api/v1/sms/template` مدیریت میشود.)
|
||||
> تگها: `otp`، `payment`، `clinic_invitation`، `pre_registration`، `notification_mobile`، `welcome`، `secretary`، `doctor_appointment`. (پیامک قالبیِ کاربر با تگ `user_template` جداگانه از طریق `POST /api/v1/sms/template` مدیریت میشود.)
|
||||
>
|
||||
> تگ `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`
|
||||
|
||||
|
||||
@@ -321,6 +321,17 @@ final class PaymentManager
|
||||
]);
|
||||
$this->smsService->dispatchAsync($mobile, $message, tag: SmsLog::TAG_PAYMENT);
|
||||
}
|
||||
|
||||
// اعلان به شمارهٔ «اعلان نوبت» دکتر — فقط برای نوبتهای پرداختشدهٔ سایت (همین مسیر).
|
||||
$notify = $doctor->getNotificationMobile();
|
||||
if ($notify) {
|
||||
$docMessage = $this->smsText->resolve(SmsLog::TAG_DOCTOR_APPOINTMENT, [
|
||||
'patient' => $appointment->getPatientName() ?? '—',
|
||||
'date' => $this->jalali->formatDateTime($appointment->getSlotStart(), false),
|
||||
'time' => date('H:i', $appointment->getSlotStart()),
|
||||
]);
|
||||
$this->smsService->dispatchAsync($notify, $docMessage, tag: SmsLog::TAG_DOCTOR_APPOINTMENT);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleSubscriptionActivation(Payment $payment): void
|
||||
|
||||
@@ -20,6 +20,7 @@ class SmsLog
|
||||
public const TAG_USER_TEMPLATE = 'user_template';
|
||||
public const TAG_WELCOME = 'welcome';
|
||||
public const TAG_SECRETARY = 'secretary';
|
||||
public const TAG_DOCTOR_APPOINTMENT = 'doctor_appointment';
|
||||
|
||||
public const TAGS = [
|
||||
self::TAG_GLOBAL,
|
||||
@@ -31,6 +32,7 @@ class SmsLog
|
||||
self::TAG_USER_TEMPLATE,
|
||||
self::TAG_WELCOME,
|
||||
self::TAG_SECRETARY,
|
||||
self::TAG_DOCTOR_APPOINTMENT,
|
||||
];
|
||||
|
||||
#[ORM\Id]
|
||||
|
||||
@@ -48,6 +48,11 @@ class SmsMessageTemplate
|
||||
'body' => "شما بهعنوان منشیِ {owner} در کلینیکپرو تعریف شدید.\nشمارهکاربری: {username}\nلینک ورود: {link}",
|
||||
'variables' => ['owner', 'username', 'link'],
|
||||
],
|
||||
SmsLog::TAG_DOCTOR_APPOINTMENT => [
|
||||
'title' => 'نوبت جدید (اعلان به پزشک)',
|
||||
'body' => "نوبت جدید ثبت شد.\nبیمار: {patient}\nتاریخ: {date} ساعت {time}\n\nبرای مدیریت نوبتهای خود بهصورت رایگان به سایت clinic-pro.ir مراجعه کنید.\nکلینیک پرو",
|
||||
'variables' => ['patient', 'date', 'time'],
|
||||
],
|
||||
];
|
||||
|
||||
#[ORM\Id]
|
||||
|
||||
Reference in New Issue
Block a user