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

12 KiB
Raw Blame History

نوبت‌دهی ادمین بر اساس کد ملی + موبایل (پرونده یکتا با کد ملی)

پروژه

clinicpro (backend Symfony + پنل ادمین React). تک‌ریپو — نیازی به تغییر nobat724_front نیست.

توجه: endpoint عمومی سایت (POST /api/v1/appointmentAppointmentController::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, خط ۲۴۲–۲۶۳)

$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, خط ۸۱–۸۷)

$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)

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 اضافه کن:

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 طبق قاعده‌ی پروژه).