diff --git a/docs/scenarios/irimc-doctor-import-ownership.html b/docs/scenarios/irimc-doctor-import-ownership.html new file mode 100644 index 00000000..37138ed0 --- /dev/null +++ b/docs/scenarios/irimc-doctor-import-ownership.html @@ -0,0 +1,385 @@ + + + + + +سناریو: ایمپورت پزشکان نظام پزشکی و مدیریت مالکیت پروفایل + + + + + + +
+

سناریو: ایمپورت پزشکان سازمان نظام پزشکی و مدیریت مالکیت پروفایل

+
+

نسخه: ۱.۰ — تاریخ: ۱۴۰۵/۰۴/۱۹ (۲۰۲۶-۰۷-۱۰) +دامنه: clinicpro (بک‌اند + پنل ادمین) · nobat724_front (سایت عمومی) · clinic-pro-tauri (اپ دسکتاپ) +وضعیت: پیش‌نویس طراحی برای پیاده‌سازی

+
+
+

۱. خلاصه اجرایی

+

یک دیتاست ۱۶۰ نفره از پزشکان از سامانه استعلام اعضای سازمان نظام پزشکی (membersearch.irimc.org) استخراج شده است. هدف، وارد کردن این پزشکان به Clinic Pro است تا در سایت عمومی Nobat724 نمایش داده شوند، پیش از آنکه پزشک واقعی در سیستم ثبت‌نام کرده باشد.

+

مشکل محوری: در مدل داده‌ی فعلی، هر پزشک (Doctor) به‌صورت اجباری و یک‌به‌یک و یکتا به یک کاربر (User) متصل است، و هر کاربر نیز الزاماً یک شماره موبایل یکتا و غیرتهی دارد. اما رکوردهای سازمان نظام پزشکی فاقد شماره موبایل هستند (mobileNumber: null). بنابراین نه می‌توان کاربر ساخت (چون موبایل لازم است) و نه می‌توان یک پزشک را بدون کاربر ذخیره کرد.

+

این سند یک مدل «مالکیت پروفایل» (Profile Ownership) طراحی می‌کند که در آن پزشکان ایمپورت‌شده در حالت «بدون‌مالک» (unclaimed) ذخیره و مدیریت می‌شوند، و بعداً از طریق یک فرایند احراز هویت‌شده در Nobat724 به پزشک واقعی منتقل (claim/transfer) می‌شوند.

+
+

۲. مسئله و محدودیت‌های سیستم فعلی

+

پیش از طراحی راه‌حل، محدودیت‌های واقعی کد فعلی مستند می‌شوند (منبع: src/Doctor/Entity/Doctor.php, src/Auth/Entity/User.php, src/Doctor/Controller/DoctorController.php).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
محدودیتجزئیات کد فعلیپیامد برای ایمپورت
کاربر برای پزشک اجباری استDoctor::$userOneToOne، JoinColumn(nullable: false, onDelete: RESTRICT)نمی‌توان پزشک بدون کاربر ذخیره کرد.
رابطه پزشک↔کاربر یکتاستUniqueConstraint idx_doctors_user (user_id)نمی‌توان چند پزشک را به یک کاربر مشترک وصل کرد — ایده‌ی «همه به یک کاربر سیستمی» با این قید نقض می‌شود.
موبایل کاربر اجباری و یکتاستUser::$mobileNumberNOT NULL، UniqueConstraint uniq_mobileبدون موبایل نمی‌توان User ساخت؛ داده‌ی نظام پزشکی موبایل ندارد.
ساخت پزشک به کاربر لاگین‌شده گره خوردهDoctorController::create() از #[CurrentUser] User $user استفاده می‌کند و اگر همان کاربر پزشک داشته باشد خطای ۴۰۹ می‌دهدمسیر فعلی ساخت پزشک برای ایمپورت انبوه مناسب نیست.
medical_system_code یکتا نیستDoctor::$medicalSystemCodenullable, بدون uniqueبرای جلوگیری از ایمپورت تکراری و برای تطبیق هنگام claim، باید کلید طبیعی یکتا شود.
+

نتیجه‌گیری کلیدی طراحی

+

خواسته‌ی اولیه («همه‌ی پزشکان ایمپورت‌شده به یک کاربر سیستمی اختصاص یابند») به‌دلیل قید یکتای user_id روی جدول doctors مستقیماً قابل اجرا نیست. بنابراین یکی از دو مسیر زیر لازم است، و این سند گزینه A را توصیه می‌کند:

+ +

مقایسه و تصمیم نهایی در بخش ۱۳ آمده است.

+
+

۳. مدل مفهومی مالکیت (Ownership Model)

+

هر پروفایل پزشک یکی از این وضعیت‌های مالکیت را دارد:

+ +

منبع پروفایل نیز ثبت می‌شود:

+ +
+

۴. بخش اول — ایمپورت پزشکان نظام پزشکی

+

۴.۱ کاربر «مالک سیستمی»

+

یک کاربر ویژه یک‌بار ساخته می‌شود (از طریق دستور کنسول، هم‌سبک CreateAdminCommand):

+ +

این کاربر صاحب user_id پزشکان نیست (چون یکتاست)؛ بلکه شناسه‌اش در ستون جدید Doctor.managed_by قرار می‌گیرد تا مشخص باشد این پزشکان توسط پلتفرم مدیریت می‌شوند و بعداً قابل واگذاری‌اند.

+

۴.۲ نگاشت فیلدها از doctors.json

+

