feat: add SeedDemoDataCommand for seeding production-like demo data for testing representations, doctors, clinics, users, appointments, and commissions
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# 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()`.
|
||||
Reference in New Issue
Block a user