146 lines
13 KiB
Markdown
146 lines
13 KiB
Markdown
# Seeder داده تست حجیم (شبیه Production) — نمایندگان، پزشکان، کلینیکها، نوبتها، کمیسیون
|
||
|
||
## زمینه
|
||
|
||
ماژول نمایندگان تازه چند-شهری و دامنهمحور شده (`representation_cities`، `representations.domain`، `is_global`، کمیسیون فقط با تطابق دامنه+مالکیت — پیادهشده در prompt `representation-multi-city-domain-commission.md`). برای تست واقعی سیستم با حجم بالا، دیتای production-like لازم است. الان فقط `app:seed-categories` (شهر/استان/تخصص) و `create_test_users.php` (چند کاربر) وجود دارد — هیچ seeder حجیمی نیست.
|
||
|
||
## مشکل / هدف
|
||
|
||
یک Command جدید `app:seed-demo-data` که در یک اجرا بسازد:
|
||
|
||
| موجودیت | حداقل | نکته |
|
||
|---|---|---|
|
||
| نماینده شهری | ۲۰ | چند-شهری (۱–۳ شهر از جدول cities)، دامنه = دامنهی شهر اصلیاش (مثل `yasuj-nobat.ir`) |
|
||
| نماینده سراسری | ۵ | `is_global=true`، دامنه اختصاصی (`x-nobat.ir`, `global-doctor.ir`, `iran-doc.ir`, `salamat-nobat.ir`, `doc24.ir`) |
|
||
| پزشک | ۵۰۰ | نام واقعی فارسی، تخصص از ۱۰ تخصص، نظامپزشکی یکتا، `representation_id` ستشده، آدرس با شهر |
|
||
| کلینیک | ۲۰۰ | نام/آدرس/شهر، `representation_id`، هر کدام ۱–۵ پزشک مرتبط |
|
||
| کاربر (بیمار) | ۱۰٬۰۰۰ | موبایل یکتای `0912xxxxxxx` |
|
||
| برنامه هفتگی | برای هر پزشک | روزها/شیفت صبحوعصر/مدت نوبت متفاوت (۱۵/۲۰/۳۰ دقیقه) |
|
||
| نوبت | ۲۰٬۰۰۰ | mix وضعیتها + پرداخت + کمیسیون (پایین) |
|
||
| اشتراک | ~۱۰۰ | خرید اشتراک پزشک/کلینیک با پرداخت موفق + کمیسیون دامنهمحور |
|
||
|
||
**Mix نوبتها (قراردادِ سناریو — عمداً برای تست کمیسیون):**
|
||
- ۶۰٪ پرداختشده و confirmed از دامنهی نمایندهی مالکِ همان پزشک → **کمیسیون ثبت میشود**
|
||
- ۱۵٪ پرداختشده از دامنهی نمایندهی دیگر (mismatch) → **کمیسیون ثبت نمیشود**
|
||
- ۱۵٪ لغوشده (cancelled)
|
||
- ۱۰٪ آزاد/pending بدون پرداخت
|
||
|
||
## فایلهای مرتبط
|
||
|
||
| فایل | نقش |
|
||
|------|-----|
|
||
| `src/Shared/Command/SeedDemoDataCommand.php` | **جدید** — کل seeder |
|
||
| `src/Category/Command/SeedCategoriesCommand.php` | الگوی Command موجود (`#[AsCommand]`, SymfonyStyle) |
|
||
| `src/Representation/Entity/Representation.php` | `setCities([City])`, `setDomain()`, `setIsGlobal()`, `setCommissionPercent()` |
|
||
| `src/Doctor/Entity/Doctor.php` + `DoctorAddress` | فیلدها/ctor را بخوان — `representationId`, تخصصها ManyToMany |
|
||
| `src/Clinic/Entity/Clinic.php` | `representationId`, رابطه پزشکان |
|
||
| `src/Appointment/Entity/Appointment.php` | ctor `(Doctor, User, slotStart, slotEnd)` + ثابتهای STATUS + `setBookingRepresentationId` |
|
||
| `src/Payment/Entity/Payment.php` | ctor واقعی: `(User $user, int $amountRials, string $gateway, string $type, string $frontendAddress = '')` |
|
||
| `src/Settlement/Service/CommissionService.php` | `processAppointment(Payment, ?int $ownerRepId, ?int $bookingRepId, ?int $doctorId)` و `processSubscription(..., $ownerRepId, $bookingRepId, ...)` — گارد دوشرطی |
|
||
| جدولهای `cities`, `specialties` | منبع شهر/تخصص (seed شده با `app:seed-categories`) |
|
||
| `create_test_users.php` | الگوی ساخت کاربر تستی |
|
||
|
||
## وضعیت فعلی
|
||
|
||
هیچ seeder دادهای وجود ندارد. الگوی Command موجود (کپی از `SeedCategoriesCommand`):
|
||
|
||
```php
|
||
#[AsCommand(name: 'app:seed-categories', description: '...')]
|
||
class SeedCategoriesCommand extends Command
|
||
{
|
||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||
{
|
||
$io = new SymfonyStyle($input, $output);
|
||
// ...
|
||
$io->success(sprintf('%s: %d رکورد seed شد', $bundle, count($normalized)));
|
||
return Command::SUCCESS;
|
||
}
|
||
}
|
||
```
|
||
|
||
فرمول کمیسیون (از `CommissionService::settle` — seeder باید یا خودِ سرویس را صدا بزند یا دقیقاً همین فرمول را برای درج مستقیم استفاده کند):
|
||
|
||
```php
|
||
$afterSms = max(0, $gross - $smsFee);
|
||
$taxRials = tax_enabled ? round($afterSms * p / (100 + p)) : 0;
|
||
$netAfterTax = $afterSms - $taxRials;
|
||
$repShare = round($netAfterTax * commissionPercent / 100);
|
||
```
|
||
|
||
## وظایف
|
||
|
||
### ۰. تحلیل پیش از کد (الزامی)
|
||
|
||
Entityهای `Doctor`, `DoctorAddress`, `Clinic`, `Appointment` (ثابتهای STATUS)، `WeeklySchedule` (ساختار سشنها/`meta.online_booking_enabled`)، `ClinicSubscription` و repository هایشان را بخوان و در گزارش طراحی، ctor/فیلدهای لازم هر کدام را فهرست کن. حجم بالا → استراتژی درج را همانجا قطعی کن (پایین).
|
||
|
||
### ۱. Command `app:seed-demo-data`
|
||
|
||
```php
|
||
#[AsCommand(name: 'app:seed-demo-data', description: 'Seed production-like demo data (reps, doctors, clinics, users, appointments, commissions)')]
|
||
```
|
||
|
||
- گزینهها: `--doctors=500 --clinics=200 --users=10000 --appointments=20000 --purge --force`
|
||
- **گارد محیط:** اگر `APP_ENV === 'prod'` و `--force` نبود → abort با پیام فارسی.
|
||
- `--purge`: حذف دادهی قبلی demo به ترتیب FK-safe (breakdowns → wallet_transactions → payments → appointments → schedules → clinic_doctors → clinics → doctor_addresses → doctors → representation_cities → representations → users غیرادمین). فقط رکوردهای demo (با marker — پایین) پاک شوند، نه کاربر ادمین/داده واقعی.
|
||
- **Marker داده demo:** موبایل کاربران demo با پیشوند `0912000`/`0912999` یا email الگودار — روش قطعی purge را خودت انتخاب و مستند کن.
|
||
|
||
### ۲. استراتژی درج (performance)
|
||
|
||
- کاربران/نوبتها/پرداختها (دهها هزار رکورد): **DBAL bulk INSERT** با chunkهای ۵۰۰تایی (`INSERT INTO ... VALUES (...), (...), ...`) — نه ORM per-row. uuid با `Uuid::v4()` در PHP.
|
||
- نمایندگان/پزشکان/کلینیکها (صدها رکورد): ORM با `flush()` هر ۵۰ رکورد + `em->clear()`.
|
||
- کل اجرا باید زیر ~۲ دقیقه روی ddev باشد؛ progress bar (`$io->progressStart`).
|
||
|
||
### ۳. نمایندگان
|
||
|
||
- ۲۰ شهری: نام واقعی فارسی متنوع (آرایه ثابت ۲۰ نام)، `commission_percent` بین ۸ تا ۱۵، هر کدام ۱–۳ شهر (گروهبندی جغرافیایی منطقی از جدول cities)، `domain` = دامنهی شهر اصلی (ستون `cities.domain`).
|
||
- نکته: کنترلر ادمین دامنهی شهری را برای rep قبول نمیکند (conflict) ولی seeder مستقیم entity میسازد و `DomainContextResolver` این حالت را پشتیبانی میکند (city-first، سپس rep همان دامنه) — عمداً همین مدل واقعی را بساز.
|
||
- ۵ سراسری: `is_global=true` + دامنههای اختصاصی بالا؛ بدون شهر یا با شهرهای پراکنده.
|
||
- هر نماینده یک User با `ROLE_REPRESENTATION` (الگوی `RepresentationController::create`).
|
||
|
||
### ۴. پزشکان + کلینیکها + برنامه هفتگی
|
||
|
||
- ۵۰۰ پزشک: ترکیب نام/نامخانوادگی فارسی از دو آرایه (۲۵×۲۵)، تخصص از ۱۰ تخصص خواستهشده (match با نام در جدول specialties؛ اگر نبود از موجودها استفاده کن و در گزارش ذکر کن)، `medical_system_code` یکتا (`100000+i`)، ~۸۵٪ فعال، `representation_id` وزندار (نمایندههای شهری بیشتر؛ ۵ سراسری هر کدام ۱۵–۳۰ پزشک؛ ~۱۰٪ پزشک بدون نماینده برای سناریوی بدون کمیسیون)، آدرس در یکی از شهرهای نمایندهاش.
|
||
- ۲۰۰ کلینیک: نام «کلینیک X شهر»، آدرس، شهر، `representation_id` همراستا، اتصال ۱–۵ پزشکِ همان نماینده.
|
||
- برنامه هفتگی هر پزشک فعال: ۳–۶ روز کاری، شیفت صبح (۸–۱۴) و/یا عصر (۱۶–۲۱)، مدت نوبت ۱۵/۲۰/۳۰ — دقیقاً با ساختار واقعی `WeeklySchedule` موجود (بعد از تحلیل مرحله ۰).
|
||
|
||
### ۵. نوبتها + پرداختها + کمیسیون (هسته تست)
|
||
|
||
برای ۲۰٬۰۰۰ نوبت در بازهی ۶۰ روز گذشته تا ۱۴ روز آینده، همراستا با برنامه هفتگی پزشک (slotStart روی مرز مدت نوبت):
|
||
|
||
- **۶۰٪ match:** کاربر تصادفی، `frontendAddress = 'https://' . <دامنهی نمایندهی مالک پزشک> . '/payment/result'`، Payment با status success + `reference_id` یکتا، Appointment وضعیت confirmed (یا done برای گذشته)، `bookingRepresentationId` = همان نماینده.
|
||
- **۱۵٪ mismatch:** همان ساختار ولی frontendAddress از دامنهی نمایندهی دیگر → کمیسیون نباید ثبت شود.
|
||
- **۱۵٪ cancelled** و **۱۰٪ pending** بدون پرداخت.
|
||
- **کمیسیون:** برای درج حجیم، ردیفهای `financial_breakdowns` + `wallet_transactions` را مستقیم با همان فرمول `CommissionService::settle` bulk-insert کن (فقط برای match ها)؛ **علاوه بر آن ۱۰۰ نوبت آخر را از مسیر واقعی `CommissionService->processAppointment()` رد کن** تا parity فرمول تضمین شود (اگر اختلاف شمارش/مبلغ دیدی، فرمول bulk را اصلاح کن).
|
||
- اشتراک: ~۱۰۰ خرید اشتراک (نیمی پزشک، نیمی کلینیک) با Payment موفق و همان قاعده دامنه — نیمی match (کمیسیون با `upgrade_commission_percent`) و نیمی mismatch.
|
||
- تنظیمات لازم را خود seeder ست کند: `appointment_commission_enabled=1`, `upgrade_commission_enabled=1`, `upgrade_commission_percent=20` (via `SiteConfigRepository::set`).
|
||
|
||
### ۶. گزارش پایانی + سناریوهای وریفای
|
||
|
||
در انتهای اجرا جدول شمارش چاپ کن (SELECT COUNT از هر جدول) و این کوئریهای وریفای را هم اجرا و نمایش بده:
|
||
|
||
```sql
|
||
-- کمیسیون فقط برای matchها
|
||
SELECT COUNT(*) FROM financial_breakdowns; -- باید ≈ تعداد matchهای پرداختشده + اشتراکهای match
|
||
-- هیچ breakdown ای برای پرداختهای mismatch (فلگ scenario را در payment.metadata ذخیره کن تا این کوئری ممکن شود)
|
||
-- مجموع سهم نماینده == مجموع wallet_transactions credit
|
||
SELECT (SELECT COALESCE(SUM(representation_share_rials),0) FROM financial_breakdowns)
|
||
= (SELECT COALESCE(SUM(amount_rials),0) FROM wallet_transactions WHERE type='credit');
|
||
```
|
||
|
||
و ۵ سناریوی تست دستی در خروجی چاپ کن (متن فارسی):
|
||
1. `GET /api/v1/doctors?domain=x-nobat.ir` → فقط پزشکان نماینده سراسری اول.
|
||
2. `GET /api/v1/doctors?domain=yasuj-nobat.ir` → رفتار شهری عادی (همه پزشکان شهر).
|
||
3. `GET /api/v1/site-context?domain=x-nobat.ir` → `type=representation, is_global=true`.
|
||
4. داشبورد نماینده یاسوج (`/api/v1/representation/dashboard/summary` با کاربر همان نماینده) → درآمد > 0.
|
||
5. پنل ادمین → نمایندگان → badge «سراسری» روی ۵ نماینده + ستون شهرها چندتایی.
|
||
|
||
## نکات مهم
|
||
|
||
- **هیچ Entity/schema تغییری لازم نیست** — فقط Command جدید + استفاده از سرویس/entityهای موجود؛ پس migration و docs/api لازم ندارد (Command داخلی است، API نیست).
|
||
- تصادفیبودن **seeded** باشد (`mt_srand(42)`) تا اجراها تکرارپذیر باشند.
|
||
- `payment.metadata` هر پرداخت demo شامل `{"demo": true, "scenario": "match|mismatch|..."}` — هم برای purge هم برای کوئریهای وریفای.
|
||
- ترتیب درج FK-safe؛ `SET FOREIGN_KEY_CHECKS` دستکاری نشود (برخلاف importer) — ترتیب درست کافی است.
|
||
- موبایلها یکتا و در رنج رزرو demo؛ با `create_test_users.php` (ادمین واقعی تست) تداخل نکند.
|
||
- تست: `ddev exec php bin/console app:seed-demo-data --purge` دوبار پشتسرهم باید بدون خطا اجرا شود (idempotent با purge). `php -l`، phpstan، و اجرای کامل با شمارشهای انتظاری در گزارش.
|
||
- زمانها Unix timestamp صحیح (الگوی پروژه)؛ نوبتهای گذشته/آینده نسبت به `time()`.
|