Task 09 shipped a powerful API that a non-technical clinic owner could not safely use. This closes that gap: activation now requires having seen what the rule actually does. - PolicySimulator runs a policy against real past appointments and writes nothing: evaluation works on facts (never entities), the whole run sits in a transaction rolled back and cleared in `finally`, and a test counts rows in five sensitive tables before and after - activate() now demands a simulation of the *same version* — a report for version 1 does not unlock version 2 - PolicyTemplateRegistry: six ready-made rules, so the common case never touches a raw condition - Severity from the affected ratio; 0% is a warning too, since a rule that changes nothing usually has a condition that never matches - An empty clinic still succeeds with a warning, otherwise a new clinic could never activate anything Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a field that exists only in the schema shows up with no frontend change, and that operators are filtered per field type. The schema response now carries per-field metadata (label, type, meaningful operators) so the form has one source of truth instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
import { renderWithProviders } from '../test/utils';
|
|
|
|
vi.mock('../lib/api', () => ({
|
|
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
|
ApiError: class extends Error {},
|
|
}));
|
|
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
|
|
import { Routes, Route } from 'react-router-dom';
|
|
import { api } from '../lib/api';
|
|
import PolicySimulationPage from './PolicySimulationPage';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
const policy = {
|
|
uuid: 'p1',
|
|
category: 'pricing',
|
|
name: 'تخفیف وفاداری',
|
|
condition: {},
|
|
effects: [{ type: 'discount_percent', value: 25 }],
|
|
priority: 0,
|
|
version: 2,
|
|
active: false,
|
|
valid_from: null,
|
|
valid_to: null,
|
|
address_uuid: null,
|
|
service_uuid: null,
|
|
catalog_category_uuid: null,
|
|
specificity: 0,
|
|
};
|
|
|
|
function run(version: number, severity: string) {
|
|
return {
|
|
uuid: 'r1',
|
|
policy_uuid: 'p1',
|
|
policy_version: version,
|
|
sample_size: 4,
|
|
affected_count: 3,
|
|
affected_percent: 75,
|
|
severity,
|
|
created_at: 1_700_000_000,
|
|
warning: null,
|
|
rows: [
|
|
{
|
|
appointment_uuid: 'a1',
|
|
patient_name: 'ز. احمدی',
|
|
slot_start: 1_700_000_000,
|
|
before: '2,000,000 ریال',
|
|
after: '1,500,000 ریال',
|
|
reason: '500,000 ریال تخفیف',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function mockApi(runs: unknown[]) {
|
|
get.mockImplementation((path: string) => {
|
|
if (path === '/api/v1/policy/p1') return Promise.resolve({ success: true, data: policy });
|
|
if (path === '/api/v1/policy/p1/simulations') return Promise.resolve({ success: true, data: runs });
|
|
return Promise.resolve({ success: true, data: null });
|
|
});
|
|
}
|
|
|
|
function renderPage() {
|
|
return renderWithProviders(
|
|
<Routes>
|
|
<Route path="/admin/policies/:policyUuid/simulate" element={<PolicySimulationPage />} />
|
|
</Routes>,
|
|
{ route: '/admin/policies/p1/simulate' },
|
|
);
|
|
}
|
|
|
|
describe('PolicySimulationPage', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('shows the before/after change of each affected appointment', async () => {
|
|
mockApi([run(2, 'high')]);
|
|
renderPage();
|
|
|
|
await waitFor(() => expect(screen.getByText('ز. احمدی')).toBeInTheDocument());
|
|
expect(screen.getByText('2,000,000 ریال')).toBeInTheDocument();
|
|
expect(screen.getByText('1,500,000 ریال')).toBeInTheDocument();
|
|
expect(screen.getByText(/تحت تأثیر: 3 نوبت/)).toBeInTheDocument();
|
|
});
|
|
|
|
/** گزارشِ نسخهٔ قبلی نباید دکمهٔ فعالسازی را باز کند — همان قاعدهٔ بکاند. */
|
|
it('disables activation when the report belongs to an older version', async () => {
|
|
mockApi([run(1, 'low')]);
|
|
renderPage();
|
|
|
|
await waitFor(() => expect(screen.getByText(/فعالسازی قانون/)).toBeDisabled());
|
|
expect(screen.getByText(/دوباره آزمایش کنید/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('enables activation when the report matches the current version', async () => {
|
|
mockApi([run(2, 'low')]);
|
|
renderPage();
|
|
|
|
await waitFor(() => expect(screen.getByText(/فعالسازی قانون/)).toBeEnabled());
|
|
});
|
|
});
|