Price lists, annual tariffs and per-branch price overrides each answered
"what does this service cost?" differently, so a single date could carry
several answers and nobody could say which one was right. Price now lives
only on ServiceItem.price_rials, edited from the services page.
- drop PriceList/PriceListItem, their repositories and the seven
/api/v1/price-list(s) endpoints; PricingController keeps only quote and
the appointment price snapshot
- drop Tariff, TariffRepository, TariffService and the two
/service-items/{uuid}/tariffs endpoints; creating or repricing a service
no longer upserts a current-year tariff
- drop price_rials from ServiceBranchOverride; the entity stays for its
duration columns, which DurationCalculator and ServiceSelectionValidator
still read
- InvoiceService reads the item price directly
- PricingEngine collapses to a single source; breakdown.sources always
reports service_item, keeping the response contract intact
- remove the price-lists admin page, its route and settings-menu entry, the
tariff modal and the service detail tariffs tab; useAppointmentInvoice
moves to its own hook file
Migration drops price_lists, price_list_items, service_tariffs and the
override price column.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
122 lines
6.5 KiB
TypeScript
122 lines
6.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { screen, fireEvent } from '@testing-library/react';
|
|
import { renderWithProviders } from '../test/utils';
|
|
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
vi.mock('../lib/api', () => ({
|
|
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
|
ApiError: class extends Error {},
|
|
}));
|
|
|
|
import { api } from '../lib/api';
|
|
import { useAuthStore } from '../stores/authStore';
|
|
import ClinicServicesPage from './ClinicServicesPage';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
// FeatureGate → useSubscription only fetches for doctor/clinic/secretary roles
|
|
useAuthStore.setState({ primaryRole: 'doctor' });
|
|
get.mockReset();
|
|
get.mockImplementation((url: string) => {
|
|
if (url.includes('/subscription/my')) return Promise.resolve({ success: true, data: {
|
|
subscription: null, used_trial: false,
|
|
effective_plan: { name: 'professional', level: 2, max_secretaries: 10, features: { services: true } },
|
|
} });
|
|
if (url.includes('/payment/config')) return Promise.resolve({ success: true, data: { test_mode: true, gateways: [] } });
|
|
if (url.includes('/service-sections')) return Promise.resolve({ success: true, data: [
|
|
{ uuid: 'sec1', name: 'کندلا ۲۰۲۱', active: true, items_count: 10 },
|
|
] });
|
|
if (url.includes('/service-items/sec1')) return Promise.resolve({ success: true, data: [
|
|
{ uuid: 'it1', name: 'فول بادی', price_rials: 35_000_000, active: true, duration_minutes: 50,
|
|
staff: { uuid: 'st1', full_name: 'مریم امینی' },
|
|
staff_members: [{ uuid: 'st1', full_name: 'مریم امینی' }, { uuid: 'st2', full_name: 'سحر رحمانی' }] },
|
|
] });
|
|
if (url.includes('/staff')) return Promise.resolve({ success: true, data: [
|
|
{ uuid: 'st1', full_name: 'مریم امینی', active: true },
|
|
{ uuid: 'st2', full_name: 'سحر رحمانی', active: true },
|
|
] });
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
});
|
|
|
|
describe('ClinicServicesPage (خدمات)', () => {
|
|
it('renders section cards with the service count inside the settings shell', async () => {
|
|
renderWithProviders(<ClinicServicesPage />, { route: '/admin/clinic-services' });
|
|
// settings shell menu + section card
|
|
expect(await screen.findByText('کندلا ۲۰۲۱')).toBeInTheDocument();
|
|
expect(screen.getByText(/۱۰ سرویس/)).toBeInTheDocument();
|
|
expect(screen.getByText('بخش جدید')).toBeInTheDocument();
|
|
});
|
|
|
|
it('drills into a section and shows a service with all its personnel chips', async () => {
|
|
renderWithProviders(<ClinicServicesPage />, { route: '/admin/clinic-services' });
|
|
fireEvent.click(await screen.findByText('کندلا ۲۰۲۱'));
|
|
|
|
expect(await screen.findByText('فول بادی')).toBeInTheDocument();
|
|
expect(screen.getByText('مریم امینی')).toBeInTheDocument();
|
|
expect(screen.getByText('سحر رحمانی')).toBeInTheDocument();
|
|
expect(screen.getByText('سرویس جدید')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens the ⋮ actions menu with the insurance entry and no tariff entry', async () => {
|
|
renderWithProviders(<ClinicServicesPage />, { route: '/admin/clinic-services' });
|
|
fireEvent.click(await screen.findByText('کندلا ۲۰۲۱'));
|
|
fireEvent.click(await screen.findByTitle('عملیات'));
|
|
|
|
expect(await screen.findByText('ویرایش')).toBeInTheDocument();
|
|
expect(screen.getByText('پوشش بیمه')).toBeInTheDocument();
|
|
expect(screen.queryByText('تعرفههای سالانه')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('فرم ویرایش سرویس دیگر سوییچ بیمه ندارد و به بخش پوشش بیمه ارجاع میدهد', async () => {
|
|
renderWithProviders(<ClinicServicesPage />, { route: '/admin/clinic-services' });
|
|
fireEvent.click(await screen.findByText('کندلا ۲۰۲۱'));
|
|
fireEvent.click(await screen.findByTitle('عملیات'));
|
|
fireEvent.click(await screen.findByText('ویرایش'));
|
|
|
|
expect(await screen.findByText('ویرایش سرویس')).toBeInTheDocument();
|
|
expect(screen.queryByText('این خدمت شامل بیمه میشود')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('قیمت تقریبی با بیمه (تومان)')).not.toBeInTheDocument();
|
|
expect(screen.getByText(/در بخش «پوشش بیمه» تنظیم میشود/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('ذخیرهی سرویس فیلدهای بیمه را نمیفرستد', async () => {
|
|
const patch = api.patch as ReturnType<typeof vi.fn>;
|
|
patch.mockResolvedValue({ success: true, data: {} });
|
|
|
|
renderWithProviders(<ClinicServicesPage />, { route: '/admin/clinic-services' });
|
|
fireEvent.click(await screen.findByText('کندلا ۲۰۲۱'));
|
|
fireEvent.click(await screen.findByTitle('عملیات'));
|
|
fireEvent.click(await screen.findByText('ویرایش'));
|
|
fireEvent.click(await screen.findByText('ذخیره سرویس'));
|
|
|
|
await vi.waitFor(() => expect(patch).toHaveBeenCalled());
|
|
const body = patch.mock.calls[0][1];
|
|
expect(body).not.toHaveProperty('insurance_covered');
|
|
expect(body).not.toHaveProperty('insurance_price_rials');
|
|
});
|
|
|
|
it('کارت سرویسِ تحت پوشش، نشان «تحت پوشش» را نمایش میدهد', async () => {
|
|
get.mockImplementation((url: string) => {
|
|
if (url.includes('/subscription/my')) return Promise.resolve({ success: true, data: {
|
|
subscription: null, used_trial: false,
|
|
effective_plan: { name: 'professional', level: 2, max_secretaries: 10, features: { services: true } },
|
|
} });
|
|
if (url.includes('/payment/config')) return Promise.resolve({ success: true, data: { test_mode: true, gateways: [] } });
|
|
if (url.includes('/service-sections')) return Promise.resolve({ success: true, data: [
|
|
{ uuid: 'sec1', name: 'کندلا ۲۰۲۱', active: true, items_count: 1 },
|
|
] });
|
|
if (url.includes('/service-items/sec1')) return Promise.resolve({ success: true, data: [
|
|
{ uuid: 'it1', name: 'فول بادی', price_rials: 35_000_000, active: true, insurance_covered: true },
|
|
] });
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
|
|
renderWithProviders(<ClinicServicesPage />, { route: '/admin/clinic-services' });
|
|
fireEvent.click(await screen.findByText('کندلا ۲۰۲۱'));
|
|
|
|
expect(await screen.findByText('تحت پوشش')).toBeInTheDocument();
|
|
});
|
|
});
|