- 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.
314 lines
8.8 KiB
Markdown
314 lines
8.8 KiB
Markdown
# تسک ۱۴: ماژول منشی
|
||
|
||
## توضیح
|
||
مدیریت منشیهای دکترها که میتوانند نوبتها و پرداختها را مدیریت کنند.
|
||
هر دکتر بسته به پلن اشتراک میتواند ۱ یا ۳ منشی فعال داشته باشد.
|
||
|
||
## 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)
|