# 🔌 Integração Pluggy

Documentação completa da integração com a API do Pluggy para dados bancários e financeiros.

## 🎯 Visão Geral

O módulo Pluggy fornece uma interface simplificada para interagir com a API do Pluggy, convertido do controller Laravel original. Permite acesso a dados bancários, transações e informações financeiras através de uma API unificada.

### ✨ Funcionalidades

- 🔄 **Requisições Genéricas** - Endpoint universal para qualquer chamada Pluggy
- 🔗 **Connect Tokens** - Geração de tokens para conexão de contas
- 📡 **Webhooks** - Processamento de eventos do Pluggy
- 🔐 **Cache Inteligente** - Cache de API keys por 50 minutos
- 📝 **Logs Detalhados** - Rastreamento completo de operações
- 🛡️ **Tratamento de Erros** - Gestão robusta de falhas

## ⚙️ Configuração

### Variáveis de Ambiente

```env
# Credenciais do Pluggy
PLUGGY_CLIENT_ID=seu_client_id_aqui
PLUGGY_CLIENT_SECRET=seu_client_secret_aqui
PLUGGY_API_URL=https://api.pluggy.ai
```

### Módulo NestJS

```typescript
@Module({
  imports: [HttpModule],
  controllers: [PluggyController],
  providers: [PluggyService],
  exports: [PluggyService],
})
export class PluggyModule {}
```

## 🚀 Endpoints Disponíveis

### 1. Requisição Genérica

```http
POST /pluggy
Content-Type: application/json

{
  "endpoint": "items",
  "method": "GET",
  "param1": "value1",
  "param2": "value2"
}
```

**Parâmetros Obrigatórios**:
- `endpoint`: Endpoint da API Pluggy (ex: "items", "accounts", "transactions")
- `method`: Método HTTP (GET, POST, PUT, DELETE, PATCH)

**Parâmetros Opcionais**:
- Qualquer outro parâmetro será enviado como query params (GET) ou body (outros métodos)

**Exemplo - Listar Items**:
```json
{
  "endpoint": "items",
  "method": "GET",
  "limit": 10,
  "offset": 0
}
```

**Exemplo - Criar Item**:
```json
{
  "endpoint": "items",
  "method": "POST",
  "name": "Minha Conta",
  "connector": {
    "id": 201
  },
  "credentials": {
    "user": "usuario@email.com",
    "password": "senha123"
  }
}
```

### 2. Connect Token

```http
POST /pluggy/connect_token
Content-Type: application/json

{
  "itemId": "optional_item_id"
}
```

**Parâmetros**:
- `itemId` (opcional): ID do item para o qual gerar o token

**Resposta**:
```json
{
  "accessToken": "pluggy_connect_token_here",
  "expiresIn": 3600,
  "itemId": "item_123"
}
```

### 3. Webhook

```http
POST /pluggy/webhook
Content-Type: application/json

{
  "event": "item.updated",
  "data": {
    "itemId": "item_123",
    "status": "UPDATED",
    "executionStatus": "SUCCESS"
  }
}
```

**Eventos Suportados**:
- `item.created`: Item criado com sucesso
- `item.updated`: Item atualizado
- `item.error`: Erro no processamento do item
- `account.updated`: Conta atualizada
- `transaction.created`: Nova transação detectada

## 🔧 Implementação do Serviço

### PluggyService

