Files
clinicpro/docs/tasks/task-14-secretary/task.md
T
hamed de1a78a235 feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
2026-06-09 22:00:34 +03:30

314 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# تسک ۱۴: ماژول منشی
## توضیح
مدیریت منشی‌های دکترها که می‌توانند نوبت‌ها و پرداخت‌ها را مدیریت کنند.
هر دکتر بسته به پلن اشتراک می‌تواند ۱ یا ۳ منشی فعال داشته باشد.
## Endpoint ها
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/secretary` | ایجاد منشی | بله (Doctor/Admin) |
| PATCH | `/api/v1/secretary/{uuid}` | ویرایش منشی | بله (Doctor/Admin) |
| GET | `/api/v1/secretary/{uuid}` | دریافت اطلاعات منشی | بله |
| DELETE | `/api/v1/secretary/{uuid}` | حذف منشی | بله (Doctor/Admin) |
| GET | `/api/v1/secretaries/{doctorUuid}` | لیست منشی‌های دکتر | بله |
## پیش‌نیازها
- تسک ۰۱، ۰۲، ۰۵ (Doctor)
## زمان تخمینی
۵ تا ۶ ساعت
---
## سیستم مجوزها — Resource-Based Permissions (مقیاس‌پذیر)
فیلد `permissions` در جدول `doctor_secretaries` یک JSON ساختاریافته با نسخه‌بندی است.
طراحی به گونه‌ای است که در آینده بتوان منابع (`resources`) و عملیات (`actions`) جدید اضافه کرد بدون تغییر در ساختار جدول.
### ساختار JSON
```json
{
"version": 1,
"resources": {
"appointments": {
"view": true,
"create": true,
"cancel": false,
"update_status": true
},
"addresses": {
"view": true,
"create": true,
"update": true,
"delete": false
},
"clinic_info": {
"view": true,
"update": false
},
"insurances": {
"view": true,
"create": true,
"update": true,
"delete": false
}
}
}
```
### منابع و عملیات فعلی
| Resource | Actions | توضیح |
|----------|---------|-------|
| `appointments` | `view`, `create`, `cancel`, `update_status` | نوبت‌ها |
| `addresses` | `view`, `create`, `update`, `delete` | آدرس‌های مطب/کلینیک |
| `clinic_info` | `view`, `update` | اطلاعات مطب یا کلینیک |
| `insurances` | `view`, `create`, `update`, `delete` | بیمه‌ها |
### مقیاس‌پذیری — اضافه کردن Resource جدید در آینده
برای اضافه کردن Resource جدید (مثلاً `patients` یا `reports`) فقط کافی است:
1. کلید جدید به JSON اضافه شود — بدون migration جدید
2. کد Permission Checker به صورت خودکار آن را پشتیبانی می‌کند
3. منشی‌های موجود که کلید جدید را ندارند، به صورت پیش‌فرض `false` دارند
### پیاده‌سازی PHP — SecretaryPermissionChecker
```php
// src/Secretary/Security/SecretaryPermissionChecker.php
class SecretaryPermissionChecker
{
/**
* بررسی مجوز منشی برای یک عملیات روی یک منبع
* مثال: $checker->can($secretary, 'appointments', 'create')
*/
public function can(Secretary $secretary, string $resource, string $action): bool
{
if (!$secretary->isActive()) {
return false;
}
$permissions = $secretary->getPermissions();
return (bool) ($permissions['resources'][$resource][$action] ?? false);
}
/**
* بررسی دسترسی کامل به یک منبع (همه actions باید true باشند)
*/
public function canAll(Secretary $secretary, string $resource, array $actions): bool
{
return array_reduce(
$actions,
fn($carry, $action) => $carry && $this->can($secretary, $resource, $action),
true
);
}
}
```
**مثال استفاده در Controller:**
```php
// در AppointmentController
if (!$this->permissionChecker->can($secretary, 'appointments', 'create')) {
throw new AccessDeniedHttpException('منشی مجاز به ثبت نوبت نیست');
}
// در InsuranceController
if (!$this->permissionChecker->can($secretary, 'insurances', 'delete')) {
throw new AccessDeniedHttpException('منشی مجاز به حذف بیمه نیست');
}
```
### پیش‌فرض هنگام ایجاد منشی
```json
{
"version": 1,
"resources": {
"appointments": {
"view": true,
"create": true,
"cancel": false,
"update_status": true
},
"addresses": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"clinic_info": {
"view": true,
"update": false
},
"insurances": {
"view": true,
"create": false,
"update": false,
"delete": false
}
}
}
```
---
## POST /api/v1/secretary
```json
// Request
{
"mobile_number": "09120671756",
"doctor_uuid": "61be915b-...",
"permissions": {
"version": 1,
"resources": {
"appointments": {
"view": true,
"create": true,
"cancel": false,
"update_status": true
},
"addresses": {
"view": true,
"create": false,
"update": false,
"delete": false
},
"clinic_info": {
"view": true,
"update": false
},
"insurances": {
"view": true,
"create": false,
"update": false,
"delete": false
}
}
}
}
// Response 201
{
"success": true,
"data": {
"uuid": "...",
"user": { "uuid": "...", "realname": "فاطمه رضایی", "mobile": "09120671756" },
"doctor": { "uuid": "...", "name": "دکتر احمدی" },
"active": true,
"permissions": { ... },
"created_at": 1748000000
}
}
// Response 422 — حد مجاز منشی
{
"success": false,
"errors": [{ "code": "ERR_SECRETARY_001", "message": "پلن فعلی اجازه منشی بیشتر را نمی‌دهد" }]
}
```
**قانون بررسی پلن (سمت سرور):**
```
پلن بیسیک → max 1 منشی فعال
پلن پیشرفته → max 3 منشی فعال
هنگام POST /secretary:
activeCount = COUNT(*) WHERE doctor_id=X AND active=true
if activeCount >= maxAllowed → 422
```
---
## PATCH /api/v1/secretary/{uuid}
```json
// Request (فقط resources موردنظر — deep merge با پیش‌فرض‌ها)
{
"active": false,
"permissions": {
"resources": {
"insurances": {
"create": true,
"update": true
}
}
}
}
// نکته: فقط resources/actions ارسال‌شده تغییر می‌کنند — بقیه دست‌نخورده می‌مانند
```
// Response 200
{
"success": true,
"data": { ... }
}
```
---
## GET /api/v1/secretary/{uuid}
```json
{
"success": true,
"data": {
"uuid": "...",
"user": {
"uuid": "...",
"realname": "فاطمه رضایی",
"mobile": "09120671756",
"picture": null
},
"doctor": { "uuid": "...", "name": "دکتر احمدی" },
"active": true,
"permissions": {
"version": 1,
"resources": {
"appointments": { "view": true, "create": true, "cancel": false, "update_status": true },
"addresses": { "view": true, "create": false, "update": false, "delete": false },
"clinic_info": { "view": true, "update": false },
"insurances": { "view": true, "create": false, "update": false, "delete": false }
}
},
"created_at": 1748000000
}
}
```
---
## GET /api/v1/secretaries/{doctorUuid}
```json
{
"success": true,
"data": [
{
"uuid": "...",
"user": { "uuid": "...", "realname": "فاطمه رضایی", "mobile": "09120671756" },
"active": true,
"permissions": { ... },
"created_at": 1748000000
}
]
}
```
---
## نکات مهم
- **کاربر منشی:** هنگام ایجاد منشی با mobile_number، ابتدا بررسی می‌شود آیا کاربر با این شماره وجود دارد — اگر نه، کاربر جدید ایجاد می‌شود
- **ROLE:** کاربر منشی باید role `doctor_s_secretary` داشته باشد
- **لاگین منشی:** منشی می‌تواند با username/password لاگین کند (تسک ۰۲)
- **بررسی پلن:** کاملاً سمت سرور انجام می‌شود، قابل دور زدن نیست
- **نوبت آفلاین:** نوبتی که منشی ثبت می‌کند (`appointments.create`) کمیسیون نماینده ندارد
- **PATCH permissions:** فقط resources/actions ارسال‌شده تغییر می‌کنند (deep merge) — بقیه دست‌نخورده
- **Resource ناشناخته:** اگر resource جدیدی در JSON باشد که سرور نمی‌شناسد، نادیده گرفته می‌شود (forward compat)
- **پیش‌فرض `false`:** اگر resource یا action در JSON وجود نداشته باشد → `false` (deny by default)