Files
hamedandClaude Opus 4.8 818506bf36 refactor(billing): rebuild payments list as flat invoice list (tauri parity)
Align /admin/my-payments with the tauri /payments source (per user): the list
is now a flat, newest-first list of the tenant's recorded invoices — one row
per invoice — instead of the per-patient aggregation built from Figma.

Backend:
- replace InvoiceRepository::patientPaymentSummary aggregation with
  tenantInvoices/countTenantInvoices (flat, joins patient name/national code).
- InvoiceService::patientPaymentList → tenantInvoiceList.
- BillingController: GET /api/v1/my/billing/patient-payments →
  GET /api/v1/my/billing/payments returning
  { invoice_uuid, patient_uuid, patient_name, national_code, issued_at,
    amount_rials, status } rows.
- node-2 patient invoices endpoint unchanged.

Frontend:
- useMyPayments: usePatientPayments → usePayments (flat PaymentRow).
- MyPaymentsPage columns match tauri DetailT: row #, avatar+name, national
  code, date-time, amount paid, مشاهده (no status column); 'اضافه کردن بیمار'
  links to /admin/patients/new. Filters (national code / status / Jalali date
  range) kept.

Tests + docs/api/billing.md updated. Intentionally omitted tauri extras:
mobile Cards view and the advanced ModalFilter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 19:00:17 +03:30

58 lines
2.2 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
vi.mock('../lib/api', () => ({
api: { get: vi.fn() },
ApiError: class extends Error {},
}));
import { api } from '../lib/api';
import { usePayments, usePatientInvoices } from './useMyPayments';
const get = api.get as ReturnType<typeof vi.fn>;
function wrapper({ children }: { children: React.ReactNode }) {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}
beforeEach(() => {
get.mockReset();
get.mockResolvedValue({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } });
});
describe('useMyPayments', () => {
it('builds the list URL with all active filters', async () => {
renderHook(() => usePayments({ page: 2, national_code: '1744', status: 'paid', from: 100, to: 200 }), { wrapper });
await waitFor(() => expect(get).toHaveBeenCalled());
const url = get.mock.calls[0][0] as string;
expect(url).toContain('/api/v1/my/billing/payments?');
expect(url).toContain('page=2');
expect(url).toContain('national_code=1744');
expect(url).toContain('status=paid');
expect(url).toContain('from=100');
expect(url).toContain('to=200');
});
it('omits empty filters from the list URL', async () => {
renderHook(() => usePayments({ page: 1 }), { wrapper });
await waitFor(() => expect(get).toHaveBeenCalled());
const url = get.mock.calls[0][0] as string;
expect(url).not.toContain('national_code=');
expect(url).not.toContain('status=');
});
it('does not fetch invoices until a patient uuid is provided', async () => {
const { rerender } = renderHook(({ uuid }: { uuid?: string }) => usePatientInvoices(uuid, 1), {
wrapper, initialProps: { uuid: undefined as string | undefined },
});
expect(get).not.toHaveBeenCalled();
rerender({ uuid: 'abc' });
await waitFor(() => expect(get).toHaveBeenCalled());
expect(get.mock.calls[0][0]).toContain('/api/v1/my/billing/patients/abc/invoices');
});
});