# 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()`.