fix: issue invoice on demand so view-invoice buttons actually work
The admin SPA never called POST /billing/invoices, so every session had invoice_uuid = null and the view-invoice buttons on the patient services tab (and MyPatients visit modal) were permanently disabled. Add a shared useIssueInvoice hook (idempotent create + finalize when draft) and wire it into: - DetailsStep: the wizard's final step now really issues the invoice - SessionServiceCard / PatientDetailPage: clicking view-invoice on a session without an invoice issues it first, then opens the summary - MyPatientsPage visit modal: same, replacing the dead disabled button Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen, fireEvent, waitFor } 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 { toast } from 'sonner';
|
||||
import DetailsStep from './DetailsStep';
|
||||
import type { SessionCardData } from '../SessionServiceCard';
|
||||
|
||||
const post = api.post as ReturnType<typeof vi.fn>;
|
||||
|
||||
const session: SessionCardData = {
|
||||
uuid: 's1',
|
||||
services: [{ service_name: 'لیزر' }],
|
||||
doctor_name: 'دکتر فتحی',
|
||||
final_price_rials: 1_000_000,
|
||||
patient_debt_rials: 0,
|
||||
discount_rials: 0,
|
||||
visit_price_rials: 0,
|
||||
payments: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
post.mockReset();
|
||||
(toast.error as ReturnType<typeof vi.fn>).mockReset();
|
||||
});
|
||||
|
||||
describe('DetailsStep — صدور فاکتور', () => {
|
||||
it('فاکتور را میسازد، draft را finalize میکند و بعد onFinish', async () => {
|
||||
post.mockImplementation((url: string) =>
|
||||
url === '/api/v1/billing/invoices'
|
||||
? Promise.resolve({ success: true, data: { data: { uuid: 'iv1', status: 'draft' } } })
|
||||
: Promise.resolve({ success: true, data: {} }),
|
||||
);
|
||||
const onFinish = vi.fn();
|
||||
renderWithProviders(<DetailsStep session={session} recordUuid="r1" onBack={() => {}} onFinish={onFinish} />);
|
||||
|
||||
fireEvent.click(screen.getByText('صدور فاکتور'));
|
||||
|
||||
await waitFor(() => expect(onFinish).toHaveBeenCalled());
|
||||
expect(post).toHaveBeenCalledWith('/api/v1/billing/invoices', { session_uuid: 's1' });
|
||||
expect(post).toHaveBeenCalledWith('/api/v1/billing/invoices/iv1/finalize', {});
|
||||
});
|
||||
|
||||
it('فاکتور غیر draft (idempotent create) دیگر finalize نمیشود', async () => {
|
||||
post.mockResolvedValue({ success: true, data: { data: { uuid: 'iv1', status: 'finalized' } } });
|
||||
const onFinish = vi.fn();
|
||||
renderWithProviders(<DetailsStep session={session} onBack={() => {}} onFinish={onFinish} />);
|
||||
|
||||
fireEvent.click(screen.getByText('صدور فاکتور'));
|
||||
|
||||
await waitFor(() => expect(onFinish).toHaveBeenCalled());
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('session با invoice_uuid موجود: بدون درخواست، مستقیم onFinish', async () => {
|
||||
const onFinish = vi.fn();
|
||||
renderWithProviders(
|
||||
<DetailsStep session={{ ...session, invoice_uuid: 'iv0' }} onBack={() => {}} onFinish={onFinish} />,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('صدور فاکتور'));
|
||||
|
||||
await waitFor(() => expect(onFinish).toHaveBeenCalled());
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('خطای صدور: toast خطا و onFinish صدا زده نمیشود', async () => {
|
||||
post.mockRejectedValue(new Error('boom'));
|
||||
const onFinish = vi.fn();
|
||||
renderWithProviders(<DetailsStep session={session} onBack={() => {}} onFinish={onFinish} />);
|
||||
|
||||
fireEvent.click(screen.getByText('صدور فاکتور'));
|
||||
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('صدور فاکتور ناموفق بود'));
|
||||
expect(onFinish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user