feat(subscription): implement effective plan logic and update free plan features
This commit is contained in:
@@ -43,6 +43,24 @@ describe('useSubscription', () => {
|
||||
expect(result.current.isExpiringSoon).toBe(true);
|
||||
});
|
||||
|
||||
it('نبود اشتراک: امکانات را از effective_plan (پلن free) میگیرد', async () => {
|
||||
useAuthStore.setState({ primaryRole: 'doctor' });
|
||||
get.mockResolvedValue({
|
||||
data: {
|
||||
subscription: null,
|
||||
effective_plan: { features: { patient_records: true, services: true, sms_panel: true }, max_secretaries: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHookWithClient(() => useSubscription());
|
||||
await waitFor(() => expect(result.current.hasFeature('patient_records')).toBe(true));
|
||||
|
||||
expect(result.current.hasFeature('services')).toBe(true);
|
||||
expect(result.current.hasFeature('sms_panel')).toBe(true);
|
||||
expect(result.current.hasPlan).toBe(false);
|
||||
expect(result.current.maxSecretaries).toBe(1);
|
||||
});
|
||||
|
||||
it('برای نقش admin غیرفعال است (query اجرا نمیشود)', async () => {
|
||||
useAuthStore.setState({ primaryRole: 'admin' });
|
||||
const { result } = renderHookWithClient(() => useSubscription());
|
||||
|
||||
@@ -16,13 +16,14 @@ export function useSubscription() {
|
||||
});
|
||||
|
||||
const sub = data?.data?.subscription ?? null;
|
||||
const features: Record<string, boolean> = sub?.plan?.features ?? {};
|
||||
const maxSecretaries: number = sub?.plan?.max_secretaries ?? 1;
|
||||
const effectivePlan = data?.data?.effective_plan ?? sub?.plan ?? null;
|
||||
const features: Record<string, boolean> = effectivePlan?.features ?? {};
|
||||
const maxSecretaries: number = effectivePlan?.max_secretaries ?? 1;
|
||||
const hasPlan = sub !== null;
|
||||
|
||||
return {
|
||||
subscription: sub,
|
||||
hasFeature: (key: string) => hasPlan && (features[key] ?? false),
|
||||
hasFeature: (key: string) => features[key] ?? false,
|
||||
maxSecretaries,
|
||||
hasPlan,
|
||||
isExpiringSoon: (sub?.days_remaining ?? 0) > 0 && (sub?.days_remaining ?? 0) <= 7,
|
||||
|
||||
@@ -181,11 +181,16 @@ function PlansTab() {
|
||||
<div className="card card-pad" style={{ color: 'var(--text-3)' }}>در حال بارگذاری...</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{plans.map((plan) => (
|
||||
{plans.map((plan) => {
|
||||
const periods: SubscriptionPeriod[] = Array.isArray(plan.periods)
|
||||
? plan.periods
|
||||
: Object.values(plan.periods ?? {});
|
||||
return (
|
||||
<div key={plan.uuid} className="card">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px', borderBottom: '1px solid var(--border)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<b style={{ fontSize: 15 }}>{PLAN_DISPLAY[plan.name] ?? plan.name}</b>
|
||||
<span className={`badge ${plan.active ? 'green' : 'gray'}`} style={{ fontSize: 11 }}>{plan.active ? 'فعال' : 'غیرفعال'}</span>
|
||||
<span className="badge blue" style={{ fontSize: 11 }}>سطح {plan.level}</span>
|
||||
<span className="muted" style={{ fontSize: 12 }}>حداکثر {plan.max_secretaries} منشی</span>
|
||||
<span style={{ fontSize: 12, display: 'flex', gap: 6 }}>
|
||||
@@ -204,7 +209,7 @@ function PlansTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plan.periods.length === 0 ? (
|
||||
{periods.length === 0 ? (
|
||||
<div style={{ padding: '20px 16px', color: 'var(--text-3)', fontSize: 13 }}>هنوز دورهای تعریف نشده</div>
|
||||
) : (
|
||||
<div className="table-wrap"><table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
@@ -218,8 +223,8 @@ function PlansTab() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.periods.map((period, i) => (
|
||||
<tr key={period.uuid} style={{ borderBottom: i < plan.periods.length - 1 ? '1px solid var(--border)' : 'none' }}>
|
||||
{periods.map((period, i) => (
|
||||
<tr key={period.uuid} style={{ borderBottom: i < periods.length - 1 ? '1px solid var(--border)' : 'none' }}>
|
||||
<td style={{ padding: '8px 16px' }}><b>{period.label}</b></td>
|
||||
<td style={{ padding: '8px 16px' }}>{period.duration_months} ماه</td>
|
||||
<td style={{ padding: '8px 16px' }}>{period.is_trial ? 'رایگان' : formatRial(period.price_rials)}</td>
|
||||
@@ -242,7 +247,8 @@ function PlansTab() {
|
||||
</table></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -362,6 +362,7 @@ export interface SubscriptionPlan {
|
||||
level: number;
|
||||
max_secretaries: number;
|
||||
features: Record<string, boolean>;
|
||||
active: boolean;
|
||||
periods: SubscriptionPeriod[];
|
||||
}
|
||||
|
||||
@@ -388,6 +389,13 @@ export interface MySubscriptionData {
|
||||
days_remaining?: number;
|
||||
} | null;
|
||||
used_trial: boolean;
|
||||
/** پلن مؤثر: پلن اشتراک فعال یا پلن پیشفرض free در نبود اشتراک. */
|
||||
effective_plan: {
|
||||
name: string;
|
||||
level: number;
|
||||
max_secretaries: number;
|
||||
features: Record<string, boolean>;
|
||||
} | null;
|
||||
}
|
||||
/** @deprecated use MySubscriptionData */
|
||||
export interface MySubscription {
|
||||
|
||||
Reference in New Issue
Block a user