feat(subscription): return plan features for users without subscription permission
This commit is contained in:
@@ -81,6 +81,29 @@
|
||||
|
||||
اگر اشتراک فعالی نداشت `subscription` برابر `null` است، اما `effective_plan` همیشه مقدار دارد: پلن اشتراک فعال، یا در نبود اشتراک، **پلن پیشفرض `free`**. فرانتاند برای تعیین دسترسی به امکانات (`hasFeature`) باید از `effective_plan` استفاده کند (نه `subscription`) تا کاربرانِ بدون اشتراک هم امکانات پلن free را داشته باشند. `subscription`/`hasPlan` صرفاً برای نمایش وضعیت اشتراک پولی است.
|
||||
|
||||
### پاسخ کاهشیافته برای کاربرِ بدون مجوزِ `subscription.view` (2026-08)
|
||||
|
||||
پیش از این، منشیِ بدون این مجوز `403` میگرفت. نتیجهاش یک **قفلِ دروغین در پنل** بود:
|
||||
سایدبار هر آیتم feature-دار (پروندهٔ بیماران، بیمه) را با `hasFeature()` گیت میکند و
|
||||
بدون این پاسخ، `features` خالی میماند و آیتم قفل و به صفحهٔ اشتراک هدایت میشد — حتی
|
||||
وقتی خودِ API آن قابلیت را به همان منشی میداد.
|
||||
|
||||
حالا پاسخ `200` است ولی فقط توانمندیهای پلن را دارد:
|
||||
|
||||
```json
|
||||
{"success":true,"data":{
|
||||
"subscription": null,
|
||||
"used_trial": false,
|
||||
"effective_plan": { "features": { "patient_records": true, "…": true }, "max_secretaries": 1 }
|
||||
}}
|
||||
```
|
||||
|
||||
- `subscription`، `used_trial` و فیلدهای هویتی/سطحِ پلن (`name`, `level`, `uuid`, `active`,
|
||||
`periods`) در این حالت **نمیآیند**.
|
||||
- افشای تازهای نیست: `GET /subscription/plans` عمومی است و همین `features` را (بههمراه
|
||||
قیمتها) برای همهٔ پلنها میدهد.
|
||||
- سایر نقشها و منشیِ دارای `subscription.view` همان پاسخ کامل بالا را میگیرند.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/subscription/trial
|
||||
|
||||
@@ -53,23 +53,43 @@ class SubscriptionController extends BaseController
|
||||
|
||||
// ── Authenticated ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* اشتراکِ محیط جاری — و برای کاربرِ بدونِ مجوزِ اشتراک، فقط **قابلیتهای** پلن.
|
||||
*
|
||||
* پنل بدون دانستنِ قابلیتهای پلن نمیتواند منو را درست بسازد: هر آیتمِ
|
||||
* feature-دار (پروندهٔ بیماران، بیمه) وقتی این فهرست نیاید «قفل» میشود و کاربر
|
||||
* را به صفحهٔ اشتراک میفرستد — حتی وقتی خودِ API آن قابلیت را به او میدهد.
|
||||
* پس ۴۰۳ اینجا به یک قفلِ دروغین در UI ترجمه میشد.
|
||||
*
|
||||
* افشای تازهای هم ندارد: `GET /subscription/plans` عمومی است و همین قابلیتها
|
||||
* (بهعلاوهٔ قیمتها) را برای همهٔ پلنها میدهد. چیزی که خصوصی میماند وضعیت و
|
||||
* تاریخِ اشتراکِ همین محیط و سابقهٔ دورهٔ آزمایشی است.
|
||||
*/
|
||||
#[Route('/api/v1/subscription/my', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function my(#[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$this->secretaryAccess->denyUnlessGranted($user, 'subscription', 'view');
|
||||
[$entityType, $entityId] = $this->resolveEntity($user);
|
||||
if ($entityId === null) {
|
||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||
}
|
||||
|
||||
$subscription = $this->subscriptionService->getActiveSubscription($entityType, $entityId);
|
||||
$usedTrial = $this->subscriptionService->hasUsedTrial($entityType, $entityId);
|
||||
$effectivePlan = $this->subscriptionService->getEffectivePlan($entityType, $entityId);
|
||||
|
||||
if (!$this->secretaryAccess->canOrNonSecretary($user, 'subscription', 'view')) {
|
||||
return $this->success([
|
||||
'subscription' => null,
|
||||
'used_trial' => false,
|
||||
'effective_plan' => $effectivePlan === null ? null : [
|
||||
'features' => $effectivePlan->getFeatures(),
|
||||
'max_secretaries' => $effectivePlan->getMaxSecretaries(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'subscription' => $subscription?->toArray(),
|
||||
'used_trial' => $usedTrial,
|
||||
'subscription' => $this->subscriptionService->getActiveSubscription($entityType, $entityId)?->toArray(),
|
||||
'used_trial' => $this->subscriptionService->hasUsedTrial($entityType, $entityId),
|
||||
'effective_plan' => $effectivePlan?->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -272,23 +272,39 @@ class SecretaryResourceEnforcementTest extends ApiTestCase
|
||||
$this->assertSame(403, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testSubscriptionDeniedByDefault(): void
|
||||
/**
|
||||
* بدون مجوز اشتراک، `subscription/my` میماند ولی فقط قابلیتهای پلن را میدهد:
|
||||
* پنل بدون این فهرست، آیتمهای feature-دار منو را قفل نشان میداد در حالی که خودِ
|
||||
* API آنها را به منشی میدهد. وضعیت/تاریخ اشتراک همچنان پنهان است.
|
||||
*/
|
||||
public function testSubscriptionWithoutPermissionReturnsFeaturesOnly(): void
|
||||
{
|
||||
[$secretary] = $this->makeClinicSecretary();
|
||||
$this->em->flush();
|
||||
|
||||
$this->authJson('GET', '/api/v1/subscription/my', $secretary);
|
||||
$this->assertSame(403, $this->responseCode());
|
||||
$res = $this->authJson('GET', '/api/v1/subscription/my', $secretary);
|
||||
$this->assertSame(200, $this->responseCode());
|
||||
|
||||
$this->assertNull($res['data']['subscription']);
|
||||
$this->assertFalse($res['data']['used_trial']);
|
||||
$this->assertArrayHasKey('features', $res['data']['effective_plan']);
|
||||
$this->assertTrue($res['data']['effective_plan']['features']['patient_records']);
|
||||
// فیلدهای مالی/هویتیِ پلن نباید بیایند.
|
||||
$this->assertArrayNotHasKey('periods', $res['data']['effective_plan']);
|
||||
$this->assertArrayNotHasKey('name', $res['data']['effective_plan']);
|
||||
}
|
||||
|
||||
/** با مجوز، پاسخ کامل است — نه نسخهٔ کاهشیافته. */
|
||||
public function testSubscriptionAllowedWhenGranted(): void
|
||||
{
|
||||
[$secretary, $rel] = $this->makeClinicSecretary();
|
||||
$rel->mergePermissions(['resources' => ['subscription' => ['view' => true]]]);
|
||||
$this->em->flush();
|
||||
|
||||
$this->authJson('GET', '/api/v1/subscription/my', $secretary);
|
||||
$res = $this->authJson('GET', '/api/v1/subscription/my', $secretary);
|
||||
$this->assertSame(200, $this->responseCode());
|
||||
$this->assertArrayHasKey('name', $res['data']['effective_plan']);
|
||||
$this->assertArrayHasKey('used_trial', $res['data']);
|
||||
}
|
||||
|
||||
public function testPatientDeleteSeparateFromUpdate(): void
|
||||
|
||||
Reference in New Issue
Block a user