createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, $name); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); return $doctor; } /** @return array{0: User, 1: Clinic} */ private function makeClinicWith(Doctor ...$doctors): array { $owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']); $clinic = new Clinic($owner); $clinic->setName('کلینیک تست'); foreach ($doctors as $d) { $clinic->getDoctors()->add($d); } $this->em->persist($clinic); $this->em->flush(); return [$owner, $clinic]; } private function makeAppointment( Doctor $doctor, ?Clinic $clinic = null, ?User $patient = null, int $visitPriceRials = 5_000_000, ): Appointment { $patient ??= $this->createUser(); // اسلات یکتا به‌ازای هر نوبت: db_test بین اجراها پاک نمی‌شود. $start = strtotime('+30 days') + random_int(0, 500_000) * 7; $appointment = new Appointment($doctor, $patient, $start, $start + 900); $appointment->setClinic($clinic); $appointment->setVisitPriceRials($visitPriceRials); $this->em->persist($appointment); $this->em->flush(); return $appointment; } private function reload(Appointment $a): Appointment { $this->em->clear(); return $this->em->getRepository(Appointment::class)->find($a->getId()); } // ── ساخت پنلی «ثبت‌شده» است، نه قطعی ───────────────────────────────────── public function testPanelBookingIsCreatedPending(): void { $doctor = $this->makeDoctor(); $start = strtotime('+40 days') + random_int(0, 500_000) * 7; $res = $this->authJson('POST', '/api/v1/my/appointment', $doctor->getUser(), [ 'doctor_uuid' => $doctor->getUuid(), 'slot_start' => $start, 'slot_end' => $start + 1_800, 'patient_mobile' => '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT), 'patient_name' => 'بیمار تست', 'patient_national_code' => '00' . str_pad((string) random_int(0, 99_999_999), 8, '0', STR_PAD_LEFT), ]); self::assertSame(201, $this->responseCode()); self::assertSame(Appointment::STATUS_PENDING, $res['data']['status']); } // ── قطعی‌کردن: بدون پرداخت / جزئی / کامل ───────────────────────────────── public function testConfirmWithoutPaymentMovesToConfirmedAndOpensSession(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $res = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); self::assertSame(Appointment::STATUS_CONFIRMED, $res['data']['appointment']['status']); self::assertSame(5_000_000, $res['data']['session']['final_price_rials']); self::assertSame(0, $res['data']['session']['paid_total_rials']); self::assertSame(5_000_000, $res['data']['session']['remaining_rials']); self::assertFalse($res['data']['session']['is_paid']); } public function testConfirmWithPartialPaymentLeavesRemainder(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $res = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), 'payments' => [['method' => 'cash', 'amount_rials' => 2_000_000]], ]); self::assertSame(200, $this->responseCode()); self::assertSame(2_000_000, $res['data']['session']['paid_total_rials']); self::assertSame(3_000_000, $res['data']['session']['remaining_rials']); self::assertFalse($res['data']['session']['is_paid']); } public function testConfirmWithFullPaymentSettlesSession(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $res = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), 'payments' => [['method' => 'pos', 'amount_rials' => 5_000_000]], ]); self::assertSame(200, $this->responseCode()); self::assertSame(0, $res['data']['session']['remaining_rials']); self::assertTrue($res['data']['session']['is_paid']); } public function testConfirmAcceptsSeveralPaymentRows(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $res = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), 'payments' => [ ['method' => 'cash', 'amount_rials' => 1_000_000], ['method' => 'pos', 'amount_rials' => 4_000_000], ], ]); self::assertSame(200, $this->responseCode()); self::assertSame(5_000_000, $res['data']['session']['paid_total_rials']); self::assertTrue($res['data']['session']['is_paid']); } // ── پرونده: استفادهٔ مجدد یا ساخت ──────────────────────────────────────── public function testConfirmReusesExistingRecordOfSameDoctor(): void { $doctor = $this->makeDoctor(); $patient = $this->createUser(); $first = $this->makeAppointment($doctor, null, $patient); $this->authJson('POST', "/api/v1/appointment/{$first->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $first->getVersion(), ]); self::assertSame(200, $this->responseCode()); $second = $this->makeAppointment($doctor, null, $patient); $this->authJson('POST', "/api/v1/appointment/{$second->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $second->getVersion(), ]); self::assertSame(200, $this->responseCode()); $records = $this->em->getRepository(PatientRecord::class)->findBy([ 'entityType' => 'doctor', 'entityId' => $doctor->getId(), 'user' => $patient, ]); self::assertCount(1, $records, 'پروندهٔ همان پزشک دوباره ساخته نمی‌شود'); $sessions = $this->em->getRepository(PatientSession::class)->findBy(['record' => $records[0]]); self::assertCount(2, $sessions, 'هر نوبت مراجعهٔ خودش را دارد'); } public function testConfirmInClinicFilesUnderClinicRecord(): void { $doctor = $this->makeDoctor(); [$owner, $clinic] = $this->makeClinicWith($doctor); $patient = $this->createUser(); $appointment = $this->makeAppointment($doctor, $clinic, $patient); $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $owner, [ 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); $clinicRecord = $this->em->getRepository(PatientRecord::class)->findOneBy([ 'entityType' => 'clinic', 'entityId' => $clinic->getId(), 'user' => $patient, ]); $doctorRecord = $this->em->getRepository(PatientRecord::class)->findOneBy([ 'entityType' => 'doctor', 'entityId' => $doctor->getId(), 'user' => $patient, ]); self::assertNotNull($clinicRecord, 'نوبت کلینیکی در پروندهٔ کلینیک می‌نشیند'); self::assertNull($doctorRecord, 'و در مطب شخصی پزشک پروندهٔ موازی نمی‌سازد'); } // ── خطاها ──────────────────────────────────────────────────────────────── public function testPaymentAboveTotalIsRejectedAndNothingIsCommitted(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), 'payments' => [['method' => 'cash', 'amount_rials' => 9_000_000]], ]); self::assertSame(422, $this->responseCode()); self::assertSame(Appointment::STATUS_PENDING, $this->reload($appointment)->getStatus(), 'تراکنش برگشته'); } public function testUnknownPaymentMethodIs422(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), 'payments' => [['method' => 'bitcoin', 'amount_rials' => 1_000]], ]); self::assertSame(422, $this->responseCode()); self::assertSame(Appointment::STATUS_PENDING, $this->reload($appointment)->getStatus()); } public function testStaleVersionIs409(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion() + 5, ]); self::assertSame(409, $this->responseCode()); } public function testConfirmingAnAlreadyConfirmedAppointmentIs422(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); $fresh = $this->reload($appointment); $this->authJson('POST', "/api/v1/appointment/{$fresh->getUuid()}/confirm", $doctor->getUser(), [ 'version' => $fresh->getVersion(), ]); self::assertSame(422, $this->responseCode(), 'confirmed → confirmed گذار مجاز نیست'); } public function testStrangerCannotConfirm(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/confirm", $this->createUser(), [ 'version' => $appointment->getVersion(), ]); self::assertSame(403, $this->responseCode()); } public function testDetailExposesServicePricesForTheModal(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor); $res = $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $doctor->getUser()); self::assertSame(200, $this->responseCode()); self::assertSame(5_000_000, $res['data']['data']['visit_price_rials']); self::assertArrayHasKey('service_items', $res['data']['data']); } }