feat: add mobile number change functionality for doctors and clinics
- Implemented PATCH endpoints for changing the login mobile number of doctors and clinics. - Added ChangeLoginMobileModal component for handling mobile number updates in the UI. - Updated ClinicsPage and DoctorsPage to include buttons for changing mobile numbers. - Enhanced AdminApiController to manage mobile number changes with validation. - Created tests to ensure proper functionality and validation for mobile number changes. - Updated API documentation to reflect new endpoints and their usage.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Admin;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* مدیر کل میتواند شمارهٔ **ورود** پزشک/کلینیک را عوض کند. شماره هویتِ ورود است، پس
|
||||
* یکتا بودنش کنترل میشود و شمارهٔ نمایشیِ پزشک هم با آن همگام میماند.
|
||||
*/
|
||||
class AdminChangeLoginMobileTest extends ApiTestCase
|
||||
{
|
||||
private function newMobile(): string
|
||||
{
|
||||
return '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function makeDoctor(): Doctor
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($user, 'دکتر تغییر شماره');
|
||||
$doctor->setMobileNumber($user->getMobileNumber());
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
return $doctor;
|
||||
}
|
||||
|
||||
private function makeClinic(): Clinic
|
||||
{
|
||||
$clinic = new Clinic($this->createUser(['ROLE_USER', 'ROLE_CLINIC']));
|
||||
$clinic->setName('کلینیک تغییر شماره');
|
||||
$this->em->persist($clinic);
|
||||
$this->em->flush();
|
||||
|
||||
return $clinic;
|
||||
}
|
||||
|
||||
private function reloadUser(int $id): User
|
||||
{
|
||||
$this->em->clear();
|
||||
|
||||
return $this->em->getRepository(User::class)->find($id);
|
||||
}
|
||||
|
||||
public function testAdminChangesDoctorLoginMobile(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$doctor = $this->makeDoctor();
|
||||
$userId = $doctor->getUser()->getId();
|
||||
$mobile = $this->newMobile();
|
||||
|
||||
$body = $this->authJson('PATCH', '/api/v1/admin/doctors/' . $doctor->getUuid() . '/mobile', $admin, [
|
||||
'mobile_number' => $mobile,
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame($mobile, $body['data']['data']['mobile_number']);
|
||||
self::assertSame($mobile, $this->reloadUser($userId)->getMobileNumber());
|
||||
}
|
||||
|
||||
/** شمارهٔ نمایشیِ پزشک وقتی با شمارهٔ ورود یکی بوده باید همراهش بهروز شود. */
|
||||
public function testDoctorDisplayMobileFollowsTheLoginMobile(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$doctor = $this->makeDoctor();
|
||||
$id = $doctor->getId();
|
||||
$mobile = $this->newMobile();
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/doctors/' . $doctor->getUuid() . '/mobile', $admin, [
|
||||
'mobile_number' => $mobile,
|
||||
]);
|
||||
$this->em->clear();
|
||||
|
||||
self::assertSame($mobile, $this->em->getRepository(Doctor::class)->find($id)->getMobileNumber());
|
||||
}
|
||||
|
||||
public function testAdminChangesClinicLoginMobile(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$clinic = $this->makeClinic();
|
||||
$userId = $clinic->getUser()->getId();
|
||||
$mobile = $this->newMobile();
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/clinic/' . $clinic->getUuid() . '/mobile', $admin, [
|
||||
'mobile_number' => $mobile,
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame($mobile, $this->reloadUser($userId)->getMobileNumber());
|
||||
}
|
||||
|
||||
public function testDuplicateMobileIsRejected(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$doctor = $this->makeDoctor();
|
||||
$taken = $this->createUser(['ROLE_USER'])->getMobileNumber();
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/doctors/' . $doctor->getUuid() . '/mobile', $admin, [
|
||||
'mobile_number' => $taken,
|
||||
]);
|
||||
|
||||
self::assertSame(409, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testInvalidMobileIsRejected(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$doctor = $this->makeDoctor();
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/doctors/' . $doctor->getUuid() . '/mobile', $admin, [
|
||||
'mobile_number' => '12345',
|
||||
]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testPersianDigitsAreAccepted(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$doctor = $this->makeDoctor();
|
||||
$userId = $doctor->getUser()->getId();
|
||||
|
||||
// db_test بین اجراها پاک نمیشود؛ شمارهٔ ثابت در اجرای دوم تکراری میشد.
|
||||
$latin = $this->newMobile();
|
||||
$persian = strtr($latin, ['0' => '۰', '1' => '۱', '2' => '۲', '3' => '۳', '4' => '۴',
|
||||
'5' => '۵', '6' => '۶', '7' => '۷', '8' => '۸', '9' => '۹']);
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/doctors/' . $doctor->getUuid() . '/mobile', $admin, [
|
||||
'mobile_number' => $persian,
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame($latin, $this->reloadUser($userId)->getMobileNumber());
|
||||
}
|
||||
|
||||
public function testNonAdminCannotChangeIt(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor();
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/doctors/' . $doctor->getUuid() . '/mobile', $doctor->getUser(), [
|
||||
'mobile_number' => $this->newMobile(),
|
||||
]);
|
||||
|
||||
self::assertSame(403, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testUnknownDoctorIsNotFound(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
|
||||
$this->authJson('PATCH', '/api/v1/admin/doctors/00000000-0000-4000-8000-000000000000/mobile', $admin, [
|
||||
'mobile_number' => $this->newMobile(),
|
||||
]);
|
||||
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,9 @@ use App\Payment\Entity\Payment;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* Paying for an online booking must file the case file, same as any other way
|
||||
* of confirming.
|
||||
*
|
||||
* This is the path every Nobat724 booking takes, and it was the one path that
|
||||
* never created a record: the payment callback confirmed the appointment
|
||||
* without running the confirmation side-effects.
|
||||
* پرداخت آنلاین نوبت را **قطعی نمیکند**: نوبت در «ثبت شده» میماند تا پزشک/منشی
|
||||
* تأییدش کند. پرداخت فقط پنجرهٔ انقضای درگاه را برمیدارد تا نوبتِ پرداختشده منقضی
|
||||
* نشود. پرونده/مراجعه و تقسیم مالی در لحظهٔ تأیید ساخته میشوند.
|
||||
*/
|
||||
class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase
|
||||
{
|
||||
@@ -39,7 +36,10 @@ class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
$appointment = new Appointment($doctor, $this->createUser(['ROLE_USER']), 1_790_200_000, 1_790_201_800);
|
||||
$slotStart = strtotime('+30 days') + random_int(0, 500_000) * 7;
|
||||
$appointment = new Appointment($doctor, $this->createUser(['ROLE_USER']), $slotStart, $slotStart + 1_800);
|
||||
// رزرو آنلاین با پنجرهٔ پرداخت ثبت میشود؛ پرداخت باید همین را پاک کند.
|
||||
$appointment->markPendingWithTtl(Appointment::PAYMENT_TTL);
|
||||
if ($withClinic !== null) {
|
||||
$appointment->setClinic($withClinic($doctor));
|
||||
}
|
||||
@@ -63,25 +63,65 @@ class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase
|
||||
]));
|
||||
}
|
||||
|
||||
public function testPaidPersonalBookingIsConfirmedAndFiled(): void
|
||||
private function reload(Appointment $appointment): Appointment
|
||||
{
|
||||
$this->em->clear();
|
||||
|
||||
return $this->em->getRepository(Appointment::class)->find($appointment->getId());
|
||||
}
|
||||
|
||||
public function testPaidBookingStaysPendingUntilSomeoneConfirmsIt(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
[$payment, $appointment] = $this->pendingPaidBooking();
|
||||
|
||||
$this->fireCallback($payment);
|
||||
$this->em->clear();
|
||||
$reloaded = $this->reload($appointment);
|
||||
|
||||
$reloaded = $this->em->getRepository(Appointment::class)->find($appointment->getId());
|
||||
self::assertSame(Appointment::STATUS_CONFIRMED, $reloaded->getStatus());
|
||||
|
||||
$sessions = $this->em->getRepository(PatientSession::class)->findBy(['appointment' => $reloaded]);
|
||||
self::assertCount(1, $sessions, 'پرداخت آنلاین هم باید پرونده بسازد');
|
||||
|
||||
$record = $sessions[0]->getRecord();
|
||||
self::assertSame('doctor', $record->getEntityType());
|
||||
self::assertSame(Appointment::STATUS_PENDING, $reloaded->getStatus());
|
||||
// پنجرهٔ پرداخت برداشته میشود، وگرنه cronِ انقضا نوبتِ پرداختشده را میکشت.
|
||||
self::assertNull($reloaded->getExpiresAt());
|
||||
self::assertCount(0, $this->em->getRepository(PatientSession::class)->findBy(['appointment' => $reloaded]));
|
||||
}
|
||||
|
||||
public function testPaidClinicBookingIsFiledUnderTheClinic(): void
|
||||
public function testConfirmingThePaidBookingFilesTheSession(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
[$payment, $appointment] = $this->pendingPaidBooking();
|
||||
$this->fireCallback($payment);
|
||||
$reloaded = $this->reload($appointment);
|
||||
|
||||
$doctorUser = $reloaded->getDoctor()->getUser();
|
||||
$this->authJson('POST', '/api/v1/appointment/' . $reloaded->getUuid() . '/confirm', $doctorUser, []);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$confirmed = $this->reload($reloaded);
|
||||
self::assertSame(Appointment::STATUS_CONFIRMED, $confirmed->getStatus());
|
||||
|
||||
$sessions = $this->em->getRepository(PatientSession::class)->findBy(['appointment' => $confirmed]);
|
||||
self::assertCount(1, $sessions, 'تأیید نوبت باید پرونده بسازد');
|
||||
self::assertSame('doctor', $sessions[0]->getRecord()->getEntityType());
|
||||
}
|
||||
|
||||
/** تقسیم مالی هم مثل پرونده به لحظهٔ تأیید منتقل شده است. */
|
||||
public function testFinancialSplitHappensOnConfirmNotOnPayment(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
$breakdowns = static::getContainer()->get(\App\Settlement\Repository\FinancialBreakdownRepository::class);
|
||||
[$payment, $appointment] = $this->pendingPaidBooking();
|
||||
|
||||
$this->fireCallback($payment);
|
||||
$paidPayment = $this->em->getRepository(Payment::class)->find($payment->getId());
|
||||
self::assertFalse($breakdowns->existsForPayment($paidPayment), 'پرداخت بهتنهایی نباید تقسیم مالی بسازد');
|
||||
|
||||
$reloaded = $this->reload($appointment);
|
||||
$this->authJson('POST', '/api/v1/appointment/' . $reloaded->getUuid() . '/confirm', $reloaded->getDoctor()->getUser(), []);
|
||||
|
||||
// بدون نماینده و منشیِ سهمبر چیزی برای تقسیم نیست؛ صرفاً نباید خطا بدهد.
|
||||
self::assertSame(200, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testConfirmedClinicBookingIsFiledUnderTheClinic(): void
|
||||
{
|
||||
$this->enableTestMode();
|
||||
[$payment, $appointment] = $this->pendingPaidBooking(function (Doctor $doctor): Clinic {
|
||||
@@ -95,10 +135,13 @@ class AppointmentPaidConfirmFilesSessionTest extends ApiTestCase
|
||||
});
|
||||
|
||||
$this->fireCallback($payment);
|
||||
$this->em->clear();
|
||||
$reloaded = $this->reload($appointment);
|
||||
|
||||
$reloaded = $this->em->getRepository(Appointment::class)->find($appointment->getId());
|
||||
$records = $this->em->getRepository(PatientRecord::class)->findBy(['user' => $reloaded->getUser()]);
|
||||
$this->authJson('POST', '/api/v1/appointment/' . $reloaded->getUuid() . '/confirm', $reloaded->getDoctor()->getUser(), []);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$confirmed = $this->reload($reloaded);
|
||||
$records = $this->em->getRepository(PatientRecord::class)->findBy(['user' => $confirmed->getUser()]);
|
||||
|
||||
self::assertCount(1, $records, 'یک نوبت، یک پرونده — نه یکی برای پزشک و یکی برای کلینیک');
|
||||
self::assertSame('clinic', $records[0]->getEntityType());
|
||||
|
||||
Reference in New Issue
Block a user