diff --git a/docs/api/auth.md b/docs/api/auth.md index bdd416e5..25be9a05 100644 --- a/docs/api/auth.md +++ b/docs/api/auth.md @@ -316,6 +316,8 @@ Authorization: Bearer | `context` | object\|null | context فعال انتخاب‌شده | | `available_contexts` | array | همه محیط‌های کاری قابل انتخاب | +> **مالکِ چند کلینیک:** اگر یک کاربر مالک بیش از یک کلینیک باشد، **همهٔ** آن‌ها در این فهرست می‌آیند و به هرکدام می‌شود سوییچ کرد. تا پیش از این فقط اولی می‌آمد و کلینیک دوم اصلاً قابل انتخاب نبود — که با جداسازی محیط یعنی دادهٔ آن کلینیک برای خودِ مالکش هم نامرئی می‌ماند. (`MultiClinicOwnerContextTest`) + **فیلد `permissions` در هر context:** | حالت context | مقدار `permissions` | diff --git a/src/Auth/Controller/AuthController.php b/src/Auth/Controller/AuthController.php index e2badc66..d9f1d3eb 100644 --- a/src/Auth/Controller/AuthController.php +++ b/src/Auth/Controller/AuthController.php @@ -734,8 +734,10 @@ class AuthController extends BaseController } } - // صاحب کلینیک (اگر قبلاً اضافه نشده) - if ($clinic = $this->clinicRepo->findByUser($user)) { + // صاحب کلینیک — **همهٔ** کلینیک‌هایش، نه فقط اولی: با جداسازی محیط، محیطی که + // در این فهرست نباشد قابل انتخاب نیست و دادهٔ همان کلینیک برای مالکش هم + // نامرئی می‌ماند. + foreach ($this->clinicRepo->findAllByUser($user) as $clinic) { $alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinic->getUuid()); if (empty($alreadyAdded)) { $contexts[] = [ diff --git a/src/Clinic/Repository/ClinicRepository.php b/src/Clinic/Repository/ClinicRepository.php index b0b75aba..6a137d04 100644 --- a/src/Clinic/Repository/ClinicRepository.php +++ b/src/Clinic/Repository/ClinicRepository.php @@ -39,6 +39,21 @@ class ClinicRepository extends ServiceEntityRepository return $this->findOneBy(['user' => $user]); } + /** + * همهٔ کلینیک‌هایی که این کاربر مالکشان است. + * + * {@see findByUser()} فقط اولی را می‌دهد و برای fallbackهایی که «یک کلینیک» + * کافی است بس است؛ ولی فهرست محیط‌های قابل انتخاب باید کامل باشد، وگرنه مالکِ + * دو کلینیک هرگز نمی‌تواند به دومی سوییچ کند و با جداسازی محیط، دادهٔ کلینیک + * دومش برای خودش هم نامرئی می‌شود. + * + * @return Clinic[] + */ + public function findAllByUser(User $user): array + { + return $this->findBy(['user' => $user], ['id' => 'ASC']); + } + public function findWithFilters(array $filters): array { $page = max(1, (int) ($filters['page'] ?? 1)); diff --git a/tests/Auth/MultiClinicOwnerContextTest.php b/tests/Auth/MultiClinicOwnerContextTest.php new file mode 100644 index 00000000..4645551a --- /dev/null +++ b/tests/Auth/MultiClinicOwnerContextTest.php @@ -0,0 +1,117 @@ +createUser(['ROLE_CLINIC']); + + $first = new Clinic($owner); + $first->setName('کلینیک اول'); + $this->em->persist($first); + + $second = new Clinic($owner); + $second->setName('کلینیک دوم'); + $this->em->persist($second); + $this->em->flush(); + + return [$owner, $first, $second]; + } + + private function switchTo(User $owner, Clinic $clinic): array + { + return $this->authJson('POST', '/api/v1/auth/switch-context', $owner, [ + 'db_uuid' => $clinic->getUuid(), + 'type' => 'clinic', + ]); + } + + /** + * ✅ هر دو کلینیک قابل انتخاب‌اند. + * + * از راه خودِ switch سنجیده می‌شود، نه از فهرست: `switchContext` ورودی را با + * همان `buildAvailableContexts` می‌سنجد، پس سوییچِ موفق یعنی محیط در فهرست بوده. + */ + public function testBothOwnedClinicsCanBeSelected(): void + { + [$owner, $first, $second] = $this->ownerOfTwoClinics(); + + $this->switchTo($owner, $first); + self::assertSame(200, $this->responseCode(), 'کلینیک اول باید قابل انتخاب باشد'); + + $this->switchTo($owner, $second); + self::assertSame(200, $this->responseCode(), 'کلینیک دوم هم باید قابل انتخاب باشد'); + } + + /** ✅ سوییچ به کلینیک دوم واقعاً انجام می‌شود. */ + public function testTheOwnerCanSwitchToTheSecondClinic(): void + { + [$owner, , $second] = $this->ownerOfTwoClinics(); + + $res = $this->authJson('POST', '/api/v1/auth/switch-context', $owner, [ + 'db_uuid' => $second->getUuid(), + 'type' => 'clinic', + ]); + + self::assertSame(200, $this->responseCode()); + self::assertSame($second->getUuid(), $res['data']['context']['db_uuid']); + self::assertSame('clinic', $res['data']['context']['role']); + } + + /** ❌ کلینیکِ شخص دیگر همچنان قابل انتخاب نیست. */ + public function testAnotherOwnersClinicIsStillRefused(): void + { + [$owner] = $this->ownerOfTwoClinics(); + [, $strangersClinic] = $this->ownerOfTwoClinics(); + + $this->authJson('POST', '/api/v1/auth/switch-context', $owner, [ + 'db_uuid' => $strangersClinic->getUuid(), + 'type' => 'clinic', + ]); + + self::assertSame(403, $this->responseCode()); + } + + /** + * ⚠️ مرزی: پس از سوییچ، دادهٔ همان کلینیک دیده می‌شود — این همان چیزی است که + * پیش از رفع باگ غیرممکن بود. + */ + public function testDataOfTheSecondClinicBecomesReachableAfterSwitching(): void + { + [$owner, , $second] = $this->ownerOfTwoClinics(); + + $this->authJson('POST', '/api/v1/auth/switch-context', $owner, [ + 'db_uuid' => $second->getUuid(), + 'type' => 'clinic', + ]); + self::assertSame(200, $this->responseCode()); + + $created = $this->authJson('POST', '/api/v1/my/payment-methods/bank-accounts', $owner, [ + 'bank_name' => 'ملی', + 'account_number' => '0101234567890', + ]); + self::assertSame(201, $this->responseCode()); + self::assertSame('clinic', $created['data']['entity_type']); + + $list = $this->authJson('GET', '/api/v1/my/payment-methods/bank-accounts', $owner); + self::assertSame(['ملی'], array_column($list['data'], 'bank_name')); + } +}