Add scenario documentation for importing doctors from IRIMC and managing profile ownership

- Introduced a new document outlining the process for importing a dataset of 160 doctors from the IRIMC system into Clinic Pro.
- Defined a new ownership model to handle doctors without a user account, allowing for management by a system owner.
- Documented the technical design changes required in the database and API for the import process.
- Included detailed steps for the import command, field mappings, and validation rules.
- Specified the workflow for claiming profiles by real doctors and the admin approval process.
This commit is contained in:
hamed
2026-07-11 08:43:26 +03:30
parent 2cfc516e2c
commit f30bf5dfbd
2 changed files with 706 additions and 0 deletions
@@ -0,0 +1,385 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>سناریو: ایمپورت پزشکان نظام پزشکی و مدیریت مالکیت پروفایل</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--bg:#f7f8fa; --card:#ffffff; --ink:#1f2933; --muted:#5b6673;
--line:#e3e8ee; --accent:#0f6fbf; --accent-soft:#eaf3fb; --code:#f2f4f7;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
font-family:'Vazirmatn',system-ui,sans-serif;
background:var(--bg); color:var(--ink);
direction:rtl; text-align:right;
line-height:1.95; font-size:16px; font-weight:400;
}
.wrap{max-width:900px;margin:0 auto;padding:40px 22px 80px}
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:38px 42px;box-shadow:0 1px 3px rgba(16,24,40,.04)}
h1{font-weight:800;font-size:1.9rem;line-height:1.5;margin:.2em 0 .6em;border-bottom:3px solid var(--accent);padding-bottom:.4em}
h2{font-weight:700;font-size:1.35rem;margin:2em 0 .7em;color:var(--accent);border-right:4px solid var(--accent);padding-right:12px}
h3{font-weight:600;font-size:1.1rem;margin:1.5em 0 .5em}
p{margin:.7em 0}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
strong{font-weight:700;color:#0b3d5c}
blockquote{margin:1.2em 0;padding:.6em 16px;background:var(--accent-soft);border-right:4px solid var(--accent);border-radius:8px;color:#124a6b}
blockquote p{margin:.3em 0}
ul,ol{padding-right:1.4em;margin:.6em 0}
li{margin:.35em 0}
hr{border:none;border-top:1px solid var(--line);margin:2em 0}
table{border-collapse:collapse;width:100%;margin:1.2em 0;font-size:.94rem}
th,td{border:1px solid var(--line);padding:9px 12px;text-align:right;vertical-align:top}
th{background:var(--accent-soft);font-weight:700;color:#0b3d5c}
tr:nth-child(even) td{background:#fafbfc}
code{font-family:'Vazirmatn',ui-monospace,monospace;background:var(--code);padding:2px 6px;border-radius:5px;font-size:.9em;direction:ltr;unicode-bidi:embed}
pre{background:#0f1b26;color:#e6edf3;padding:18px 20px;border-radius:12px;overflow-x:auto;direction:ltr;text-align:left;line-height:1.7}
pre code{background:transparent;color:inherit;padding:0;font-size:.88rem}
</style>
</head>
<body>
<div class="wrap"><div class="card">
<h1 id="_1">سناریو: ایمپورت پزشکان سازمان نظام پزشکی و مدیریت مالکیت پروفایل</h1>
<blockquote>
<p>نسخه: ۱.۰ — تاریخ: ۱۴۰۵/۰۴/۱۹ (۲۰۲۶-۰۷-۱۰)
دامنه: <code>clinicpro</code> (بک‌اند + پنل ادمین) · <code>nobat724_front</code> (سایت عمومی) · <code>clinic-pro-tauri</code> (اپ دسکتاپ)
وضعیت: پیش‌نویس طراحی برای پیاده‌سازی</p>
</blockquote>
<hr />
<h2 id="_2">۱. خلاصه اجرایی</h2>
<p>یک دیتاست ۱۶۰ نفره از پزشکان از سامانه استعلام اعضای سازمان نظام پزشکی (<code>membersearch.irimc.org</code>) استخراج شده است. هدف، وارد کردن این پزشکان به Clinic Pro است تا در سایت عمومی Nobat724 نمایش داده شوند، <strong>پیش از آنکه پزشک واقعی در سیستم ثبت‌نام کرده باشد</strong>.</p>
<p>مشکل محوری: در مدل داده‌ی فعلی، هر پزشک (<code>Doctor</code>) به‌صورت اجباری و <strong>یک‌به‌یک و یکتا</strong> به یک کاربر (<code>User</code>) متصل است، و هر کاربر نیز الزاماً یک <strong>شماره موبایل یکتا و غیرتهی</strong> دارد. اما رکوردهای سازمان نظام پزشکی فاقد شماره موبایل هستند (<code>mobileNumber: null</code>). بنابراین نه می‌توان کاربر ساخت (چون موبایل لازم است) و نه می‌توان یک پزشک را بدون کاربر ذخیره کرد.</p>
<p>این سند یک مدل «مالکیت پروفایل» (Profile Ownership) طراحی می‌کند که در آن پزشکان ایمپورت‌شده در حالت <strong>«بدون‌مالک» (unclaimed)</strong> ذخیره و مدیریت می‌شوند، و بعداً از طریق یک فرایند احراز هویت‌شده در Nobat724 به پزشک واقعی <strong>منتقل (claim/transfer)</strong> می‌شوند.</p>
<hr />
<h2 id="_3">۲. مسئله و محدودیت‌های سیستم فعلی</h2>
<p>پیش از طراحی راه‌حل، محدودیت‌های واقعی کد فعلی مستند می‌شوند (منبع: <code>src/Doctor/Entity/Doctor.php</code>, <code>src/Auth/Entity/User.php</code>, <code>src/Doctor/Controller/DoctorController.php</code>).</p>
<table>
<thead>
<tr>
<th>محدودیت</th>
<th>جزئیات کد فعلی</th>
<th>پیامد برای ایمپورت</th>
</tr>
</thead>
<tbody>
<tr>
<td>کاربر برای پزشک اجباری است</td>
<td><code>Doctor::$user</code><code>OneToOne</code>، <code>JoinColumn(nullable: false, onDelete: RESTRICT)</code></td>
<td>نمی‌توان پزشک بدون کاربر ذخیره کرد.</td>
</tr>
<tr>
<td>رابطه پزشک↔کاربر یکتاست</td>
<td><code>UniqueConstraint idx_doctors_user (user_id)</code></td>
<td><strong>نمی‌توان چند پزشک را به یک کاربر مشترک وصل کرد</strong> — ایده‌ی «همه به یک کاربر سیستمی» با این قید نقض می‌شود.</td>
</tr>
<tr>
<td>موبایل کاربر اجباری و یکتاست</td>
<td><code>User::$mobileNumber</code><code>NOT NULL</code>، <code>UniqueConstraint uniq_mobile</code></td>
<td>بدون موبایل نمی‌توان <code>User</code> ساخت؛ داده‌ی نظام پزشکی موبایل ندارد.</td>
</tr>
<tr>
<td>ساخت پزشک به کاربر لاگین‌شده گره خورده</td>
<td><code>DoctorController::create()</code> از <code>#[CurrentUser] User $user</code> استفاده می‌کند و اگر همان کاربر پزشک داشته باشد خطای ۴۰۹ می‌دهد</td>
<td>مسیر فعلی ساخت پزشک برای ایمپورت انبوه مناسب نیست.</td>
</tr>
<tr>
<td><code>medical_system_code</code> یکتا نیست</td>
<td><code>Doctor::$medicalSystemCode</code><code>nullable</code>, بدون <code>unique</code></td>
<td>برای جلوگیری از ایمپورت تکراری و برای تطبیق هنگام claim، باید کلید طبیعی یکتا شود.</td>
</tr>
</tbody>
</table>
<h3 id="_4">نتیجه‌گیری کلیدی طراحی</h3>
<p>خواسته‌ی اولیه («همه‌ی پزشکان ایمپورت‌شده به یک کاربر سیستمی اختصاص یابند») به‌دلیل قید یکتای <code>user_id</code> روی جدول <code>doctors</code> <strong>مستقیماً قابل اجرا نیست</strong>. بنابراین یکی از دو مسیر زیر لازم است، و این سند <strong>گزینه A</strong> را توصیه می‌کند:</p>
<ul>
<li><strong>گزینه A (توصیه‌شده): جداسازی «مالکیت» از «کاربر».</strong> ستون <code>Doctor.user</code> اختیاری (<code>nullable</code>) می‌شود. یک کاربر سیستمی به‌نام «مالک سیستمی» (System Owner) صرفاً به‌عنوان <strong>مدیرِ منطقیِ</strong> پزشکان بدون‌مالک عمل می‌کند (نه از طریق ستون <code>user_id</code>، بلکه از طریق فیلد جدید <code>managed_by</code>). این کار قید یکتا را نقض نمی‌کند و مدل تمیزتری می‌سازد.</li>
<li><strong>گزینه B (جایگزین کم‌تغییر): کاربر جانشین (Placeholder User) به‌ازای هر پزشک.</strong> برای هر پزشک یک <code>User</code> غیرفعال با شناسه‌ی مصنوعی (مثلاً موبایل رزروشده‌ی <code>IRIMC-&lt;code&gt;</code>) ساخته می‌شود. اسکیمای <code>doctors</code> تقریباً دست‌نخورده می‌ماند اما جدول <code>users</code> با ۱۶۰ کاربر جعلی شلوغ می‌شود و هنگام claim باید ادغام (merge) انجام شود.</li>
</ul>
<p>مقایسه و تصمیم نهایی در بخش ۱۳ آمده است.</p>
<hr />
<h2 id="ownership-model">۳. مدل مفهومی مالکیت (Ownership Model)</h2>
<p>هر پروفایل پزشک یکی از این وضعیت‌های مالکیت را دارد:</p>
<ul>
<li><strong><code>unclaimed</code> (بدون‌مالک):</strong> ایمپورت‌شده از نظام پزشکی، هنوز به پزشک واقعی وصل نشده. توسط «مالک سیستمی» مدیریت می‌شود. در Nobat724 نمایش داده می‌شود اما قابل ویرایش توسط عموم نیست و نوبت‌دهی آنلاین آن پیش‌فرض <strong>غیرفعال</strong> است.</li>
<li><strong><code>pending_transfer</code> (در انتظار انتقال):</strong> پزشک واقعی درخواست تصاحب داده و در حال احراز هویت / انتظار تأیید ادمین است.</li>
<li><strong><code>claimed</code> (تصاحب‌شده):</strong> مالکیت به پزشک واقعی منتقل شده؛ پروفایل به کاربر واقعی او متصل است و او کنترل کامل دارد.</li>
</ul>
<p>منبع پروفایل نیز ثبت می‌شود:</p>
<ul>
<li><strong><code>source</code></strong>: <code>irimc</code> (نظام پزشکی) یا <code>manual</code> (ساخت دستی/ثبت‌نام عادی — رفتار فعلی).</li>
<li><strong><code>source_ref</code></strong>: شناسه‌ی یکتای رکورد مبدأ (<code>profile_url</code> id یا <code>medicalSystemCode</code>) برای idempotency و ممیزی.</li>
</ul>
<hr />
<h2 id="_5">۴. بخش اول — ایمپورت پزشکان نظام پزشکی</h2>
<h3 id="_6">۴.۱ کاربر «مالک سیستمی»</h3>
<p>یک کاربر ویژه یک‌بار ساخته می‌شود (از طریق دستور کنسول، هم‌سبک <code>CreateAdminCommand</code>):</p>
<ul>
<li>موبایل رزروشده و ثابت، مثلاً <code>0000000000</code> (خارج از فضای شماره‌های واقعی ایران، ۱۱ رقمی نامعتبر).</li>
<li>نقش‌ها: <code>['ROLE_USER', 'ROLE_ADMIN']</code> یا نقش اختصاصی <code>ROLE_SYSTEM_OWNER</code>.</li>
<li><code>status = 0</code> (غیرفعال برای لاگین) تا امکان ورود با آن وجود نداشته باشد.</li>
<li><code>real_name = 'مالک سیستمی نوبت۷۲۴'</code>.</li>
</ul>
<p>این کاربر <strong>صاحب <code>user_id</code> پزشکان نیست</strong> (چون یکتاست)؛ بلکه شناسه‌اش در ستون جدید <code>Doctor.managed_by</code> قرار می‌گیرد تا مشخص باشد این پزشکان توسط پلتفرم مدیریت می‌شوند و بعداً قابل واگذاری‌اند.</p>
<h3 id="doctorsjson">۴.۲ نگاشت فیلدها از <code>doctors.json</code></h3>
<p>هر رکورد ورودی به این شکل به موجودیت <code>Doctor</code> نگاشت می‌شود:</p>
<table>
<thead>
<tr>
<th>فیلد ورودی (JSON)</th>
<th>مقصد در <code>Doctor</code></th>
<th>توضیح</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>name</code></td>
<td>مثلاً «دکتر فرخنده حسینی»</td>
</tr>
<tr>
<td><code>gender</code> (<code>woman</code>/<code>man</code>)</td>
<td><code>gender</code></td>
<td>با <code>Doctor::GENDERS</code> سازگار است</td>
</tr>
<tr>
<td><code>medicalSystemCode</code></td>
<td><code>medical_system_code</code> + <code>source_ref</code></td>
<td>کلید طبیعی یکتا برای dedup</td>
</tr>
<tr>
<td><code>mobileNumber</code> (<code>null</code>)</td>
<td><code>mobile_number</code> = <code>null</code></td>
<td>اجازه دارد null بماند</td>
</tr>
<tr>
<td><code>degree</code> (<code>general</code>)</td>
<td><code>degree</code></td>
<td>با <code>Doctor::DEGREES</code> سازگار است</td>
</tr>
<tr>
<td><code>info</code></td>
<td><code>info</code></td>
<td>«دکترای حرفه‌ای پزشکی»</td>
</tr>
<tr>
<td><code>specialty_id</code> / <code>specialty_uuid</code></td>
<td>رابطه <code>specialties</code></td>
<td>تطبیق با جدول <code>specialties</code> (fallback با uuid)</td>
</tr>
<tr>
<td><code>state_id</code> / <code>state_uuid</code></td>
<td>رابطه <code>provinces</code></td>
<td>استان محل فعالیت</td>
</tr>
<tr>
<td><code>city_id</code> / <code>city_uuid</code></td>
<td>رابطه <code>cities</code></td>
<td>شهر محل فعالیت</td>
</tr>
<tr>
<td><code>images</code> (<code>[]</code>)</td>
<td><code>images</code></td>
<td>خالی → <code>null</code></td>
</tr>
<tr>
<td><code>socialMedia</code></td>
<td><code>social_media</code></td>
<td>نگاشت به کلیدهای مجاز</td>
</tr>
<tr>
<td><code>profile_url</code> / <code>source_url</code></td>
<td>متادیتای ایمپورت</td>
<td>برای ممیزی و لینک بازبینی</td>
</tr>
</tbody>
</table>
<p>فیلدهای ثابت هنگام ایمپورت: <code>owner_status = 'unclaimed'</code>، <code>source = 'irimc'</code>، <code>managed_by = &lt;systemOwnerId&gt;</code>، <code>active_doctor_appointment = false</code> (تا وقتی مالک واقعی برنامه‌ی کاری تعریف کند نوبت‌دهی روشن نشود).</p>
<h3 id="idempotency">۴.۳ قواعد Idempotency و اعتبارسنجی</h3>
<ul>
<li>کلید یکتای ایمپورت: <code>(source = 'irimc', medical_system_code)</code>. اجرای مجدد ایمپورت رکورد موجود را <strong>به‌روزرسانی</strong> می‌کند نه تکراری‌سازی.</li>
<li>رکوردهای بدون <code>medicalSystemCode</code> رد و در گزارش ایمپورت لاگ می‌شوند.</li>
<li>تطبیق تخصص/استان/شهر ابتدا با <code>*_id</code> و در صورت نبود، با <code>*_uuid</code> انجام می‌شود؛ عدم تطبیق باعث رد کل رکورد نمی‌شود بلکه فقط آن رابطه خالی می‌ماند و در گزارش ثبت می‌شود.</li>
<li>خروجی دستور ایمپورت: تعداد ساخته‌شده / به‌روزشده / ردشده + مسیر فایل گزارش.</li>
</ul>
<h3 id="_7">۴.۴ روش اجرا</h3>
<p>دستور کنسول اختصاصی (هم‌سبک <code>SeedDemoDataCommand</code> و <code>SeedCategoriesCommand</code>):</p>
<pre><code class="language-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
</code></pre>
<p><code>--dry-run</code> فقط گزارش می‌دهد و چیزی ذخیره نمی‌کند. ایمپورت درون یک تراکنش دیتابیس و به‌صورت دسته‌ای (batch/flush هر ۵۰ رکورد) انجام می‌شود.</p>
<hr />
<h2 id="_8">۵. طراحی فنی دیتابیس</h2>
<p>تغییرات روی موجودیت <code>Doctor</code> (به‌همراه یک migration در <code>migrations/</code>):</p>
<pre><code>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 -- (موجود) + ایندکس یکتای جزئی
</code></pre>
<p>قیود و ایندکس‌ها:</p>
<ul>
<li>حذف/تعدیل <code>UniqueConstraint idx_doctors_user</code>: یکتایی فقط باید برای پزشکانِ <strong>دارای کاربر</strong> اعمال شود. چون MariaDB از partial unique index پشتیبانی مستقیم ندارد، یکتایی <code>user_id</code> در سطح اپلیکیشن (هنگام claim) تضمین می‌شود و ایندکس دیتابیس به <code>INDEX</code> ساده تبدیل می‌شود.</li>
<li>ایندکس یکتای طبیعی: <code>UNIQUE (source, medical_system_code)</code> برای idempotency ایمپورت.</li>
<li>ایندکس <code>owner_status</code> برای فیلتر سریع «پزشکان بدون‌مالک».</li>
</ul>
<p>سازگاری با داده‌ی موجود: تمام پزشکان فعلی هنگام migration مقدار <code>owner_status = 'claimed'</code> و <code>source = 'manual'</code> می‌گیرند تا رفتارشان تغییر نکند.</p>
<blockquote>
<p>نکته سازگاری: طبق <code>CLAUDE.md</code>، <code>medical_system_code</code> تا الان <code>nullable</code> و بدون یکتایی بوده؛ پیش از افزودن ایندکس یکتا باید داده‌ی موجود از نظر تکراری بودن پاک‌سازی شود.</p>
</blockquote>
<hr />
<h2 id="api-clinicpro">۶. طراحی API (بک‌اند clinicpro)</h2>
<p>پاسخ‌ها از پوشش <code>BaseController</code> پیروی می‌کنند: <code>{ success, data }</code> / <code>{ success, errors }</code> / صفحه‌بندی <code>{ data, meta }</code>. مطابق قانون پروژه، هر تغییر کنترلر باید در <code>docs/api/*</code> هم مستند شود.</p>
<h3 id="_9">۶.۱ ایمپورت (داخلی / ادمین)</h3>
<p>معمولاً از طریق دستور کنسول انجام می‌شود؛ در صورت نیاز به تریگر از پنل ادمین:</p>
<pre><code>POST /api/v1/admin/doctors/import-irimc [ROLE_ADMIN]
body: { source_url?, dry_run?: bool, records: [...] }
→ 200 { success, data: { created, updated, skipped, report_url } }
</code></pre>
<h3 id="_10">۶.۲ فهرست پزشکان بدون‌مالک</h3>
<p>اندپوینت موجود <code>GET /api/v1/doctors</code> با فیلتر جدید <code>owner_status</code> توسعه می‌یابد تا هم برای پنل ادمین و هم برای صفحه‌ی «تصاحب پروفایل» در Nobat724 قابل‌استفاده باشد:</p>
<pre><code>GET /api/v1/doctors?owner_status=unclaimed&amp;search=&amp;city_id=&amp;specialty_id=
→ 200 { success, data: [...], meta }
</code></pre>
<h3 id="claim">۶.۳ درخواست تصاحب (Claim) — عمومی و احراز هویت‌شده</h3>
<pre><code>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' } }
</code></pre>
<h3 id="_11">۶.۴ تأیید/رد توسط ادمین و نهایی‌سازی انتقال</h3>
<pre><code>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 }
</code></pre>
<p>هنگام approve، عملیات انتقال مالکیت (بخش ۷) به‌صورت اتمیک اجرا می‌شود.</p>
<blockquote>
<p>امکان «انتقال خودکار» (بدون ادمین) نیز قابل تعریف است: اگر <code>national_code</code> کاربر تأییدشده باشد و <code>medical_system_code</code> و نام کاملاً منطبق باشند، سیستم می‌تواند مستقیماً claim را تأیید کند. تصمیم پیش‌فرض این سند: <strong>تأیید ادمین اجباری برای فاز اول</strong> (بخش ۱۳).</p>
</blockquote>
<hr />
<h2 id="_12">۷. بخش دوم — مکانیزم انتقال مالکیت</h2>
<p>جریان کامل تصاحب پروفایل توسط پزشک واقعی:</p>
<ol>
<li><strong>کشف:</strong> پزشک در Nobat724 نام خود را می‌بیند (پروفایل <code>unclaimed</code>) و روی «این پروفایل من است» کلیک می‌کند.</li>
<li><strong>احراز هویت پایه:</strong> اگر لاگین نیست، با موبایل + OTP ثبت‌نام/ورود می‌کند. در این مرحله یک <code>User</code> واقعی با موبایل واقعی ساخته می‌شود (مسیر عادی auth موجود).</li>
<li><strong>تطبیق هویت:</strong> فرم تصاحب، <code>national_code</code> و <code>medical_system_code</code> را می‌گیرد و با رکورد پزشک تطبیق می‌دهد (<code>POST .../claim</code>). پروفایل به <code>pending_transfer</code> می‌رود.</li>
<li><strong>بازبینی:</strong> ادمین در پنل، درخواست را با <code>profile_url</code> سازمان نظام پزشکی بازبینی و approve/reject می‌کند.</li>
<li><strong>نهایی‌سازی انتقال (اتمیک):</strong></li>
<li><code>Doctor.user</code> = کاربر واقعی پزشک (پر شدن ستونی که تا الان null بود).</li>
<li><code>Doctor.managed_by</code> = <code>null</code>؛ <code>owner_status = 'claimed'</code>؛ <code>claimed_at = time()</code>.</li>
<li>افزودن <code>ROLE_DOCTOR</code> به کاربر واقعی (همان منطق موجود در <code>DoctorController::create</code>).</li>
<li>از این پس ویرایش پروفایل توسط خود پزشک از طریق <code>PATCH /api/v1/doctor/{uuid}</code> مجاز است (چک مالکیت فعلی <code>getUser()-&gt;getId() === user</code> اکنون درست کار می‌کند).</li>
<li><strong>اطلاع‌رسانی:</strong> پیامک/نوتیف تأیید به پزشک (هم‌سبک <code>Sms</code> موجود).</li>
</ol>
<p>قواعد یکتایی هنگام انتقال: چون یک کاربر واقعی نباید صاحب دو پزشک شود، پیش از اتصال باید بررسی شود که <code>user_id</code> مقصد در جدول <code>doctors</code> تکراری نشود (تضمین در سطح اپلیکیشن، جایگزین قید یکتای حذف‌شده).</p>
<hr />
<h2 id="nobat724-nobat724_front">۸. اتصال با Nobat724 (<code>nobat724_front</code>)</h2>
<p>مصرف‌کننده‌ی API از طریق <code>services/response.js</code> است (که هم‌اکنون <code>getDoctors</code>, <code>postDoctor</code>, ... را دارد). تغییرات لازم:</p>
<ul>
<li><strong>نمایش پزشکان بدون‌مالک:</strong> فهرست فعلی پزشکان (<code>api/v1/doctors</code>) به‌طور خودکار پزشکان <code>unclaimed</code> را هم شامل می‌شود؛ در کارت پزشک، به‌جای دکمه‌ی «رزرو نوبت»، دکمه‌ی «این پروفایل من است / تکمیل پروفایل» نمایش داده می‌شود چون <code>active=false</code> است.</li>
<li><strong>صفحه/فرم تصاحب:</strong> فراخوانی <code>POST api/v1/doctor/{uuid}/claim</code> پس از ورود با OTP. نگهداری <code>access_token</code>/<code>refresh_token</code> در کوکی (مطابق الگوی فعلی Nobat724).</li>
<li><strong>زبان/تقویم:</strong> تمام رشته‌های جدید فارسی و تاریخ‌ها جلالی (شمسی) بمانند.</li>
<li>رعایت قرارداد: تغییر قالب پاسخ در بک‌اند باید در <code>nobat724_front/services/response.js</code> هم منعکس شود، چون در زمان build خطا نمی‌دهد.</li>
</ul>
<hr />
<h2 id="clinic-pro-tauri">۹. اثر بر <code>clinic-pro-tauri</code></h2>
<p>اپ دسکتاپ نیز کلاینت همان API است (<code>src/service/response.js</code>) و از CASL برای نقش‌ها استفاده می‌کند (<code>clinic</code>, <code>doctor</code>, <code>clinic_doctor</code>, <code>secretary</code>).</p>
<ul>
<li>اگر لیست پزشکان در اپ نمایش داده می‌شود، باید فیلد <code>owner_status</code> و رفتار <code>active=false</code> را مدیریت کند (پزشک بدون‌مالک قابل رزرو آنلاین نیست).</li>
<li>مرز sync آفلاین/آنلاین این اپ هنوز کامل نگاشت نشده؛ پیش از فرض هم‌ترازی، رفتار <code>owner_status</code> در دیتابیس محلی SQLite باید بررسی شود (طبق هشدار <code>AGENTS.md</code>).</li>
<li>برای فاز اول، تغییر در Tauri <strong>اختیاری</strong> است؛ فقط در صورتی که این اپ پزشکان <code>unclaimed</code> را نشان دهد لازم می‌شود.</li>
</ul>
<hr />
<h2 id="_13">۱۰. جریان کاربری (خلاصه‌ی گام‌به‌گام)</h2>
<pre><code>[نظام پزشکی 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 → نوبت‌دهی آنلاین فعال
</code></pre>
<hr />
<h2 id="_14">۱۱. حالات مرزی و قواعد کسب‌وکار</h2>
<ul>
<li><strong>درخواست تصاحب هم‌زمان دو نفر برای یک پروفایل:</strong> فقط اولین <code>pending_transfer</code> پذیرفته می‌شود؛ بقیه با ۴۰۹ رد می‌شوند تا تعیین تکلیف قبلی روشن شود.</li>
<li><strong>کاربری که قبلاً پزشک دارد:</strong> نمی‌تواند پروفایل دوم را تصاحب کند (قید یکتای منطقی <code>user_id</code>).</li>
<li><strong>عدم تطابق کد نظام پزشکی:</strong> رد با ۴۲۲ و بدون تغییر وضعیت.</li>
<li><strong>رد توسط ادمین:</strong> پروفایل به <code>unclaimed</code> بازمی‌گردد و برای تصاحب مجدد آزاد می‌شود.</li>
<li><strong>حذف پزشک بدون‌مالک:</strong> مجاز برای ادمین (مسیر فعلی <code>DELETE</code>); اما پزشکِ <code>claimed</code> طبق رفتار فعلی محافظت می‌شود.</li>
<li><strong>ایمپورت مجدد یک پزشکِ از قبل claimed:</strong> فیلدهای هویتی به‌روز نمی‌شوند (مالک واقعی اولویت دارد)؛ فقط در گزارش «skipped/claimed» ثبت می‌شود.</li>
<li><strong>نوبت‌دهی:</strong> تا زمانی که پروفایل <code>unclaimed</code> است، <code>active_doctor_appointment=false</code> و برنامه‌ی کاری وجود ندارد؛ لذا در <code>toListArray</code> مقدار <code>active=false</code> می‌شود و رزرو ممکن نیست.</li>
</ul>
<hr />
<h2 id="_15">۱۲. مراحل پیاده‌سازی (به‌ترتیب و به‌تفکیک ریپو)</h2>
<p>مطابق <code>CLAUDE.md</code>: ابتدا بک‌اند <code>clinicpro</code>، سپس مستندسازی API، سپس کلاینت‌ها.</p>
<p><strong>الف) <code>clinicpro</code> (بک‌اند):</strong>
1. افزودن فیلدهای <code>managed_by</code>, <code>owner_status</code>, <code>source</code>, <code>source_ref</code>, <code>claimed_at</code> و nullable کردن <code>user_id</code> در <code>Doctor</code> + migration در <code>migrations/</code>.
2. پاک‌سازی داده و افزودن ایندکس یکتای <code>(source, medical_system_code)</code>.
3. دستور کنسول <code>app:doctors:import-irimc</code> (با <code>--dry-run</code>، گزارش، تراکنش).
4. دستور/سیدر ساخت کاربر «مالک سیستمی».
5. موجودیت/جدول <code>DoctorClaim</code> + اندپوینت‌های claim و approve/reject.
6. توسعه‌ی فیلتر <code>owner_status</code> در <code>GET /api/v1/doctors</code> و به‌روزرسانی چک‌های مالکیت.
7. به‌روزرسانی <code>docs/api/*</code> (طبق قانون استاندارد پروژه) و افزودن این سند به مستندات.</p>
<p><strong>ب) <code>nobat724_front</code>:</strong>
8. هم‌ترازی <code>services/response.js</code> با قالب‌های جدید.
9. دکمه‌ی «این پروفایل من است» روی کارت پزشکِ <code>unclaimed</code> + صفحه‌ی فرم تصاحب (فارسی، جلالی، RTL).</p>
<p><strong>ج) <code>clinic-pro-tauri</code> (در صورت نیاز):</strong>
10. مدیریت <code>owner_status</code>/<code>active=false</code> در لیست پزشکان و بررسی مرز sync محلی.</p>
<p><strong>د) بازبینی نهایی:</strong>
11. تست ایمپورت روی نمونه‌ی ۱۶۰ رکورد، تست جریان claim سرتاسری، و بازسازی کاربران تست (<code>ddev exec php create_test_users.php</code>).</p>
<hr />
<h2 id="_16">۱۳. تصمیمات باز و ریسک‌ها</h2>
<ul>
<li><strong>گزینه A در برابر B:</strong> این سند گزینه A (nullable کردن <code>user_id</code> + <code>managed_by</code>) را توصیه می‌کند چون جدول <code>users</code> را با کاربران جعلی آلوده نمی‌کند و مدل مالکیت را صریح می‌سازد. هزینه‌اش: از دست رفتن قید یکتای دیتابیسی روی <code>user_id</code> و انتقال آن به سطح اپلیکیشن.</li>
<li><strong>تأیید ادمین در برابر انتقال خودکار:</strong> پیش‌فرض فاز اول تأیید دستی ادمین است (امن‌تر برای هویت پزشک). خودکارسازی بعداً با اتکا به تأیید کد ملی افزوده می‌شود.</li>
<li><strong>کیفیت داده‌ی نظام پزشکی:</strong> برخی رکوردها ممکن است فاقد <code>specialty_id</code>/<code>city_id</code> معتبر باشند؛ گزارش ایمپورت باید این‌ها را شفاف کند.</li>
<li><strong>حریم خصوصی:</strong> نمایش عمومی نام و کد نظام پزشکی پیش از رضایت پزشک، ملاحظه‌ی حقوقی دارد و باید با سیاست پلتفرم بررسی شود.</li>
<li><strong>یکتایی موبایل مالک سیستمی:</strong> مقدار رزروشده باید تضمیناً هرگز با موبایل واقعی کاربر تداخل نکند.</li>
</ul>
<hr />
<h2 id="_17">۱۴. مرجع نمونه‌ی داده</h2>
<p>نمونه‌ی یک رکورد ورودی از <code>doctors.json</code> (۱۶۰ رکورد، همگی <code>mobileNumber: null</code>):</p>
<pre><code class="language-json">{
&quot;name&quot;: &quot;دکتر فرخنده حسینی&quot;,
&quot;gender&quot;: &quot;woman&quot;,
&quot;medicalSystemCode&quot;: &quot;145657&quot;,
&quot;mobileNumber&quot;: null,
&quot;degree&quot;: &quot;general&quot;,
&quot;info&quot;: &quot;دکترای حرفه‌ای پزشکی&quot;,
&quot;specialty_id&quot;: 1,
&quot;specialty_name&quot;: &quot;پزشک عمومی&quot;,
&quot;state_id&quot;: 23,
&quot;state_name&quot;: &quot;کهگیلویه و بویراحمد&quot;,
&quot;city_id&quot;: 123,
&quot;city_name&quot;: &quot;یاسوج&quot;,
&quot;profile_url&quot;: &quot;https://membersearch.irimc.org/member/profile?id=02058131-...&quot;,
&quot;source_url&quot;: &quot;https://membersearch.irimc.org&quot;
}
</code></pre>
</div></div>
</body>
</html>
@@ -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-<code>`) ساخته می‌شود. اسکیمای `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 = <systemOwnerId>`، `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"
}
```