Files
clinicpro/.claude/prompt/national-code-unique-profile.md

9.4 KiB

یکتا بودن کد ملی در پروفایل بیمار (هر کد ملی فقط یک پروفایل)

پروژه

clinicpro (Backend — منبع واحد داده و constraint).

این تغییر cross-repo اثر دارد: سایت عمومی nobat724_front و پنل ادمین React (داخل همین پروژه) فقط باید خطای 409/422 جدید را نمایش دهند. قرارداد پاسخ خطا در «نکات مهم» آمده تا کلاینت‌ها مصرف کنند. تغییر منطق فقط backend است.

زمینه

هر کاربر (User) دقیقاً یک پروفایل (UserProfile, جدول profiles, رابطه OneToOne با کلید یکتای idx_profiles_user) دارد. کد ملی هم روی User و هم روی UserProfile ذخیره می‌شود ولی در هیچ‌کدام یکتا نیست — فقط یک ایندکس غیر-یکتا روی profiles.national_code هست:

// src/UserProfile/Entity/UserProfile.php:12
#[ORM\Index(columns: ['national_code'], name: 'idx_profiles_national_code')]
// src/UserProfile/Entity/UserProfile.php:36
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
private ?string $nationalCode = null;

// src/Auth/Entity/User.php:35
#[ORM\Column(name: 'national_code', type: 'string', length: 10, nullable: true)]
private ?string $nationalCode = null;

پرامپت قبلی fix-server-side-mobile-nationalcode-validation.md فقط فرمت کد ملی (رقم کنترلی ایرانی) را سمت سرور اضافه کرد، نه یکتایی را. در نتیجه دو پروفایل مختلف می‌توانند یک کد ملی داشته باشند — چه از مسیر پروفایل عمومی، چه نوبت‌دهی توسط منشی.

مشکل / هدف

کد ملی باید در سطح بیمار یکتا باشد: نباید دو پروفایل جدا با یک کد ملی وجود داشته باشد. وقتی کاربر/منشی/هر مسیرِ نوبت‌دهی کد ملی‌ای ثبت می‌کند که قبلاً به پروفایل کاربر دیگری تعلق دارد، باید با خطای 409 رد شود (نه ذخیره).

دامنه‌ی یکتایی: UserProfile.national_code (منبع نهاییِ هویت بیمار، رابطه‌ی ۱:۱ با کاربر). User.nationalCode در حد همگام‌سازی است و قید یکتایی روی پروفایل کافی است.

فایل‌های مرتبط

فایل نقش
src/UserProfile/Entity/UserProfile.php افزودن قید یکتای partial روی national_code
src/UserProfile/Repository/UserProfileRepository.php متد findOneByNationalCode() جدید
src/UserProfile/Controller/UserProfileController.php hydrate() / update() — چک یکتایی قبل از set
src/Shared/Constant/ErrorCodes.php کد خطای جدید برای کد ملی تکراری
migrations/VersionXXXX.php (جدید) unique index روی profiles.national_code
docs/api/user-profile.md مستندسازی پاسخ 409

وضعیت فعلی (کد واقعی)

UserProfileController::hydrate — بدون چک یکتایی ذخیره می‌شود:

// src/UserProfile/Controller/UserProfileController.php:211
if (array_key_exists('national_code', $data))  $profile->setNationalCode($data['national_code']);

update() فقط فرمت را چک می‌کند (از پرامپت قبلی)، یکتایی نه:

// src/UserProfile/Controller/UserProfileController.php:134
if (array_key_exists('national_code', $data) && $data['national_code'] !== null && $data['national_code'] !== '') {
    $code = InputValidator::toEnglishDigits((string) $data['national_code']);
    if (!InputValidator::isValidIranNationalCode($code)) {
        return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'national_code');
    }
    $data['national_code'] = $code;
}

وظایف

۱. قید یکتای دیتابیس (partial — فقط مقادیر non-null)

در UserProfile.php ایندکس فعلی را به unique تبدیل کن. چون national_code nullable است و چند پروفایلِ بدون کد ملی باید مجاز بمانند، در MariaDB رفتار پیش‌فرضِ unique index این است که چندین NULL مجاز است (دقیقاً همان partial-unique موردنظر) — پس کافی است index را unique کنیم:

// به‌جای خط 12
#[ORM\UniqueConstraint(name: 'uniq_profiles_national_code', columns: ['national_code'])]

سپس migration بساز:

ddev exec php bin/console doctrine:migrations:diff --no-interaction

