import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock apiClient vi.mock('../client', () => ({ default: { get: vi.fn(), post: vi.fn(), }, })); import { getQuotaStatus, getQuotaResources, archiveResources, unarchiveResource, getOverageDetail, QuotaStatus, QuotaResourcesResponse, ArchiveResponse, QuotaOverageDetail, } from '../quota'; import apiClient from '../client'; describe('quota API', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getQuotaStatus', () => { it('fetches quota status from API', async () => { const mockQuotaStatus: QuotaStatus = { active_overages: [ { id: 1, quota_type: 'resources', display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 7, grace_period_ends_at: '2025-12-14T00:00:00Z', }, ], usage: { resources: { current: 15, limit: 10, display_name: 'Resources', }, staff: { current: 3, limit: 5, display_name: 'Staff Members', }, services: { current: 8, limit: 20, display_name: 'Services', }, }, }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockQuotaStatus }); const result = await getQuotaStatus(); expect(apiClient.get).toHaveBeenCalledWith('/quota/status/'); expect(result).toEqual(mockQuotaStatus); expect(result.active_overages).toHaveLength(1); expect(result.usage.resources.current).toBe(15); }); it('returns empty active_overages when no overages exist', async () => { const mockQuotaStatus: QuotaStatus = { active_overages: [], usage: { resources: { current: 5, limit: 10, display_name: 'Resources', }, }, }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockQuotaStatus }); const result = await getQuotaStatus(); expect(result.active_overages).toHaveLength(0); expect(result.usage.resources.current).toBeLessThan(result.usage.resources.limit); }); it('handles multiple quota types in usage', async () => { const mockQuotaStatus: QuotaStatus = { active_overages: [], usage: { resources: { current: 5, limit: 10, display_name: 'Resources', }, staff: { current: 2, limit: 5, display_name: 'Staff Members', }, services: { current: 15, limit: 20, display_name: 'Services', }, customers: { current: 100, limit: 500, display_name: 'Customers', }, }, }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockQuotaStatus }); const result = await getQuotaStatus(); expect(Object.keys(result.usage)).toHaveLength(4); expect(result.usage).toHaveProperty('resources'); expect(result.usage).toHaveProperty('staff'); expect(result.usage).toHaveProperty('services'); expect(result.usage).toHaveProperty('customers'); }); }); describe('getQuotaResources', () => { it('fetches resources for a specific quota type', async () => { const mockResourcesResponse: QuotaResourcesResponse = { quota_type: 'resources', resources: [ { id: 1, name: 'Conference Room A', type: 'room', created_at: '2025-01-01T10:00:00Z', is_archived: false, archived_at: null, }, { id: 2, name: 'Conference Room B', type: 'room', created_at: '2025-01-02T11:00:00Z', is_archived: false, archived_at: null, }, ], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourcesResponse }); const result = await getQuotaResources('resources'); expect(apiClient.get).toHaveBeenCalledWith('/quota/resources/resources/'); expect(result).toEqual(mockResourcesResponse); expect(result.quota_type).toBe('resources'); expect(result.resources).toHaveLength(2); }); it('fetches staff members for staff quota type', async () => { const mockStaffResponse: QuotaResourcesResponse = { quota_type: 'staff', resources: [ { id: 10, name: 'John Doe', email: 'john@example.com', role: 'staff', created_at: '2025-01-15T09:00:00Z', is_archived: false, archived_at: null, }, { id: 11, name: 'Jane Smith', email: 'jane@example.com', role: 'manager', created_at: '2025-01-16T09:00:00Z', is_archived: false, archived_at: null, }, ], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockStaffResponse }); const result = await getQuotaResources('staff'); expect(apiClient.get).toHaveBeenCalledWith('/quota/resources/staff/'); expect(result.quota_type).toBe('staff'); expect(result.resources[0]).toHaveProperty('email'); expect(result.resources[0]).toHaveProperty('role'); }); it('fetches services for services quota type', async () => { const mockServicesResponse: QuotaResourcesResponse = { quota_type: 'services', resources: [ { id: 20, name: 'Haircut', duration: 30, price: '25.00', created_at: '2025-02-01T10:00:00Z', is_archived: false, archived_at: null, }, { id: 21, name: 'Color Treatment', duration: 90, price: '75.00', created_at: '2025-02-02T10:00:00Z', is_archived: false, archived_at: null, }, ], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockServicesResponse }); const result = await getQuotaResources('services'); expect(apiClient.get).toHaveBeenCalledWith('/quota/resources/services/'); expect(result.quota_type).toBe('services'); expect(result.resources[0]).toHaveProperty('duration'); expect(result.resources[0]).toHaveProperty('price'); }); it('includes archived resources', async () => { const mockResourcesResponse: QuotaResourcesResponse = { quota_type: 'resources', resources: [ { id: 1, name: 'Active Resource', created_at: '2025-01-01T10:00:00Z', is_archived: false, archived_at: null, }, { id: 2, name: 'Archived Resource', created_at: '2024-12-01T10:00:00Z', is_archived: true, archived_at: '2025-12-01T15:30:00Z', }, ], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockResourcesResponse }); const result = await getQuotaResources('resources'); expect(result.resources).toHaveLength(2); expect(result.resources[0].is_archived).toBe(false); expect(result.resources[1].is_archived).toBe(true); expect(result.resources[1].archived_at).toBe('2025-12-01T15:30:00Z'); }); it('handles empty resources list', async () => { const mockEmptyResponse: QuotaResourcesResponse = { quota_type: 'resources', resources: [], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockEmptyResponse }); const result = await getQuotaResources('resources'); expect(result.resources).toHaveLength(0); expect(result.quota_type).toBe('resources'); }); }); describe('archiveResources', () => { it('archives multiple resources successfully', async () => { const mockArchiveResponse: ArchiveResponse = { archived_count: 3, current_usage: 7, limit: 10, is_resolved: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockArchiveResponse }); const result = await archiveResources('resources', [1, 2, 3]); expect(apiClient.post).toHaveBeenCalledWith('/quota/archive/', { quota_type: 'resources', resource_ids: [1, 2, 3], }); expect(result).toEqual(mockArchiveResponse); expect(result.archived_count).toBe(3); expect(result.is_resolved).toBe(true); }); it('archives single resource', async () => { const mockArchiveResponse: ArchiveResponse = { archived_count: 1, current_usage: 9, limit: 10, is_resolved: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockArchiveResponse }); const result = await archiveResources('staff', [5]); expect(apiClient.post).toHaveBeenCalledWith('/quota/archive/', { quota_type: 'staff', resource_ids: [5], }); expect(result.archived_count).toBe(1); }); it('indicates overage is still not resolved after archiving', async () => { const mockArchiveResponse: ArchiveResponse = { archived_count: 2, current_usage: 12, limit: 10, is_resolved: false, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockArchiveResponse }); const result = await archiveResources('resources', [1, 2]); expect(result.is_resolved).toBe(false); expect(result.current_usage).toBeGreaterThan(result.limit); }); it('handles archiving with different quota types', async () => { const mockArchiveResponse: ArchiveResponse = { archived_count: 5, current_usage: 15, limit: 20, is_resolved: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockArchiveResponse }); await archiveResources('services', [10, 11, 12, 13, 14]); expect(apiClient.post).toHaveBeenCalledWith('/quota/archive/', { quota_type: 'services', resource_ids: [10, 11, 12, 13, 14], }); }); it('handles empty resource_ids array', async () => { const mockArchiveResponse: ArchiveResponse = { archived_count: 0, current_usage: 10, limit: 10, is_resolved: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockArchiveResponse }); const result = await archiveResources('resources', []); expect(apiClient.post).toHaveBeenCalledWith('/quota/archive/', { quota_type: 'resources', resource_ids: [], }); expect(result.archived_count).toBe(0); }); }); describe('unarchiveResource', () => { it('unarchives a resource successfully', async () => { const mockUnarchiveResponse = { success: true, resource_id: 5, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockUnarchiveResponse }); const result = await unarchiveResource('resources', 5); expect(apiClient.post).toHaveBeenCalledWith('/quota/unarchive/', { quota_type: 'resources', resource_id: 5, }); expect(result).toEqual(mockUnarchiveResponse); expect(result.success).toBe(true); expect(result.resource_id).toBe(5); }); it('unarchives staff member', async () => { const mockUnarchiveResponse = { success: true, resource_id: 10, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockUnarchiveResponse }); const result = await unarchiveResource('staff', 10); expect(apiClient.post).toHaveBeenCalledWith('/quota/unarchive/', { quota_type: 'staff', resource_id: 10, }); expect(result.success).toBe(true); }); it('unarchives service', async () => { const mockUnarchiveResponse = { success: true, resource_id: 20, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockUnarchiveResponse }); const result = await unarchiveResource('services', 20); expect(apiClient.post).toHaveBeenCalledWith('/quota/unarchive/', { quota_type: 'services', resource_id: 20, }); expect(result.resource_id).toBe(20); }); it('handles unsuccessful unarchive', async () => { const mockUnarchiveResponse = { success: false, resource_id: 5, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockUnarchiveResponse }); const result = await unarchiveResource('resources', 5); expect(result.success).toBe(false); }); }); describe('getOverageDetail', () => { it('fetches detailed overage information', async () => { const mockOverageDetail: QuotaOverageDetail = { id: 1, quota_type: 'resources', display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 7, grace_period_ends_at: '2025-12-14T00:00:00Z', status: 'active', created_at: '2025-12-07T10:00:00Z', initial_email_sent_at: '2025-12-07T10:05:00Z', week_reminder_sent_at: null, day_reminder_sent_at: null, archived_resource_ids: [], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockOverageDetail }); const result = await getOverageDetail(1); expect(apiClient.get).toHaveBeenCalledWith('/quota/overages/1/'); expect(result).toEqual(mockOverageDetail); expect(result.status).toBe('active'); expect(result.overage_amount).toBe(5); }); it('includes sent email timestamps', async () => { const mockOverageDetail: QuotaOverageDetail = { id: 2, quota_type: 'staff', display_name: 'Staff Members', current_usage: 8, allowed_limit: 5, overage_amount: 3, days_remaining: 3, grace_period_ends_at: '2025-12-10T00:00:00Z', status: 'active', created_at: '2025-11-30T10:00:00Z', initial_email_sent_at: '2025-11-30T10:05:00Z', week_reminder_sent_at: '2025-12-03T09:00:00Z', day_reminder_sent_at: '2025-12-06T09:00:00Z', archived_resource_ids: [], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockOverageDetail }); const result = await getOverageDetail(2); expect(result.initial_email_sent_at).toBe('2025-11-30T10:05:00Z'); expect(result.week_reminder_sent_at).toBe('2025-12-03T09:00:00Z'); expect(result.day_reminder_sent_at).toBe('2025-12-06T09:00:00Z'); }); it('includes archived resource IDs', async () => { const mockOverageDetail: QuotaOverageDetail = { id: 3, quota_type: 'resources', display_name: 'Resources', current_usage: 10, allowed_limit: 10, overage_amount: 0, days_remaining: 5, grace_period_ends_at: '2025-12-12T00:00:00Z', status: 'resolved', created_at: '2025-12-01T10:00:00Z', initial_email_sent_at: '2025-12-01T10:05:00Z', week_reminder_sent_at: null, day_reminder_sent_at: null, archived_resource_ids: [1, 3, 5, 7], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockOverageDetail }); const result = await getOverageDetail(3); expect(result.archived_resource_ids).toHaveLength(4); expect(result.archived_resource_ids).toEqual([1, 3, 5, 7]); expect(result.status).toBe('resolved'); }); it('handles resolved overage with zero overage_amount', async () => { const mockOverageDetail: QuotaOverageDetail = { id: 4, quota_type: 'services', display_name: 'Services', current_usage: 18, allowed_limit: 20, overage_amount: 0, days_remaining: 0, grace_period_ends_at: '2025-12-05T00:00:00Z', status: 'resolved', created_at: '2025-11-25T10:00:00Z', initial_email_sent_at: '2025-11-25T10:05:00Z', week_reminder_sent_at: '2025-11-28T09:00:00Z', day_reminder_sent_at: null, archived_resource_ids: [20, 21], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockOverageDetail }); const result = await getOverageDetail(4); expect(result.overage_amount).toBe(0); expect(result.status).toBe('resolved'); expect(result.current_usage).toBeLessThanOrEqual(result.allowed_limit); }); it('handles expired overage', async () => { const mockOverageDetail: QuotaOverageDetail = { id: 5, quota_type: 'resources', display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 0, grace_period_ends_at: '2025-12-06T00:00:00Z', status: 'expired', created_at: '2025-11-20T10:00:00Z', initial_email_sent_at: '2025-11-20T10:05:00Z', week_reminder_sent_at: '2025-11-27T09:00:00Z', day_reminder_sent_at: '2025-12-05T09:00:00Z', archived_resource_ids: [], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockOverageDetail }); const result = await getOverageDetail(5); expect(result.status).toBe('expired'); expect(result.days_remaining).toBe(0); expect(result.overage_amount).toBeGreaterThan(0); }); it('handles null email timestamps when no reminders sent', async () => { const mockOverageDetail: QuotaOverageDetail = { id: 6, quota_type: 'staff', display_name: 'Staff Members', current_usage: 6, allowed_limit: 5, overage_amount: 1, days_remaining: 14, grace_period_ends_at: '2025-12-21T00:00:00Z', status: 'active', created_at: '2025-12-07T10:00:00Z', initial_email_sent_at: null, week_reminder_sent_at: null, day_reminder_sent_at: null, archived_resource_ids: [], }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockOverageDetail }); const result = await getOverageDetail(6); expect(result.initial_email_sent_at).toBeNull(); expect(result.week_reminder_sent_at).toBeNull(); expect(result.day_reminder_sent_at).toBeNull(); }); }); });