Files
clinicpro/assets/admin/pages/PolicySimulationPage.test.tsx
T
hamedandClaude Opus 5 bcfa87bfad feat(policy): rule builder and mandatory dry-run sandbox
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>
2026-07-31 10:44:28 +03:30

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());
});
});