fix(subscription): report the acting environment's plan for gating

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>
This commit is contained in:
hamed
2026-08-20 14:48:13 +03:30
co-authored by Claude Opus 5
parent 25a12e9a06
commit 601d211d6f
5 changed files with 99 additions and 12 deletions
+8 -4
View File
@@ -21,13 +21,17 @@ export function useSubscription() {
const sub = data?.data?.subscription ?? null;
const effectivePlan = data?.data?.effective_plan ?? sub?.plan ?? null;
const features: Record<string, boolean> = effectivePlan?.features ?? {};
const maxSecretaries: number = effectivePlan?.max_secretaries ?? 1;
// سقف‌ها و قابلیت‌ها از پلنِ محیطِ فعال می‌آیند، نه از پلنِ محیطِ مالکیت: سرور هم با
// همین محیط می‌سنجد. پزشکِ مهمانِ یک کلینیک، منابعش را از سهمیهٔ کلینیک میزبان
// برمی‌دارد ولی سقفِ پلنِ خودش نمایش داده می‌شد و عددِ سهمیه بی‌معنی بود.
const gatePlan = data?.data?.context_plan ?? effectivePlan;
const features: Record<string, boolean> = gatePlan?.features ?? {};
const maxSecretaries: number = gatePlan?.max_secretaries ?? 1;
// `-1` یعنی بی‌نهایت.
const maxResources: number = effectivePlan?.max_resources ?? 1;
const maxResources: number = gatePlan?.max_resources ?? 1;
// نقش‌هایی که اشتراک ندارند (ادمین) و لحظهٔ پیش از رسیدن پاسخ: سقف ناشناخته است و
// نباید با پیش‌فرضِ ۱ به‌جای کاربر تصمیم گرفت — گیت‌کردن کارِ سرور است.
const planLoaded = effectivePlan !== null;
const planLoaded = gatePlan !== null;
const hasPlan = sub !== null;
return {
+11 -1
View File
@@ -604,7 +604,7 @@ export interface MySubscriptionData {
days_remaining?: number;
} | null;
used_trial: boolean;
/** پلن مؤثر: پلن اشتراک فعال یا پلن پیش‌فرض free در نبود اشتراک. */
/** پلن مؤثرِ محیطِ **مالکیت** — مبنای خرید و ارتقا. */
effective_plan: {
name: string;
level: number;
@@ -612,6 +612,16 @@ export interface MySubscriptionData {
max_resources: number;
features: Record<string, boolean>;
} | null;
/**
* پلن محیطی که کاربر همین حالا در آن ایستاده — مبنای سقف‌ها و قفل قابلیت‌ها،
* چون سرور هم با همین محیط می‌سنجد. برای کاربری که فقط محیط خودش را دارد با
* `effective_plan` یکی است.
*/
context_plan: {
max_secretaries: number;
max_resources: number;
features: Record<string, boolean>;
} | null;
}
/** @deprecated use MySubscriptionData */
export interface MySubscription {