```typescript
@Injectable()
export class PluggyService {
  private readonly logger = new Logger(PluggyService.name);
  private apiKeyCache: { key: string; expiresAt: Date } | null = null;

  constructor(private readonly httpService: HttpService) {}

  // Obter API key com cache
  async getApiKey(): Promise<string> {
    if (this.apiKeyCache && this.apiKeyCache.expiresAt > new Date()) {
      return this.apiKeyCache.key;
    }

    const response = await this.httpService.post(
      `${process.env.PLUGGY_API_URL}/auth`,
      {
        clientId: process.env.PLUGGY_CLIENT_ID,
        clientSecret: process.env.PLUGGY_CLIENT_SECRET,
      }
    ).toPromise();

    const apiKey = response.data.apiKey;
    const expiresAt = new Date(Date.now() + 50 * 60 * 1000); // 50 minutos

    this.apiKeyCache = { key: apiKey, expiresAt };
    this.logger.log('API key cached successfully');

    return apiKey;
  }

  // Requisição genérica para Pluggy
  async makeRequest(
    endpoint: string,
    method: string,
    params: Record<string, any> = {}
  ): Promise<any> {
    const apiKey = await this.getApiKey();
    const url = `${process.env.PLUGGY_API_URL}/${endpoint}`;

    const config: any = {
      method: method.toUpperCase(),
      url,
      headers: {
        'X-API-KEY': apiKey,
        'Content-Type': 'application/json',
      },
    };

    if (method.toUpperCase() === 'GET') {
      config.params = params;
    } else {
      config.data = params;
    }

    try {
      this.logger.log(`Making ${method} request to ${endpoint}`);
      const response = await this.httpService.request(config).toPromise();
      this.logger.log(`Request successful: ${method} ${endpoint}`);
      return response.data;
    } catch (error) {
      this.logger.error(`Request failed: ${method} ${endpoint}`, error.message);
      throw new BadRequestException(`Pluggy API error: ${error.message}`);
    }
  }

  // Obter connect token
  async getConnectToken(itemId?: string): Promise<any> {
    const params = itemId ? { itemId } : {};
    return this.makeRequest('connect_token', 'POST', params);
  }

  // Processar webhook
  async processWebhook(webhookData: any): Promise<any> {
    this.logger.log('Processing webhook:', webhookData);

    const { event, data } = webhookData;

    switch (event) {
      case 'item.created':
        return this.handleItemCreated(data);
      case 'item.updated':
        return this.handleItemUpdated(data);
      case 'item.error':
        return this.handleItemError(data);
      case 'account.updated':
        return this.handleAccountUpdated(data);
      case 'transaction.created':
        return this.handleTransactionCreated(data);
      default:
        this.logger.warn(`Unknown webhook event: ${event}`);
        return { message: 'Event processed', event };
    }
  }

  private async handleItemCreated(data: any) {
    this.logger.log('Item created:', data.itemId);
    // Lógica específica para item criado
    return { message: 'Item created successfully', itemId: data.itemId };
  }

  private async handleItemUpdated(data: any) {
    this.logger.log('Item updated:', data.itemId);
    // Lógica específica para item atualizado
    return { message: 'Item updated successfully', itemId: data.itemId };
  }

  private async handleItemError(data: any) {
    this.logger.error('Item error:', data);
    // Lógica específica para erro no item
    return { message: 'Item error processed', error: data };
  }

  private async handleAccountUpdated(data: any) {
    this.logger.log('Account updated:', data.accountId);
    // Lógica específica para conta atualizada
    return { message: 'Account updated successfully', accountId: data.accountId };
  }

  private async handleTransactionCreated(data: any) {
    this.logger.log('Transaction created:', data.transactionId);
    // Lógica específica para nova transação
    return { message: 'Transaction processed', transactionId: data.transactionId };
  }
}
```

### PluggyController

```typescript
@Controller('pluggy')
export class PluggyController {
  private readonly logger = new Logger(PluggyController.name);

  constructor(private readonly pluggyService: PluggyService) {}

  @Post()
  async makeGenericRequest(@Body() body: PluggyRequestDto) {
    this.logger.log(`Generic request: ${body.method} ${body.endpoint}`);
    
    const { endpoint, method, ...params } = body;
    return this.pluggyService.makeRequest(endpoint, method, params);
  }

  @Post('connect_token')
  async getConnectToken(@Body() body: ConnectTokenDto) {
    this.logger.log('Connect token request:', body);
    return this.pluggyService.getConnectToken(body.itemId);
  }

  @Post('webhook')
  async processWebhook(@Body() webhookData: any) {
    this.logger.log('Webhook received:', webhookData.event);
    return this.pluggyService.processWebhook(webhookData);
  }
}
```

## 📝 DTOs e Validação

### PluggyRequestDto

```typescript
export class PluggyRequestDto {
  @IsString()
  @IsNotEmpty()
  endpoint: string;

  @IsString()
  @IsIn(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
  method: string;

  // Permite propriedades adicionais para parâmetros dinâmicos
  [key: string]: any;
}
```

### ConnectTokenDto

```typescript
export class ConnectTokenDto {
  @IsOptional()
  @IsString()
  itemId?: string;
}
```

### WebhookDto

```typescript
export class WebhookDto {
  @IsString()
  @IsNotEmpty()
  event: string;

  @IsObject()
  data: any;
}
```

## 🧪 Exemplos de Uso

### Frontend JavaScript

```javascript
// Listar items do usuário
const listItems = async () => {
  const response = await fetch('/pluggy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      endpoint: 'items',
      method: 'GET',
      limit: 10
    })
  });
  
  const items = await response.json();
  console.log('Items:', items);
};

// Obter contas de um item
const getAccounts = async (itemId) => {
  const response = await fetch('/pluggy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      endpoint: 'accounts',
      method: 'GET',
      itemId: itemId
    })
  });
  
  const accounts = await response.json();
  console.log('Accounts:', accounts);
};

// Obter transações
const getTransactions = async (accountId) => {
  const response = await fetch('/pluggy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      endpoint: 'transactions',
      method: 'GET',
      accountId: accountId,
      from: '2024-01-01',
      to: '2024-12-31'
    })
  });
  
  const transactions = await response.json();
  console.log('Transactions:', transactions);
};

// Obter connect token
const getConnectToken = async () => {
  const response = await fetch('/pluggy/connect_token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({})
  });
  
  const tokenData = await response.json();
  console.log('Connect Token:', tokenData.accessToken);
};
```

