get(EntityManagerInterface::class); } protected function tearDown(): void { $filters = $this->em()->getFilters(); if ($filters->isEnabled(TenantFilter::NAME)) { $filters->disable(TenantFilter::NAME); } parent::tearDown(); } private function makePricedPeriod(int $priceRials): \App\Subscription\Entity\SubscriptionPeriod { $plan = new \App\Subscription\Entity\SubscriptionPlan('plan-' . bin2hex(random_bytes(4)), 5, 1, []); $this->em->persist($plan); $period = new \App\Subscription\Entity\SubscriptionPeriod($plan, 'یک ماهه', 1, $priceRials); $this->em->persist($period); $this->em->flush(); return $period; } private function makeDoctor(): Doctor { $doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر پرداخت'); $this->em->persist($doctor); $this->em->flush(); return $doctor; } private function makeClinic(): Clinic { $clinic = new Clinic($this->createUser(['ROLE_CLINIC'])); $clinic->setName('کلینیک پرداخت'); $this->em->persist($clinic); $this->em->flush(); return $clinic; } /** پرداخت را از دیتابیس می‌خواند، نه از پاسخ — پاسخ محیط را برنمی‌گرداند. */ private function storedPayment(string $uuid): Payment { $this->em->clear(); $payment = $this->em->getRepository(Payment::class)->findOneBy(['uuid' => $uuid]); self::assertNotNull($payment, 'پرداخت باید ذخیره شده باشد'); return $payment; } private function payForAppointment(?Clinic $clinic): Payment { $doctor = $this->makeDoctor(); $patient = $this->createUser(['ROLE_USER']); $start = strtotime('+10 days') + random_int(0, 500_000) * 7; $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic); $this->em->persist($appointment); $this->em->flush(); $res = $this->authJson('POST', '/api/v1/payment/appointment', $patient, [ 'appointment_uuid' => $appointment->getUuid(), 'gateway' => 'mellat', ]); self::assertSame(200, $this->responseCode(), 'شروع پرداخت نوبت باید موفق باشد'); return $this->storedPayment($res['data']['payment_uuid']); } /** ✅ پرداخت نوبتِ یک کلینیک به همان کلینیک می‌نشیند. */ public function testAppointmentPaymentBelongsToTheClinicOfTheAppointment(): void { $clinic = $this->makeClinic(); $payment = $this->payForAppointment($clinic); self::assertSame('clinic', $payment->getEntityType()); self::assertSame($clinic->getId(), $payment->getEntityId()); } /** ✅ نوبتِ مطب شخصی به خودِ پزشک. */ public function testAppointmentPaymentOfAPersonalPracticeBelongsToTheDoctor(): void { $payment = $this->payForAppointment(null); self::assertSame('doctor', $payment->getEntityType()); } /** ✅ اشتراک به محیطی که خریدار صاحبش است. */ public function testSubscriptionPaymentBelongsToTheEnvironmentTheBuyerOwns(): void { $doctor = $this->makeDoctor(); // مبلغ از خودِ دوره می‌آید (قیمت خالص + مالیات)، نه از بدنهٔ درخواست. $res = $this->authJson('POST', '/api/v1/subscription-payment', $doctor->getUser(), [ 'gateway' => 'mellat', 'period_uuid' => $this->makePricedPeriod(1_000_000)->getUuid(), ]); self::assertSame(200, $this->responseCode()); $payment = $this->storedPayment($res['data']['payment_uuid']); self::assertSame('doctor', $payment->getEntityType()); self::assertSame($doctor->getId(), $payment->getEntityId()); } /** * ❌ کاربری که نه پزشک است نه کلینیک، اشتراک برای هیچ محیطی نمی‌خرد. بدون این * گارد، ردیفی با محیطِ نامعتبر ساخته می‌شد یا flush بی‌پیام می‌شکست. */ public function testSubscriptionPaymentWithoutAnOwnedEnvironmentIsRejected(): void { $res = $this->authJson('POST', '/api/v1/subscription-payment', $this->createUser(['ROLE_USER']), [ 'gateway' => 'mellat', 'period_uuid' => $this->makePricedPeriod(1_000_000)->getUuid(), ]); self::assertSame(422, $this->responseCode()); self::assertSame(ErrorCodes::ERR_PAYMENT_004, $res['errors'][0]['code']); } /** ⚠️ مرزی: بیمار محیطی ندارد، پس فیلتر خاموش است و پرداخت خودش را می‌بیند. */ public function testThePayingPatientStillSeesTheirOwnPayment(): void { $clinic = $this->makeClinic(); $payment = $this->payForAppointment($clinic); $patient = $payment->getUser(); $res = $this->authJson('GET', '/api/v1/payment/' . $payment->getUuid(), $patient); self::assertSame(200, $this->responseCode(), 'بیمار باید پرداخت خودش را ببیند'); self::assertSame($payment->getUuid(), $res['data']['uuid']); } /** ⚠️ محیط دیگر همان پرداخت را اصلاً نمی‌بیند — تور ایمنیِ TenantFilter. */ public function testAnotherEnvironmentDoesNotSeeThePaymentAtAll(): void { $clinic = $this->makeClinic(); $payment = $this->payForAppointment($clinic); $uuid = $payment->getUuid(); $this->em->clear(); $this->em()->getFilters() ->enable(TenantFilter::NAME) ->setParameter(TenantFilter::PARAM_TYPE, 'clinic', 'string') ->setParameter(TenantFilter::PARAM_ID, $clinic->getId() + 1000, 'integer'); self::assertNull( $this->em->getRepository(Payment::class)->findOneBy(['uuid' => $uuid]), 'پرداخت محیط دیگر نباید دیده شود', ); } }