From 8b419d0272d676ac665e1e97871d657aa57de207 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Wed, 24 Jun 2026 12:16:16 +0330 Subject: [PATCH] feat(profile): enhance national_code uniqueness error message with masked mobile number --- docs/api/patient.md | 2 +- docs/api/user-profile.md | 2 +- src/Patient/Controller/PatientController.php | 3 +- src/Shared/Service/InputValidator.php | 30 +++++++++---------- .../Controller/UserProfileController.php | 3 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/api/patient.md b/docs/api/patient.md index 4d40bc9d..016adef5 100644 --- a/docs/api/patient.md +++ b/docs/api/patient.md @@ -86,7 +86,7 @@ Creates a patient record for a user under the current entity. If the record alre - اگر `user_uuid` و `mobile` هر دو خالی باشند → خطا. - `national_code` فقط وقتی روی کاربر ست می‌شود که کاربر کد ملی نداشته باشد. - موبایل تکراری duplicate نمی‌سازد؛ همان کاربر استفاده می‌شود. -- **یکتایی کد ملی:** اگر `national_code` ارسالی قبلاً به پروفایل کاربر دیگری تعلق داشته باشد → `409` با کد `ERR_PROFILE_001` (`field: national_code`). یک کد ملی = یک بیمار در کل سیستم (هم‌راستا با قید یکتای `profiles.national_code`). +- **یکتایی کد ملی:** اگر `national_code` ارسالی قبلاً به پروفایل کاربر دیگری تعلق داشته باشد → `409` با کد `ERR_PROFILE_001` (`field: national_code`). پیام خطا شامل شماره موبایلِ ماسک‌شده‌ی صاحب کد است (مثلاً «این کد ملی قبلاً با شماره 0912****56 ثبت شده است»). یک کد ملی = یک بیمار در کل سیستم (هم‌راستا با قید یکتای `profiles.national_code`). **Response 201:** diff --git a/docs/api/user-profile.md b/docs/api/user-profile.md index 56a5ba61..e7db854c 100644 --- a/docs/api/user-profile.md +++ b/docs/api/user-profile.md @@ -129,7 +129,7 @@ Same fields as POST — all optional. > **`national_code` server-side validation:** اگر `national_code` ارسال شود و **غیرخالی** باشد، با الگوریتم رقم کنترلیِ کد ملی ایران اعتبارسنجی می‌شود (ارقام فارسی/عربی به انگلیسی نرمال و به‌صورت لاتین ذخیره می‌شوند). مقدارِ نامعتبر با `422` رد می‌شود. ارسال `null` یا رشته‌ی خالی مجاز است (کد ملی اختیاری) و فیلد را پاک می‌کند. > -> **یکتایی کد ملی:** کد ملی در کل سیستم یکتاست (یک کد ملی = یک بیمار). اگر کد ملی ارسالی قبلاً به پروفایل **کاربر دیگری** تعلق داشته باشد، با `409` و کد `ERR_PROFILE_001` رد می‌شود (`field: national_code`). در سطح دیتابیس هم با `UNIQUE INDEX uniq_profiles_national_code` تضمین شده (مقادیر `NULL` آزادند). همین قید روی مسیر ساخت بیمار توسط منشی (`POST /api/v1/patient`) نیز اعمال می‌شود. +> **یکتایی کد ملی:** کد ملی در کل سیستم یکتاست (یک کد ملی = یک بیمار). اگر کد ملی ارسالی قبلاً به پروفایل **کاربر دیگری** تعلق داشته باشد، با `409` و کد `ERR_PROFILE_001` رد می‌شود (`field: national_code`). پیام خطا شامل **شماره موبایلِ ماسک‌شده‌ی** صاحب کد است (مثلاً «این کد ملی قبلاً با شماره 0912****56 ثبت شده است»). در سطح دیتابیس هم با `UNIQUE INDEX uniq_profiles_national_code` تضمین شده (مقادیر `NULL` آزادند). همین قید روی مسیر ساخت بیمار توسط منشی (`POST /api/v1/patient`) نیز اعمال می‌شود. ### Response `200` Updated profile object. diff --git a/src/Patient/Controller/PatientController.php b/src/Patient/Controller/PatientController.php index b5ce2737..b46caed1 100644 --- a/src/Patient/Controller/PatientController.php +++ b/src/Patient/Controller/PatientController.php @@ -149,9 +149,10 @@ class PatientController extends BaseController if ($nationalCode !== '') { $owner = $this->profileRepo->findOneByNationalCode($nationalCode); if ($owner !== null && ($patient === null || $owner->getUser()->getId() !== $patient->getId())) { + $masked = \App\Shared\Service\InputValidator::maskMobile($owner->getUser()->getMobileNumber()); return $this->error( ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN, - 'این کد ملی قبلاً برای کاربر دیگری ثبت شده است', + "این کد ملی قبلاً با شماره {$masked} ثبت شده است", 409, 'national_code' ); diff --git a/src/Shared/Service/InputValidator.php b/src/Shared/Service/InputValidator.php index 19b032d1..9599faa2 100644 --- a/src/Shared/Service/InputValidator.php +++ b/src/Shared/Service/InputValidator.php @@ -13,25 +13,25 @@ final class InputValidator return (bool) preg_match('/^09\d{9}$/', self::toEnglishDigits($mobile)); } - /** کد ملی ایران: ۱۰ رقم + الگوریتم رقم کنترلی؛ ارقام یکسان نامعتبر. */ + /** کد ملی: فقط باید دقیقاً ۱۰ رقم باشد (بدون بررسی رقم کنترلی). */ public static function isValidIranNationalCode(string $code): bool { - $code = self::toEnglishDigits($code); - if (!preg_match('/^\d{10}$/', $code)) { - return false; - } - if (preg_match('/^(\d)\1{9}$/', $code)) { - return false; - } + return (bool) preg_match('/^\d{10}$/', self::toEnglishDigits($code)); + } - $check = (int) $code[9]; - $sum = 0; - for ($i = 0; $i < 9; $i++) { - $sum += (int) $code[$i] * (10 - $i); + /** + * موبایل را برای نمایش ماسک می‌کند: 09120675433 → 0912****33 (۴ رقم اول + ۲ رقم آخر). + * نتیجه با Unicode LTR-isolate (U+2066…U+2069) محصور می‌شود تا داخلِ متنِ فارسیِ RTL + * جهتِ ارقام/ستاره به‌هم نریزد. + */ + public static function maskMobile(string $mobile): string + { + $mobile = self::toEnglishDigits($mobile); + if (strlen($mobile) < 6) { + return $mobile; } - $r = $sum % 11; - - return $r < 2 ? $check === $r : $check === 11 - $r; + $masked = substr($mobile, 0, 4) . '****' . substr($mobile, -2); + return "\u{2066}{$masked}\u{2069}"; } public static function toEnglishDigits(string $s): string diff --git a/src/UserProfile/Controller/UserProfileController.php b/src/UserProfile/Controller/UserProfileController.php index 4f6a1024..2e29eb14 100644 --- a/src/UserProfile/Controller/UserProfileController.php +++ b/src/UserProfile/Controller/UserProfileController.php @@ -222,9 +222,10 @@ class UserProfileController extends BaseController $existing = $this->repository->findOneByNationalCode($code); if ($existing !== null && ($current === null || $existing->getUuid() !== $current->getUuid())) { + $masked = InputValidator::maskMobile($existing->getUser()->getMobileNumber()); return $this->error( ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN, - 'این کد ملی قبلاً برای کاربر دیگری ثبت شده است', + "این کد ملی قبلاً با شماره {$masked} ثبت شده است", 409, 'national_code' );