Files

13 KiB
Raw Permalink Blame History

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_enabledClinicSubscription و 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 همان دامنه) — عمداً همین مدل واقعی را بساز.
  • ۵ سراسری: 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 از هر جدول) و این کوئری‌های وریفای را هم اجرا و نمایش بده:

-- کمیسیون فقط برای 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.irtype=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().