Files
clinicpro/.claude/prompt/appointment-book-national-code.md
T
hamedandClaude Opus 4.8 5548d79d4c feat(appointment): identify admin-booked patient by national code
Admin-side booking (POST /api/v1/my/appointment and
/api/v1/admin/appointment) resolved the patient User by mobile only, so
one person booked under two mobiles produced two User rows — and two
case-files, since PatientRecord is keyed on user_id. National code is the
real unique identity (User.national_code is already unique); a person may
have several mobiles.

Booking now requires + validates patient_national_code and resolves the
patient national-code-first (then mobile) via a shared PatientResolver, so
the case-file stays unique per national code even across mobiles. Reusing a
mobile already bound to a different national code returns 422
ERR_PROFILE_MOBILE_TAKEN. The admin create form and NewAppointmentDrawer
gain a national-code field and send it; both had a dead patient-picker URL
(/api/v1/patient) fixed to the real /api/v1/patients, whose payload already
carries user_national_code for autofill.

Docs (appointment.md, admin.md) and tests updated; new
AppointmentNationalCodeTest covers success, single-file reuse, missing,
invalid, and identity-conflict cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:11:40 +03:30

155 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نوبت‌دهی ادمین بر اساس کد ملی + موبایل (پرونده یکتا با کد ملی)
## پروژه
`clinicpro` (backend Symfony + پنل ادمین React). تک‌ریپو — نیازی به تغییر `nobat724_front` نیست.
> توجه: endpoint عمومی سایت (`POST /api/v1/appointment` → `AppointmentController::book`) **از قبل** کد ملی را الزامی و اعتبارسنجی می‌کند. این تسک فقط شکافِ مسیر **ادمین/کلینیک/منشی/پزشک** را می‌بندد که هنوز بیمار را فقط با موبایل resolve می‌کند.
## زمینه
پرونده‌ی بیمار (`PatientRecord`) روی `user_id` کلید می‌خورد (UniqueConstraint: `entity_type + entity_id + user_id`) و در `PatientService::autoCreateForEntity` از `$appointment->getUser()` ساخته می‌شود. یعنی هویت پرونده = رکورد `User`. اما رکورد `User` در مسیر ثبت نوبتِ ادمین فقط با **موبایل** پیدا/ساخته می‌شود:
- `src/Appointment/Controller/MyAppointmentsController.php` خط ۸۱: `findOneBy(['mobileNumber' => $mobile])`
- `src/Admin/Controller/AdminApiController.php` خط ۸۷۰: `findOneBy(['mobileNumber' => $mobile])`
نتیجه: یک شخص با دو موبایل مختلف → دو `User` مجزا → دو پرونده‌ی مجزا. در حالی که کد ملی یکتاست (`User.national_code` هم‌اکنون `unique: true, nullable: true`). پس هویت درستِ بیمار = **کد ملی**، و موبایل صرفاً یک راه تماس است.
## هدف
در ثبت نوبتِ ادمین، بیمار باید با **کد ملی + موبایل** شناسایی شود:
1. کد ملی در فرم و در هر دو endpoint ادمین **الزامی و معتبر** شود.
2. رکورد `User` بیمار **اول با کد ملی** resolve شود (نه صرفاً موبایل)، تا پرونده برای یک کد ملی یکتا بماند حتی اگر موبایل عوض شود.
3. `patient_national_code` روی `Appointment` ذخیره شود (فیلد و setter از قبل موجود است: `Appointment::setPatientNationalCode`).
## فایل‌های مرتبط
| فایل | نقش | تغییر |
|------|-----|-------|
| `src/Appointment/Controller/MyAppointmentsController.php` | endpoint `POST /api/v1/my/appointment` (doctor/clinic/secretary/admin) | الزام + resolve با کد ملی |
| `src/Admin/Controller/AdminApiController.php` | endpoint `POST /api/v1/admin/appointment` (فقط admin) | الزام + resolve با کد ملی |
| `src/Auth/Repository/UserRepository.php` | فقط `findByMobile` دارد | افزودن `findByNationalCode` |
| `src/Patient/Service/PatientService.php` | `resolvePatientUser` مشترک (اختیاری، ضدتکرار) | استخراج منطق resolve |
| `assets/admin/pages/AppointmentCreatePage.tsx` | فرم ثبت نوبت | افزودن فیلد کد ملی + ارسال در payload |
| `src/Shared/…/InputValidator.php` | `toEnglishDigits` + `isValidIranNationalCode` (استفاده‌شده در `book`) | فقط استفاده |
| `docs/api/appointment.md` + `docs/api/admin.md` | مستندات endpoint | به‌روزرسانی |
## وضعیت فعلی (کد واقعی)
### `book()` عمومی — الگوی درستِ موجود (کپی از `AppointmentController::book`, خط ۲۴۲–۲۶۳)
```php
$nationalCode = InputValidator::toEnglishDigits(trim((string) ($data['patient_national_code'] ?? '')));
if ($nationalCode === '') {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی بیمار الزامی است', 422, 'patient_national_code');
}
if (!InputValidator::isValidIranNationalCode($nationalCode)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'patient_national_code');
}
$appointment = new Appointment($doctor, $user, $slotStart, $slotEnd);
$appointment->setPatientNationalCode($nationalCode);
```
### مسیر ادمین — بیمار فقط با موبایل (کپی از `MyAppointmentsController::createAppointment`, خط ۸۱–۸۷)
```php
$patient = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
if (!$patient) {
$patient = new User($mobile);
$patient->setRealName($patientName);
$patient->setRoles(['ROLE_USER']);
$this->em->persist($patient);
}
$appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
```
(`AdminApiController::createAppointment` خط ۸۷۰–۸۷۶ دقیقاً همین است.)
### فرم — بدون فیلد کد ملی (کپی از `AppointmentCreatePage.tsx`)
```tsx
const [name, setName] = useState('');
const [mobile, setMobile] = useState('');
// ...
const effectiveName = picked?.user_name || name.trim();
const effectiveMobile = picked?.user_mobile || mobile.trim();
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10 && !!start && !!end;
// payload:
patient_name: effectiveName,
patient_mobile: effectiveMobile,
```
> نکته: ردیف‌های جستجوی بیمار (`PatientRow`) فقط `user_name` و `user_mobile` دارند؛ برای پرکردن خودکارِ کد ملیِ بیمارِ انتخاب‌شده باید `user_national_code` هم از endpoint جستجو (`GET /api/v1/patient`) بیاید — بررسی کن آیا برمی‌گردد؛ اگر نه، آن را هم به خروجی اضافه کن (این فیلد در `PatientRecord::toArray` خط ۱۱۶ موجود است).
## وظایف
### ۱. `UserRepository::findByNationalCode`
در `src/Auth/Repository/UserRepository.php` کنار `findByMobile` اضافه کن:
```php
public function findByNationalCode(string $nationalCode): ?User
{
return $this->findOneBy(['nationalCode' => $nationalCode]);
}
```
### ۲. منطق resolve بیمار با کد ملی (اولویت با کد ملی، سپس موبایل)
یک متد مشترک بساز تا در هر دو endpoint استفاده شود (DRY + SOLID). مکان پیشنهادی: `PatientService::resolvePatientUser` (یا یک سرویس کوچک اختصاصی اگر تزریق `PatientService` سنگین بود — تصمیم را در کد بنویس).
قاعده‌ی resolve:
```
nationalCode معتبر ورودی + mobile + name داریم:
1) user = userRepo.findByNationalCode(nationalCode)
2) اگر نبود: user = userRepo.findByMobile(mobile)
- اگر پیدا شد و nationalCode او خالی است → user.setNationalCode(nationalCode)
- اگر پیدا شد و nationalCode او با ورودی فرق دارد → خطای 422
«این شماره موبایل به کد ملی دیگری تعلق دارد» (تعارض هویت)
3) اگر هیچ‌کدام نبود: user جدید با mobile، setRealName(name)، setNationalCode(nationalCode)، ROLE_USER، persist
4) اگر user با کد ملی پیدا شد ولی mobileنش با ورودی فرق دارد → موبایل را به‌روز نکن
(کد ملی مرجع است؛ یک کد ملی می‌تواند چند موبایل داشته باشد — فقط پرونده یکتا بماند).
نامِ خالیِ user را با name پر کن.
```
> چرا اولویت با کد ملی: خواسته‌ی صریح — «یک کاربر ممکن است با چند موبایل باشد و پرونده برای یک کد ملی یکتا». چون `PatientRecord` روی `user_id` است، تا وقتی برای یک کد ملی همان `User` برگردد، پرونده یکتا می‌ماند.
### ۳. الزام + اعتبارسنجی کد ملی در دو endpoint ادمین
در **هر دو** `MyAppointmentsController::createAppointment` و `AdminApiController::createAppointment`:
- بعد از خواندن `$mobile`/`$patientName`، `patient_national_code` را با همان الگوی `book()` بخوان، `toEnglishDigits` کن، خالی‌بودن و `isValidIranNationalCode` را چک کن (خطای 422 با فیلد `patient_national_code`).
- `$patient` را با متد resolve وظیفه‌ی ۲ بگیر (به‌جای `findOneBy(['mobileNumber' => $mobile])`).
- `$appointment->setPatientNationalCode($nationalCode)` را ست کن (مثل `book`).
- `patient_gender` را **الزامی نکن** مگر اینکه قبلاً در این مسیر الزامی بوده باشد — `book` عمومی جنسیت را الزامی می‌کند ولی مسیر ادمین تاکنون نمی‌کرده؛ رفتار فعلی را حفظ کن و فقط کد ملی را اضافه کن (اسکوپ حداقلی).
- به `ErrorCodes` مسیر ادمین دقت کن: این کنترلرها از ثابت‌های کوتاه (`ErrorCodes::VALIDATION`, `ErrorCodes::DOCTOR_NOT_FOUND` …) استفاده می‌کنند، نه `ERR_VALIDATION_001`. از همان سبکِ همان فایل استفاده کن.
### ۴. فرم `AppointmentCreatePage.tsx`
- state جدید: `const [nationalCode, setNationalCode] = useState('')`.
- در بلوک «مراجعه کننده جدید» (`picked === null`) یک فیلد ورودی کد ملی اضافه کن (کنار نام/موبایل). ورودی فارسی/انگلیسی را بپذیر ولی فقط رقم؛ maxLength=10، `dir="ltr"`.
- `effectiveNationalCode = picked?.user_national_code || nationalCode.trim()`.
- `valid` را گسترش بده: کد ملی باید ۱۰ رقم باشد (اعتبارسنجی کاملِ کد ملی سمت بک‌اند است؛ سمت فرانت فقط طول/رقم).
- در `payload`: `patient_national_code: effectiveNationalCode`.
- `PatientRow` را با `user_national_code?: string` گسترش بده و اگر endpoint جستجو آن را برنگرداند، در وظیفه‌ی مرتبط بک‌اند اضافه‌اش کن تا انتخاب بیمارِ موجود، فیلد را پر کند.
### ۵. مستندات
`docs/api/appointment.md` (برای `/api/v1/my/appointment`) و `docs/api/admin.md` (برای `/api/v1/admin/appointment`) را به‌روز کن: افزوده‌شدن فیلد الزامی `patient_national_code`، خطای 422 تعارض موبایل/کد ملی، و رفتار «resolve با کد ملی».
## نکات مهم
- **SOLID/DRY:** منطق resolve بیمار را یک‌جا بنویس؛ در دو کنترلر کپی‌پیست نکن. دلیلِ محلِ قرارگیری را در کامنت بنویس.
- **یکتایی DB:** `User.national_code` هم‌اکنون `unique: true` است — نیازی به migration نیست مگر تغییری در entity بدهی. اگر تغییری ندادی، migration نساز.
- **تعارض هویت (edge مهم):** موبایلی که قبلاً با کد ملیِ X ثبت شده، حالا با کد ملیِ Y بیاید → باید خطای روشن بدهی، نه اینکه کد ملی را عوض کنی (چون verify قبلی را باطل و داده را خراب می‌کند؛ `User::setNationalCode` خط ۹۵ خودش `nationalCodeVerified=false` می‌کند).
- **`for_self` نداریم اینجا:** مسیر ادمین همیشه برای «دیگری» است؛ برخلاف `book`، `$user` جاری پزشک/منشی است نه بیمار. بیمار همیشه از موبایل/کد ملیِ ورودی resolve می‌شود.
- **ارقام فارسی:** همیشه `InputValidator::toEnglishDigits` روی کد ملی و موبایل قبل از جستجو/ذخیره (منشی معمولاً فارسی تایپ می‌کند).
- **تست (الزامی — موفق/خطا/مرزی):**
- موفق: بیمار جدید با کد ملی → `User` با `national_code` ساخته شد + نوبت ثبت شد.
- موفق (یکتایی پرونده): همان کد ملی با موبایلِ متفاوت در نوبت دوم → همان `User` برگردد (نه User جدید)؛ پس از confirm، `PatientRecord` یکتا بماند.
- خطا: کد ملی خالی → 422 `patient_national_code`.
- خطا: کد ملی نامعتبر (checksum) → 422.
- مرزی/تعارض: موبایلِ موجود با کد ملیِ متفاوت → 422 تعارض هویت.
- تست‌ها را با `ddev exec php bin/phpunit` و type-check فرانت را با `npx tsc --noEmit` اجرا کن. بدون سبز شدن، تسک تمام نیست.
- بعد از تغییر کد: `graphify update .` (اول commit طبق قاعده‌ی پروژه).