setPlanResourceQuota(1); [$user, , $address] = $this->clinicWithAddress(); $type = $this->resourceType($address); $this->createResource($user, $address, $type, ['name' => 'اتاق ۱']); self::assertSame(201, $this->responseCode()); $body = $this->createResource($user, $address, $type, ['name' => 'اتاق ۲']); self::assertSame(422, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); self::assertSame(ErrorCodes::ERR_RESOURCE_LIMIT_001, $body['errors'][0]['code']); } public function testBasicPlanAllowsThreeResourcesAndRejectsTheFourth(): void { $this->setPlanResourceQuota(3); [$user, , $address] = $this->clinicWithAddress(); $type = $this->resourceType($address); foreach (['اتاق ۱', 'اتاق ۲', 'اتاق ۳'] as $name) { $this->createResource($user, $address, $type, ['name' => $name]); self::assertSame(201, $this->responseCode(), $name); } $body = $this->createResource($user, $address, $type, ['name' => 'اتاق ۴']); self::assertSame(422, $this->responseCode()); self::assertSame(ErrorCodes::ERR_RESOURCE_LIMIT_001, $body['errors'][0]['code']); self::assertStringContainsString('۳', $this->toPersianDigits($body['errors'][0]['message'])); } public function testProfessionalPlanIsUnlimited(): void { $this->setPlanResourceQuota(SubscriptionPlan::UNLIMITED); [$user, , $address] = $this->clinicWithAddress(); $type = $this->resourceType($address); foreach (range(1, 5) as $i) { $this->createResource($user, $address, $type, ['name' => "اتاق $i"]); self::assertSame(201, $this->responseCode(), "resource #$i"); } } /** غیرفعال‌کردن منبع، جای خالی نمی‌سازد. */ public function testDeactivatedResourcesStillCountTowardTheQuota(): void { $this->setPlanResourceQuota(1); [$user, , $address] = $this->clinicWithAddress(); $type = $this->resourceType($address); $first = $this->createResource($user, $address, $type, ['name' => 'اتاق ۱']); $this->authJson('PATCH', '/api/v1/resource/' . $first['data']['uuid'], $user, ['active' => false]); self::assertSame(200, $this->responseCode()); $this->createResource($user, $address, $type, ['name' => 'اتاق ۲']); self::assertSame(422, $this->responseCode()); } /** حذف منبع، جا را واقعاً آزاد می‌کند — شمارش زندهٔ دیتابیس است نه شمارندهٔ ذخیره‌شده. */ public function testDeletingAResourceFreesTheSlot(): void { $this->setPlanResourceQuota(1); [$user, , $address] = $this->clinicWithAddress(); $type = $this->resourceType($address); $first = $this->createResource($user, $address, $type, ['name' => 'اتاق ۱']); $this->authJson('DELETE', '/api/v1/resource/' . $first['data']['uuid'], $user); self::assertSame(200, $this->responseCode()); $this->createResource($user, $address, $type, ['name' => 'اتاق ۲']); self::assertSame(201, $this->responseCode()); } /** سقف مالِ همان محیط است؛ پر شدن یک کلینیک، کلینیک دیگر را قفل نمی‌کند. */ public function testQuotaIsCountedPerTenantPair(): void { $this->setPlanResourceQuota(1); [$userA, , $addressA] = $this->clinicWithAddress(); $this->createResource($userA, $addressA, $this->resourceType($addressA), ['name' => 'اتاق A']); self::assertSame(201, $this->responseCode()); [$userB, , $addressB] = $this->clinicWithAddress('شعبهٔ کلینیک دوم'); $this->createResource($userB, $addressB, $this->resourceType($addressB), ['name' => 'اتاق B']); self::assertSame(201, $this->responseCode()); self::assertSame(1, $this->em->getRepository(ClinicResource::class)->countForPair( $addressB->tenantEntityType(), $addressB->tenantEntityId(), )); } private function toPersianDigits(string $value): string { return strtr($value, ['0' => '۰', '1' => '۱', '2' => '۲', '3' => '۳', '4' => '۴', '5' => '۵', '6' => '۶', '7' => '۷', '8' => '۸', '9' => '۹']); } }