هشدار داده‌ی موجود: قبل از migrate، تکراری‌های فعلی را پیدا کن وگرنه migration روی ALTER TABLE می‌شکند:

SELECT national_code, COUNT(*) c FROM profiles
WHERE national_code IS NOT NULL AND national_code <> ''
GROUP BY national_code HAVING c > 1;

اگر تکراری بود، در توضیحات خروجی به کاربر گزارش بده (پاک‌سازی دستی لازم است؛ خودسرانه merge/حذف نکن).

۲. متد repository

در UserProfileRepository.php:

public function findOneByNationalCode(string $nationalCode): ?UserProfile
{
    return $this->findOneBy(['nationalCode' => $nationalCode]);
}

۳. چک یکتایی در سطح اپلیکیشن (قبل از flush)

قید DB لایه‌ی آخر است؛ برای پیام تمیز و جلوگیری از UniqueConstraintViolationException، در UserProfileController::update() بعد از validation فرمت و قبل از hydrate، یکتایی را چک کن. اگر کد ملی به پروفایلی با uuid متفاوت تعلق دارد → 409:

// داخل update()، بعد از بلوک normalize/validate فرمت (خط ~140)
if (isset($data['national_code']) && $data['national_code'] !== '') {
    $existing = $this->repository->findOneByNationalCode($data['national_code']);
    if ($existing !== null && $existing->getUuid() !== $profile->getUuid()) {
        return $this->error(
            ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
            'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
            409,
            'national_code'
        );
    }
}

۴. کد خطا

در src/Shared/Constant/ErrorCodes.php کنار سایر ERR_VALIDATION_* اضافه کن:

public const ERR_PROFILE_NATIONAL_CODE_TAKEN = 'ERR_PROFILE_001';

و پیام فارسی متناظرش را در همان نگاشتِ پیام‌ها (همان‌جا که بقیه کدها پیام دارند) قرار بده: «این کد ملی قبلاً برای کاربر دیگری ثبت شده است».

۵. مسیرهای نوبت‌دهی منشی / سایر

هر نقطه‌ای که از مسیرِ غیر-پروفایل کد ملی روی پروفایل یا کاربر می‌نشاند، باید همین چک findOneByNationalCode را قبل از set اجرا کند. بررسی کن این نقاط را و در صورت وجود set کد ملی روی UserProfile/User، چک را اضافه کن:

  • src/Patient/Controller/PatientController.php (createPatient — خطوط ~133–160، set روی User)
  • src/Staff/Controller/StaffController.php (خطوط 64, 91 — ClinicStaff.nationalCode؛ این پرسنل است نه بیمار، فقط در صورتی که staff به پروفایل بیمار map می‌شود چک لازم است — اگر نه، دست نزن)

برای مسیر منشی/پنل که از UserProfileController::update می‌گذرد، وظیفه‌ی ۳ کافی است و نیازی به تکرار نیست.

نکات مهم

  • partial-unique با NULL: عمداً NULL را آزاد می‌گذاریم تا پروفایل‌های ناقصِ بدون کد ملی (که سیستم به‌صورت lazy می‌سازد — resolveProfile، خط 193) نشکنند. هرگز رشته‌ی خالی '' ذخیره نکن؛ همیشه null. اگر جایی '' می‌نشیند، به null نرمالایز کن وگرنه دو پروفایلِ '' قید را می‌شکنند.
  • دامنه‌ی یکتایی = global (نه per-clinic). یک کد ملی در کل سیستم = یک بیمار.
  • چک اپلیکیشنی race-condition دارد (دو request همزمان)؛ قید DB لایه‌ی نهایی است. اگر خواستی، UniqueConstraintViolationException را در یک try/catch دور save() بگیر و همان 409 را برگردان تا حالت رقابتی هم پیام تمیز بدهد.
  • همه‌ی پاسخ‌ها از BaseController؛ خطا با $this->error($code, $msg, 409, 'national_code').
  • بعد از تغییر UserProfileController، طبق قانون استاندارد پروژه docs/api/user-profile.md را در همین session به‌روز کن (افزودن پاسخ 409).
  • مصرف‌کنندگان (cross-repo): سایت عمومی و پنل ادمین باید کد ERR_PROFILE_001 / status 409 را روی فیلد national_code نمایش دهند. این در همین پرامپت پیاده نمی‌شود؛ فقط قرارداد را در doc ثبت کن تا کلاینت‌ها وصل شوند.