هر رکورد ورودی به این شکل به موجودیت Doctor نگاشت می‌شود:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
فیلد ورودی (JSON)مقصد در Doctorتوضیح
namenameمثلاً «دکتر فرخنده حسینی»
gender (woman/man)genderبا Doctor::GENDERS سازگار است
medicalSystemCodemedical_system_code + source_refکلید طبیعی یکتا برای dedup
mobileNumber (null)mobile_number = nullاجازه دارد null بماند
degree (general)degreeبا Doctor::DEGREES سازگار است
infoinfo«دکترای حرفه‌ای پزشکی»
specialty_id / specialty_uuidرابطه specialtiesتطبیق با جدول specialties (fallback با uuid)
state_id / state_uuidرابطه provincesاستان محل فعالیت
city_id / city_uuidرابطه citiesشهر محل فعالیت
images ([])imagesخالی → null
socialMediasocial_mediaنگاشت به کلیدهای مجاز
profile_url / source_urlمتادیتای ایمپورتبرای ممیزی و لینک بازبینی
+

فیلدهای ثابت هنگام ایمپورت: owner_status = 'unclaimed'، source = 'irimc'، managed_by = <systemOwnerId>، active_doctor_appointment = false (تا وقتی مالک واقعی برنامه‌ی کاری تعریف کند نوبت‌دهی روشن نشود).

+

۴.۳ قواعد Idempotency و اعتبارسنجی

+ +

۴.۴ روش اجرا

+

دستور کنسول اختصاصی (هم‌سبک SeedDemoDataCommand و SeedCategoriesCommand):

+
ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json --dry-run
+ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json
+
+

--dry-run فقط گزارش می‌دهد و چیزی ذخیره نمی‌کند. ایمپورت درون یک تراکنش دیتابیس و به‌صورت دسته‌ای (batch/flush هر ۵۰ رکورد) انجام می‌شود.

+
+

۵. طراحی فنی دیتابیس

+

تغییرات روی موجودیت Doctor (به‌همراه یک migration در migrations/):

+
doctors:
+  user_id           INT NULL            -- تغییر از NOT NULL به NULL (گزینه A)
+  managed_by        INT NULL            -- FK به users.id؛ کاربر «مالک سیستمی»
+  owner_status      VARCHAR(20) NOT NULL DEFAULT 'claimed'
+                                        -- unclaimed | pending_transfer | claimed
+  source            VARCHAR(20) NOT NULL DEFAULT 'manual'   -- irimc | manual
+  source_ref        VARCHAR(100) NULL   -- شناسه رکورد مبدأ
+  claimed_at        INT NULL
+  medical_system_code VARCHAR(25) NULL  -- (موجود) + ایندکس یکتای جزئی
+
+

قیود و ایندکس‌ها:

+ +

سازگاری با داده‌ی موجود: تمام پزشکان فعلی هنگام migration مقدار owner_status = 'claimed' و source = 'manual' می‌گیرند تا رفتارشان تغییر نکند.

+
+

نکته سازگاری: طبق CLAUDE.md، medical_system_code تا الان nullable و بدون یکتایی بوده؛ پیش از افزودن ایندکس یکتا باید داده‌ی موجود از نظر تکراری بودن پاک‌سازی شود.

+
+
+

۶. طراحی API (بک‌اند clinicpro)

+

