Files
clinicpro/.claude/prompt/fix-server-side-mobile-nationalcode-validation.md
T

141 lines
8.5 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.
# رفع باگ: اعتبارسنجی سمت سرورِ موبایل و کد ملی (BUG-2 و BUG-3)
## پروژه
`clinicpro` (Backend). مرجع: `docs/qa/bug-report-2026-06-20.md`.
## زمینه
ممیزی نشان داد اعتبارسنجی **موبایل** و **کد ملی** فقط در فرانت انجام می‌شود و سمت سرور غایب است. در نتیجه:
- **BUG-3 (HIGH):** `POST /api/v1/representation` با `mobile_number:"not-a-mobile"``201` و یک نماینده/کاربر آشغال ساخت (فقط `empty()` چک می‌شود، نه فرمت).
- **BUG-2 (MEDIUM):** `PATCH /api/v1/user-profile/{uuid}` با `national_code:"1111111111"` (رقم کنترلیِ غلط) → `200` و ذخیره شد (هیچ validationی نیست).
regex موبایل از قبل به‌صورت inline در دو جا هست (`AuthController:143` و `NotificationMobileController:44`: `^09\d{9}$`) ولی منطق پخش/تکراری است و در نقاط ساختِ کاربر استفاده نشده. هیچ ولیدیتورِ کد ملیِ سمت سروری وجود ندارد.
## مشکل / هدف
یک سرویس اعتبارسنجی مشترک سمت سرور بساز (`InputValidator`) و آن را در همه‌ی نقاطِ ورودیِ موبایل/کد ملی اعمال کن تا داده‌ی نامعتبر با `422` رد شود (نه ذخیره/ساخت).
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Shared/Service/InputValidator.php` (جدید) | اعتبارسنجی مشترک موبایل + کد ملی |
| `src/Representation/Controller/RepresentationController.php` | `create` — BUG-3، موبایل بدون فرمت |
| `src/Representation/Controller/RepresentationActionController.php` | `createDoctor` (`mobile``createClinic` (`owner_mobile`) — همان خلأ |
| `src/Admin/Controller/AdminApiController.php` | `createDoctor` (`mobile``createClinic` (`owner_mobile`) — همان خلأ |
| `src/UserProfile/Controller/UserProfileController.php` | `hydrate()` — BUG-2، کد ملی بدون validation |
| `src/Shared/Constant/ErrorCodes.php` | `ERR_VALIDATION_001` (موجود) |
| `docs/api/representation.md`, `docs/api/admin.md`, `docs/api/user-profile.md` | مستندسازی پاسخ ۴۲۲ |
## وضعیت فعلی (کد واقعی)
`RepresentationController::create` — فقط empty چک می‌شود:
```php
$mobile = trim($data['mobile_number'] ?? '');
$fullName = trim($data['full_name'] ?? '');
if (empty($mobile) || empty($fullName)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'mobile_number و full_name الزامی است', 422);
}
$user = $this->userRepo->findByMobile($mobile);
if ($user === null) { $user = new User($mobile); ... } // ← موبایل نامعتبر هم کاربر می‌سازد
```
`UserProfileController::hydrate` — کد ملی بدون validation ذخیره می‌شود:
```php
if (array_key_exists('national_code', $data)) $profile->setNationalCode($data['national_code']);
```
regex موبایلِ موجود (برای الگو):
```php
// AuthController:143 و NotificationMobileController:44
if (!preg_match('/^09\d{9}$/', $mobile)) { ... 422 ... }
```
## وظایف
### ۱. سرویس مشترک `InputValidator`
`src/Shared/Service/InputValidator.php` بساز با متدهای static (یا سرویسِ بدون state):
```php
namespace App\Shared\Service;
final class InputValidator
{
public static function isValidIranMobile(string $mobile): bool
{
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;
$check = (int) $code[9];
$sum = 0;
for ($i = 0; $i < 9; $i++) $sum += (int) $code[$i] * (10 - $i);
$r = $sum % 11;
return $r < 2 ? $check === $r : $check === 11 - $r;
}
public static function toEnglishDigits(string $s): string
{
return strtr($s,
['۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
'٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9']);
}
}
```
> این دقیقاً معادلِ سرورِ `isValidIranNationalCode`/`sanitizeMobileInput` فرانت است (تا رفتار یکدست شود).
### ۲. اعمال در ساخت نماینده (BUG-3)
در `RepresentationController::create`، بعد از چک empty و قبل از ساخت `User`:
```php
if (!InputValidator::isValidIranMobile($mobile)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422);
}
```
- `$mobile` را به ارقام انگلیسی نرمال هم بکن (`InputValidator::toEnglishDigits`) قبل از `findByMobile`/`new User` تا «۰۹...» فارسی هم درست ذخیره شود.
### ۳. اعمال در سایر نقاطِ ساختِ کاربر از موبایل
همان چک را اضافه کن به:
- `RepresentationActionController::createDoctor` (فیلد `mobile`) و `createClinic` (فیلد `owner_mobile`).
- `AdminApiController::createDoctor` (`mobile`) و `createClinic` (`owner_mobile`).
هرجا `new User($mobile)` ساخته می‌شود، قبلش موبایل باید معتبر باشد؛ در غیر این صورت `422`.
### ۴. اعمال در کد ملی (BUG-2)
در `UserProfileController` (متد `update` یا `hydrate`)، اگر `national_code` در ورودی هست و **غیرخالی** است، اعتبارسنجی کن:
```php
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; // ذخیره‌ی نرمال‌شده
}
```
> اجازه‌ی خالی/`null` بماند (کد ملی اختیاری است)؛ فقط مقدارِ **واردشده‌ی نامعتبر** رد شود. این چک باید **قبل از** `hydrate()` و در همان action باشد (چون `hydrate` خروجی JsonResponse ندارد).
### ۵. مستندسازی
- `docs/api/representation.md` و `docs/api/admin.md`: در createDoctor/createClinic/representation، افزودن پاسخ `422 ERR_VALIDATION_001` برای موبایل نامعتبر.
- `docs/api/user-profile.md`: افزودن `422 ERR_VALIDATION_001` برای کد ملی نامعتبر در PATCH.
## نکات مهم
- **یک‌بار تعریف، همه‌جا استفاده:** منطق موبایل/کد ملی فقط در `InputValidator` باشد؛ regexهای inline موجود (`AuthController:143`, `NotificationMobileController:44`) را هم می‌توان به این سرویس واگذار کرد (اختیاری، اگر ریسک کم بود) ولی **رفتار `send-code` را تغییر نده** مگر مطمئن باشی.
- نرمال‌سازی ارقام فارسی/عربی به انگلیسی **قبل از ذخیره** انجام شود تا موبایل/کد ملیِ ذخیره‌شده همیشه لاتین باشد.
- کد ملی **اختیاری** است؛ خالی/null نباید ۴۲۲ بدهد — فقط مقدارِ نامعتبر.
- پاسخ‌ها از `BaseController` (`error(code, msg, 422, field?)`)؛ تاریخ‌ها دست نخورد؛ migration لازم نیست (فقط منطق).
- بعد از تغییر، `cache:clear --env=prod` و تست:
- `POST /api/v1/representation` با `mobile_number:"not-a-mobile"``422` (قبلاً ۲۰۱).
- با موبایل معتبر `09123456789``201` و در DB لاتین ذخیره شود.
- `PATCH /api/v1/user-profile/{own}` با `national_code:"1111111111"``422`؛ با کد معتبر (مثل `0079070875`) → `200`؛ با `null``200` (پاک شود).
- رگرسیون: `createDoctor`/`createClinic` ادمین و نماینده با موبایل معتبر همچنان کار کنند.
- بعد از تست، رکوردهای تستی را پاک کن (هیچ نماینده/کاربر/پروفایلِ آشغال نماند).