import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, waitFor, act } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; // Mock the payments API module vi.mock('../../api/payments', () => ({ getTransactions: vi.fn(), getTransaction: vi.fn(), getTransactionSummary: vi.fn(), getStripeCharges: vi.fn(), getStripePayouts: vi.fn(), getStripeBalance: vi.fn(), exportTransactions: vi.fn(), getTransactionDetail: vi.fn(), refundTransaction: vi.fn(), })); import { useTransactions, useTransaction, useTransactionSummary, useStripeCharges, useStripePayouts, useStripeBalance, useExportTransactions, useInvalidateTransactions, useTransactionDetail, useRefundTransaction, } from '../useTransactionAnalytics'; import * as paymentsApi from '../../api/payments'; // Create wrapper with fresh QueryClient for each test const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return function Wrapper({ children }: { children: React.ReactNode }) { return React.createElement(QueryClientProvider, { client: queryClient }, children); }; }; describe('useTransactionAnalytics hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useTransactions', () => { it('fetches paginated transaction list without filters', async () => { const mockTransactions = { results: [ { id: 1, business: 1, business_name: 'Test Business', stripe_payment_intent_id: 'pi_123', stripe_charge_id: 'ch_123', transaction_type: 'payment' as const, status: 'succeeded' as const, amount: 5000, amount_display: '$50.00', application_fee_amount: 150, fee_display: '$1.50', net_amount: 4850, currency: 'usd', customer_email: 'customer@example.com', customer_name: 'John Doe', created_at: '2025-12-07T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }, ], count: 1, page: 1, page_size: 20, total_pages: 1, }; vi.mocked(paymentsApi.getTransactions).mockResolvedValue({ data: mockTransactions } as any); const { result } = renderHook(() => useTransactions(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getTransactions).toHaveBeenCalledWith(undefined); expect(result.current.data).toEqual(mockTransactions); }); it('fetches transactions with filters', async () => { const mockTransactions = { results: [], count: 0, page: 1, page_size: 10, total_pages: 0, }; const filters = { start_date: '2025-12-01', end_date: '2025-12-07', status: 'succeeded' as const, transaction_type: 'payment' as const, page: 1, page_size: 10, }; vi.mocked(paymentsApi.getTransactions).mockResolvedValue({ data: mockTransactions } as any); const { result } = renderHook(() => useTransactions(filters), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getTransactions).toHaveBeenCalledWith(filters); expect(result.current.data).toEqual(mockTransactions); }); it('uses 30 second staleTime', async () => { vi.mocked(paymentsApi.getTransactions).mockResolvedValue({ data: { results: [] } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useTransactions(), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(['transactions', undefined]); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(['transactions', undefined]); expect(queryState?.dataUpdatedAt).toBeDefined(); }); it('creates different query keys for different filters', async () => { vi.mocked(paymentsApi.getTransactions).mockResolvedValue({ data: { results: [] } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const filters1 = { start_date: '2025-12-01' }; const filters2 = { start_date: '2025-12-07' }; const { rerender } = renderHook( ({ filters }) => useTransactions(filters), { wrapper, initialProps: { filters: filters1 }, } ); await waitFor(() => { expect(queryClient.getQueryState(['transactions', filters1])).toBeDefined(); }); rerender({ filters: filters2 }); await waitFor(() => { expect(queryClient.getQueryState(['transactions', filters2])).toBeDefined(); }); expect(paymentsApi.getTransactions).toHaveBeenCalledWith(filters1); expect(paymentsApi.getTransactions).toHaveBeenCalledWith(filters2); }); }); describe('useTransaction', () => { it('fetches a single transaction by ID', async () => { const mockTransaction = { id: 1, business: 1, business_name: 'Test Business', stripe_payment_intent_id: 'pi_123', stripe_charge_id: 'ch_123', transaction_type: 'payment' as const, status: 'succeeded' as const, amount: 5000, amount_display: '$50.00', application_fee_amount: 150, fee_display: '$1.50', net_amount: 4850, currency: 'usd', customer_email: 'customer@example.com', customer_name: 'John Doe', created_at: '2025-12-07T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }; vi.mocked(paymentsApi.getTransaction).mockResolvedValue({ data: mockTransaction } as any); const { result } = renderHook(() => useTransaction(1), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getTransaction).toHaveBeenCalledWith(1); expect(result.current.data).toEqual(mockTransaction); }); it('is disabled when ID is falsy', async () => { vi.mocked(paymentsApi.getTransaction).mockResolvedValue({ data: {} } as any); const { result } = renderHook(() => useTransaction(0), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isPending).toBe(true); }); expect(paymentsApi.getTransaction).not.toHaveBeenCalled(); }); }); describe('useTransactionSummary', () => { it('fetches transaction summary without filters', async () => { const mockSummary = { total_transactions: 100, total_volume: 50000, total_volume_display: '$500.00', total_fees: 1500, total_fees_display: '$15.00', net_revenue: 48500, net_revenue_display: '$485.00', successful_transactions: 95, failed_transactions: 3, refunded_transactions: 2, average_transaction: 500, average_transaction_display: '$5.00', }; vi.mocked(paymentsApi.getTransactionSummary).mockResolvedValue({ data: mockSummary } as any); const { result } = renderHook(() => useTransactionSummary(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getTransactionSummary).toHaveBeenCalledWith(undefined); expect(result.current.data).toEqual(mockSummary); }); it('fetches transaction summary with date filters', async () => { const mockSummary = { total_transactions: 50, total_volume: 25000, total_volume_display: '$250.00', total_fees: 750, total_fees_display: '$7.50', net_revenue: 24250, net_revenue_display: '$242.50', successful_transactions: 48, failed_transactions: 1, refunded_transactions: 1, average_transaction: 500, average_transaction_display: '$5.00', }; const filters = { start_date: '2025-12-01', end_date: '2025-12-07', }; vi.mocked(paymentsApi.getTransactionSummary).mockResolvedValue({ data: mockSummary } as any); const { result } = renderHook(() => useTransactionSummary(filters), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getTransactionSummary).toHaveBeenCalledWith(filters); expect(result.current.data).toEqual(mockSummary); }); it('uses 1 minute staleTime', async () => { vi.mocked(paymentsApi.getTransactionSummary).mockResolvedValue({ data: {} } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useTransactionSummary(), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(['transactionSummary', undefined]); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(['transactionSummary', undefined]); expect(queryState?.dataUpdatedAt).toBeDefined(); }); }); describe('useStripeCharges', () => { it('fetches Stripe charges with default limit', async () => { const mockCharges = { charges: [ { id: 'ch_123', amount: 5000, amount_display: '$50.00', amount_refunded: 0, currency: 'usd', status: 'succeeded', paid: true, refunded: false, description: 'Test charge', receipt_email: 'customer@example.com', receipt_url: 'https://stripe.com/receipt', created: 1733572800, payment_method_details: null, billing_details: null, }, ], has_more: false, }; vi.mocked(paymentsApi.getStripeCharges).mockResolvedValue({ data: mockCharges } as any); const { result } = renderHook(() => useStripeCharges(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getStripeCharges).toHaveBeenCalledWith(20); expect(result.current.data).toEqual(mockCharges); }); it('fetches Stripe charges with custom limit', async () => { const mockCharges = { charges: [], has_more: false, }; vi.mocked(paymentsApi.getStripeCharges).mockResolvedValue({ data: mockCharges } as any); const { result } = renderHook(() => useStripeCharges(50), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getStripeCharges).toHaveBeenCalledWith(50); }); it('uses 30 second staleTime', async () => { vi.mocked(paymentsApi.getStripeCharges).mockResolvedValue({ data: { charges: [] } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useStripeCharges(), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(['stripeCharges', 20]); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(['stripeCharges', 20]); expect(queryState?.dataUpdatedAt).toBeDefined(); }); }); describe('useStripePayouts', () => { it('fetches Stripe payouts with default limit', async () => { const mockPayouts = { payouts: [ { id: 'po_123', amount: 10000, amount_display: '$100.00', currency: 'usd', status: 'paid', arrival_date: 1733659200, created: 1733572800, description: 'STRIPE PAYOUT', destination: 'ba_123', failure_message: null, method: 'standard', type: 'bank_account', }, ], has_more: false, }; vi.mocked(paymentsApi.getStripePayouts).mockResolvedValue({ data: mockPayouts } as any); const { result } = renderHook(() => useStripePayouts(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getStripePayouts).toHaveBeenCalledWith(20); expect(result.current.data).toEqual(mockPayouts); }); it('fetches Stripe payouts with custom limit', async () => { const mockPayouts = { payouts: [], has_more: true, }; vi.mocked(paymentsApi.getStripePayouts).mockResolvedValue({ data: mockPayouts } as any); const { result } = renderHook(() => useStripePayouts(10), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getStripePayouts).toHaveBeenCalledWith(10); }); it('uses 30 second staleTime', async () => { vi.mocked(paymentsApi.getStripePayouts).mockResolvedValue({ data: { payouts: [] } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useStripePayouts(), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(['stripePayouts', 20]); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(['stripePayouts', 20]); expect(queryState?.dataUpdatedAt).toBeDefined(); }); }); describe('useStripeBalance', () => { it('fetches Stripe balance', async () => { const mockBalance = { available: [ { amount: 10000, currency: 'usd', amount_display: '$100.00', }, ], pending: [ { amount: 5000, currency: 'usd', amount_display: '$50.00', }, ], available_total: 10000, pending_total: 5000, }; vi.mocked(paymentsApi.getStripeBalance).mockResolvedValue({ data: mockBalance } as any); const { result } = renderHook(() => useStripeBalance(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getStripeBalance).toHaveBeenCalledTimes(1); expect(result.current.data).toEqual(mockBalance); }); it('uses 1 minute staleTime and 5 minute refetchInterval', async () => { vi.mocked(paymentsApi.getStripeBalance).mockResolvedValue({ data: { available: [], pending: [], available_total: 0, pending_total: 0, }, } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useStripeBalance(), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(['stripeBalance']); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(['stripeBalance']); expect(queryState?.dataUpdatedAt).toBeDefined(); }); }); describe('useExportTransactions', () => { beforeEach(() => { // Mock URL and Blob APIs global.URL.createObjectURL = vi.fn(() => 'blob:mock-url'); global.URL.revokeObjectURL = vi.fn(); // Spy on document methods vi.spyOn(document, 'createElement'); vi.spyOn(document.body, 'appendChild'); vi.spyOn(document.body, 'removeChild'); }); it('exports transactions as CSV', async () => { const mockBlob = { type: 'text/csv', size: 100 }; const mockResponse = { data: mockBlob, headers: { 'content-type': 'text/csv' }, }; vi.mocked(paymentsApi.exportTransactions).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useExportTransactions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ format: 'csv', start_date: '2025-12-01', end_date: '2025-12-07', }); }); expect(paymentsApi.exportTransactions).toHaveBeenCalledWith({ format: 'csv', start_date: '2025-12-01', end_date: '2025-12-07', }); }); it('exports transactions as XLSX', async () => { const mockBlob = { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size: 100, }; const mockResponse = { data: mockBlob, headers: { 'content-type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, }; vi.mocked(paymentsApi.exportTransactions).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useExportTransactions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ format: 'xlsx', }); }); expect(paymentsApi.exportTransactions).toHaveBeenCalledWith({ format: 'xlsx', }); }); it('exports transactions as PDF', async () => { const mockBlob = { type: 'application/pdf', size: 100 }; const mockResponse = { data: mockBlob, headers: { 'content-type': 'application/pdf' }, }; vi.mocked(paymentsApi.exportTransactions).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useExportTransactions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ format: 'pdf', include_details: true, }); }); expect(paymentsApi.exportTransactions).toHaveBeenCalledWith({ format: 'pdf', include_details: true, }); }); it('exports transactions as QuickBooks IIF', async () => { const mockBlob = { type: 'text/plain', size: 100 }; const mockResponse = { data: mockBlob, headers: { 'content-type': 'text/plain' }, }; vi.mocked(paymentsApi.exportTransactions).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useExportTransactions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ format: 'quickbooks', }); }); expect(paymentsApi.exportTransactions).toHaveBeenCalledWith({ format: 'quickbooks', }); }); it('triggers file download on success', async () => { const mockBlob = { type: 'text/csv', size: 100 }; const mockResponse = { data: mockBlob, headers: { 'content-type': 'text/csv' }, }; vi.mocked(paymentsApi.exportTransactions).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useExportTransactions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ format: 'csv' }); }); expect(global.URL.createObjectURL).toHaveBeenCalled(); expect(document.createElement).toHaveBeenCalledWith('a'); expect(document.body.appendChild).toHaveBeenCalled(); expect(document.body.removeChild).toHaveBeenCalled(); expect(global.URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url'); }); it('uses correct file extension for each format', async () => { const mockBlob = { type: 'text/plain', size: 100 }; const mockResponse = { data: mockBlob, headers: { 'content-type': 'text/plain' }, }; vi.mocked(paymentsApi.exportTransactions).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useExportTransactions(), { wrapper: createWrapper(), }); // CSV await act(async () => { await result.current.mutateAsync({ format: 'csv' }); }); const csvLink = (document.createElement as any).mock.results.find((r: any) => r.value?.download?.endsWith('.csv'))?.value; expect(csvLink?.download).toBe('transactions.csv'); // XLSX await act(async () => { await result.current.mutateAsync({ format: 'xlsx' }); }); const xlsxLink = (document.createElement as any).mock.results.find((r: any) => r.value?.download?.endsWith('.xlsx'))?.value; expect(xlsxLink?.download).toBe('transactions.xlsx'); // PDF await act(async () => { await result.current.mutateAsync({ format: 'pdf' }); }); const pdfLink = (document.createElement as any).mock.results.find((r: any) => r.value?.download?.endsWith('.pdf'))?.value; expect(pdfLink?.download).toBe('transactions.pdf'); // QuickBooks await act(async () => { await result.current.mutateAsync({ format: 'quickbooks' }); }); const iifLink = (document.createElement as any).mock.results.find((r: any) => r.value?.download?.endsWith('.iif'))?.value; expect(iifLink?.download).toBe('transactions.iif'); }); }); describe('useInvalidateTransactions', () => { it('invalidates all transaction-related queries', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useInvalidateTransactions(), { wrapper }); act(() => { result.current(); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['transactions'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['transactionSummary'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['stripeCharges'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['stripePayouts'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['stripeBalance'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['transactionDetail'] }); expect(invalidateQueriesSpy).toHaveBeenCalledTimes(6); }); }); describe('useTransactionDetail', () => { it('fetches detailed transaction information', async () => { const mockDetailedTransaction = { id: 1, business: 1, business_name: 'Test Business', stripe_payment_intent_id: 'pi_123', stripe_charge_id: 'ch_123', transaction_type: 'payment' as const, status: 'partially_refunded' as const, amount: 5000, amount_display: '$50.00', application_fee_amount: 150, fee_display: '$1.50', net_amount: 4850, currency: 'usd', customer_email: 'customer@example.com', customer_name: 'John Doe', created_at: '2025-12-07T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', refunds: [ { id: 're_123', amount: 1000, amount_display: '$10.00', status: 'succeeded', reason: 'requested_by_customer', created: 1733572800, }, ], refundable_amount: 4000, total_refunded: 1000, can_refund: true, payment_method_info: { type: 'card', brand: 'visa', last4: '4242', exp_month: 12, exp_year: 2026, funding: 'credit', }, description: 'Payment for service', }; vi.mocked(paymentsApi.getTransactionDetail).mockResolvedValue({ data: mockDetailedTransaction, } as any); const { result } = renderHook(() => useTransactionDetail(1), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getTransactionDetail).toHaveBeenCalledWith(1); expect(result.current.data).toEqual(mockDetailedTransaction); }); it('returns null when ID is null', async () => { vi.mocked(paymentsApi.getTransactionDetail).mockResolvedValue({ data: null } as any); const { result } = renderHook(() => useTransactionDetail(null), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isPending).toBe(true); }); expect(paymentsApi.getTransactionDetail).not.toHaveBeenCalled(); }); it('is disabled when ID is null', async () => { vi.mocked(paymentsApi.getTransactionDetail).mockResolvedValue({ data: {} } as any); const { result } = renderHook(() => useTransactionDetail(null), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isPending).toBe(true); }); expect(paymentsApi.getTransactionDetail).not.toHaveBeenCalled(); }); it('uses 10 second staleTime for live data', async () => { vi.mocked(paymentsApi.getTransactionDetail).mockResolvedValue({ data: {} } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useTransactionDetail(1), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(['transactionDetail', 1]); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(['transactionDetail', 1]); expect(queryState?.dataUpdatedAt).toBeDefined(); }); }); describe('useRefundTransaction', () => { it('issues a full refund successfully', async () => { const mockRefundResponse = { success: true, refund_id: 're_123', amount: 5000, amount_display: '$50.00', status: 'succeeded', reason: null, transaction_status: 'refunded', }; vi.mocked(paymentsApi.refundTransaction).mockResolvedValue({ data: mockRefundResponse, } as any); const { result } = renderHook(() => useRefundTransaction(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync({ transactionId: 1, }); expect(response).toEqual(mockRefundResponse); }); expect(paymentsApi.refundTransaction).toHaveBeenCalledWith(1, undefined); }); it('issues a partial refund successfully', async () => { const mockRefundResponse = { success: true, refund_id: 're_456', amount: 1000, amount_display: '$10.00', status: 'succeeded', reason: 'requested_by_customer', transaction_status: 'partially_refunded', }; vi.mocked(paymentsApi.refundTransaction).mockResolvedValue({ data: mockRefundResponse, } as any); const { result } = renderHook(() => useRefundTransaction(), { wrapper: createWrapper(), }); const refundRequest = { amount: 1000, reason: 'requested_by_customer' as const, }; await act(async () => { const response = await result.current.mutateAsync({ transactionId: 1, request: refundRequest, }); expect(response).toEqual(mockRefundResponse); }); expect(paymentsApi.refundTransaction).toHaveBeenCalledWith(1, refundRequest); }); it('issues refund with metadata', async () => { const mockRefundResponse = { success: true, refund_id: 're_789', amount: 2000, amount_display: '$20.00', status: 'succeeded', reason: 'duplicate', transaction_status: 'partially_refunded', }; vi.mocked(paymentsApi.refundTransaction).mockResolvedValue({ data: mockRefundResponse, } as any); const { result } = renderHook(() => useRefundTransaction(), { wrapper: createWrapper(), }); const refundRequest = { amount: 2000, reason: 'duplicate' as const, metadata: { refund_reason: 'Duplicate charge', refunded_by: 'admin@example.com', }, }; await act(async () => { await result.current.mutateAsync({ transactionId: 1, request: refundRequest, }); }); expect(paymentsApi.refundTransaction).toHaveBeenCalledWith(1, refundRequest); }); it('invalidates relevant queries on success', async () => { vi.mocked(paymentsApi.refundTransaction).mockResolvedValue({ data: { success: true, refund_id: 're_123' }, } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useRefundTransaction(), { wrapper }); await act(async () => { await result.current.mutateAsync({ transactionId: 1, }); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['transactions'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['transactionSummary'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['transactionDetail', 1] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['stripeCharges'] }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['stripeBalance'] }); }); it('handles refund errors', async () => { const mockError = new Error('Refund failed: Insufficient funds'); vi.mocked(paymentsApi.refundTransaction).mockRejectedValue(mockError); const { result } = renderHook(() => useRefundTransaction(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync({ transactionId: 1, }); // If no error thrown, fail the test throw new Error('Expected mutation to fail'); } catch (error) { expect(error).toEqual(mockError); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); });