# Repository Pattern

The Repository Pattern provides a clean abstraction layer for data access, separating business logic from data fetching concerns.

## Core Concepts

### What is a Repository?

A repository acts as an in-memory collection of domain objects. It encapsulates:
- Data fetching logic
- API endpoint configuration
- Request cancellation handling
- Error transformation
- Response mapping

### Why Use Repositories?

1. **Separation of Concerns**: Business logic doesn't know about HTTP details
2. **Testability**: Easy to mock for unit tests
3. **Consistency**: Standardized data access patterns
4. **Maintainability**: Centralized API configuration
5. **Request Cancellation**: Built-in AbortSignal support

## Base Repository Class

All repositories extend from `BaseRepository`:

```typescript
// src/services/repositories/BaseRepository.ts
export abstract class BaseRepository {
  protected abstract baseUrl: string;

  /**
   * ⚠️ CRITICAL: All methods MUST accept AbortSignal
   * This enables request cancellation when components unmount
   */

  protected async fetch<T>(
    endpoint: string,
    signal?: AbortSignal
  ): Promise<T> {
    const response = await apiClient.get(`${this.baseUrl}${endpoint}`, {
      signal
    });
    return response.data;
  }

  protected async post<T>(
    endpoint: string,
    data: unknown,
    signal?: AbortSignal
  ): Promise<T> {
    const response = await apiClient.post(
      `${this.baseUrl}${endpoint}`,
      data,
      { signal }
    );
    return response.data;
  }

  protected async put<T>(
    endpoint: string,
    data: unknown,
    signal?: AbortSignal
  ): Promise<T> {
    const response = await apiClient.put(
      `${this.baseUrl}${endpoint}`,
      data,
      { signal }
    );
    return response.data;
  }

  protected async delete<T>(
    endpoint: string,
    signal?: AbortSignal
  ): Promise<T> {
    const response = await apiClient.delete(`${this.baseUrl}${endpoint}`, {
      signal
    });
    return response.data;
  }
}
```

## Creating a Domain Repository

### Step 1: Define Your Entity Types

```typescript
// src/types/policy.ts
export interface Policy {
  id: string;
  number: string;
  customerId: string;
  type: 'auto' | 'home' | 'life';
  premium: number;
  startDate: string;
  endDate: string;
  status: 'active' | 'cancelled' | 'expired';
}

export interface CreatePolicyRequest {
  customerId: string;
  type: Policy['type'];
  premium: number;
  startDate: string;
  endDate: string;
}

export interface UpdatePolicyRequest {
  premium?: number;
  endDate?: string;
  status?: Policy['status'];
}
```

### Step 2: Create the Repository

```typescript
// src/services/repositories/PolicyRepository.ts
import { BaseRepository } from './BaseRepository';
import type { Policy, CreatePolicyRequest, UpdatePolicyRequest } from '@/types/policy';

class PolicyRepository extends BaseRepository {
  protected baseUrl = '/api/policies';

  async getByCustomerId(customerId: string, signal?: AbortSignal): Promise<Policy[]> {
    return this.fetch<Policy[]>(`/customer/${customerId}`, signal);
  }
  async getById(id: string, signal?: AbortSignal): Promise<Policy> {
    return this.fetch<Policy>(`/${id}`, signal);
  }
  async create(data: CreatePolicyRequest, signal?: AbortSignal): Promise<Policy> {
    return this.post<Policy>('', data, signal);
  }
  async update(id: string, data: UpdatePolicyRequest, signal?: AbortSignal): Promise<Policy> {
    return this.put<Policy>(`/${id}`, data, signal);
  }
  async cancel(id: string, signal?: AbortSignal): Promise<Policy> {
    return this.put<Policy>(`/${id}/cancel`, {}, signal);
  }
}

export const policyRepository = new PolicyRepository();
```

### Step 3: Use in Custom Hooks

Create hooks with `AbortController` for cleanup. Key pattern:

