fix(admin): drop cached environment data when switching context

A doctor who also owns a clinic runs two environments, and a subscription
belongs to an environment, not to the user. The panel treated it as the
user's: the subscription query was keyed ['subscription-my'] with no
context, and switching environments never touched the react-query cache. So
upgrading the clinic left the personal practice showing the clinic's plan
with its feature-gated menu items unlocked, and vice versa.

The query key now carries the active dbUuid, and the context switch clears
the whole cache — every cached response belongs to the environment it was
fetched in, not just this one.

The API was already correct: DualEnvironmentSubscriptionTest pins that
granting one environment leaves the other on free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-19 22:15:56 +03:30
co-authored by Claude Opus 5
parent 5548d6248c
commit 1ce9957538
5 changed files with 229 additions and 1 deletions
@@ -0,0 +1,74 @@
import React from 'react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { makeClient } from '../test/utils';
import { useAuthStore } from '../stores/authStore';
vi.mock('../lib/api', () => ({
api: { get: vi.fn() },
}));
import { api } from '../lib/api';
import { useSubscription } from './useSubscription';
const get = api.get as ReturnType<typeof vi.fn>;
const initial = useAuthStore.getInitialState();
function planResponse(name: string) {
return {
success: true,
data: {
subscription: null,
used_trial: false,
effective_plan: { name, features: { patient_records: name !== 'free' }, max_secretaries: 1, max_resources: 1 },
},
};
}
beforeEach(() => {
localStorage.clear();
useAuthStore.setState(initial, true);
get.mockReset();
});
describe('useSubscription', () => {
/**
* پزشکی که هم مطب شخصی دارد و هم کلینیک، دو محیط دارد و اشتراک روی محیط می‌نشیند.
* با کلیدِ بدون محیط، پاسخِ cache شدهٔ محیط قبلی در محیط تازه سرو می‌شد و هر دو
* محیط ارتقایافته به‌نظر می‌رسیدند.
*/
it('پاسخِ cache شدهٔ محیط دیگر را سرو نمی‌کند', async () => {
const client = makeClient();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
);
// محیط کلینیک از قبل در cache نشسته است.
client.setQueryData(['subscription-my', 'clinic-1'], planResponse('professional'));
useAuthStore.setState({ primaryRole: 'doctor', dbUuid: 'doctor-1' });
get.mockResolvedValue(planResponse('free'));
const { result } = renderHook(() => useSubscription(), { wrapper });
await waitFor(() => expect(result.current.planLoaded).toBe(true));
expect(result.current.hasFeature('patient_records')).toBe(false);
expect(get).toHaveBeenCalledWith('/api/v1/subscription/my');
});
it('در همان محیط، پاسخِ cache شده دوباره درخواست نمی‌شود', async () => {
const client = makeClient();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
);
client.setQueryData(['subscription-my', 'clinic-1'], planResponse('professional'));
useAuthStore.setState({ primaryRole: 'clinic', dbUuid: 'clinic-1' });
const { result } = renderHook(() => useSubscription(), { wrapper });
await waitFor(() => expect(result.current.hasFeature('patient_records')).toBe(true));
expect(get).not.toHaveBeenCalled();
});
});
+5 -1
View File
@@ -6,10 +6,14 @@ import type { MySubscriptionData } from '../types';
export function useSubscription() {
const primaryRole = useAuthStore((s) => s.primaryRole);
const dbUuid = useAuthStore((s) => s.dbUuid);
const enabled = primaryRole === 'doctor' || primaryRole === 'clinic' || primaryRole === 'secretary';
// کلید شامل محیط فعال است: اشتراک روی محیط می‌نشیند، نه روی کاربر. پزشکی که هم
// مطب شخصی دارد و هم کلینیک، با کلیدِ بدون محیط پلنِ محیط قبلی را می‌دید و هر دو
// محیط ارتقایافته به‌نظر می‌رسیدند.
const { data } = useQuery<ApiResponse<MySubscriptionData>>({
queryKey: ['subscription-my'],
queryKey: ['subscription-my', dbUuid],
queryFn: () => api.get('/api/v1/subscription/my'),
enabled,
staleTime: 2 * 60 * 1000,