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:
hamed
2026-07-25 21:40:38 +03:30
parent 3cc4a59459
commit 50ba7e44ff
13 changed files with 520 additions and 38 deletions
@@ -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());