feat(doctor): complete IRIMC import feature — claim flow, least-privilege importer, unique import key
- Extract import logic from AdminApiController into DoctorImportService (thin DoctorImportController keeps the same route/contract) - Surrogate users get marker role ROLE_UNCLAIMED_DOCTOR (+ backfill command app:doctors:backfill-surrogate-role) enabling safe deletion after claim - DB-level UNIQUE (source, medical_system_code) + concurrent-import retry - Doctor profile claim flow (climed.md): shahkar + PersonInfo identity checks via existing ApiIrService, Persian name normalization (PersianText), pessimistic-lock race protection, DoctorClaimRequest audit table (national code hashed, mobile masked), doctor_claim rate limiter, public claim-info endpoint, welcome SMS - Admin support tools: manual transfer endpoint + paginated doctor-claims audit list + owner_status filter/fields in admin doctors list - Least privilege: system owner now gets ROLE_IMPORTER (ROLE_ADMIN stripped), import endpoint accepts ADMIN|IMPORTER, isStaff includes IMPORTER - Headless crawler login: X-Service-Token header bypasses captcha only (rate limit + password checks intact; empty env = no bypass) - docs: doctor-claim.md (new), doctor-import.md, admin.md, doctor.md - tests: DoctorImportTest (6), DoctorClaimTest (11), PersianTextTest (5) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,31 @@ class ApiIrService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* استعلام هویت شخص از روی کد ملی و تاریخ تولد (PersonInfo).
|
||||
*
|
||||
* @param string $birthDateJalali تاریخ تولد شمسی به فرمت Y/m/d (مثلاً 1371/1/1)
|
||||
* @return array{firstName: string, lastName: string, alive: bool}|null null یعنی رکوردی مطابقت نکرد.
|
||||
*/
|
||||
public function personInfo(string $nationalCode, string $birthDateJalali): ?array
|
||||
{
|
||||
$data = $this->post('/api/sw1/PersonInfo', [
|
||||
'nationalCode' => $nationalCode,
|
||||
'birthDate' => $birthDateJalali,
|
||||
]);
|
||||
|
||||
$person = $data['data'] ?? null;
|
||||
if (!is_array($person) || ($person['nationalCode'] ?? '') === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'firstName' => (string) ($person['firstName'] ?? ''),
|
||||
'lastName' => (string) ($person['lastName'] ?? ''),
|
||||
'alive' => filter_var($person['alive'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,mixed>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Util;
|
||||
|
||||
/**
|
||||
* نرمالسازی متن فارسی برای مقایسهٔ نامها.
|
||||
*
|
||||
* تفاوتهای رایج را یکدست میکند: ي/ی عربی-فارسی، ك/ک، ارقام عربی/فارسی،
|
||||
* نیمفاصله و فاصلههای تکراری، فاصلهٔ ابتدا/انتها و Unicode normalization —
|
||||
* تا مقایسهٔ نام هرگز با compare خام رشته انجام نشود.
|
||||
*/
|
||||
final class PersianText
|
||||
{
|
||||
public static function normalize(string $text): string
|
||||
{
|
||||
if (class_exists(\Normalizer::class)) {
|
||||
$text = \Normalizer::normalize($text, \Normalizer::FORM_KC) ?: $text;
|
||||
}
|
||||
|
||||
$text = strtr($text, [
|
||||
"\u{064A}" => 'ی', // ي عربی
|
||||
"\u{0649}" => 'ی', // ى الف مقصوره
|
||||
"\u{0643}" => 'ک', // ك عربی
|
||||
"\u{200C}" => ' ', // نیمفاصله → فاصله
|
||||
"\u{200B}" => '', // zero-width space
|
||||
"\u{FEFF}" => '', // BOM
|
||||
"\u{0640}" => '', // کشیده ـ
|
||||
]);
|
||||
|
||||
// ارقام فارسی/عربی → لاتین
|
||||
$text = strtr($text, array_combine(
|
||||
['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹','٠','١','٢','٣','٤','٥','٦','٧','٨','٩'],
|
||||
['0','1','2','3','4','5','6','7','8','9','0','1','2','3','4','5','6','7','8','9'],
|
||||
));
|
||||
|
||||
return trim(preg_replace('/\s+/u', ' ', $text) ?? $text);
|
||||
}
|
||||
|
||||
/** مقایسهٔ دو نام فارسی پس از نرمالسازی. */
|
||||
public static function sameName(string $a, string $b): bool
|
||||
{
|
||||
return self::normalize($a) === self::normalize($b);
|
||||
}
|
||||
|
||||
/** حذف عنوان «دکتر» از ابتدای نام (برای مقایسهٔ نام پروفایل با نام ثبت احوال). */
|
||||
public static function stripDoctorTitle(string $name): string
|
||||
{
|
||||
return trim(preg_replace('/^\s*دکتر\s+/u', '', self::normalize($name)) ?? $name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user