0 → 422؛ فلگ-فقط بدون ردیف → ردیف ساخته نشود. * - POST /patient/{uuid}/session: با فلگ فعال، visit_price_rials <= 0 → 422. * - POST /my/appointment و /admin/appointment: فلگ برای پزشکِ نوبت resolve می‌شود * (ردیف پزشک، وگرنه کلینیکِ واحد او)؛ بدون هزینه ویزیت → 422. */ class RequireVisitPriceTest extends ApiTestCase { /** @return array{0: User, 1: Doctor} */ private function doctor(): array { $owner = $this->createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر'); $this->em->persist($doctor); $this->em->flush(); return [$owner, $doctor]; } private function pricingRow(string $type, int $entityId, int $priceRials, bool $require): EntityInsurancePricing { $row = new EntityInsurancePricing($type, $entityId, null, $priceRials); $row->setRequireVisitPrice($require); $this->em->persist($row); $this->em->flush(); return $row; } private function recordFor(Doctor $doctor): PatientRecord { $patient = $this->createUser(['ROLE_USER']); $record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId()); $this->em->persist($record); $this->em->flush(); return $record; } private function appointmentBody(string $doctorUuid): array { $start = time() + 86_400 + random_int(0, 3_600) * 100; return [ 'doctor_uuid' => $doctorUuid, '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), ]; } // ── PUT /api/v1/insurance-pricing ──────────────────────────────────────── public function testSaveFlagOnWithZeroPriceIs422(): void { [$owner] = $this->doctor(); $res = $this->authJson('PUT', '/api/v1/insurance-pricing', $owner, [ 'free_visit_price_rials' => 0, 'require_visit_price' => true, ]); self::assertSame(422, $this->responseCode()); self::assertSame(ErrorCodes::ERR_VALIDATION_001, $res['errors'][0]['code']); } public function testSaveFlagOnWithValidPricePersistsBoth(): void { [$owner] = $this->doctor(); $this->authJson('PUT', '/api/v1/insurance-pricing', $owner, [ 'free_visit_price_rials' => 500_000, 'require_visit_price' => true, ]); self::assertSame(200, $this->responseCode()); $res = $this->authJson('GET', '/api/v1/insurance-pricing', $owner); self::assertSame(500_000, $res['data']['free_visit_price_rials']); self::assertTrue($res['data']['require_visit_price']); } public function testSaveFlagAloneKeepsStoredPrice(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 300_000, false); $this->authJson('PUT', '/api/v1/insurance-pricing', $owner, ['require_visit_price' => true]); self::assertSame(200, $this->responseCode()); $res = $this->authJson('GET', '/api/v1/insurance-pricing', $owner); self::assertSame(300_000, $res['data']['free_visit_price_rials']); self::assertTrue($res['data']['require_visit_price']); } public function testSaveFlagAloneOnEnabledRowWithZeroStoredPriceIs422(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 0, false); $this->authJson('PUT', '/api/v1/insurance-pricing', $owner, ['require_visit_price' => true]); self::assertSame(422, $this->responseCode()); } public function testSaveFlagFalseWithoutRowDoesNotCreateRow(): void { [$owner, $doctor] = $this->doctor(); $this->authJson('PUT', '/api/v1/insurance-pricing', $owner, ['require_visit_price' => false]); self::assertSame(200, $this->responseCode()); $row = $this->em->getRepository(EntityInsurancePricing::class)->findOneBy([ 'entityType' => EntityInsurancePricing::TYPE_DOCTOR, 'entityId' => $doctor->getId(), 'insuranceId' => null, ]); self::assertNull($row); } // ── POST /api/v1/patient/{uuid}/session ───────────────────────────────── public function testSessionWithoutVisitPriceIs422WhenFlagOn(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, true); $record = $this->recordFor($doctor); $res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [ 'visit_price_rials' => 0, ]); self::assertSame(422, $this->responseCode()); self::assertSame(ErrorCodes::ERR_VALIDATION_001, $res['errors'][0]['code']); } public function testSessionWithVisitPriceIs201WhenFlagOn(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, true); $record = $this->recordFor($doctor); $res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [ 'visit_price_rials' => 400_000, ]); self::assertSame(201, $this->responseCode()); self::assertSame(400_000, $res['data']['visit_price_rials']); } public function testSessionWithZeroVisitPriceStaysAllowedWhenFlagOff(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, false); $record = $this->recordFor($doctor); $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [ 'visit_price_rials' => 0, ]); self::assertSame(201, $this->responseCode()); } // ── POST /api/v1/my/appointment ───────────────────────────────────────── public function testMyAppointmentWithoutVisitPriceIs422WhenFlagOn(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, true); $res = $this->authJson('POST', '/api/v1/my/appointment', $owner, $this->appointmentBody($doctor->getUuid())); self::assertSame(422, $this->responseCode()); self::assertSame('visit_price_rials', $res['errors'][0]['field']); } public function testMyAppointmentWithVisitPriceIs201AndStored(): void { [$owner, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, true); $body = $this->appointmentBody($doctor->getUuid()); $body['visit_price_rials'] = 500_000; $res = $this->authJson('POST', '/api/v1/my/appointment', $owner, $body); self::assertSame(201, $this->responseCode()); $appointment = $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $res['data']['uuid']]); self::assertSame(500_000, $appointment->getVisitPriceRials()); } public function testMyAppointmentWithoutVisitPriceIs201WhenFlagOff(): void { [$owner, $doctor] = $this->doctor(); $this->authJson('POST', '/api/v1/my/appointment', $owner, $this->appointmentBody($doctor->getUuid())); self::assertSame(201, $this->responseCode()); } public function testFlagFallsBackToSingleClinicOfDoctor(): void { [$owner, $doctor] = $this->doctor(); $clinicOwner = $this->createUser(['ROLE_CLINIC']); $clinic = new Clinic($clinicOwner); $clinic->getDoctors()->add($doctor); $this->em->persist($clinic); $this->em->flush(); $this->pricingRow(EntityInsurancePricing::TYPE_CLINIC, $clinic->getId(), 500_000, true); $this->authJson('POST', '/api/v1/my/appointment', $owner, $this->appointmentBody($doctor->getUuid())); self::assertSame(422, $this->responseCode()); } // ── POST /api/v1/admin/appointment ────────────────────────────────────── public function testAdminAppointmentWithoutVisitPriceIs422WhenFlagOn(): void { [, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, true); $admin = $this->createUser(['ROLE_ADMIN']); $res = $this->authJson('POST', '/api/v1/admin/appointment', $admin, $this->appointmentBody($doctor->getUuid())); self::assertSame(422, $this->responseCode()); self::assertSame('visit_price_rials', $res['errors'][0]['field']); } public function testAdminAppointmentWithVisitPriceIs201(): void { [, $doctor] = $this->doctor(); $this->pricingRow(EntityInsurancePricing::TYPE_DOCTOR, $doctor->getId(), 500_000, true); $admin = $this->createUser(['ROLE_ADMIN']); $body = $this->appointmentBody($doctor->getUuid()); $body['visit_price_rials'] = 500_000; $res = $this->authJson('POST', '/api/v1/admin/appointment', $admin, $body); self::assertSame(201, $this->responseCode()); $appointment = $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $res['data']['uuid']]); self::assertSame(500_000, $appointment->getVisitPriceRials()); } }