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:
hamed
2026-06-09 22:00:34 +03:30
commit de1a78a235
222 changed files with 36388 additions and 0 deletions
@@ -0,0 +1,103 @@
# معماری — تسک ۰۳: ماژول پروفایل کاربر
## ساختار فایل‌ها
```
src/Module/UserProfile/
├── Controller/
│ └── UserProfileController.php
├── Service/
│ └── UserProfileService.php
├── Repository/
│ └── UserProfileRepository.php
├── Entity/
│ └── UserProfile.php
├── DTO/
│ ├── Request/
│ │ ├── CreateUserProfileRequest.php
│ │ └── UpdateUserProfileRequest.php
│ └── Response/
│ └── UserProfileResponse.php
└── Voter/
└── UserProfileVoter.php
```
## Entity: UserProfile
```php
#[ORM\Entity]
#[ORM\Table(name: 'user_profiles')]
class UserProfile
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\OneToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private User $user;
#[ORM\Column(length: 200, nullable: true)]
private ?string $name;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $description;
#[ORM\Column(length: 20, nullable: true)]
private ?string $birthday;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $basicInsurance;
#[ORM\Column(length: 30, nullable: true)]
private ?string $bloodType;
#[ORM\Column(length: 50, nullable: true)]
private ?string $education;
#[ORM\Column(length: 100, nullable: true)]
private ?string $fathersName;
#[ORM\Column(length: 10, nullable: true)]
private ?string $gender;
#[ORM\Column(length: 20, nullable: true)]
private ?string $homePhone;
#[ORM\Column(length: 100, nullable: true)]
private ?string $job;
#[ORM\Column(length: 30, nullable: true)]
private ?string $maritalStatus;
#[ORM\Column(length: 50, nullable: true)]
private ?string $supplementaryInsurance;
#[ORM\Column(length: 20, nullable: true)]
private ?string $workPhone;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $address;
// اطلاعات پزشکی پیچیده به صورت JSON
#[ORM\Column(type: 'json', nullable: true)]
private ?array $diseases;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $allergies;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $medications;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $surgeries;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $familyHistory;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $relatives;
// TimestampableTrait
}
```
+109
View File
@@ -0,0 +1,109 @@
# پایگاه داده — تسک ۰۳: ماژول پروفایل کاربر
## جدول: profiles
_(entity_type=profile — از DB backup و config تأیید شده)_
| ستون | نوع | نام Drupal | توضیح |
|------|-----|-----------|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
| user_id | INT FK → users.id UNIQUE NOT NULL | uid | کاربر مرتبط |
| label | VARCHAR(255) NULL | label | نام نمایشی پروفایل |
| family | VARCHAR(25) NULL | field_family | نام خانوادگی (max 25) |
| fathers_name | VARCHAR(255) NULL | field_fathers_name | نام پدر |
| national_code | VARCHAR(10) NULL | field_national_code | کد ملی (max 10) |
| national_code_approved | TINYINT(1) DEFAULT 0 | field_national_code_approved | کد ملی تأیید شده |
| gender | VARCHAR(10) NULL | field_gender | `male` یا `female` |
| date_of_birth | INT NULL | field_date_of_birth | تاریخ تولد (Unix timestamp) |
| blood_type | VARCHAR(20) NULL | field_blood_type | گروه خونی |
| marital_status | VARCHAR(30) NULL | field_marital_status | وضعیت تأهل |
| education | VARCHAR(100) NULL | field_education | تحصیلات |
| job | VARCHAR(100) NULL | field_job | شغل |
| address | LONGTEXT NULL | field_address | آدرس |
| home_phone | VARCHAR(30) NULL | field_home_phone | تلفن منزل |
| work_phone | VARCHAR(30) NULL | field_work_phone | تلفن کار |
| insurance_id | VARCHAR(50) NULL | field_insurance_id | شماره بیمه |
| basic_insurance_id | INT FK → categories.id NULL | field_basic_insurance | entity ref → category (بیمه پایه) |
| supplementary_insurance_id | INT FK → categories.id NULL | field_supplementary_insurance | entity ref → category (بیمه تکمیلی) |
| other | LONGTEXT NULL | field_other | سایر اطلاعات |
| sharing_with_user | TINYINT(1) DEFAULT 0 | field_sharing_with_user | اشتراک با کاربر دیگر |
| description | LONGTEXT NULL | description | توضیحات (base field) |
| created_at | INT NOT NULL | created | Unix timestamp |
| updated_at | INT NOT NULL | changed | Unix timestamp |
## مقادیر field_gender (profile — از config)
```
male → آقا
female → خانم
```
## مقادیر field_blood_type (از config)
```
a_positive → A+ a_negative → A-
b_positive → B+ b_negative → B-
o_positive → O+ o_negative → O-
ab_positive → AB+ ab_negative → AB-
```
## مقادیر field_education (از Manual)
```
diploma → دیپلم
postgraduate_diploma → فوق دیپلم
bachelor_s_degree → لیسانس
master_s_degree → فوق لیسانس
doctorate → دکترا
```
## ساختار field_other (JSON — از Manual و نمونه Request واقعی)
```json
{
"disease": [
{ "id": 1, "name": "فشار خون", "status": "false" },
{ "id": 2, "name": "دیابت", "status": "false" }
],
"allergies": [
{ "substance": "پنی‌سیلین", "reaction": "کهیر", "severity": "شدید" }
],
"medications": [
{ "name": "لورازپام", "dose": "1mg", "frequency": "شب‌ها قبل خواب" }
],
"surgeries": [
{ "type": "آپاندکتومی", "year": 2015, "hospital": "بیمارستان امام خمینی" }
],
"family_history": [
{ "relation": "پدر", "disease": "فشار خون" }
],
"relatives": [
{
"name": "علی",
"relation": "پسر عمو",
"contact": { "phone": "+989121234567", "email": "...", "address": "..." }
}
]
}
```
## ایندکس‌ها
```sql
CREATE UNIQUE INDEX idx_profiles_user ON profiles(user_id);
CREATE INDEX idx_profiles_national_code ON profiles(national_code);
```
## رابطه
- `profiles.user_id``users.id` (OneToOne, CASCADE DELETE)
- `profiles.basic_insurance_id``categories.id`
- `profiles.supplementary_insurance_id``categories.id`
## نمونه داده واقعی از DB backup
```
id=1, uid=10, label='hamed', status=1
id=3, uid=32, label='hamed', status=1
```
## نکات مهم
- `field_family` VARCHAR(25) است — نام خانوادگی کوتاه ذخیره می‌شود
- `field_national_code` VARCHAR(10) است — کد ملی ۱۰ رقمی
- `date_of_birth` از نوع timestamp (INT) است، نه DATE
- `basic_insurance` و `supplementary_insurance` entity reference به جدول categories هستند
- `field_sharing_with_user` احتمالاً برای اشتراک پروفایل با دکتر/منشی است
@@ -0,0 +1,67 @@
# نکات پیاده‌سازی — تسک ۰۳: ماژول پروفایل کاربر
## جریان بعد از ذخیره پروفایل
طبق مستندات Drupal، بعد از ذخیره پروفایل باید:
1. `POST /oauth/token` با refresh_token اجرا شود (دریافت access_token جدید)
2. `GET /oauth/userinfo` اجرا شود
در Symfony این جریان در سمت **کلاینت** انجام می‌شود، نه سرور.
→ در پاسخ POST /api/v1/user-profile، token های به‌روز شده نیز برگردان:
```json
{
"data": {
"profile": { "uuid": "...", "name": "..." },
"access_token": "eyJ...",
"refresh_token": "eyJ..."
},
"message": "پروفایل با موفقیت ذخیره شد"
}
```
## اعتبارسنجی blood_type
مقادیر مجاز:
```php
#[Assert\Choice(choices: [
'a_positive', 'a_negative',
'b_positive', 'b_negative',
'ab_positive', 'ab_negative',
'o_positive', 'o_negative'
])]
```
## اعتبارسنجی gender
```php
#[Assert\Choice(choices: ['male', 'female', 'other'])]
```
## اعتبارسنجی education
```php
#[Assert\Choice(choices: [
'primary', 'secondary', 'diploma',
'associate', 'bachelor', 'master',
'postgraduate_diploma', 'doctorate'
])]
```
## Partial Update (PATCH)
endpoint PATCH باید فقط فیلدهایی که ارسال شده را آپدیت کند.
→ از `$request->request->has('field')` یا DTO با nullable fields استفاده کن.
→ مثال: اگر فقط `diseases` در body باشد، بقیه فیلدها تغییر نکنند.
## مجوزها
```
POST /api/v1/user-profile → کاربر احراز هویت‌شده (برای خودش)
GET /api/v1/user-profile/{uuid} → owner یا ROLE_ADMIN یا ROLE_DOCTOR (دکتر مرتبط)
PATCH /api/v1/user-profile/{uuid} → owner یا ROLE_ADMIN
DELETE /api/v1/user-profile/{uuid} → فقط ROLE_ADMIN
```
## نکته UUID در URL
در Drupal از UUID واقعی در URL استفاده می‌شد.
در Symfony نیز همان رویکرد حفظ می‌شود.
ParamConverter می‌تواند UUID را به Entity تبدیل کند:
```php
#[Route('/api/v1/user-profile/{uuid}', methods: ['GET'])]
public function get(UserProfile $userProfile): Response
// Doctrine ParamConverter به طور خودکار uuid را به UserProfile تبدیل می‌کند
```
+60
View File
@@ -0,0 +1,60 @@
# تسک ۰۳: ماژول پروفایل کاربر
## توضیح
پیاده‌سازی CRUD پروفایل پزشکی کاربر شامل اطلاعات شخصی،
سابقه بیماری، آلرژی‌ها، داروها، عمل‌های جراحی، سابقه خانوادگی و بستگان.
## Endpoint ها
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/user-profile` | ایجاد پروفایل | بله |
| GET | `/api/v1/user-profile/{uuid}` | دریافت پروفایل | بله |
| PATCH | `/api/v1/user-profile/{uuid}` | ویرایش پروفایل | بله (Owner/Admin) |
| DELETE | `/api/v1/user-profile/{uuid}` | حذف پروفایل | بله (Admin) |
## پیش‌نیازها
- تسک ۰۱ و ۰۲ کامل شده باشند
## خروجی‌های مورد انتظار
- [ ] Entity پروفایل با تمام فیلدها
- [ ] پشتیبانی از JSON column برای داده‌های پزشکی پیچیده
- [ ] بعد از ذخیره پروفایل: refresh token و get userinfo اجرا می‌شود
- [ ] فقط owner یا admin می‌تواند پروفایل را ببیند/ویرایش کند
## زمان تخمینی
۸ تا ۱۰ ساعت
## نمونه Request
### POST /api/v1/user-profile
```json
{
"name": "علی رضایی",
"description": [{ "value": "متن توضیحات", "format": "basic_html" }],
"birthday": "1370-05-15",
"basic_insurance": [251],
"blood_type": "ab_negative",
"education": "postgraduate_diploma",
"fathers_name": "محمد",
"gender": "male",
"home_phone": "07433332178",
"job": "مهندس",
"marital_status": "married",
"supplementary_insurance": "308",
"work_phone": "07433332178",
"address": "تهران، خیابان ولیعصر",
"other": [{
"disease": [{ "id": 1, "name": "فشار خون", "status": "true" }],
"allergies": [{ "substance": "پنی‌سیلین", "reaction": "کهیر", "severity": "شدید" }],
"medications": [{ "name": "آتنولول", "dose": "50mg", "frequency": "صبح‌ها" }],
"surgeries": [{ "type": "آپاندکتومی", "year": 2015, "hospital": "بیمارستان امام خمینی" }],
"family_history": [{ "relation": "پدر", "disease": "فشار خون" }],
"relatives": [{
"name": "سارا",
"relation": "خواهر",
"contact": { "phone": "09121234567", "email": "sara@example.com", "address": "تهران" }
}]
}]
}
```