Files
clinicpro/.claude/prompt/seed-demo-data.md
T

146 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()`.