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:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* کلید طبیعی ایمپورت IRIMC را در سطح دیتابیس یکتا میکند.
|
||||
*
|
||||
* ایندکس قبلی (source, medical_system_code) ساده بود؛ dedup فقط application-level
|
||||
* بود و دو درخواست همزمان میتوانست رکورد تکراری بسازد. MariaDB چند NULL را در
|
||||
* ایندکس یکتا مجاز میداند، پس پزشکان manual بدون کد تحت تأثیر نیستند.
|
||||
*
|
||||
* غیرمخرب: اگر دادهٔ تکراری وجود داشته باشد migration متوقف میشود (هیچ حذفی
|
||||
* انجام نمیدهد) — تکراریها باید جداگانه و دستی تعیین تکلیف شوند.
|
||||
*/
|
||||
final class Version20260711150000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Unique index on doctors (source, medical_system_code) for concurrency-safe IRIMC import';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$dupes = $this->connection->fetchAllAssociative(<<<'SQL'
|
||||
SELECT source, medical_system_code, COUNT(*) c FROM doctors
|
||||
WHERE medical_system_code IS NOT NULL AND medical_system_code <> ''
|
||||
GROUP BY source, medical_system_code HAVING c > 1
|
||||
SQL);
|
||||
$this->abortIf(
|
||||
$dupes !== [],
|
||||
'Duplicate (source, medical_system_code) rows exist — resolve them manually before adding the unique index: '
|
||||
. json_encode($dupes, JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
$this->addSql('DROP INDEX idx_doctors_source ON doctors');
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_doctors_source_code ON doctors (source, medical_system_code)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX uniq_doctors_source_code ON doctors');
|
||||
$this->addSql('CREATE INDEX idx_doctors_source ON doctors (source, medical_system_code)');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user