'دکتر صفورا حجازی نیا', '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()); } 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()); } }