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.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# معماری — تسک ۱۴: ماژول منشی
|
||||
|
||||
## ساختار فایلها
|
||||
```
|
||||
src/Module/Secretary/
|
||||
├── Controller/
|
||||
│ └── SecretaryController.php
|
||||
├── Service/
|
||||
│ └── SecretaryService.php
|
||||
├── Repository/
|
||||
│ └── SecretaryRepository.php
|
||||
├── Entity/
|
||||
│ └── Secretary.php
|
||||
├── DTO/
|
||||
│ ├── Request/
|
||||
│ │ ├── CreateSecretaryRequest.php
|
||||
│ │ └── UpdateSecretaryRequest.php
|
||||
│ └── Response/
|
||||
│ └── SecretaryResponse.php
|
||||
└── Voter/
|
||||
└── SecretaryVoter.php
|
||||
```
|
||||
|
||||
## Entity: Secretary
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'secretaries')]
|
||||
class Secretary
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
private Uuid $uuid;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private User $user; // حساب کاربری منشی
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Doctor::class)]
|
||||
private Doctor $doctor; // دکتر مربوطه
|
||||
|
||||
#[ORM\Column(length: 20, default: 'active')]
|
||||
private string $status;
|
||||
|
||||
#[ORM\Column(type: 'json', nullable: true)]
|
||||
private ?array $permissions; // ['manage_appointments', 'view_payments', ...]
|
||||
|
||||
// TimestampableTrait
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
# پایگاه داده — تسک ۱۴: ماژول منشی
|
||||
|
||||
## جدول: doctor_secretaries
|
||||
_(entity_type=clinic_pro, bundle=doctor_secretary — از config تأیید شده)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| user_id | INT FK → users.id NOT NULL | uid | مالک رکورد |
|
||||
| doctor_id | INT FK → doctors.id NOT NULL | field_doctor | دکتر (entity ref → clinic_pro/doctor) |
|
||||
| secretary_id | INT FK → users.id NOT NULL | field_secretary | منشی (entity ref → user) |
|
||||
| telephone | VARCHAR(50) NULL | field_telephone | تلفن تماس منشی |
|
||||
| permission | LONGTEXT NULL | field_permission | مجوزها (JSON یا متن) |
|
||||
| active | TINYINT(1) DEFAULT 1 | field_active | فعال/غیرفعال (نه status VARCHAR!) |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## ایندکسها
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_secretary_doctor_user ON doctor_secretaries(doctor_id, secretary_id);
|
||||
CREATE INDEX idx_secretary_doctor ON doctor_secretaries(doctor_id);
|
||||
CREATE INDEX idx_secretary_user ON doctor_secretaries(secretary_id);
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
- فیلد وضعیت: `active` (TINYINT boolean) — نه `status` با مقادیر string
|
||||
- `field_permission` نوع string_long است (LONGTEXT)، میتواند JSON یا متن ساده باشد
|
||||
- `doctor_id` → FK به doctors.id (نه clinic_pro.id) — در Symfony به entity doctor اشاره میکند
|
||||
- `secretary_id` → FK به users.id — کاربری که نقش منشی دارد
|
||||
- UNIQUE(doctor_id, secretary_id): یک منشی نمیتواند دو بار برای یک دکتر ثبت شود
|
||||
@@ -0,0 +1,25 @@
|
||||
# نکات پیادهسازی — تسک ۱۴: ماژول منشی
|
||||
|
||||
## نقش کاربری
|
||||
هنگام ایجاد secretary، نقش `ROLE_SECRETARY` به user مرتبط اضافه میشود.
|
||||
هنگام حذف، نقش را remove کن (اگر منشی دکتر دیگری نیست).
|
||||
|
||||
## مجوزهای منشی
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
"manage_appointments", // مدیریت نوبتها
|
||||
"view_payments", // مشاهده پرداختها
|
||||
"manage_schedule" // مدیریت برنامه
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## مجوزها در سیستم
|
||||
```
|
||||
POST → دکتر (برای خودش) یا ROLE_ADMIN
|
||||
PATCH → دکتر مرتبط یا ROLE_ADMIN
|
||||
DELETE → دکتر مرتبط یا ROLE_ADMIN
|
||||
GET → دکتر مرتبط، خود منشی، یا ROLE_ADMIN
|
||||
GET list → دکتر مرتبط یا ROLE_ADMIN
|
||||
```
|
||||
@@ -0,0 +1,313 @@
|
||||
# تسک ۱۴: ماژول منشی
|
||||
|
||||
## توضیح
|
||||
مدیریت منشیهای دکترها که میتوانند نوبتها و پرداختها را مدیریت کنند.
|
||||
هر دکتر بسته به پلن اشتراک میتواند ۱ یا ۳ منشی فعال داشته باشد.
|
||||
|
||||
## 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)
|
||||
Reference in New Issue
Block a user