13 KiB
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):
#[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 باید یا خودِ سرویس را صدا بزند یا دقیقاً همین فرمول را برای درج مستقیم استفاده کند):
$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
#[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 همان دامنه) — عمداً همین مدل واقعی را بساز.
- نکته: کنترلر ادمین دامنهی شهری را برای rep قبول نمیکند (conflict) ولی seeder مستقیم entity میسازد و
- ۵ سراسری:
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::settlebulk-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(viaSiteConfigRepository::set).
۶. گزارش پایانی + سناریوهای وریفای
در انتهای اجرا جدول شمارش چاپ کن (SELECT COUNT از هر جدول) و این کوئریهای وریفای را هم اجرا و نمایش بده:
-- کمیسیون فقط برای 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');
و ۵ سناریوی تست دستی در خروجی چاپ کن (متن فارسی):
GET /api/v1/doctors?domain=x-nobat.ir→ فقط پزشکان نماینده سراسری اول.GET /api/v1/doctors?domain=yasuj-nobat.ir→ رفتار شهری عادی (همه پزشکان شهر).GET /api/v1/site-context?domain=x-nobat.ir→type=representation, is_global=true.- داشبورد نماینده یاسوج (
/api/v1/representation/dashboard/summaryبا کاربر همان نماینده) → درآمد > 0. - پنل ادمین → نمایندگان → 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().