Feature gates and the resource cap are enforced against the environment the user is standing in, but /subscription/my only ever returned the plan of the environment they own. A doctor working as a guest in another clinic consumed the host clinic's resource quota while the panel showed their own plan's cap, so the quota number and the menu locks disagreed with what the server would allow. /subscription/my now also returns context_plan — limits and features of the acting environment, without the other environment's plan identity. effective_plan, subscription and used_trial stay on the owned environment so the purchase flow is unchanged, and useSubscription reads its caps and hasFeature from context_plan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
134 lines
5.4 KiB
PHP
134 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Subscription;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Auth\Entity\UserActiveContext;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Clinic\Entity\ClinicDoctorPermission;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Shared\Context\EntityContext;
|
|
use App\Subscription\Entity\SubscriptionPeriod;
|
|
use App\Subscription\Entity\SubscriptionPlan;
|
|
use App\Subscription\Service\SubscriptionService;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* A doctor who also owns a clinic runs two environments. Upgrading one must not
|
|
* upgrade the other: the subscription belongs to the environment, not the user.
|
|
*/
|
|
class DualEnvironmentSubscriptionTest extends ApiTestCase
|
|
{
|
|
private function makeUserWithBothEnvironments(): array
|
|
{
|
|
$user = $this->createUser(['ROLE_DOCTOR', 'ROLE_CLINIC']);
|
|
|
|
$doctor = new Doctor($user, 'دکتر تست');
|
|
$this->em->persist($doctor);
|
|
|
|
$clinic = new Clinic($user);
|
|
$clinic->setName('کلینیک تست');
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
return [$user, $doctor, $clinic];
|
|
}
|
|
|
|
private function makePeriod(): SubscriptionPeriod
|
|
{
|
|
$plan = new SubscriptionPlan('pro-' . bin2hex(random_bytes(4)), 5, 2, ['patient_records' => true]);
|
|
$this->em->persist($plan);
|
|
|
|
$period = new SubscriptionPeriod($plan, 'سه ماهه', 3, 1_000_000);
|
|
$this->em->persist($period);
|
|
$this->em->flush();
|
|
|
|
return $period;
|
|
}
|
|
|
|
private function setActiveContext(User $user, string $dbUuid, string $dbType): void
|
|
{
|
|
$this->em->persist(new UserActiveContext($user, $dbUuid, $dbType));
|
|
$this->em->flush();
|
|
}
|
|
|
|
private function service(): SubscriptionService
|
|
{
|
|
return static::getContainer()->get(SubscriptionService::class);
|
|
}
|
|
|
|
public function testGrantingTheClinicLeavesThePersonalPracticeOnFree(): void
|
|
{
|
|
[$user, $doctor, $clinic] = $this->makeUserWithBothEnvironments();
|
|
$period = $this->makePeriod();
|
|
|
|
$this->service()->grant('clinic', $clinic->getId(), $period->getUuid(), $user);
|
|
|
|
self::assertSame(
|
|
$period->getPlan()->getId(),
|
|
$this->service()->getEffectivePlan('clinic', $clinic->getId())?->getId(),
|
|
);
|
|
self::assertSame('free', $this->service()->getEffectivePlan('doctor', $doctor->getId())?->getName());
|
|
self::assertNull($this->service()->getActiveSubscription('doctor', $doctor->getId()));
|
|
}
|
|
|
|
public function testMyReportsTheClinicPlanOnlyWhileStandingInTheClinic(): void
|
|
{
|
|
[$user, $doctor, $clinic] = $this->makeUserWithBothEnvironments();
|
|
$period = $this->makePeriod();
|
|
$this->service()->grant('clinic', $clinic->getId(), $period->getUuid(), $user);
|
|
|
|
$this->setActiveContext($user, $clinic->getUuid(), EntityContext::TYPE_CLINIC);
|
|
$body = $this->authJson('GET', '/api/v1/subscription/my', $user);
|
|
self::assertSame($period->getPlan()->getName(), $body['data']['effective_plan']['name']);
|
|
self::assertNotNull($body['data']['subscription']);
|
|
}
|
|
|
|
/**
|
|
* پزشکِ مهمانِ کلینیکِ دیگری سقف و قابلیتهایش را از کلینیکِ میزبان میگیرد — چون
|
|
* سرور هم با همان محیط میسنجد — ولی اشتراکِ خودش دستنخورده میماند.
|
|
*/
|
|
public function testContextPlanFollowsTheHostClinicForAGuestDoctor(): void
|
|
{
|
|
$guest = $this->createUser(['ROLE_DOCTOR']);
|
|
$doctor = new Doctor($guest, 'دکتر مهمان');
|
|
$this->em->persist($doctor);
|
|
|
|
$host = new Clinic($this->createUser(['ROLE_CLINIC']));
|
|
$host->setName('کلینیک میزبان');
|
|
$this->em->persist($host);
|
|
$this->em->flush();
|
|
|
|
$host->getDoctors()->add($doctor);
|
|
$this->em->persist(new ClinicDoctorPermission($host, $doctor));
|
|
$this->em->flush();
|
|
|
|
$period = $this->makePeriod();
|
|
$this->service()->grant('clinic', $host->getId(), $period->getUuid(), $host->getUser());
|
|
|
|
$this->setActiveContext($guest, $host->getUuid(), EntityContext::TYPE_CLINIC);
|
|
$body = $this->authJson('GET', '/api/v1/subscription/my', $guest);
|
|
|
|
// سقفها از کلینیک میزبان
|
|
self::assertSame($period->getPlan()->getMaxResources(), $body['data']['context_plan']['max_resources']);
|
|
self::assertTrue($body['data']['context_plan']['features']['patient_records']);
|
|
// ولی اشتراکِ خودِ پزشک همچنان free و بدون اشتراک فعال
|
|
self::assertSame('free', $body['data']['effective_plan']['name']);
|
|
self::assertNull($body['data']['subscription']);
|
|
// هویتِ پلنِ محیطِ میزبان افشا نمیشود
|
|
self::assertArrayNotHasKey('name', $body['data']['context_plan']);
|
|
}
|
|
|
|
public function testMyFallsBackToFreeWhileStandingInThePersonalPractice(): void
|
|
{
|
|
[$user, $doctor, $clinic] = $this->makeUserWithBothEnvironments();
|
|
$period = $this->makePeriod();
|
|
$this->service()->grant('clinic', $clinic->getId(), $period->getUuid(), $user);
|
|
|
|
$this->setActiveContext($user, $doctor->getUuid(), EntityContext::TYPE_DOCTOR);
|
|
$body = $this->authJson('GET', '/api/v1/subscription/my', $user);
|
|
self::assertSame('free', $body['data']['effective_plan']['name']);
|
|
self::assertNull($body['data']['subscription']);
|
|
}
|
|
}
|