Files
clinicpro/assets/admin/components/InvoiceSummaryModal.test.tsx

188 lines
9.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {},
}));
import { api } from '../lib/api';
import { useAuthStore } from '../stores/authStore';
import InvoiceSummaryModal from './InvoiceSummaryModal';
const get = api.get as ReturnType<typeof vi.fn>;
const baseInvoice = {
uuid: 'iv1', status: 'finalized', issued_at: 1700000000, total_rials: 2_400_000,
base_insurance_rials: 0, supplementary_rials: 0, patient_rials: 900_000,
items: [{ uuid: 'it1', title: 'فول بادی', quantity: 1, total_rials: 2_400_000, patient_rials: 900_000 }],
};
const fullSession = {
session_at: 1700000000, paid_at: 1700100000,
services_total_rials: 2_400_000, consumables_total_rials: 40_000,
discount_rials: 200_000, final_price_rials: 2_240_000, paid_total_rials: 1_500_000,
// API واقعی همیشه این را می‌فرستد: max(0, final discount paid)
remaining_rials: 540_000,
payments: [
{ uuid: 'p1', method: 'wallet', amount_rials: 1_500_000, paid_at: 1700100000, created_by_name: 'منشی تست' },
],
consumables: [
{ uuid: 'c1', item_name: 'عینک', quantity: 2, line_total_rials: 40_000 },
],
};
beforeEach(() => {
get.mockReset();
});
function mockInvoice(invoice: object) {
get.mockResolvedValue({ success: true, data: { data: invoice } });
}
describe('InvoiceSummaryModal', () => {
it('با session کامل: جدول کالای مصرفی، پرداختی‌ها و تخفیف را نشان می‌دهد', async () => {
mockInvoice({ ...baseInvoice, session: fullSession });
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
await waitFor(() => expect(screen.getByText('اطلاعات فاکتور')).toBeInTheDocument());
// کالای مصرفی
expect(screen.getByText('اطلاعات کالای مصرفی')).toBeInTheDocument();
expect(screen.getByText('عینک')).toBeInTheDocument();
// پرداختی‌ها با label فارسی روش و ثبت‌کننده
expect(screen.getByText('پرداختی ها')).toBeInTheDocument();
expect(screen.getByText('پرداخت از کیف پول')).toBeInTheDocument();
expect(screen.getByText('منشی تست')).toBeInTheDocument();
// خلاصه مالی با ستون تخفیف و جمع کالا
expect(screen.getByText('تخفیف')).toBeInTheDocument();
expect(screen.getByText('جمع مبلغ کالا')).toBeInTheDocument();
// وضعیت — مبالغ واقعی (نمایش تومان = ریال ÷ ۱۰):
// پرداخت‌شده ۱۵۰٬۰۰۰ (هم در جدول پرداختی‌ها هم وضعیت) و باقی‌مانده ۵۴٬۰۰۰
// (۲۲۴۰۰۰۰ − ۲۰۰۰۰۰ تخفیف − ۱۵۰۰۰۰۰ پرداختی = ۵۴۰۰۰۰ ریال؛ final_price پیش از تخفیف است)
expect(screen.getAllByText(/۱۵۰٬۰۰۰/).length).toBeGreaterThanOrEqual(2);
expect(screen.getByText(/۵۴٬۰۰۰/)).toBeInTheDocument();
});
it('بدون session (فاکتور قدیمی): رفتار قبلی حفظ می‌شود', async () => {
mockInvoice({ ...baseInvoice, session: null });
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
await waitFor(() => expect(screen.getByText('اطلاعات فاکتور')).toBeInTheDocument());
// جدول‌های session-محور رندر نمی‌شوند
expect(screen.queryByText('اطلاعات کالای مصرفی')).not.toBeInTheDocument();
expect(screen.queryByText('پرداختی ها')).not.toBeInTheDocument();
// خلاصه مالی قدیمی با سهم بیمار
expect(screen.getByText('سهم بیمار')).toBeInTheDocument();
});
it('session با payments/consumables خالی: ردیف خط تیره', async () => {
mockInvoice({
...baseInvoice,
session: { ...fullSession, payments: [], consumables: [], paid_total_rials: 0, discount_rials: 0 },
});
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
await waitFor(() => expect(screen.getByText('اطلاعات فاکتور')).toBeInTheDocument());
expect(screen.getByText('اطلاعات کالای مصرفی')).toBeInTheDocument();
expect(screen.getByText('پرداختی ها')).toBeInTheDocument();
// ردیف‌های '-' برای هر دو جدول خالی + ستون تخفیف صفر
expect(screen.getAllByText('-').length).toBeGreaterThanOrEqual(8);
});
it('invoiceUuid=null: مودال بسته و بدون fetch', () => {
renderWithProviders(<InvoiceSummaryModal invoiceUuid={null} onClose={() => {}} />);
expect(get).not.toHaveBeenCalled();
expect(screen.queryByText('خلاصه فاکتور')).not.toBeInTheDocument();
});
it('فاکتور بیمه‌ای: نوع خدمت، نام بیمه و سهم‌ها درج می‌شوند', async () => {
mockInvoice({
...baseInvoice,
base_insurance_rials: 1_785_600,
patient_rials: 4_166_400,
total_rials: 5_952_000,
service_category: 'inpatient',
service_category_label: 'خدمات بستری',
base_insurance_name: 'بیمه ایران',
items: [{ uuid: 'it1', title: 'ویزیت', quantity: 1, total_rials: 5_952_000, patient_rials: 4_166_400 }],
});
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
await waitFor(() => expect(screen.getByText('اطلاعات بیمه')).toBeInTheDocument());
expect(screen.getByText('نوع خدمت بیمه')).toBeInTheDocument();
expect(screen.getByText('خدمات بستری')).toBeInTheDocument();
expect(screen.getByText('بیمه ایران')).toBeInTheDocument();
});
/**
* ستون «ثبت‌کننده»: نامِ حل‌شدهٔ سرور، و لینک فقط وقتی بیننده صفحهٔ آن پروفایل را
* می‌تواند باز کند. عکسِ لحظهٔ ثبت (`created_by_name`) فقط fallback است.
*/
describe('ثبت‌کنندهٔ پرداخت', () => {
const withRecorder = (recorder: object | null) => ({
...baseInvoice,
session: {
...fullSession,
payments: [{
uuid: 'p1', method: 'wallet', amount_rials: 1_500_000, paid_at: 1700100000,
created_by_name: '09120000000', created_by: recorder,
}],
},
});
it('پزشکِ ثبت‌کننده به پروفایل پزشک لینک می‌شود', async () => {
useAuthStore.setState({ primaryRole: 'clinic' } as any);
mockInvoice(withRecorder({ user_uuid: 'u1', name: 'دکتر رضایی', role: 'doctor', doctor_uuid: 'doc-9' }));
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
const link = await screen.findByRole('link', { name: 'دکتر رضایی' });
expect(link).toHaveAttribute('href', '/admin/doctors/doc-9');
// نامِ زنده جای شمارهٔ ذخیره‌شده می‌نشیند.
expect(screen.queryByText('09120000000')).toBeNull();
});
it('منشیِ ثبت‌کننده برای کلینیک به فهرست منشی‌های خودش لینک می‌شود', async () => {
useAuthStore.setState({ primaryRole: 'clinic' } as any);
mockInvoice(withRecorder({ user_uuid: 'u2', name: 'منشی مدیسا', role: 'secretary', doctor_uuid: null }));
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
const link = await screen.findByRole('link', { name: 'منشی مدیسا' });
expect(link).toHaveAttribute('href', '/admin/my-secretaries');
});
it('بیننده‌ای که آن صفحه را ندارد، فقط نام می‌بیند نه لینک', async () => {
useAuthStore.setState({ primaryRole: 'secretary' } as any);
mockInvoice(withRecorder({ user_uuid: 'u2', name: 'منشی مدیسا', role: 'secretary', doctor_uuid: null }));
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
expect(await screen.findByText('منشی مدیسا')).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'منشی مدیسا' })).toBeNull();
});
it('بدون created_by (کاربر حذف‌شده): همان عکسِ ذخیره‌شده، بدون لینک', async () => {
useAuthStore.setState({ primaryRole: 'clinic' } as any);
mockInvoice(withRecorder(null));
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
expect(await screen.findByText('09120000000')).toBeInTheDocument();
expect(screen.queryByRole('link', { name: '09120000000' })).toBeNull();
});
});
it('فاکتور بدون بیمه، جدول بیمه ندارد', async () => {
mockInvoice(baseInvoice);
renderWithProviders(<InvoiceSummaryModal invoiceUuid="iv1" onClose={() => {}} />);
await waitFor(() => expect(screen.getByText('اطلاعات فاکتور')).toBeInTheDocument());
expect(screen.queryByText('اطلاعات بیمه')).not.toBeInTheDocument();
});
});