### Comandos cURL

```bash
# Listar conectores disponíveis
curl -X POST "http://localhost:3333/pluggy" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "connectors",
    "method": "GET",
    "sandbox": true
  }'

# Criar um item (conectar conta)
curl -X POST "http://localhost:3333/pluggy" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "items",
    "method": "POST",
    "connector": { "id": 201 },
    "credentials": {
      "user": "user-ok",
      "password": "password-ok"
    }
  }'

# Obter connect token
curl -X POST "http://localhost:3333/pluggy/connect_token" \
  -H "Content-Type: application/json" \
  -d '{
    "itemId": "item_123"
  }'

# Simular webhook
curl -X POST "http://localhost:3333/pluggy/webhook" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "item.updated",
    "data": {
      "itemId": "item_123",
      "status": "UPDATED",
      "executionStatus": "SUCCESS"
    }
  }'
```

## 🔐 Cache de API Key

### Implementação

```typescript
interface ApiKeyCache {
  key: string;
  expiresAt: Date;
}

class PluggyService {
  private apiKeyCache: ApiKeyCache | null = null;

  async getApiKey(): Promise<string> {
    // Verificar se tem cache válido
    if (this.apiKeyCache && this.apiKeyCache.expiresAt > new Date()) {
      this.logger.log('Using cached API key');
      return this.apiKeyCache.key;
    }

    // Solicitar nova API key
    this.logger.log('Requesting new API key from Pluggy');
    const response = await this.authenticateWithPluggy();
    
    // Cachear por 50 minutos (token expira em 1 hora)
    this.apiKeyCache = {
      key: response.data.apiKey,
      expiresAt: new Date(Date.now() + 50 * 60 * 1000)
    };

    return this.apiKeyCache.key;
  }

  private async authenticateWithPluggy() {
    return this.httpService.post(
      `${process.env.PLUGGY_API_URL}/auth`,
      {
        clientId: process.env.PLUGGY_CLIENT_ID,
        clientSecret: process.env.PLUGGY_CLIENT_SECRET,
      }
    ).toPromise();
  }
}
```

### Vantagens do Cache

- ✅ **Performance** - Evita requisições desnecessárias
- ✅ **Rate Limiting** - Reduz chamadas à API de autenticação
- ✅ **Confiabilidade** - Menos pontos de falha
- ✅ **Custo** - Reduz uso da API Pluggy

## 🚨 Tratamento de Erros

### Tipos de Erro

```typescript
// Erro de autenticação
catch (error) {
  if (error.response?.status === 401) {
    this.logger.error('Pluggy authentication failed');
    this.apiKeyCache = null; // Limpar cache
    throw new UnauthorizedException('Pluggy authentication failed');
  }
}

// Erro de rate limiting
if (error.response?.status === 429) {
  this.logger.warn('Pluggy rate limit exceeded');
  throw new TooManyRequestsException('Rate limit exceeded');
}

// Erro de dados inválidos
if (error.response?.status === 400) {
  this.logger.error('Invalid data sent to Pluggy:', error.response.data);
  throw new BadRequestException(`Invalid data: ${error.response.data.message}`);
}

// Erro interno do Pluggy
if (error.response?.status >= 500) {
  this.logger.error('Pluggy internal error:', error.response.data);
  throw new ServiceUnavailableException('Pluggy service temporarily unavailable');
}
```

### Retry Logic

```typescript
async makeRequestWithRetry(
  endpoint: string,
  method: string,
  params: any,
  maxRetries = 3
): Promise<any> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await this.makeRequest(endpoint, method, params);
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }

      if (error.response?.status === 401) {
        // Limpar cache de API key em caso de 401
        this.apiKeyCache = null;
      }

      // Backoff exponencial
      const delay = Math.pow(2, attempt) * 1000;
      this.logger.warn(`Attempt ${attempt} failed, retrying in ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

## 📊 Logs e Monitoramento

### Logs Estruturados

