feat(profile): enforce uniqueness of national_code across user profiles and update related error handling

This commit is contained in:
hamed
2026-06-24 11:52:13 +03:30
parent 3eb82ffa7d
commit 650e36bce2
9 changed files with 247 additions and 7 deletions
@@ -145,6 +145,19 @@ class PatientController extends BaseController
$patient = $this->userRepo->findByMobile($mobile);
}
// کد ملی باید در سطح بیمار یکتا باشد: اگر به پروفایلِ کاربر دیگری تعلق دارد، رد کن
if ($nationalCode !== '') {
$owner = $this->profileRepo->findOneByNationalCode($nationalCode);
if ($owner !== null && ($patient === null || $owner->getUser()->getId() !== $patient->getId())) {
return $this->error(
ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
409,
'national_code'
);
}
}
// بیمار جدید بدون ثبت‌نام قبلی: موبایل + نام آمده ولی کاربری وجود ندارد
if ($patient === null) {
if ($mobile === '' || $name === '') {
+4
View File
@@ -63,6 +63,9 @@ class ErrorCodes
public const ERR_PATIENT_NOT_FOUND = 'ERR_PATIENT_NOT_FOUND';
public const ERR_SESSION_NOT_FOUND = 'ERR_SESSION_NOT_FOUND';
// Profile
public const ERR_PROFILE_NATIONAL_CODE_TAKEN = 'ERR_PROFILE_001';
// SMS Wallet
public const ERR_SMS_WALLET_INSUFFICIENT = 'ERR_SMS_WALLET_INSUFFICIENT';
@@ -106,6 +109,7 @@ class ErrorCodes
self::ERR_SERVICE_ITEM_IN_USE => 'این سرویس در پرونده بیمار ثبت شده است',
self::ERR_SERVICE_NOT_FOUND => 'سرویس یافت نشد',
self::ERR_PATIENT_NOT_FOUND => 'پرونده بیمار یافت نشد',
self::ERR_PROFILE_NATIONAL_CODE_TAKEN => 'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
self::ERR_SESSION_NOT_FOUND => 'مراجعه یافت نشد',
self::ERR_SMS_WALLET_INSUFFICIENT => 'موجودی کیف پیامک کافی نیست',
self::ERR_RATING_NOT_ELIGIBLE => 'برای ثبت نظر یا امتیاز باید در یک ماه گذشته نوبت تایید‌شده نزد این پزشک داشته باشید',
@@ -91,6 +91,11 @@ class UserProfileController extends BaseController
}
$data = json_decode($request->getContent(), true) ?? [];
if ($error = $this->guardNationalCode($data, null)) {
return $error;
}
$profile = new UserProfile($user);
$this->hydrate($profile, $data);
$this->repository->save($profile);
@@ -131,12 +136,8 @@ class UserProfileController extends BaseController
$data = json_decode($request->getContent(), true) ?? [];
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;
if ($error = $this->guardNationalCode($data, $profile)) {
return $error;
}
$this->hydrate($profile, $data);
@@ -202,6 +203,36 @@ class UserProfileController extends BaseController
|| $currentUser->hasRole('ROLE_ADMIN');
}
/**
* Normalize, format-validate and uniqueness-check national_code in $data.
* Mutates $data['national_code'] to the latin-digit form. Returns an error
* response if invalid or already taken by another profile, otherwise null.
*/
private function guardNationalCode(array &$data, ?UserProfile $current): ?JsonResponse
{
if (!array_key_exists('national_code', $data) || $data['national_code'] === null || $data['national_code'] === '') {
return null;
}
$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;
$existing = $this->repository->findOneByNationalCode($code);
if ($existing !== null && ($current === null || $existing->getUuid() !== $current->getUuid())) {
return $this->error(
ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
409,
'national_code'
);
}
return null;
}
private function hydrate(UserProfile $profile, array $data): void
{
if (array_key_exists('name', $data)) $profile->setLabel($data['name']);
+1 -1
View File
@@ -9,7 +9,7 @@ use Symfony\Component\Uid\Uuid;
#[ORM\Entity]
#[ORM\Table(name: 'profiles')]
#[ORM\UniqueConstraint(name: 'idx_profiles_user', columns: ['user_id'])]
#[ORM\Index(columns: ['national_code'], name: 'idx_profiles_national_code')]
#[ORM\UniqueConstraint(name: 'uniq_profiles_national_code', columns: ['national_code'])]
class UserProfile
{
#[ORM\Id]
@@ -24,6 +24,11 @@ class UserProfileRepository extends ServiceEntityRepository
return $this->findOneBy(['uuid' => $uuid]);
}
public function findOneByNationalCode(string $nationalCode): ?UserProfile
{
return $this->findOneBy(['nationalCode' => $nationalCode]);
}
public function save(UserProfile $profile, bool $flush = true): void
{
$this->getEntityManager()->persist($profile);