get(EntityManagerInterface::class); } protected function tearDown(): void { $filters = $this->em()->getFilters(); if ($filters->isEnabled(TenantFilter::NAME)) { $filters->disable(TenantFilter::NAME); } parent::tearDown(); } private function enableFilterFor(string $type, int $id): void { $this->em()->getFilters() ->enable(TenantFilter::NAME) ->setParameter(TenantFilter::PARAM_TYPE, $type, 'string') ->setParameter(TenantFilter::PARAM_ID, $id, 'integer'); } private function makeDoctor(): Doctor { $doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر زنجیره'); $this->em->persist($doctor); $this->em->flush(); return $doctor; } /** یک سهم منشی از پرداختِ محیطِ داده‌شده، با کل زنجیره‌اش. */ private function earningFor(Doctor $doctor, User $secretary): SecretaryEarning { $payment = new Payment($this->createUser(), 1_000_000, 'mock', Payment::TYPE_APPOINTMENT); $payment->assignTenantPair('doctor', $doctor->getId()); $this->em->persist($payment); $breakdown = new FinancialBreakdown( $payment, FinancialBreakdown::SOURCE_APPOINTMENT, $secretary, 1_000_000, 0, '0.00', 0, 1_000_000, '10.00', 100_000, 900_000, null, null, null, ); $this->em->persist($breakdown); $earning = new SecretaryEarning($breakdown, $secretary, 'rel-' . bin2hex(random_bytes(4)), 5.0, 50_000); $this->em->persist($earning); $this->em->flush(); return $earning; } /** @return string[] */ private function reportedUuids(User $secretary): array { $repo = static::getContainer()->get(SecretaryEarningRepository::class); $report = $repo->reportFor($secretary, 1, 50); return array_column($report['items'] ?? [], 'uuid'); } /** ✅ سهمِ محیط خودی از راه لنگر به پرداخت دیده می‌شود. */ public function testAnEarningIsVisibleInsideItsOwnEnvironment(): void { $doctor = $this->makeDoctor(); $secretary = $this->createUser(['ROLE_SECRETARY']); $earning = $this->earningFor($doctor, $secretary); $this->em->clear(); $this->enableFilterFor('doctor', $doctor->getId()); self::assertContains($earning->getUuid(), $this->reportedUuids($secretary)); } /** * ❌ همان سهم در محیط دیگر ناپدید می‌شود — نه به‌خاطر شرط دستی، بلکه چون * کوئری به `payments` لنگر زده و فیلتر روی همان join نشسته است. */ public function testTheSameEarningDisappearsInAnotherEnvironment(): void { $doctor = $this->makeDoctor(); $secretary = $this->createUser(['ROLE_SECRETARY']); $earning = $this->earningFor($doctor, $secretary); $this->em->clear(); $this->enableFilterFor('doctor', $doctor->getId() + 1000); self::assertNotContains($earning->getUuid(), $this->reportedUuids($secretary)); } /** * ⚠️ مرزی: کیف پول شخص محیط ندارد و در هر محیطی دیده می‌شود. این نشتی نیست، * تصمیم است — موجودی از مجموع credit−debitِ همان کاربر مشتق می‌شود و تفکیکش * به محیط، خودِ موجودی را بی‌معنا می‌کند. */ public function testTheUserWalletStaysGlobalAcrossEnvironments(): void { $patient = $this->createUser(['ROLE_USER']); $txn = new WalletTransaction($patient, 500_000, WalletTransaction::TYPE_CREDIT, 500_000); $this->em->persist($txn); $this->em->flush(); $uuid = $txn->getUuid(); $this->em->clear(); $this->enableFilterFor('clinic', 987_654); $repo = static::getContainer()->get(WalletTransactionRepository::class); self::assertNotNull( $repo->findOneBy(['uuid' => $uuid]), 'کیف پول شخص نباید به محیط قفل شود', ); } }