```typescript
// Log de requisição
this.logger.log('Pluggy request', {
  endpoint,
  method,
  params: Object.keys(params),
  timestamp: new Date().toISOString()
});

// Log de resposta
this.logger.log('Pluggy response', {
  endpoint,
  method,
  statusCode: response.status,
  responseTime: Date.now() - startTime,
  dataSize: JSON.stringify(response.data).length
});

// Log de erro
this.logger.error('Pluggy error', {
  endpoint,
  method,
  error: error.message,
  statusCode: error.response?.status,
  responseData: error.response?.data
});
```

### Métricas

```typescript
interface PluggyMetrics {
  totalRequests: number;
  successfulRequests: number;
  failedRequests: number;
  averageResponseTime: number;
  cacheHits: number;
  cacheMisses: number;
}

@Injectable()
export class PluggyMetricsService {
  private metrics: PluggyMetrics = {
    totalRequests: 0,
    successfulRequests: 0,
    failedRequests: 0,
    averageResponseTime: 0,
    cacheHits: 0,
    cacheMisses: 0
  };

  recordRequest(success: boolean, responseTime: number) {
    this.metrics.totalRequests++;
    
    if (success) {
      this.metrics.successfulRequests++;
    } else {
      this.metrics.failedRequests++;
    }

    // Calcular média móvel do tempo de resposta
    this.metrics.averageResponseTime = 
      (this.metrics.averageResponseTime + responseTime) / 2;
  }

  recordCacheHit() {
    this.metrics.cacheHits++;
  }

  recordCacheMiss() {
    this.metrics.cacheMisses++;
  }

  getMetrics(): PluggyMetrics {
    return { ...this.metrics };
  }
}
```

## 🧪 Testes

### Testes Unitários

```typescript
describe('PluggyService', () => {
  let service: PluggyService;
  let httpService: HttpService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        PluggyService,
        {
          provide: HttpService,
          useValue: {
            post: jest.fn(),
            request: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<PluggyService>(PluggyService);
    httpService = module.get<HttpService>(HttpService);
  });

  describe('getApiKey', () => {
    it('should cache API key', async () => {
      const mockResponse = { data: { apiKey: 'test-key' } };
      jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse));

      const key1 = await service.getApiKey();
      const key2 = await service.getApiKey();

      expect(key1).toBe('test-key');
      expect(key2).toBe('test-key');
      expect(httpService.post).toHaveBeenCalledTimes(1);
    });
  });

  describe('makeRequest', () => {
    it('should make GET request with params', async () => {
      const mockApiKey = 'test-key';
      const mockResponse = { data: { items: [] } };
      
      jest.spyOn(service, 'getApiKey').mockResolvedValue(mockApiKey);
      jest.spyOn(httpService, 'request').mockReturnValue(of(mockResponse));

      const result = await service.makeRequest('items', 'GET', { limit: 10 });

      expect(result).toEqual({ items: [] });
      expect(httpService.request).toHaveBeenCalledWith({
        method: 'GET',
        url: `${process.env.PLUGGY_API_URL}/items`,
        headers: {
          'X-API-KEY': mockApiKey,
          'Content-Type': 'application/json',
        },
        params: { limit: 10 }
      });
    });
  });
});
```

### Testes E2E

```typescript
describe('Pluggy (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [PluggyModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/pluggy (POST) - should make generic request', () => {
    return request(app.getHttpServer())
      .post('/pluggy')
      .send({
        endpoint: 'connectors',
        method: 'GET',
        sandbox: true
      })
      .expect(200)
      .expect((res) => {
        expect(Array.isArray(res.body)).toBe(true);
      });
  });

  it('/pluggy/connect_token (POST) - should generate connect token', () => {
    return request(app.getHttpServer())
      .post('/pluggy/connect_token')
      .send({})
      .expect(200)
      .expect((res) => {
        expect(res.body).toHaveProperty('accessToken');
        expect(res.body).toHaveProperty('expiresIn');
      });
  });
});
```

## 📚 Referências

- [Pluggy API Documentation](https://docs.pluggy.ai/)
- [Pluggy Connect Widget](https://docs.pluggy.ai/docs/pluggy-connect)
- [Webhooks Documentation](https://docs.pluggy.ai/docs/webhooks)
- [NestJS HTTP Module](https://docs.nestjs.com/techniques/http-module)

---

## 🎉 Conclusão

A integração Pluggy oferece:

- 🔄 **Interface Unificada** - Um endpoint para todas as operações
- 🔐 **Autenticação Automática** - Cache inteligente de API keys
- 📡 **Webhooks Robustos** - Processamento completo de eventos
- 🛡️ **Tratamento de Erros** - Gestão robusta de falhas
- 📊 **Monitoramento** - Logs detalhados e métricas
- 🧪 **Testabilidade** - Cobertura completa de testes

Use esta integração para conectar facilmente com dados bancários através do Pluggy! 🔌
