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:
@@ -17,7 +17,9 @@ use Symfony\Component\Uid\Uuid;
|
||||
#[ORM\Entity(repositoryClass: DoctorRepository::class)]
|
||||
#[ORM\Table(name: 'doctors')]
|
||||
#[ORM\UniqueConstraint(name: 'idx_doctors_user', columns: ['user_id'])]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_doctors_source_code', columns: ['source', 'medical_system_code'])]
|
||||
#[ORM\Index(columns: ['active_doctor_appointment'], name: 'idx_doctors_active')]
|
||||
#[ORM\Index(columns: ['owner_status'], name: 'idx_doctors_owner')]
|
||||
class Doctor
|
||||
{
|
||||
public const DEGREES = ['expert', 'general', 'specialist', 'subspecialistplus'];
|
||||
@@ -79,11 +81,11 @@ class Doctor
|
||||
|
||||
// ── Profile ownership (IRIMC import) ───────────────────────────────────────
|
||||
// owner_status: claimed | unclaimed | pending_transfer
|
||||
#[ORM\Column(name: 'owner_status', type: 'string', length: 20)]
|
||||
#[ORM\Column(name: 'owner_status', type: 'string', length: 20, options: ['default' => 'claimed'])]
|
||||
private string $ownerStatus = 'claimed';
|
||||
|
||||
// source: manual | irimc
|
||||
#[ORM\Column(type: 'string', length: 20)]
|
||||
#[ORM\Column(type: 'string', length: 20, options: ['default' => 'manual'])]
|
||||
private string $source = 'manual';
|
||||
|
||||
// شناسه رکورد مبدأ (profile_url یا کد نظام پزشکی) برای idempotency و ممیزی
|
||||
@@ -521,6 +523,7 @@ class Doctor
|
||||
'free_turn' => $sf['free_turn'],
|
||||
'hours_of_work' => $sf['hours_of_work'],
|
||||
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
||||
'owner_status' => $this->ownerStatus,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -553,6 +556,7 @@ class Doctor
|
||||
], $this->services->toArray()),
|
||||
'satisfaction' => (string) $this->doctorRatePercentage,
|
||||
'point' => (string) $this->doctorRate,
|
||||
'owner_status' => $this->ownerStatus,
|
||||
'free_turn' => $sf['free_turn'],
|
||||
'hours_of_work' => $sf['hours_of_work'],
|
||||
'address' => array_map(fn(DoctorAddress $a) => $a->toArray(), $this->addresses->toArray()),
|
||||
|
||||
Reference in New Issue
Block a user