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,54 @@
|
||||
# معماری — تسک ۱۶: ماژول داشبورد دکتر
|
||||
|
||||
## ساختار فایلها
|
||||
```
|
||||
src/Module/Representation/
|
||||
├── Controller/
|
||||
│ └── RepresentationController.php
|
||||
├── Service/
|
||||
│ ├── DashboardService.php ← آمار کلی
|
||||
│ ├── IncomeService.php ← محاسبه درآمد
|
||||
│ └── AppointmentReportService.php ← گزارش نوبتها
|
||||
├── Repository/
|
||||
│ └── RepresentationRepository.php ← کوئریهای پیچیده آماری
|
||||
└── DTO/
|
||||
└── Response/
|
||||
├── DashboardResponse.php
|
||||
├── YearlyIncomeResponse.php
|
||||
└── AppointmentListResponse.php
|
||||
```
|
||||
|
||||
## نمودار جریان
|
||||
```
|
||||
GET /representation/{uuid}
|
||||
│
|
||||
▼
|
||||
DashboardService
|
||||
├─► شمارش appointments (ماه جاری، کل، pending)
|
||||
├─► جمع payments (ماه جاری، کل)
|
||||
└─► average_rating از doctors.average_rating
|
||||
```
|
||||
|
||||
## کوئری درآمد سالانه (بر اساس ماههای شمسی)
|
||||
```php
|
||||
// RepresentationRepository.php
|
||||
// توجه: محاسبه بر اساس ماه شمسی است، نه میلادی
|
||||
// JalaliDateService بازه timestamp هر ماه را میدهد
|
||||
public function getMonthIncome(int $representationId, int $startTs, int $endTs): float
|
||||
{
|
||||
return (float) $this->createQueryBuilder('p')
|
||||
->select('SUM(p.amount)')
|
||||
->where('p.representationId = :repr')
|
||||
->andWhere('p.status = :status')
|
||||
->andWhere('p.createdAt >= :start')
|
||||
->andWhere('p.createdAt < :end')
|
||||
->setParameters([
|
||||
'repr' => $representationId,
|
||||
'status' => 'received', // نه 'paid'!
|
||||
'start' => (new \DateTime())->setTimestamp($startTs),
|
||||
'end' => (new \DateTime())->setTimestamp($endTs),
|
||||
])
|
||||
->getQuery()
|
||||
->getSingleScalarResult() ?? 0.0;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
# پایگاه داده — تسک ۱۶: ماژول Representation
|
||||
|
||||
## جدول: representations
|
||||
_(entity_type=clinic_pro, bundle=representation — از DB backup تأیید شده)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| domain_name | VARCHAR(255) UNIQUE NULL | field_domain_name | دامنه سایت نماینده |
|
||||
| city_id | INT FK → categories.id NULL | field_city | entity ref → category (city bundle) |
|
||||
| state_id | INT FK → categories.id NULL | field_state | entity ref → category (state bundle) |
|
||||
| active | TINYINT(1) DEFAULT 1 | field_active | فعال/غیرفعال |
|
||||
| commission_percent | INT NULL | field_commission_percent | درصد کمیسیون (نوع INT نه decimal) |
|
||||
| address | LONGTEXT NULL | field_address | آدرس نماینده |
|
||||
| bank_account | LONGTEXT NULL | field_bank_account | اطلاعات حساب بانکی (JSON/text) |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## نمونه داده واقعی (از DB backup)
|
||||
```
|
||||
id=41, bundle='representation', uid=33
|
||||
uuid='bc2a0518-f20d-4542-9376-c2b4fd264706'
|
||||
```
|
||||
|
||||
## ایندکسها
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_representations_domain ON representations(domain_name);
|
||||
CREATE INDEX idx_representations_city ON representations(city_id);
|
||||
```
|
||||
|
||||
## روابط با جداول دیگر
|
||||
- `doctors.representation_id` → `representations.id` (تسک ۰۵)
|
||||
- `appointments.representation_id` → `representations.id` (تسک ۱۰)
|
||||
- `payments.representation_id` → `representations.id` (تسک ۱۵)
|
||||
|
||||
## کوئریهای آماری (بر اساس ماه شمسی)
|
||||
|
||||
### پرداختهای یک ماه شمسی (از RepresentationService.php)
|
||||
```sql
|
||||
-- startTimestamp و endTimestamp از JalaliDateService میآیند
|
||||
SELECT SUM(p.amount) as total_price, COUNT(p.id) as count
|
||||
FROM payments p
|
||||
WHERE p.representation_id = :id
|
||||
AND p.status = 'received' -- نه 'paid'!
|
||||
AND p.created_at >= :startTimestamp
|
||||
AND p.created_at < :endTimestamp
|
||||
```
|
||||
|
||||
### بیماران یک نماینده (total_patients)
|
||||
```sql
|
||||
SELECT COUNT(DISTINCT a.patient_id)
|
||||
FROM appointments a
|
||||
WHERE a.representation_id = :id
|
||||
```
|
||||
|
||||
### نوبتهای امروز
|
||||
```sql
|
||||
SELECT COUNT(*)
|
||||
FROM appointments a
|
||||
WHERE a.representation_id = :id
|
||||
AND a.start_time >= :todayStartTimestamp
|
||||
AND a.start_time < :tomorrowStartTimestamp
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
- **domain_name** شامل scheme و slash انتها است: `http://yasuj-nobat.localhost:3000/`
|
||||
- آمار مالی از `payments` با `status='received'` است، نه `paid`
|
||||
- محاسبه ماه/سال با تقویم **شمسی** انجام میشود (نه میلادی)
|
||||
- `JalaliDateService` باید قبل از این تسک پیادهسازی شده باشد
|
||||
@@ -0,0 +1,133 @@
|
||||
# نکات پیادهسازی — تسک ۱۶: ماژول Representation (داشبورد دکتر)
|
||||
|
||||
## مهم: این ماژول دو نقش دارد
|
||||
۱. **مدیریت نمایندگی (Multi-tenant)** — هر نماینده یک دامنه دارد
|
||||
۲. **داشبورد دکتر** — آمار نوبتها، درآمد، بیماران
|
||||
|
||||
## ساختار Representation در Drupal
|
||||
```php
|
||||
// فیلدهای entity (clinic_pro, bundle=representation):
|
||||
field_domain_name // دامنه سایت نماینده (مثل: http://yasuj-nobat.localhost:3000/)
|
||||
field_city // entity reference به category (شهر)
|
||||
field_active // boolean
|
||||
field_commission_percent // درصد کمیسیون
|
||||
|
||||
// دکتر به نماینده از طریق field_representation لینک میشود
|
||||
// نوبت هم با field_representation لینک میشود (از HTTP Host)
|
||||
```
|
||||
|
||||
## شناسایی نماینده از Host
|
||||
```php
|
||||
// در زمان ثبت نوبت:
|
||||
$host = 'http://yasuj-nobat.localhost:3000/';
|
||||
$representation = $this->representationRepo->findByDomainName($host);
|
||||
// در Symfony: $request->getSchemeAndHttpHost() . '/'
|
||||
```
|
||||
|
||||
## endpoint: my-doctor (برای بیمار)
|
||||
```
|
||||
GET /api/v1/representation/my-doctor/{userId}
|
||||
```
|
||||
→ لیست دکترهایی که این کاربر نوبت گرفته را برمیگرداند.
|
||||
فیلد search: `field_representation = $representationId`
|
||||
|
||||
## endpoint: my-appointments (برای نماینده)
|
||||
```
|
||||
GET /api/v1/representation/my-appointments/{representationId}
|
||||
```
|
||||
→ تمام نوبتهای مرتبط با این نماینده
|
||||
کوئری روی `appointment.field_representation = $id`
|
||||
|
||||
## endpoint: filter (داشبورد ماهانه)
|
||||
```
|
||||
GET /api/v1/representation/filter/{representationId}?timestamp=...
|
||||
```
|
||||
→ از `timestamp` برای تعیین ماه جاری شمسی استفاده میکند
|
||||
|
||||
```php
|
||||
// محاسبه بازه ماه جاری شمسی:
|
||||
$persianMonthRange = $jalaliService->getCurrentPersianMonthRange($timestamp);
|
||||
$startTimestamp = $persianMonthRange['start'];
|
||||
|
||||
// آمار پرداختها در این ماه:
|
||||
// field_status = 'received' AND field_representation = $id AND created >= $startTimestamp
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"payments_total": { "total_price": 12500000, "count": 25 },
|
||||
"total_patients": 142,
|
||||
"today_appointments": 8
|
||||
}
|
||||
```
|
||||
|
||||
## endpoint: yearly-income (درآمد سالانه)
|
||||
```
|
||||
GET /api/v1/representation/yearly-income/{id}?timestamp=...
|
||||
```
|
||||
|
||||
این endpoint از تقویم **شمسی (جلالی)** استفاده میکند:
|
||||
```php
|
||||
// محاسبه بازه سال شمسی:
|
||||
$persianYearRange = $jalaliService->getPersianYearRange($timestamp);
|
||||
$jalaliYear = $persianYearRange['year'];
|
||||
|
||||
// Loop از ماه ۱ تا ۱۲ شمسی:
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$monthRange = $this->getMonthRangeByYearAndMonth($jalaliYear, $month);
|
||||
$income = $this->getMonthIncome($representationId, $monthRange['start'], $monthRange['end']);
|
||||
// income = SUM(payment.amount) WHERE status='received' AND representation=$id AND created BETWEEN start AND end
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"year": 1403,
|
||||
"monthly_income": [
|
||||
{ "month": 1, "income": 8500000 },
|
||||
{ "month": 2, "income": 9200000 },
|
||||
...
|
||||
{ "month": 12, "income": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## JalaliDateService پیادهسازی در Symfony
|
||||
از کد `custom_service/src/jalali/JalaliDateService.php` برای پیادهسازی استفاده کن.
|
||||
توابع مورد نیاز:
|
||||
```php
|
||||
class JalaliDateService {
|
||||
public function getCurrentPersianMonthRange(?int $timestamp): array;
|
||||
// return: ['start' => timestamp, 'end' => timestamp]
|
||||
|
||||
public function getPersianYearRange(?int $timestamp): array;
|
||||
// return: ['year' => int, 'start' => timestamp, 'end' => timestamp]
|
||||
|
||||
public function getMonthRangeByYearAndMonth(int $year, int $month): array;
|
||||
// return: ['start' => timestamp, 'end' => timestamp]
|
||||
|
||||
public function gregorianToPersian(int $gy, int $gm, int $gd): array;
|
||||
// return: [$jy, $jm, $jd]
|
||||
|
||||
public function persianToGregorian(int $jy, int $jm, int $jd): array;
|
||||
// return: [$gy, $gm, $gd]
|
||||
}
|
||||
```
|
||||
|
||||
## کشینگ آمار داشبورد
|
||||
آمار داشبورد را با Redis کش کن (TTL = 5 دقیقه):
|
||||
```php
|
||||
$cacheKey = "dashboard_representation_{$representation->getId()}";
|
||||
// بعد از هر payment جدید → cache invalidate
|
||||
```
|
||||
|
||||
## مجوزها
|
||||
```
|
||||
GET /representation/{uuid} → دکتر مرتبط یا ROLE_ADMIN
|
||||
GET /representation/my-appointments/{id} → دکتر/نماینده یا ROLE_ADMIN
|
||||
GET /representation/my-doctor/{userId} → کاربر خودش
|
||||
GET /representation/filter/{id} → دکتر/نماینده یا ROLE_ADMIN
|
||||
GET /representation/yearly-income/{id} → دکتر/نماینده یا ROLE_ADMIN
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
# تسک ۱۶: ماژول Representation (نمایندگی + داشبورد)
|
||||
|
||||
## توضیح
|
||||
این ماژول دو کارکرد دارد:
|
||||
۱. **مدیریت نمایندگیها (Multi-tenant)** — هر نماینده دامنهای دارد؛ نوبتها و دکترها به نماینده مرتبط میشوند
|
||||
۲. **داشبورد دکتر/نماینده** — آمار نوبتها، درآمد ماهانه/سالانه، بیماران
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | توضیح | نیاز به Auth |
|
||||
|-----|------|-------|-------------|
|
||||
| GET | `/api/v1/representation/{uuid}` | اطلاعات نمایندگی | بله (Admin) |
|
||||
| POST | `/api/v1/representations/{id}/bank-accounts` | اضافه کردن کارت بانکی | بله (Admin) |
|
||||
| GET | `/api/v1/representation/my-appointments/{representationId}` | نوبتهای نماینده | بله |
|
||||
| GET | `/api/v1/representation/my-doctor/{userId}` | دکترهای یک بیمار | بله |
|
||||
| GET | `/api/v1/representation/filter/{representationId}` | آمار ماه جاری شمسی | بله |
|
||||
| GET | `/api/v1/representation/yearly-income/{representationId}` | درآمد سالانه شمسی | بله |
|
||||
|
||||
## پیشنیازها
|
||||
- تسک ۰۱، ۰۲، ۰۵ (Doctor)، ۱۰ (Appointment)، ۱۵ (Payment)
|
||||
- پیادهسازی `JalaliDateService` (برای تبدیل تاریخ شمسی)
|
||||
|
||||
> **تسویه نماینده** در تسک ۱۸ پوشش داده میشود (کیف پول، درخواست برداشت، تأیید ادمین)
|
||||
|
||||
## زمان تخمینی
|
||||
۸ تا ۱۰ ساعت
|
||||
|
||||
## Query Params
|
||||
|
||||
### GET /api/v1/representation/filter/{representationId}
|
||||
```
|
||||
?timestamp=1704067200 (اختیاری — Unix timestamp — برای تعیین ماه شمسی)
|
||||
```
|
||||
اگر timestamp نداده شود، ماه جاری شمسی استفاده میشود.
|
||||
|
||||
### GET /api/v1/representation/yearly-income/{representationId}
|
||||
```
|
||||
?timestamp=1704067200 (اختیاری — Unix timestamp — برای تعیین سال شمسی)
|
||||
```
|
||||
|
||||
## نمونه Responseها
|
||||
|
||||
### GET /api/v1/representation/{uuid}
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "...",
|
||||
"domain_name": "http://yasuj-nobat.localhost:3000/",
|
||||
"city": { "id": 5, "uuid": "...", "label": "یاسوج" },
|
||||
"active": true,
|
||||
"commission_percent": 10.0,
|
||||
"created": 1704067200,
|
||||
"changed": 1716000000
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/representation/filter/{id}
|
||||
```json
|
||||
{
|
||||
"payments_total": { "total_price": 12500000, "count": 25 },
|
||||
"total_patients": 142,
|
||||
"today_appointments": 8
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/representation/yearly-income/{id}
|
||||
```json
|
||||
{
|
||||
"year": 1403,
|
||||
"monthly_income": [
|
||||
{ "month": 1, "income": 8500000 },
|
||||
{ "month": 2, "income": 9200000 },
|
||||
{ "month": 3, "income": 0 },
|
||||
...
|
||||
{ "month": 12, "income": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/representation/my-appointments/{id}
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 10, "uuid": "...",
|
||||
"start_time": 1716000000, "end_time": 1716001800,
|
||||
"status": "confirmed",
|
||||
"slot": { "start": "09:00", "end": "09:30", "duration": 30, "location_id": 42 },
|
||||
"doctor": { "id": 5, "uuid": "...", "label": "دکتر محمدی" },
|
||||
"address": { "id": 42, "uuid": "...", "label": "مطب شیراز" },
|
||||
"representation": { "id": 1, "uuid": "...", "label": "نمایندگی یاسوج" },
|
||||
"owner": { "id": 20, "uuid": "...", "name": "علی رضایی" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/representations/{id}/bank-accounts
|
||||
|
||||
```
|
||||
ورودی:
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
Body:
|
||||
{
|
||||
"card_number": "6037-9999-1234-5678",
|
||||
"bank_name": "ملت",
|
||||
"is_default": true
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// خروجی HTTP 201:
|
||||
{
|
||||
"success": true,
|
||||
"bank_account": {
|
||||
"card_number": "6037-9999-1234-5678",
|
||||
"bank_name": "ملت",
|
||||
"is_default": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **⚠ نکات مهم:**
|
||||
> - فیلد `bank_account` در جدول Representation بهصورت JSON Array ذخیره میشود
|
||||
> - هر آیتم شامل: `card_number`, `bank_name`, `is_default`
|
||||
> - اگر `is_default=true` باشد، `is_default` سایر کارتها باید `false` شود
|
||||
> - حداقل یک کارت باید `is_default=true` داشته باشد
|
||||
|
||||
---
|
||||
|
||||
## نمونه `bank_account` در GET /api/v1/representation/{uuid}
|
||||
```json
|
||||
{
|
||||
"bank_account": [
|
||||
{ "card_number": "6037-9999-1234-5678", "bank_name": "ملت", "is_default": true },
|
||||
{ "card_number": "5859-3312-4455-6677", "bank_name": "صادرات", "is_default": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## فیلدهای Representation Entity
|
||||
```
|
||||
field_domain_name → دامنه (مثل: http://yasuj-nobat.localhost:3000/)
|
||||
field_city → entity reference → category (شهر)
|
||||
field_active → boolean
|
||||
field_commission_percent → درصد کمیسیون
|
||||
```
|
||||
Reference in New Issue
Block a user