پاسخ‌ها از پوشش BaseController پیروی می‌کنند: { success, data } / { success, errors } / صفحه‌بندی { data, meta }. مطابق قانون پروژه، هر تغییر کنترلر باید در docs/api/* هم مستند شود.

+

۶.۱ ایمپورت (داخلی / ادمین)

+

معمولاً از طریق دستور کنسول انجام می‌شود؛ در صورت نیاز به تریگر از پنل ادمین:

+
POST /api/v1/admin/doctors/import-irimc      [ROLE_ADMIN]
+  body: { source_url?, dry_run?: bool, records: [...] }
+  → 200 { success, data: { created, updated, skipped, report_url } }
+
+

۶.۲ فهرست پزشکان بدون‌مالک

+

اندپوینت موجود GET /api/v1/doctors با فیلتر جدید owner_status توسعه می‌یابد تا هم برای پنل ادمین و هم برای صفحه‌ی «تصاحب پروفایل» در Nobat724 قابل‌استفاده باشد:

+
GET /api/v1/doctors?owner_status=unclaimed&search=&city_id=&specialty_id=
+  → 200 { success, data: [...], meta }
+
+

۶.۳ درخواست تصاحب (Claim) — عمومی و احراز هویت‌شده

+
POST /api/v1/doctor/{uuid}/claim            [IS_AUTHENTICATED_FULLY]
+  body: { national_code, medical_system_code, activity_time? }
+  قواعد:
+   - پروفایل باید owner_status = 'unclaimed' باشد، وگرنه 409.
+   - medical_system_code ورودی باید با رکورد پزشک مطابقت کند، وگرنه 422.
+   - کاربر لاگین‌شده (که موبایلش قبلاً با OTP تأیید شده) نباید از قبل پزشکِ دیگری داشته باشد.
+   - رکورد به pending_transfer می‌رود و یک ClaimRequest ثبت می‌شود.
+  → 202 { success, data: { claim_id, status: 'pending_transfer' } }
+
+

۶.۴ تأیید/رد توسط ادمین و نهایی‌سازی انتقال

+
GET   /api/v1/admin/doctor-claims?status=pending        [ROLE_ADMIN]
+POST  /api/v1/admin/doctor-claims/{claimId}/approve      [ROLE_ADMIN]
+POST  /api/v1/admin/doctor-claims/{claimId}/reject       [ROLE_ADMIN] { reason }
+
+

هنگام approve، عملیات انتقال مالکیت (بخش ۷) به‌صورت اتمیک اجرا می‌شود.

+
+

امکان «انتقال خودکار» (بدون ادمین) نیز قابل تعریف است: اگر national_code کاربر تأییدشده باشد و medical_system_code و نام کاملاً منطبق باشند، سیستم می‌تواند مستقیماً claim را تأیید کند. تصمیم پیش‌فرض این سند: تأیید ادمین اجباری برای فاز اول (بخش ۱۳).

+
+
+

۷. بخش دوم — مکانیزم انتقال مالکیت

+

جریان کامل تصاحب پروفایل توسط پزشک واقعی:

+
    +
  1. کشف: پزشک در Nobat724 نام خود را می‌بیند (پروفایل unclaimed) و روی «این پروفایل من است» کلیک می‌کند.
  2. +
  3. احراز هویت پایه: اگر لاگین نیست، با موبایل + OTP ثبت‌نام/ورود می‌کند. در این مرحله یک User واقعی با موبایل واقعی ساخته می‌شود (مسیر عادی auth موجود).
  4. +
  5. تطبیق هویت: فرم تصاحب، national_code و medical_system_code را می‌گیرد و با رکورد پزشک تطبیق می‌دهد (POST .../claim). پروفایل به pending_transfer می‌رود.
  6. +
  7. بازبینی: ادمین در پنل، درخواست را با profile_url سازمان نظام پزشکی بازبینی و approve/reject می‌کند.
  8. +
  9. نهایی‌سازی انتقال (اتمیک):
  10. +
  11. Doctor.user = کاربر واقعی پزشک (پر شدن ستونی که تا الان null بود).
  12. +
  13. Doctor.managed_by = null؛ owner_status = 'claimed'؛ claimed_at = time().
  14. +
  15. افزودن ROLE_DOCTOR به کاربر واقعی (همان منطق موجود در DoctorController::create).
  16. +
  17. از این پس ویرایش پروفایل توسط خود پزشک از طریق PATCH /api/v1/doctor/{uuid} مجاز است (چک مالکیت فعلی getUser()->getId() === user اکنون درست کار می‌کند).
  18. +
  19. اطلاع‌رسانی: پیامک/نوتیف تأیید به پزشک (هم‌سبک Sms موجود).
  20. +
+

قواعد یکتایی هنگام انتقال: چون یک کاربر واقعی نباید صاحب دو پزشک شود، پیش از اتصال باید بررسی شود که user_id مقصد در جدول doctors تکراری نشود (تضمین در سطح اپلیکیشن، جایگزین قید یکتای حذف‌شده).

+
+

۸. اتصال با Nobat724 (nobat724_front)

+

مصرف‌کننده‌ی API از طریق services/response.js است (که هم‌اکنون getDoctors, postDoctor, ... را دارد). تغییرات لازم:

+ +
+

۹. اثر بر clinic-pro-tauri

+

اپ دسکتاپ نیز کلاینت همان API است (src/service/response.js) و از CASL برای نقش‌ها استفاده می‌کند (clinic, doctor, clinic_doctor, secretary).

+ +
+

۱۰. جریان کاربری (خلاصه‌ی گام‌به‌گام)

+
[نظام پزشکی JSON] → دستور ایمپورت → پزشکِ unclaimed (managed_by = System Owner)
+         │
+         ▼
+   نمایش در Nobat724 (active=false، بدون نوبت آنلاین)
+         │  پزشک واقعی: «این پروفایل من است»
+         ▼
+   ورود با موبایل + OTP  →  ساخت User واقعی
+         │
+         ▼
+   فرم تصاحب (کد ملی + کد نظام پزشکی) → POST /claim → pending_transfer
+         │
+         ▼
+   بازبینی ادمین (approve) → انتقال اتمیک:
+        user_id=واقعی، owner_status=claimed، +ROLE_DOCTOR
+         │
+         ▼
+   پزشک پروفایل و برنامه‌ی کاری را کامل می‌کند → active=true → نوبت‌دهی آنلاین فعال
+
+
+

۱۱. حالات مرزی و قواعد کسب‌وکار

+ +
+

۱۲. مراحل پیاده‌سازی (به‌ترتیب و به‌تفکیک ریپو)

+

مطابق CLAUDE.md: ابتدا بک‌اند clinicpro، سپس مستندسازی API، سپس کلاینت‌ها.

+

الف) clinicpro (بک‌اند): +1. افزودن فیلدهای managed_by, owner_status, source, source_ref, claimed_at و nullable کردن user_id در Doctor + migration در migrations/. +2. پاک‌سازی داده و افزودن ایندکس یکتای (source, medical_system_code). +3. دستور کنسول app:doctors:import-irimc (با --dry-run، گزارش، تراکنش). +4. دستور/سیدر ساخت کاربر «مالک سیستمی». +5. موجودیت/جدول DoctorClaim + اندپوینت‌های claim و approve/reject. +6. توسعه‌ی فیلتر owner_status در GET /api/v1/doctors و به‌روزرسانی چک‌های مالکیت. +7. به‌روزرسانی docs/api/* (طبق قانون استاندارد پروژه) و افزودن این سند به مستندات.

+

ب) nobat724_front: +8. هم‌ترازی services/response.js با قالب‌های جدید. +9. دکمه‌ی «این پروفایل من است» روی کارت پزشکِ unclaimed + صفحه‌ی فرم تصاحب (فارسی، جلالی، RTL).

+

ج) clinic-pro-tauri (در صورت نیاز): +10. مدیریت owner_status/active=false در لیست پزشکان و بررسی مرز sync محلی.

+

د) بازبینی نهایی: +11. تست ایمپورت روی نمونه‌ی ۱۶۰ رکورد، تست جریان claim سرتاسری، و بازسازی کاربران تست (ddev exec php create_test_users.php).

+
+

۱۳. تصمیمات باز و ریسک‌ها

+ +
+

۱۴. مرجع نمونه‌ی داده

+

نمونه‌ی یک رکورد ورودی از doctors.json (۱۶۰ رکورد، همگی mobileNumber: null):

+
{
+  "name": "دکتر فرخنده حسینی",
+  "gender": "woman",
+  "medicalSystemCode": "145657",
+  "mobileNumber": null,
+  "degree": "general",
+  "info": "دکترای حرفه‌ای پزشکی",
+  "specialty_id": 1,
+  "specialty_name": "پزشک عمومی",
+  "state_id": 23,
+  "state_name": "کهگیلویه و بویراحمد",
+  "city_id": 123,
+  "city_name": "یاسوج",
+  "profile_url": "https://membersearch.irimc.org/member/profile?id=02058131-...",
+  "source_url": "https://membersearch.irimc.org"
+}
+
+
+ + \ No newline at end of file diff --git a/docs/scenarios/irimc-doctor-import-ownership.md b/docs/scenarios/irimc-doctor-import-ownership.md new file mode 100644 index 00000000..174a7fb0 --- /dev/null +++ b/docs/scenarios/irimc-doctor-import-ownership.md @@ -0,0 +1,321 @@ +# سناریو: ایمپورت پزشکان سازمان نظام پزشکی و مدیریت مالکیت پروفایل + +> نسخه: ۱.۰ — تاریخ: ۱۴۰۵/۰۴/۱۹ (۲۰۲۶-۰۷-۱۰) +> دامنه: `clinicpro` (بک‌اند + پنل ادمین) · `nobat724_front` (سایت عمومی) · `clinic-pro-tauri` (اپ دسکتاپ) +> وضعیت: پیش‌نویس طراحی برای پیاده‌سازی + +--- + +## ۱. خلاصه اجرایی + +یک دیتاست ۱۶۰ نفره از پزشکان از سامانه استعلام اعضای سازمان نظام پزشکی (`membersearch.irimc.org`) استخراج شده است. هدف، وارد کردن این پزشکان به Clinic Pro است تا در سایت عمومی Nobat724 نمایش داده شوند، **پیش از آنکه پزشک واقعی در سیستم ثبت‌نام کرده باشد**. + +مشکل محوری: در مدل داده‌ی فعلی، هر پزشک (`Doctor`) به‌صورت اجباری و **یک‌به‌یک و یکتا** به یک کاربر (`User`) متصل است، و هر کاربر نیز الزاماً یک **شماره موبایل یکتا و غیرتهی** دارد. اما رکوردهای سازمان نظام پزشکی فاقد شماره موبایل هستند (`mobileNumber: null`). بنابراین نه می‌توان کاربر ساخت (چون موبایل لازم است) و نه می‌توان یک پزشک را بدون کاربر ذخیره کرد. + +این سند یک مدل «مالکیت پروفایل» (Profile Ownership) طراحی می‌کند که در آن پزشکان ایمپورت‌شده در حالت **«بدون‌مالک» (unclaimed)** ذخیره و مدیریت می‌شوند، و بعداً از طریق یک فرایند احراز هویت‌شده در Nobat724 به پزشک واقعی **منتقل (claim/transfer)** می‌شوند. + +--- + +## ۲. مسئله و محدودیت‌های سیستم فعلی + +پیش از طراحی راه‌حل، محدودیت‌های واقعی کد فعلی مستند می‌شوند (منبع: `src/Doctor/Entity/Doctor.php`, `src/Auth/Entity/User.php`, `src/Doctor/Controller/DoctorController.php`). + +| محدودیت | جزئیات کد فعلی | پیامد برای ایمپورت | +|---|---|---| +| کاربر برای پزشک اجباری است | `Doctor::$user` → `OneToOne`، `JoinColumn(nullable: false, onDelete: RESTRICT)` | نمی‌توان پزشک بدون کاربر ذخیره کرد. | +| رابطه پزشک↔کاربر یکتاست | `UniqueConstraint idx_doctors_user (user_id)` | **نمی‌توان چند پزشک را به یک کاربر مشترک وصل کرد** — ایده‌ی «همه به یک کاربر سیستمی» با این قید نقض می‌شود. | +| موبایل کاربر اجباری و یکتاست | `User::$mobileNumber` → `NOT NULL`، `UniqueConstraint uniq_mobile` | بدون موبایل نمی‌توان `User` ساخت؛ داده‌ی نظام پزشکی موبایل ندارد. | +| ساخت پزشک به کاربر لاگین‌شده گره خورده | `DoctorController::create()` از `#[CurrentUser] User $user` استفاده می‌کند و اگر همان کاربر پزشک داشته باشد خطای ۴۰۹ می‌دهد | مسیر فعلی ساخت پزشک برای ایمپورت انبوه مناسب نیست. | +| `medical_system_code` یکتا نیست | `Doctor::$medicalSystemCode` → `nullable`, بدون `unique` | برای جلوگیری از ایمپورت تکراری و برای تطبیق هنگام claim، باید کلید طبیعی یکتا شود. | + +### نتیجه‌گیری کلیدی طراحی + +خواسته‌ی اولیه («همه‌ی پزشکان ایمپورت‌شده به یک کاربر سیستمی اختصاص یابند») به‌دلیل قید یکتای `user_id` روی جدول `doctors` **مستقیماً قابل اجرا نیست**. بنابراین یکی از دو مسیر زیر لازم است، و این سند **گزینه A** را توصیه می‌کند: + +- **گزینه A (توصیه‌شده): جداسازی «مالکیت» از «کاربر».** ستون `Doctor.user` اختیاری (`nullable`) می‌شود. یک کاربر سیستمی به‌نام «مالک سیستمی» (System Owner) صرفاً به‌عنوان **مدیرِ منطقیِ** پزشکان بدون‌مالک عمل می‌کند (نه از طریق ستون `user_id`، بلکه از طریق فیلد جدید `managed_by`). این کار قید یکتا را نقض نمی‌کند و مدل تمیزتری می‌سازد. +- **گزینه B (جایگزین کم‌تغییر): کاربر جانشین (Placeholder User) به‌ازای هر پزشک.** برای هر پزشک یک `User` غیرفعال با شناسه‌ی مصنوعی (مثلاً موبایل رزروشده‌ی `IRIMC-`) ساخته می‌شود. اسکیمای `doctors` تقریباً دست‌نخورده می‌ماند اما جدول `users` با ۱۶۰ کاربر جعلی شلوغ می‌شود و هنگام claim باید ادغام (merge) انجام شود. + +مقایسه و تصمیم نهایی در بخش ۱۳ آمده است. + +--- + +## ۳. مدل مفهومی مالکیت (Ownership Model) + +هر پروفایل پزشک یکی از این وضعیت‌های مالکیت را دارد: + +- **`unclaimed` (بدون‌مالک):** ایمپورت‌شده از نظام پزشکی، هنوز به پزشک واقعی وصل نشده. توسط «مالک سیستمی» مدیریت می‌شود. در Nobat724 نمایش داده می‌شود اما قابل ویرایش توسط عموم نیست و نوبت‌دهی آنلاین آن پیش‌فرض **غیرفعال** است. +- **`pending_transfer` (در انتظار انتقال):** پزشک واقعی درخواست تصاحب داده و در حال احراز هویت / انتظار تأیید ادمین است. +- **`claimed` (تصاحب‌شده):** مالکیت به پزشک واقعی منتقل شده؛ پروفایل به کاربر واقعی او متصل است و او کنترل کامل دارد. + +منبع پروفایل نیز ثبت می‌شود: + +- **`source`**: `irimc` (نظام پزشکی) یا `manual` (ساخت دستی/ثبت‌نام عادی — رفتار فعلی). +- **`source_ref`**: شناسه‌ی یکتای رکورد مبدأ (`profile_url` id یا `medicalSystemCode`) برای idempotency و ممیزی. + +--- + +## ۴. بخش اول — ایمپورت پزشکان نظام پزشکی + +### ۴.۱ کاربر «مالک سیستمی» + +یک کاربر ویژه یک‌بار ساخته می‌شود (از طریق دستور کنسول، هم‌سبک `CreateAdminCommand`): + +- موبایل رزروشده و ثابت، مثلاً `0000000000` (خارج از فضای شماره‌های واقعی ایران، ۱۱ رقمی نامعتبر). +- نقش‌ها: `['ROLE_USER', 'ROLE_ADMIN']` یا نقش اختصاصی `ROLE_SYSTEM_OWNER`. +- `status = 0` (غیرفعال برای لاگین) تا امکان ورود با آن وجود نداشته باشد. +- `real_name = 'مالک سیستمی نوبت۷۲۴'`. + +این کاربر **صاحب `user_id` پزشکان نیست** (چون یکتاست)؛ بلکه شناسه‌اش در ستون جدید `Doctor.managed_by` قرار می‌گیرد تا مشخص باشد این پزشکان توسط پلتفرم مدیریت می‌شوند و بعداً قابل واگذاری‌اند. + +### ۴.۲ نگاشت فیلدها از `doctors.json` + +هر رکورد ورودی به این شکل به موجودیت `Doctor` نگاشت می‌شود: + +| فیلد ورودی (JSON) | مقصد در `Doctor` | توضیح | +|---|---|---| +| `name` | `name` | مثلاً «دکتر فرخنده حسینی» | +| `gender` (`woman`/`man`) | `gender` | با `Doctor::GENDERS` سازگار است | +| `medicalSystemCode` | `medical_system_code` + `source_ref` | کلید طبیعی یکتا برای dedup | +| `mobileNumber` (`null`) | `mobile_number` = `null` | اجازه دارد null بماند | +| `degree` (`general`) | `degree` | با `Doctor::DEGREES` سازگار است | +| `info` | `info` | «دکترای حرفه‌ای پزشکی» | +| `specialty_id` / `specialty_uuid` | رابطه `specialties` | تطبیق با جدول `specialties` (fallback با uuid) | +| `state_id` / `state_uuid` | رابطه `provinces` | استان محل فعالیت | +| `city_id` / `city_uuid` | رابطه `cities` | شهر محل فعالیت | +| `images` (`[]`) | `images` | خالی → `null` | +| `socialMedia` | `social_media` | نگاشت به کلیدهای مجاز | +| `profile_url` / `source_url` | متادیتای ایمپورت | برای ممیزی و لینک بازبینی | + +فیلدهای ثابت هنگام ایمپورت: `owner_status = 'unclaimed'`، `source = 'irimc'`، `managed_by = `، `active_doctor_appointment = false` (تا وقتی مالک واقعی برنامه‌ی کاری تعریف کند نوبت‌دهی روشن نشود). + +### ۴.۳ قواعد Idempotency و اعتبارسنجی + +- کلید یکتای ایمپورت: `(source = 'irimc', medical_system_code)`. اجرای مجدد ایمپورت رکورد موجود را **به‌روزرسانی** می‌کند نه تکراری‌سازی. +- رکوردهای بدون `medicalSystemCode` رد و در گزارش ایمپورت لاگ می‌شوند. +- تطبیق تخصص/استان/شهر ابتدا با `*_id` و در صورت نبود، با `*_uuid` انجام می‌شود؛ عدم تطبیق باعث رد کل رکورد نمی‌شود بلکه فقط آن رابطه خالی می‌ماند و در گزارش ثبت می‌شود. +- خروجی دستور ایمپورت: تعداد ساخته‌شده / به‌روزشده / ردشده + مسیر فایل گزارش. + +### ۴.۴ روش اجرا + +دستور کنسول اختصاصی (هم‌سبک `SeedDemoDataCommand` و `SeedCategoriesCommand`): + +```bash +ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json --dry-run +ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json +``` + +`--dry-run` فقط گزارش می‌دهد و چیزی ذخیره نمی‌کند. ایمپورت درون یک تراکنش دیتابیس و به‌صورت دسته‌ای (batch/flush هر ۵۰ رکورد) انجام می‌شود. + +--- + +## ۵. طراحی فنی دیتابیس + +تغییرات روی موجودیت `Doctor` (به‌همراه یک migration در `migrations/`): + +``` +doctors: + user_id INT NULL -- تغییر از NOT NULL به NULL (گزینه A) + managed_by INT NULL -- FK به users.id؛ کاربر «مالک سیستمی» + owner_status VARCHAR(20) NOT NULL DEFAULT 'claimed' + -- unclaimed | pending_transfer | claimed + source VARCHAR(20) NOT NULL DEFAULT 'manual' -- irimc | manual + source_ref VARCHAR(100) NULL -- شناسه رکورد مبدأ + claimed_at INT NULL + medical_system_code VARCHAR(25) NULL -- (موجود) + ایندکس یکتای جزئی +``` + +قیود و ایندکس‌ها: + +- حذف/تعدیل `UniqueConstraint idx_doctors_user`: یکتایی فقط باید برای پزشکانِ **دارای کاربر** اعمال شود. چون MariaDB از partial unique index پشتیبانی مستقیم ندارد، یکتایی `user_id` در سطح اپلیکیشن (هنگام claim) تضمین می‌شود و ایندکس دیتابیس به `INDEX` ساده تبدیل می‌شود. +- ایندکس یکتای طبیعی: `UNIQUE (source, medical_system_code)` برای idempotency ایمپورت. +- ایندکس `owner_status` برای فیلتر سریع «پزشکان بدون‌مالک». + +سازگاری با داده‌ی موجود: تمام پزشکان فعلی هنگام migration مقدار `owner_status = 'claimed'` و `source = 'manual'` می‌گیرند تا رفتارشان تغییر نکند. + +> نکته سازگاری: طبق `CLAUDE.md`، `medical_system_code` تا الان `nullable` و بدون یکتایی بوده؛ پیش از افزودن ایندکس یکتا باید داده‌ی موجود از نظر تکراری بودن پاک‌سازی شود. + +--- + +## ۶. طراحی API (بک‌اند clinicpro) + +پاسخ‌ها از پوشش `BaseController` پیروی می‌کنند: `{ success, data }` / `{ success, errors }` / صفحه‌بندی `{ data, meta }`. مطابق قانون پروژه، هر تغییر کنترلر باید در `docs/api/*` هم مستند شود. + +### ۶.۱ ایمپورت (داخلی / ادمین) + +معمولاً از طریق دستور کنسول انجام می‌شود؛ در صورت نیاز به تریگر از پنل ادمین: + +``` +POST /api/v1/admin/doctors/import-irimc [ROLE_ADMIN] + body: { source_url?, dry_run?: bool, records: [...] } + → 200 { success, data: { created, updated, skipped, report_url } } +``` + +### ۶.۲ فهرست پزشکان بدون‌مالک + +اندپوینت موجود `GET /api/v1/doctors` با فیلتر جدید `owner_status` توسعه می‌یابد تا هم برای پنل ادمین و هم برای صفحه‌ی «تصاحب پروفایل» در Nobat724 قابل‌استفاده باشد: + +``` +GET /api/v1/doctors?owner_status=unclaimed&search=&city_id=&specialty_id= + → 200 { success, data: [...], meta } +``` + +### ۶.۳ درخواست تصاحب (Claim) — عمومی و احراز هویت‌شده + +``` +POST /api/v1/doctor/{uuid}/claim [IS_AUTHENTICATED_FULLY] + body: { national_code, medical_system_code, activity_time? } + قواعد: + - پروفایل باید owner_status = 'unclaimed' باشد، وگرنه 409. + - medical_system_code ورودی باید با رکورد پزشک مطابقت کند، وگرنه 422. + - کاربر لاگین‌شده (که موبایلش قبلاً با OTP تأیید شده) نباید از قبل پزشکِ دیگری داشته باشد. + - رکورد به pending_transfer می‌رود و یک ClaimRequest ثبت می‌شود. + → 202 { success, data: { claim_id, status: 'pending_transfer' } } +``` + +### ۶.۴ تأیید/رد توسط ادمین و نهایی‌سازی انتقال + +``` +GET /api/v1/admin/doctor-claims?status=pending [ROLE_ADMIN] +POST /api/v1/admin/doctor-claims/{claimId}/approve [ROLE_ADMIN] +POST /api/v1/admin/doctor-claims/{claimId}/reject [ROLE_ADMIN] { reason } +``` + +هنگام approve، عملیات انتقال مالکیت (بخش ۷) به‌صورت اتمیک اجرا می‌شود. + +> امکان «انتقال خودکار» (بدون ادمین) نیز قابل تعریف است: اگر `national_code` کاربر تأییدشده باشد و `medical_system_code` و نام کاملاً منطبق باشند، سیستم می‌تواند مستقیماً claim را تأیید کند. تصمیم پیش‌فرض این سند: **تأیید ادمین اجباری برای فاز اول** (بخش ۱۳). + +--- + +## ۷. بخش دوم — مکانیزم انتقال مالکیت + +جریان کامل تصاحب پروفایل توسط پزشک واقعی: + +1. **کشف:** پزشک در Nobat724 نام خود را می‌بیند (پروفایل `unclaimed`) و روی «این پروفایل من است» کلیک می‌کند. +2. **احراز هویت پایه:** اگر لاگین نیست، با موبایل + OTP ثبت‌نام/ورود می‌کند. در این مرحله یک `User` واقعی با موبایل واقعی ساخته می‌شود (مسیر عادی auth موجود). +3. **تطبیق هویت:** فرم تصاحب، `national_code` و `medical_system_code` را می‌گیرد و با رکورد پزشک تطبیق می‌دهد (`POST .../claim`). پروفایل به `pending_transfer` می‌رود. +4. **بازبینی:** ادمین در پنل، درخواست را با `profile_url` سازمان نظام پزشکی بازبینی و approve/reject می‌کند. +5. **نهایی‌سازی انتقال (اتمیک):** + - `Doctor.user` = کاربر واقعی پزشک (پر شدن ستونی که تا الان null بود). + - `Doctor.managed_by` = `null`؛ `owner_status = 'claimed'`؛ `claimed_at = time()`. + - افزودن `ROLE_DOCTOR` به کاربر واقعی (همان منطق موجود در `DoctorController::create`). + - از این پس ویرایش پروفایل توسط خود پزشک از طریق `PATCH /api/v1/doctor/{uuid}` مجاز است (چک مالکیت فعلی `getUser()->getId() === user` اکنون درست کار می‌کند). +6. **اطلاع‌رسانی:** پیامک/نوتیف تأیید به پزشک (هم‌سبک `Sms` موجود). + +قواعد یکتایی هنگام انتقال: چون یک کاربر واقعی نباید صاحب دو پزشک شود، پیش از اتصال باید بررسی شود که `user_id` مقصد در جدول `doctors` تکراری نشود (تضمین در سطح اپلیکیشن، جایگزین قید یکتای حذف‌شده). + +--- + +## ۸. اتصال با Nobat724 (`nobat724_front`) + +مصرف‌کننده‌ی API از طریق `services/response.js` است (که هم‌اکنون `getDoctors`, `postDoctor`, ... را دارد). تغییرات لازم: + +- **نمایش پزشکان بدون‌مالک:** فهرست فعلی پزشکان (`api/v1/doctors`) به‌طور خودکار پزشکان `unclaimed` را هم شامل می‌شود؛ در کارت پزشک، به‌جای دکمه‌ی «رزرو نوبت»، دکمه‌ی «این پروفایل من است / تکمیل پروفایل» نمایش داده می‌شود چون `active=false` است. +- **صفحه/فرم تصاحب:** فراخوانی `POST api/v1/doctor/{uuid}/claim` پس از ورود با OTP. نگهداری `access_token`/`refresh_token` در کوکی (مطابق الگوی فعلی Nobat724). +- **زبان/تقویم:** تمام رشته‌های جدید فارسی و تاریخ‌ها جلالی (شمسی) بمانند. +- رعایت قرارداد: تغییر قالب پاسخ در بک‌اند باید در `nobat724_front/services/response.js` هم منعکس شود، چون در زمان build خطا نمی‌دهد. + +--- + +## ۹. اثر بر `clinic-pro-tauri` + +اپ دسکتاپ نیز کلاینت همان API است (`src/service/response.js`) و از CASL برای نقش‌ها استفاده می‌کند (`clinic`, `doctor`, `clinic_doctor`, `secretary`). + +- اگر لیست پزشکان در اپ نمایش داده می‌شود، باید فیلد `owner_status` و رفتار `active=false` را مدیریت کند (پزشک بدون‌مالک قابل رزرو آنلاین نیست). +- مرز sync آفلاین/آنلاین این اپ هنوز کامل نگاشت نشده؛ پیش از فرض هم‌ترازی، رفتار `owner_status` در دیتابیس محلی SQLite باید بررسی شود (طبق هشدار `AGENTS.md`). +- برای فاز اول، تغییر در Tauri **اختیاری** است؛ فقط در صورتی که این اپ پزشکان `unclaimed` را نشان دهد لازم می‌شود. + +--- + +## ۱۰. جریان کاربری (خلاصه‌ی گام‌به‌گام) + +``` +[نظام پزشکی JSON] → دستور ایمپورت → پزشکِ unclaimed (managed_by = System Owner) + │ + ▼ + نمایش در Nobat724 (active=false، بدون نوبت آنلاین) + │ پزشک واقعی: «این پروفایل من است» + ▼ + ورود با موبایل + OTP → ساخت User واقعی + │ + ▼ + فرم تصاحب (کد ملی + کد نظام پزشکی) → POST /claim → pending_transfer + │ + ▼ + بازبینی ادمین (approve) → انتقال اتمیک: + user_id=واقعی، owner_status=claimed، +ROLE_DOCTOR + │ + ▼ + پزشک پروفایل و برنامه‌ی کاری را کامل می‌کند → active=true → نوبت‌دهی آنلاین فعال +``` + +--- + +## ۱۱. حالات مرزی و قواعد کسب‌وکار + +- **درخواست تصاحب هم‌زمان دو نفر برای یک پروفایل:** فقط اولین `pending_transfer` پذیرفته می‌شود؛ بقیه با ۴۰۹ رد می‌شوند تا تعیین تکلیف قبلی روشن شود. +- **کاربری که قبلاً پزشک دارد:** نمی‌تواند پروفایل دوم را تصاحب کند (قید یکتای منطقی `user_id`). +- **عدم تطابق کد نظام پزشکی:** رد با ۴۲۲ و بدون تغییر وضعیت. +- **رد توسط ادمین:** پروفایل به `unclaimed` بازمی‌گردد و برای تصاحب مجدد آزاد می‌شود. +- **حذف پزشک بدون‌مالک:** مجاز برای ادمین (مسیر فعلی `DELETE`); اما پزشکِ `claimed` طبق رفتار فعلی محافظت می‌شود. +- **ایمپورت مجدد یک پزشکِ از قبل claimed:** فیلدهای هویتی به‌روز نمی‌شوند (مالک واقعی اولویت دارد)؛ فقط در گزارش «skipped/claimed» ثبت می‌شود. +- **نوبت‌دهی:** تا زمانی که پروفایل `unclaimed` است، `active_doctor_appointment=false` و برنامه‌ی کاری وجود ندارد؛ لذا در `toListArray` مقدار `active=false` می‌شود و رزرو ممکن نیست. + +--- + +## ۱۲. مراحل پیاده‌سازی (به‌ترتیب و به‌تفکیک ریپو) + +مطابق `CLAUDE.md`: ابتدا بک‌اند `clinicpro`، سپس مستندسازی API، سپس کلاینت‌ها. + +**الف) `clinicpro` (بک‌اند):** +1. افزودن فیلدهای `managed_by`, `owner_status`, `source`, `source_ref`, `claimed_at` و nullable کردن `user_id` در `Doctor` + migration در `migrations/`. +2. پاک‌سازی داده و افزودن ایندکس یکتای `(source, medical_system_code)`. +3. دستور کنسول `app:doctors:import-irimc` (با `--dry-run`، گزارش، تراکنش). +4. دستور/سیدر ساخت کاربر «مالک سیستمی». +5. موجودیت/جدول `DoctorClaim` + اندپوینت‌های claim و approve/reject. +6. توسعه‌ی فیلتر `owner_status` در `GET /api/v1/doctors` و به‌روزرسانی چک‌های مالکیت. +7. به‌روزرسانی `docs/api/*` (طبق قانون استاندارد پروژه) و افزودن این سند به مستندات. + +**ب) `nobat724_front`:** +8. هم‌ترازی `services/response.js` با قالب‌های جدید. +9. دکمه‌ی «این پروفایل من است» روی کارت پزشکِ `unclaimed` + صفحه‌ی فرم تصاحب (فارسی، جلالی، RTL). + +**ج) `clinic-pro-tauri` (در صورت نیاز):** +10. مدیریت `owner_status`/`active=false` در لیست پزشکان و بررسی مرز sync محلی. + +**د) بازبینی نهایی:** +11. تست ایمپورت روی نمونه‌ی ۱۶۰ رکورد، تست جریان claim سرتاسری، و بازسازی کاربران تست (`ddev exec php create_test_users.php`). + +--- + +## ۱۳. تصمیمات باز و ریسک‌ها + +- **گزینه A در برابر B:** این سند گزینه A (nullable کردن `user_id` + `managed_by`) را توصیه می‌کند چون جدول `users` را با کاربران جعلی آلوده نمی‌کند و مدل مالکیت را صریح می‌سازد. هزینه‌اش: از دست رفتن قید یکتای دیتابیسی روی `user_id` و انتقال آن به سطح اپلیکیشن. +- **تأیید ادمین در برابر انتقال خودکار:** پیش‌فرض فاز اول تأیید دستی ادمین است (امن‌تر برای هویت پزشک). خودکارسازی بعداً با اتکا به تأیید کد ملی افزوده می‌شود. +- **کیفیت داده‌ی نظام پزشکی:** برخی رکوردها ممکن است فاقد `specialty_id`/`city_id` معتبر باشند؛ گزارش ایمپورت باید این‌ها را شفاف کند. +- **حریم خصوصی:** نمایش عمومی نام و کد نظام پزشکی پیش از رضایت پزشک، ملاحظه‌ی حقوقی دارد و باید با سیاست پلتفرم بررسی شود. +- **یکتایی موبایل مالک سیستمی:** مقدار رزروشده باید تضمیناً هرگز با موبایل واقعی کاربر تداخل نکند. + +--- + +## ۱۴. مرجع نمونه‌ی داده + +نمونه‌ی یک رکورد ورودی از `doctors.json` (۱۶۰ رکورد، همگی `mobileNumber: null`): + +```json +{ + "name": "دکتر فرخنده حسینی", + "gender": "woman", + "medicalSystemCode": "145657", + "mobileNumber": null, + "degree": "general", + "info": "دکترای حرفه‌ای پزشکی", + "specialty_id": 1, + "specialty_name": "پزشک عمومی", + "state_id": 23, + "state_name": "کهگیلویه و بویراحمد", + "city_id": 123, + "city_name": "یاسوج", + "profile_url": "https://membersearch.irimc.org/member/profile?id=02058131-...", + "source_url": "https://membersearch.irimc.org" +} +```