```typescript
export const usePolicies = (customerId: string) => {
  const [policies, setPolicies] = useState<Policy[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const ac = new AbortController();
    setLoading(true);
    policyRepository.getByCustomerId(customerId, ac.signal)
      .then(setPolicies)
      .catch(err => { if (err.name !== 'AbortError') setError(err); })
      .finally(() => setLoading(false));
    return () => ac.abort();
  }, [customerId]);

  return { policies, loading, error };
};
```

## Advanced Patterns

### Pattern 1: Query Parameters

Build `URLSearchParams` from filter object, append to endpoint path.

### Pattern 2: Pagination

Return `PaginatedResponse<T>` with `{ data: T[], total, page, pageSize, totalPages }`. Hook tracks `page` state and passes to `getPage()`.

### Pattern 3: File Upload

Use `FormData` with `this.post('/upload', formData, signal)`. Axios handles Content-Type. For downloads, use `apiClient.get` with `responseType: 'blob'`.

### Pattern 4: Data Transformation

Transform DTOs to domain models in repository methods:

```typescript
class PolicyRepository extends BaseRepository {
  protected baseUrl = '/api/policies';

  private transformPolicy(dto: PolicyDTO): Policy {
    return {
      ...dto,
      startDate: new Date(dto.startDate),
      endDate: new Date(dto.endDate)
    };
  }

  async getById(
    policyId: string,
    signal?: AbortSignal
  ): Promise<Policy> {
    const dto = await this.fetch<PolicyDTO>(`/${policyId}`, signal);
    return this.transformPolicy(dto);
  }

  async getAll(signal?: AbortSignal): Promise<Policy[]> {
    const dtos = await this.fetch<PolicyDTO[]>('', signal);
    return dtos.map(this.transformPolicy);
  }
}
```

## Testing Repositories

### Unit Testing with Mocks

```typescript
// src/services/repositories/__tests__/PolicyRepository.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { policyRepository } from '../PolicyRepository';
import { apiClient } from '@/config/apiClient';

vi.mock('@/config/apiClient');

describe('PolicyRepository', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getByCustomerId', () => {
    it('should fetch policies for a customer', async () => {
      const mockPolicies = [
        { id: '1', customerId: 'C123', number: 'POL-001' },
        { id: '2', customerId: 'C123', number: 'POL-002' }
      ];

      vi.mocked(apiClient.get).mockResolvedValue({
        data: mockPolicies
      });

      const result = await policyRepository.getByCustomerId('C123');

      expect(apiClient.get).toHaveBeenCalledWith(
        '/api/policies/customer/C123',
        { signal: undefined }
      );
      expect(result).toEqual(mockPolicies);
    });

  });
});
```

Integration tests: use MSW `setupServer` with `rest.get` handlers. Override handlers with `server.use()` for error scenarios.

## Best Practices

1. **Export singleton instances**: `export const policyRepository = new PolicyRepository();`
2. **Always accept AbortSignal** on every method
3. **Descriptive method names**: `getByCustomerId()`, `search()` — not `get()`, `fetch()`
4. **Type responses explicitly**: `Promise<Policy>` not implicit any
5. **Use URLSearchParams** for query parameters, never manual string concat

## Common Mistakes

- **Not passing AbortSignal**: Always create `AbortController` in hooks, abort on cleanup
- **Business logic in repositories**: Keep validation in hooks/components, repositories only fetch
- **Side effects in repositories**: No state updates, navigation, or dispatches — only data access

---

## Integración con TanStack Query

Los repositories son llamados desde hooks de TanStack Query:

```typescript
// hooks/useAccounts.ts
import { useQuery } from '@tanstack/react-query';
import { getAccounts } from '../services/repositories/accountsRepository';

export function useAccounts() {
  return useQuery({
    queryKey: ['accounts'],
    queryFn: ({ signal }) => getAccounts(signal),  // ← Repository
  });
}
```

**El repository NO maneja:**
- ❌ Estado (loading, error)
- ❌ Cache
- ❌ Re-fetching

**Eso es responsabilidad de TanStack Query.**

El repository SOLO:
- ✅ Llama a API o retorna mocks
- ✅ Transforma datos (mappers)
- ✅ Soporta AbortSignal para cancelación
