288 lines
13 KiB
PHP
288 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Doctor;
|
|
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Location\Entity\City;
|
|
use App\Location\Entity\Province;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* Regression contract for POST /api/v1/admin/doctors/import (IRIMC import).
|
|
* Written BEFORE extracting the logic into DoctorImportService — the HTTP
|
|
* contract (routes, statuses, {uuid, created, skipped} payload) must not change.
|
|
*/
|
|
class DoctorImportTest extends ApiTestCase
|
|
{
|
|
private function importPayload(string $code): array
|
|
{
|
|
return [
|
|
'name' => 'دکتر صفورا حجازی نیا',
|
|
'medical_system_code' => $code,
|
|
'source_ref' => 'https://membersearch.irimc.org/member/profile?id=test',
|
|
'gender' => 'man',
|
|
'degree' => 'general',
|
|
'info' => 'دکترای حرفهای پزشکی',
|
|
];
|
|
}
|
|
|
|
/** db_test is never reset — randomise the natural key per run. */
|
|
private function freshCode(): string
|
|
{
|
|
return 'T' . random_int(100_000, 999_999) . random_int(100, 999);
|
|
}
|
|
|
|
public function testImportCreatesUnclaimedDoctorWithSurrogateUser(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$code = $this->freshCode();
|
|
|
|
$data = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->importPayload($code));
|
|
|
|
$this->assertSame(201, $this->responseCode());
|
|
$this->assertTrue($data['success']);
|
|
$this->assertTrue($data['data']['created']);
|
|
$this->assertNotEmpty($data['data']['uuid']);
|
|
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $data['data']['uuid']]);
|
|
$this->assertSame('unclaimed', $doctor->getOwnerStatus());
|
|
$this->assertSame('irimc', $doctor->getSource());
|
|
$this->assertTrue($doctor->isActiveDoctorAppointment());
|
|
// نام بدون پیشوند «دکتر» ذخیره میشود (UI خودش «دکتر» را جلو میگذارد)
|
|
$this->assertSame('صفورا حجازی نیا', $doctor->getName());
|
|
|
|
$surrogate = $doctor->getUser();
|
|
$this->assertStringStartsWith('imp_', $surrogate->getMobileNumber());
|
|
$this->assertSame(0, $surrogate->getStatus());
|
|
$this->assertTrue($surrogate->hasRole('ROLE_UNCLAIMED_DOCTOR'));
|
|
}
|
|
|
|
public function testReimportUpdatesInsteadOfDuplicating(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$code = $this->freshCode();
|
|
|
|
$first = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->importPayload($code));
|
|
$this->assertSame(201, $this->responseCode());
|
|
|
|
$payload = $this->importPayload($code);
|
|
$payload['name'] = 'دکتر تست ویرایششده';
|
|
$second = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $payload);
|
|
|
|
$this->assertSame(200, $this->responseCode());
|
|
$this->assertFalse($second['data']['created']);
|
|
$this->assertSame($first['data']['uuid'], $second['data']['uuid']);
|
|
|
|
$count = $this->em->getRepository(Doctor::class)->count(['source' => 'irimc', 'medicalSystemCode' => $code]);
|
|
$this->assertSame(1, $count);
|
|
|
|
$this->em->clear();
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $first['data']['uuid']]);
|
|
// نیمفاصله در normalize به فاصله تبدیل میشود
|
|
$this->assertSame('تست ویرایش شده', $doctor->getName());
|
|
}
|
|
|
|
public function testClaimedDoctorIsNeverOverwritten(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$code = $this->freshCode();
|
|
|
|
$created = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->importPayload($code));
|
|
$uuid = $created['data']['uuid'];
|
|
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]);
|
|
$owner = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
|
$doctor->transferOwnershipTo($owner);
|
|
$this->em->flush();
|
|
$originalName = $doctor->getName();
|
|
|
|
$payload = $this->importPayload($code);
|
|
$payload['name'] = 'دکتر بازنویسی ممنوع';
|
|
$reimport = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $payload);
|
|
|
|
$this->assertSame(200, $this->responseCode());
|
|
$this->assertSame('claimed', $reimport['data']['skipped']);
|
|
|
|
$this->em->clear();
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]);
|
|
$this->assertSame($originalName, $doctor->getName());
|
|
$this->assertSame('claimed', $doctor->getOwnerStatus());
|
|
}
|
|
|
|
public function testImportCreatesDefaultOfficeAddressWithCityAndProvince(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$code = $this->freshCode();
|
|
|
|
$province = new Province('استان تست ' . $code);
|
|
$city = new City('شهر تست ' . $code, $province);
|
|
$this->em->persist($province);
|
|
$this->em->persist($city);
|
|
$this->em->flush();
|
|
|
|
$payload = $this->importPayload($code);
|
|
$payload['states'] = [$province->getId()];
|
|
$payload['cities'] = [$city->getId()];
|
|
|
|
$data = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $payload);
|
|
$this->assertSame(201, $this->responseCode());
|
|
|
|
$cityId = $city->getId();
|
|
$provinceId = $province->getId();
|
|
$this->em->clear();
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $data['data']['uuid']]);
|
|
$this->assertCount(1, $doctor->getAddresses());
|
|
$address = $doctor->getAddresses()->first();
|
|
$this->assertSame('مطب دکتر صفورا حجازی نیا', $address->getName());
|
|
$this->assertSame($cityId, $address->getCity()?->getId());
|
|
$this->assertSame($provinceId, $address->getProvince()?->getId());
|
|
|
|
// ایمپورت مجدد نباید آدرس تکراری بسازد یا نام آدرس را بازنویسی کند
|
|
$address->setName('مطب ویرایششده توسط کاربر');
|
|
$this->em->flush();
|
|
|
|
$this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $payload);
|
|
$this->assertSame(200, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $data['data']['uuid']]);
|
|
$this->assertCount(1, $doctor->getAddresses());
|
|
$this->assertSame('مطب ویرایششده توسط کاربر', $doctor->getAddresses()->first()->getName());
|
|
}
|
|
|
|
public function testImportWithoutLocationStillCreatesOfficeAddress(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$code = $this->freshCode();
|
|
|
|
$data = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->importPayload($code));
|
|
$this->assertSame(201, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $data['data']['uuid']]);
|
|
$this->assertCount(1, $doctor->getAddresses());
|
|
$this->assertSame('مطب دکتر صفورا حجازی نیا', $doctor->getAddresses()->first()->getName());
|
|
}
|
|
|
|
public function testManualDoctorWithSameCodeIsNeverDuplicated(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$code = $this->freshCode();
|
|
|
|
$owner = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
|
$manual = new Doctor($owner, 'پزشک دستی');
|
|
$manual->setMedicalSystemCode($code);
|
|
$this->em->persist($manual);
|
|
$this->em->flush();
|
|
|
|
$data = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->importPayload($code));
|
|
|
|
$this->assertSame(200, $this->responseCode());
|
|
$this->assertFalse($data['data']['created']);
|
|
$this->assertSame('duplicate', $data['data']['skipped']);
|
|
$this->assertSame($manual->getUuid(), $data['data']['uuid']);
|
|
|
|
$this->em->clear();
|
|
$count = $this->em->getRepository(Doctor::class)->count(['medicalSystemCode' => $code]);
|
|
$this->assertSame(1, $count);
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['medicalSystemCode' => $code]);
|
|
$this->assertSame('پزشک دستی', $doctor->getName());
|
|
$this->assertSame('manual', $doctor->getSource());
|
|
}
|
|
|
|
/** UUID تازه برای هر اجرا — یونیکایندکس (source, source_profile_id) تکرار را رد میکند. */
|
|
private function freshProfileId(): string
|
|
{
|
|
return bin2hex(random_bytes(16)); // ۳۲ نویسهٔ hex، منطبق با استخراجکنندهٔ profile id
|
|
}
|
|
|
|
private function payloadWithProfile(string $code, string $profileId): array
|
|
{
|
|
$p = $this->importPayload($code);
|
|
$p['source_ref'] = "https://membersearch.irimc.org/member/profile?id=$profileId";
|
|
return $p;
|
|
}
|
|
|
|
public function testSameProfileIdWithDifferentCodeUpdatesInsteadOfDuplicating(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$profileId = $this->freshProfileId();
|
|
$code1 = $this->freshCode();
|
|
$code2 = $this->freshCode();
|
|
|
|
$first = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->payloadWithProfile($code1, $profileId));
|
|
$this->assertSame(201, $this->responseCode());
|
|
|
|
// همان پروفایل irimc، ولی کد نظام پزشکی متفاوت (نمایش دیگرِ همان پزشک)
|
|
$second = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->payloadWithProfile($code2, $profileId));
|
|
$this->assertSame(200, $this->responseCode());
|
|
$this->assertFalse($second['data']['created']);
|
|
$this->assertSame($first['data']['uuid'], $second['data']['uuid']);
|
|
|
|
$this->em->clear();
|
|
$count = $this->em->getRepository(Doctor::class)->count(['source' => 'irimc', 'sourceProfileId' => $profileId]);
|
|
$this->assertSame(1, $count, 'یک پروفایل نباید دو رکورد بسازد حتی با کد متفاوت');
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['sourceProfileId' => $profileId]);
|
|
$this->assertSame($code2, $doctor->getMedicalSystemCode(), 'کد باید به آخرین مقدار بهروزرسانی شود');
|
|
}
|
|
|
|
public function testDifferentProfileIdsAreKeptSeparate(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
|
|
// همان نام، ولی دو پروفایل متمایز irimc → دو رکورد، بدون ادغام خودکار
|
|
$a = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->payloadWithProfile($this->freshCode(), $this->freshProfileId()));
|
|
$this->assertSame(201, $this->responseCode());
|
|
$b = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->payloadWithProfile($this->freshCode(), $this->freshProfileId()));
|
|
$this->assertSame(201, $this->responseCode());
|
|
|
|
$this->assertTrue($b['data']['created']);
|
|
$this->assertNotSame($a['data']['uuid'], $b['data']['uuid']);
|
|
}
|
|
|
|
public function testProfileIdIsBackfilledFromSourceRefOnImport(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$profileId = $this->freshProfileId();
|
|
|
|
$data = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, $this->payloadWithProfile($this->freshCode(), $profileId));
|
|
$this->assertSame(201, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $data['data']['uuid']]);
|
|
$this->assertSame($profileId, $doctor->getSourceProfileId());
|
|
}
|
|
|
|
public function testValidationErrors(): void
|
|
{
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
|
|
$this->authJson('POST', '/api/v1/admin/doctors/import', $admin, ['medical_system_code' => $this->freshCode()]);
|
|
$this->assertSame(422, $this->responseCode());
|
|
|
|
$this->authJson('POST', '/api/v1/admin/doctors/import', $admin, ['name' => 'دکتر بیکد']);
|
|
$this->assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testNonAdminIsRejected(): void
|
|
{
|
|
$user = $this->createUser(['ROLE_USER']);
|
|
$this->authJson('POST', '/api/v1/admin/doctors/import', $user, $this->importPayload($this->freshCode()));
|
|
$this->assertSame(403, $this->responseCode());
|
|
}
|
|
|
|
public function testImporterRoleCanImportButNothingElse(): void
|
|
{
|
|
$importer = $this->createUser(['ROLE_USER', 'ROLE_IMPORTER']);
|
|
|
|
$this->authJson('POST', '/api/v1/admin/doctors/import', $importer, $this->importPayload($this->freshCode()));
|
|
$this->assertSame(201, $this->responseCode(), 'ROLE_IMPORTER must be able to import');
|
|
|
|
$this->authJson('GET', '/api/v1/admin/users', $importer);
|
|
$this->assertSame(403, $this->responseCode(), 'ROLE_IMPORTER must NOT reach other admin endpoints');
|
|
|
|
$this->authJson('GET', '/api/v1/admin/doctor-claims', $importer);
|
|
$this->assertSame(403, $this->responseCode());
|
|
}
|
|
}
|