createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, $name); $this->em->persist($doctor); $this->em->flush(); return [$owner, $doctor]; } private function recordFor(Doctor $doctor, User $patient): PatientRecord { $record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId()); $this->em->persist($record); $this->em->flush(); return $record; } private function charge(User $owner, PatientRecord $record, int $amount, string $description): void { $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/wallet/charge', $owner, [ 'amount_rials' => $amount, 'description' => $description, ]); self::assertSame(201, $this->responseCode(), 'شارژ کیف پول باید موفق باشد'); } /** @return array توضیحِ تراکنش‌هایی که این محیط می‌بیند */ private function ledger(User $owner, PatientRecord $record): array { $res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet/transactions', $owner); self::assertSame(200, $this->responseCode()); return array_column($res['data'], 'description'); } private function balance(User $owner, PatientRecord $record): int { $res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet', $owner); self::assertSame(200, $this->responseCode()); return $res['data']['balance_rials']; } /** * یک بیمار، دو محیط: هر محیط پرونده و شارژ خودش را دارد. * * @return array{0: User, 1: PatientRecord, 2: User, 3: PatientRecord} */ private function sharedPatientInTwoEnvironments(): array { $patient = $this->createUser(['ROLE_USER']); [$ownerA, $doctorA] = $this->makeDoctor('دکتر الف'); [$ownerB, $doctorB] = $this->makeDoctor('دکتر ب'); $recordA = $this->recordFor($doctorA, $patient); $recordB = $this->recordFor($doctorB, $patient); $this->charge($ownerA, $recordA, 300_000, 'شارژ نزد الف'); $this->charge($ownerB, $recordB, 500_000, 'شارژ نزد ب'); return [$ownerA, $recordA, $ownerB, $recordB]; } /** ✅ هر محیط تراکنش خودش را می‌بیند. */ public function testEachEnvironmentSeesItsOwnEntries(): void { [$ownerA, $recordA, $ownerB, $recordB] = $this->sharedPatientInTwoEnvironments(); self::assertSame(['شارژ نزد الف'], $this->ledger($ownerA, $recordA)); self::assertSame(['شارژ نزد ب'], $this->ledger($ownerB, $recordB)); } /** ❌ تراکنشِ محیط دیگر — همان نشتی — دیگر در دفتر نمی‌آید. */ public function testOneEnvironmentCannotReadWhatThePatientPaidInAnother(): void { [$ownerA, $recordA] = $this->sharedPatientInTwoEnvironments(); self::assertNotContains('شارژ نزد ب', $this->ledger($ownerA, $recordA)); } /** * ⚠️ مرزی: موجودی عمداً سراسری می‌ماند. پول مالِ بیمار است و اگر per-محیط * می‌شد، همان بیمار در هر مطب نصف پولش را می‌دید. */ public function testTheBalanceStaysGlobalEvenThoughTheLedgerIsScoped(): void { [$ownerA, $recordA, $ownerB, $recordB] = $this->sharedPatientInTwoEnvironments(); self::assertSame(800_000, $this->balance($ownerA, $recordA)); self::assertSame(800_000, $this->balance($ownerB, $recordB)); } /** * ⚠️ مرزی: ردیف بی‌محیط (ثبت‌شده پیش از این تفکیک) در همهٔ محیط‌ها دیده می‌شود. * پنهان‌کردنش تاریخچهٔ موجودِ یک بیمار را ناپدید می‌کرد. */ public function testUnattributedLegacyRowsRemainVisibleEverywhere(): void { [$ownerA, $recordA, $ownerB, $recordB] = $this->sharedPatientInTwoEnvironments(); // کاربر بعد از درخواست‌های API از EntityManager جدا شده؛ دوباره از همین EM. $patient = $this->em->find(User::class, $recordA->getUser()->getId()); $legacy = new WalletTransaction($patient, 100_000, WalletTransaction::TYPE_CREDIT, 900_000); $legacy->setDescription('شارژ قدیمی بدون محیط'); $this->em->persist($legacy); $this->em->flush(); self::assertContains('شارژ قدیمی بدون محیط', $this->ledger($ownerA, $recordA)); self::assertContains('شارژ قدیمی بدون محیط', $this->ledger($ownerB, $recordB)); } /** ❌ خلاصهٔ کیف پول هم همان قاعده را دارد؛ فقط موجودی سراسری می‌ماند. */ public function testTheWalletSummaryOnlyShowsThisEnvironmentsRecentEntries(): void { [$ownerA, $recordA] = $this->sharedPatientInTwoEnvironments(); $res = $this->authJson('GET', '/api/v1/patient/' . $recordA->getUuid() . '/wallet', $ownerA); self::assertSame(200, $this->responseCode()); self::assertSame(800_000, $res['data']['balance_rials'], 'موجودی سراسری است'); self::assertSame( ['شارژ نزد الف'], array_column($res['data']['recent_transactions'], 'description'), ); } /** پرداختِ مراجعه از کیف پول، محیطش را از پروندهٔ همان مراجعه می‌گیرد. */ public function testAVisitPaidFromTheWalletIsAttributedToTheRecordsEnvironment(): void { [$ownerA, $recordA, $ownerB, $recordB] = $this->sharedPatientInTwoEnvironments(); $session = $this->authJson('POST', '/api/v1/patient/' . $recordA->getUuid() . '/session', $ownerA, [ 'visit_price_rials' => 200_000, ]); self::assertSame(201, $this->responseCode()); $this->authJson('POST', '/api/v1/session/' . $session['data']['uuid'] . '/payments', $ownerA, [ 'amount_rials' => 200_000, 'method' => 'wallet', ]); self::assertSame(201, $this->responseCode(), 'پرداخت از کیف پول باید ثبت شود'); self::assertNotSame([], array_filter( $this->ledger($ownerA, $recordA), static fn (string $d) => str_starts_with($d, 'پرداخت سرویس'), )); self::assertSame([], array_filter( $this->ledger($ownerB, $recordB), static fn (string $d) => str_starts_with($d, 'پرداخت سرویس'), ), 'محیط دیگر نباید پرداختِ مراجعهٔ این محیط را ببیند'); } }