@natyapp/meta 1.6.7 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +1540 -0
- package/README.md +122 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +11 -2
- package/dist/interfaces/IConnection.d.ts +2 -2
- package/dist/interfaces/ILog.d.ts +2 -2
- package/dist/interfaces/ILogger.d.ts +62 -0
- package/dist/interfaces/ILogger.js +2 -0
- package/dist/interfaces/ISdk.d.ts +4 -2
- package/dist/interfaces/IWebhook.d.ts +2 -2
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/interfaces/index.js +1 -0
- package/dist/queue/messageQueue.d.ts +1 -1
- package/dist/queue/messageQueue.js +45 -0
- package/dist/routes/webhooks/methods/connection.js +78 -11
- package/dist/routes/webhooks/methods/messages.js +18 -3
- package/dist/services/axiosInstances.d.ts +14 -5
- package/dist/services/axiosInstances.js +111 -23
- package/dist/services/middlewares/validations.d.ts +2 -2
- package/dist/services/middlewares/validations.js +1 -2
- package/dist/services/mutations/connection.js +1 -1
- package/dist/services/mutations/logs.js +1 -1
- package/dist/services/mutations/messages.js +1 -1
- package/dist/services/mutations/validation.d.ts +1 -1
- package/dist/services/mutations/validation.js +1 -1
- package/dist/services/mutations/webhooks.js +1 -1
- package/dist/types/logs.d.ts +1 -1
- package/dist/types/requestTypes.d.ts +2 -0
- package/dist/useCases/connection/index.d.ts +9 -9
- package/dist/useCases/connection/index.js +156 -7
- package/dist/useCases/events/NatyEvents.d.ts +4 -2
- package/dist/useCases/events/NatyEvents.js +13 -1
- package/dist/useCases/log/index.d.ts +5 -5
- package/dist/useCases/log/index.js +65 -3
- package/dist/useCases/message/whatsappResponse.d.ts +33 -23
- package/dist/useCases/message/whatsappResponse.js +655 -109
- package/dist/useCases/messages/index.d.ts +5 -5
- package/dist/useCases/messages/index.js +66 -3
- package/dist/useCases/sdk/index.d.ts +8 -4
- package/dist/useCases/sdk/index.js +38 -6
- package/dist/useCases/webhook/index.d.ts +9 -9
- package/dist/useCases/webhook/index.js +154 -7
- package/dist/utils/consoleLogger.d.ts +20 -0
- package/dist/utils/consoleLogger.js +51 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/loggerContext.d.ts +57 -0
- package/dist/utils/loggerContext.js +90 -0
- package/dist/utils/methodContext.d.ts +34 -0
- package/dist/utils/methodContext.js +48 -0
- package/dist/utils/parseError.d.ts +12 -0
- package/dist/utils/parseError.js +27 -3
- package/dist/utils/pinoAdapter.d.ts +30 -0
- package/dist/utils/pinoAdapter.js +68 -0
- package/dist/utils/sanitize.d.ts +42 -0
- package/dist/utils/sanitize.js +120 -0
- package/dist/utils/tryCatch.d.ts +10 -1
- package/dist/utils/tryCatch.js +40 -5
- package/docs/01-visao-geral.md +355 -0
- package/docs/02-contexto-negocio.md +596 -0
- package/docs/03-arquitetura.md +925 -0
- package/docs/04-fluxos-funcionais.md +887 -0
- package/docs/05-integracoes.md +960 -0
- package/docs/06-entidades.md +849 -0
- package/docs/07-guia-pratico.md +1133 -0
- package/docs/08-troubleshooting.md +816 -0
- package/docs/README.md +125 -0
- package/examples/logger-example.ts +279 -0
- package/package.json +2 -2
- /package/dist/{Entities → entities}/Logs.d.ts +0 -0
- /package/dist/{Entities → entities}/Logs.js +0 -0
- /package/dist/{Entities → entities}/connection.d.ts +0 -0
- /package/dist/{Entities → entities}/connection.js +0 -0
- /package/dist/{Entities → entities}/errorLogs.d.ts +0 -0
- /package/dist/{Entities → entities}/errorLogs.js +0 -0
- /package/dist/{Entities → entities}/index.d.ts +0 -0
- /package/dist/{Entities → entities}/index.js +0 -0
- /package/dist/{Entities → entities}/messages.d.ts +0 -0
- /package/dist/{Entities → entities}/messages.js +0 -0
- /package/dist/{Entities → entities}/webhooks.d.ts +0 -0
- /package/dist/{Entities → entities}/webhooks.js +0 -0
- /package/dist/{Entities → entities}/whatsappMessage.d.ts +0 -0
- /package/dist/{Entities → entities}/whatsappMessage.js +0 -0
- /package/dist/{Errors → errors}/Either.d.ts +0 -0
- /package/dist/{Errors → errors}/Either.js +0 -0
- /package/dist/{Errors → errors}/ErrorHandling.d.ts +0 -0
- /package/dist/{Errors → errors}/ErrorHandling.js +0 -0
- /package/dist/{Errors → errors}/index.d.ts +0 -0
- /package/dist/{Errors → errors}/index.js +0 -0
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
# GitHub Copilot Custom Instructions - @natyapp/meta SDK
|
|
2
|
+
|
|
3
|
+
## 📋 Visão Geral
|
|
4
|
+
|
|
5
|
+
**@natyapp/meta** é um SDK TypeScript que abstrai a integração com a WhatsApp Business API da Meta. Este documento define os padrões arquiteturais, convenções de código e práticas obrigatórias para manter consistência e qualidade no desenvolvimento.
|
|
6
|
+
|
|
7
|
+
### Filosofia do Projeto
|
|
8
|
+
|
|
9
|
+
- **Type-Safe First**: TypeScript strict mode com interfaces explícitas
|
|
10
|
+
- **Functional Error Handling**: Either monad ao invés de exceptions
|
|
11
|
+
- **Imutabilidade**: Entities com campos readonly
|
|
12
|
+
- **Separation of Concerns**: Arquitetura em 4 camadas bem definidas
|
|
13
|
+
- **Multi-Tenancy Native**: `companyId` obrigatório em todas operações
|
|
14
|
+
- **Developer Experience**: APIs fluentes, builders, e abstrações intuitivas
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🏗️ Arquitetura em Camadas
|
|
19
|
+
|
|
20
|
+
O SDK segue uma arquitetura em 4 camadas com responsabilidades claras:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ Layer 1: PUBLIC API (src/index.ts, src/useCases/sdk/) │
|
|
25
|
+
│ • Exports públicos que consumidores do SDK usam │
|
|
26
|
+
│ • Classe NatyMeta como ponto de entrada principal │
|
|
27
|
+
│ • Tipos e interfaces exportadas │
|
|
28
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
29
|
+
│ Layer 2: USE CASES (src/useCases/) │
|
|
30
|
+
│ • Lógica de negócio e orquestração │
|
|
31
|
+
│ • Implementam interfaces (IConnection, IMessage, etc.) │
|
|
32
|
+
│ • Coordenam chamadas entre services e entities │
|
|
33
|
+
│ • Gerenciam eventos e estado │
|
|
34
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
35
|
+
│ Layer 3: SERVICES (src/services/) │
|
|
36
|
+
│ • Comunicação com APIs externas (Meta, Naty backend) │
|
|
37
|
+
│ • Mutations (CRUD operations) │
|
|
38
|
+
│ • Axios instances e interceptors │
|
|
39
|
+
│ • Middlewares e validações │
|
|
40
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
41
|
+
│ Layer 4: DATA & DOMAIN (src/entities/, src/interfaces/) │
|
|
42
|
+
│ • Domain models (ConnectionEntity, MessageEntity, etc.) │
|
|
43
|
+
│ • Interfaces que definem contratos │
|
|
44
|
+
│ • Types e constantes │
|
|
45
|
+
│ • Message builders (src/elements/) │
|
|
46
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Onde cada tipo de código deve residir
|
|
50
|
+
|
|
51
|
+
| Tipo de Código | Localização | Exemplo |
|
|
52
|
+
| ----------------------- | -------------------------------- | -------------------------------------------- |
|
|
53
|
+
| **CRUD de API externa** | `src/services/mutations/` | `connectionMutations.getSingle()` |
|
|
54
|
+
| **Lógica de negócio** | `src/useCases/` | `Connections.getSingle()` com error handling |
|
|
55
|
+
| **Domain models** | `src/entities/` | `ConnectionEntity`, `MessageEntity` |
|
|
56
|
+
| **Type definitions** | `src/types/` | `WhatsappResponse`, `contactType` |
|
|
57
|
+
| **Interfaces** | `src/interfaces/` | `IConnection`, `IMessage` |
|
|
58
|
+
| **Message builders** | `src/elements/` | `Button`, `List`, `Text` |
|
|
59
|
+
| **HTTP clients** | `src/services/axiosInstances.ts` | Factory functions para axios |
|
|
60
|
+
| **Utility functions** | `src/utils/` | `genUuid`, `parseError`, `tryCatch` |
|
|
61
|
+
| **Routes/webhooks** | `src/routes/` | Express route handlers |
|
|
62
|
+
| **Error handling** | `src/errors/` | `Either`, `ErrorHandling` |
|
|
63
|
+
|
|
64
|
+
### Fluxo de Dados Típico
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
User Code
|
|
68
|
+
↓
|
|
69
|
+
sdk.connection.getSingle(id) ← Layer 1: Public API
|
|
70
|
+
↓
|
|
71
|
+
Connections.getSingle(id) ← Layer 2: Use Case (wraps with Try)
|
|
72
|
+
↓
|
|
73
|
+
connectionMutations.getSingle ← Layer 3: Service (axios call)
|
|
74
|
+
↓
|
|
75
|
+
api.get('/connections') ← External API
|
|
76
|
+
↓
|
|
77
|
+
ConnectionEntity constructor ← Layer 4: Domain model
|
|
78
|
+
↓
|
|
79
|
+
Either<Error, ConnectionEntity> ← Return to user
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ⚡ Error Handling Funcional (OBRIGATÓRIO)
|
|
85
|
+
|
|
86
|
+
### Either Monad Pattern
|
|
87
|
+
|
|
88
|
+
**REGRA FUNDAMENTAL**: Todas operações assíncronas DEVEM retornar `Either<Error, T>`. NUNCA use `throw`.
|
|
89
|
+
|
|
90
|
+
#### Definição do Either Type
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// src/errors/Either.ts
|
|
94
|
+
export type Either<T, U> =
|
|
95
|
+
| { isError: T; isSuccess?: never }
|
|
96
|
+
| { isError?: never; isSuccess: U };
|
|
97
|
+
|
|
98
|
+
export const throwError = <T>(value: T): { isError: T } => ({ isError: value });
|
|
99
|
+
export const throwSuccess = <U>(value: U): { isSuccess: U } => ({
|
|
100
|
+
isSuccess: value,
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Try Wrapper (src/utils/tryCatch.ts)
|
|
105
|
+
|
|
106
|
+
Use `Try` para envolver qualquer operação que possa falhar:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { Try } from "../utils/tryCatch";
|
|
110
|
+
|
|
111
|
+
// ✅ CORRETO
|
|
112
|
+
const result = await Try(connectionMutations.getSingle, connectionId);
|
|
113
|
+
if (result.isError) {
|
|
114
|
+
console.error("Failed:", result.isError);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const connection = result.isSuccess; // Type-safe!
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Pattern Completo: Mutation → Use Case → Consumer
|
|
121
|
+
|
|
122
|
+
**1. Service Layer (Mutation)**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// src/services/mutations/connection.ts
|
|
126
|
+
import { throwError, throwSuccess } from "../../errors";
|
|
127
|
+
import { api } from "../axiosInstances";
|
|
128
|
+
|
|
129
|
+
export const connectionMutations = {
|
|
130
|
+
getSingle: async (id: string) => {
|
|
131
|
+
const { data } = await api.get(`/connections/${id}`);
|
|
132
|
+
return throwSuccess(new ConnectionEntity(data));
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**2. Use Case Layer**
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// src/useCases/connection/index.ts
|
|
141
|
+
import { Try } from "../../utils/tryCatch";
|
|
142
|
+
import { IConnection } from "../../interfaces";
|
|
143
|
+
|
|
144
|
+
export class Connections implements IConnection {
|
|
145
|
+
getSingle = async (id: string) => {
|
|
146
|
+
return Try(connectionMutations.getSingle, id);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**3. Consumer (User Code)**
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const result = await sdk.connection.getSingle(connectionId);
|
|
155
|
+
|
|
156
|
+
if (result.isError) {
|
|
157
|
+
// Handle error - result.isError has error details
|
|
158
|
+
console.error("Failed to get connection:", result.isError);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Type-safe access to success value
|
|
163
|
+
const connection = result.isSuccess;
|
|
164
|
+
console.log("Phone:", connection.phone_number);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Error Parsing
|
|
168
|
+
|
|
169
|
+
Use `parseError` para extrair mensagens consistentes:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// src/utils/parseError.ts
|
|
173
|
+
import { parseError } from "../utils";
|
|
174
|
+
|
|
175
|
+
const result = await someOperation();
|
|
176
|
+
if (result.isError) {
|
|
177
|
+
const { message, code } = parseError(result.isError, "Operation failed");
|
|
178
|
+
console.error(`[${code}] ${message}`);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### ❌ ANTI-PATTERNS de Error Handling
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// ❌ NUNCA faça isso
|
|
186
|
+
async function badExample() {
|
|
187
|
+
throw new Error("Something failed"); // ❌ Não use throw
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ❌ NUNCA acesse .isSuccess sem checar .isError
|
|
191
|
+
const result = await operation();
|
|
192
|
+
console.log(result.isSuccess.id); // ❌ Pode crashear!
|
|
193
|
+
|
|
194
|
+
// ❌ NUNCA use try/catch diretamente (use Try wrapper)
|
|
195
|
+
try {
|
|
196
|
+
await externalCall();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// ❌ Pattern inconsistente
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ✅ CORRETO
|
|
202
|
+
async function goodExample() {
|
|
203
|
+
const result = await Try(externalOperation);
|
|
204
|
+
if (result.isError) return throwError(result.isError);
|
|
205
|
+
return throwSuccess(result.isSuccess);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 📝 Convenções de Código (OBRIGATÓRIAS)
|
|
212
|
+
|
|
213
|
+
### Naming Conventions
|
|
214
|
+
|
|
215
|
+
| Tipo | Pattern | Exemplos |
|
|
216
|
+
| --------------------- | --------------------------------------------- | -------------------------------------------- |
|
|
217
|
+
| **Entities** | PascalCase, campos `readonly` | `ConnectionEntity`, `MessageEntity` |
|
|
218
|
+
| **Interfaces** | `I` prefix + PascalCase | `IConnection`, `IMessage`, `IWebhook` |
|
|
219
|
+
| **Types** | camelCase + `Type` suffix | `contactType`, `flowMessageType`, `btnType` |
|
|
220
|
+
| **Enums** | PascalCase para enum, UPPER_CASE para valores | `enum Status { ACTIVE = 'ACTIVE' }` |
|
|
221
|
+
| **Files (classes)** | camelCase matching entity name | `connection.ts`, `messages.ts` |
|
|
222
|
+
| **Files (utilities)** | camelCase descriptive | `tryCatch.ts`, `parseError.ts`, `genUuid.ts` |
|
|
223
|
+
| **Folders** | camelCase or PascalCase by type | `useCases/`, `Entities/`, `elements/` |
|
|
224
|
+
| **Functions** | camelCase | `getSingle`, `sendMessage`, `updateToken` |
|
|
225
|
+
| **Constants** | UPPER_SNAKE_CASE | `API_META`, `VERIFY_TOKEN` |
|
|
226
|
+
| **Private fields** | camelCase with `_` prefix | `_connection`, `_axiosInstance` |
|
|
227
|
+
| **Props/Params** | camelCase | `phoneNumberId`, `accessToken`, `companyId` |
|
|
228
|
+
|
|
229
|
+
### File Organization
|
|
230
|
+
|
|
231
|
+
**Regra: One Class Per File**
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// ✅ CORRETO: src/entities/connection.ts
|
|
235
|
+
export class ConnectionEntity {
|
|
236
|
+
// Single class per file
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ❌ ERRADO: Múltiplas classes no mesmo arquivo
|
|
240
|
+
export class ConnectionEntity {}
|
|
241
|
+
export class MessageEntity {} // ❌ Separar em outro arquivo
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Barrel Exports via index.ts**
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// ✅ CORRETO: src/entities/index.ts
|
|
248
|
+
export * from "./connection";
|
|
249
|
+
export * from "./messages";
|
|
250
|
+
export * from "./webhooks";
|
|
251
|
+
export * from "./whatsappMessage";
|
|
252
|
+
|
|
253
|
+
// Permite imports limpos:
|
|
254
|
+
import { ConnectionEntity, MessageEntity } from "../entities";
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Import Organization
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// 1. External dependencies
|
|
261
|
+
import axios from "axios";
|
|
262
|
+
import { EventEmitter } from "events";
|
|
263
|
+
|
|
264
|
+
// 2. Internal modules (absolute paths via tsconfig)
|
|
265
|
+
import { ConnectionEntity } from "../entities";
|
|
266
|
+
import { Try } from "../utils/tryCatch";
|
|
267
|
+
|
|
268
|
+
// 3. Types e interfaces
|
|
269
|
+
import type { IConnection, IMessage } from "../interfaces";
|
|
270
|
+
import type { WhatsappResponse } from "../types";
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Function Signatures
|
|
274
|
+
|
|
275
|
+
**Sempre inclua type annotations explícitas:**
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// ✅ CORRETO
|
|
279
|
+
async function getSingle(id: string): Promise<Either<Error, ConnectionEntity>> {
|
|
280
|
+
return Try(connectionMutations.getSingle, id);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ✅ CORRETO - Arrow function
|
|
284
|
+
const sendMessage = async (
|
|
285
|
+
params: SendMessageParams,
|
|
286
|
+
): Promise<Either<Error, WhatsappResponse>> => {
|
|
287
|
+
return Try(messageMutations.send, params);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// ❌ ERRADO - Sem type annotations
|
|
291
|
+
async function getSingle(id) {
|
|
292
|
+
// ❌ Tipo inferido
|
|
293
|
+
return Try(connectionMutations.getSingle, id);
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Entity Fields (Imutabilidade)
|
|
298
|
+
|
|
299
|
+
**REGRA: Todos campos de entities são `readonly`**
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// ✅ CORRETO
|
|
303
|
+
export class ConnectionEntity {
|
|
304
|
+
readonly _id: string;
|
|
305
|
+
readonly id: string;
|
|
306
|
+
readonly companyId: string;
|
|
307
|
+
readonly phone_number_id: string;
|
|
308
|
+
readonly waba_id: string;
|
|
309
|
+
readonly accessToken: string;
|
|
310
|
+
readonly systemUserAccessToken: string;
|
|
311
|
+
readonly businessAccountId: string;
|
|
312
|
+
|
|
313
|
+
constructor(data: any) {
|
|
314
|
+
Object.assign(this, data);
|
|
315
|
+
this.id = data.id || genId("connection");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ❌ ERRADO - Campos mutáveis
|
|
320
|
+
export class ConnectionEntity {
|
|
321
|
+
_id: string; // ❌ Sem readonly
|
|
322
|
+
companyId: string; // ❌ Permite mutação acidental
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### ID Generation Pattern
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { genId } from '../utils';
|
|
330
|
+
|
|
331
|
+
// ✅ CORRETO - Cada entity type tem seu prefix
|
|
332
|
+
const connectionId = genId("connection"); // → "CONNECTION_a1b2-34567"
|
|
333
|
+
const messageId = genId("message"); // → "MESSAGE_xyz9-12345"
|
|
334
|
+
const webhookId = genId("webhook"); // → "WEBHOOK_def3-98765"
|
|
335
|
+
|
|
336
|
+
// Entity constructor deve usar genId se ID não fornecido
|
|
337
|
+
constructor(data: any) {
|
|
338
|
+
Object.assign(this, data);
|
|
339
|
+
this.id = data.id || genId("connection"); // ✅ Fallback para novo ID
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 🎨 Design Patterns do SDK
|
|
346
|
+
|
|
347
|
+
### 1. Builder Pattern (Message Construction)
|
|
348
|
+
|
|
349
|
+
Todos os message builders em `src/elements/` usam fluent APIs:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { List, Button, Text } from "@natyapp/meta/elements";
|
|
353
|
+
|
|
354
|
+
// ✅ Builder Pattern - Fluent API
|
|
355
|
+
const list = new List()
|
|
356
|
+
.setBody("Escolha uma opção:")
|
|
357
|
+
.setButtonText("Ver Menu")
|
|
358
|
+
.addSection("Produtos")
|
|
359
|
+
.addItem({
|
|
360
|
+
id: "prod1",
|
|
361
|
+
title: "Produto 1",
|
|
362
|
+
description: "Descrição do produto",
|
|
363
|
+
})
|
|
364
|
+
.addItem({ id: "prod2", title: "Produto 2" })
|
|
365
|
+
.addSection("Serviços")
|
|
366
|
+
.addItem({ id: "srv1", title: "Serviço A" })
|
|
367
|
+
.build();
|
|
368
|
+
|
|
369
|
+
// ✅ Button Builder
|
|
370
|
+
const buttons = new Button()
|
|
371
|
+
.setBodyText("Mensagem principal")
|
|
372
|
+
.addButton({ id: "btn1", title: "Opção 1" })
|
|
373
|
+
.addButton({ id: "btn2", title: "Opção 2" })
|
|
374
|
+
.build();
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Pattern para criar novos builders:**
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// src/elements/newType.ts
|
|
381
|
+
export class NewTypeBuilder {
|
|
382
|
+
private body: string = "";
|
|
383
|
+
private items: any[] = [];
|
|
384
|
+
|
|
385
|
+
setBody(text: string): this {
|
|
386
|
+
this.body = text;
|
|
387
|
+
return this; // ✅ Retorna 'this' para chaining
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
addItem(item: any): this {
|
|
391
|
+
this.items.push(item);
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
build(): object {
|
|
396
|
+
// Validação antes de retornar
|
|
397
|
+
if (!this.body) throw new Error("Body is required");
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
type: "new_type",
|
|
401
|
+
body: { text: this.body },
|
|
402
|
+
items: this.items,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 2. Factory Pattern (Axios Instances)
|
|
409
|
+
|
|
410
|
+
Diferentes axios instances são criados por factory functions baseado no contexto:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// src/services/axiosInstances.ts
|
|
414
|
+
|
|
415
|
+
// ✅ Factory para mensagens WhatsApp
|
|
416
|
+
export const newAxiosInstance = ({
|
|
417
|
+
phone_number_id,
|
|
418
|
+
accessToken,
|
|
419
|
+
}: AxiosInstanceParams) => {
|
|
420
|
+
return axios.create({
|
|
421
|
+
baseURL: `https://graph.facebook.com/v18.0/${phone_number_id}`,
|
|
422
|
+
headers: {
|
|
423
|
+
"Content-Type": "application/json",
|
|
424
|
+
Authorization: `Bearer ${accessToken}`,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// ✅ Factory para download de media
|
|
430
|
+
export const axiosInstanceForDownloadMedia = ({ accessToken }: TokenParam) => {
|
|
431
|
+
return axios.create({
|
|
432
|
+
baseURL: "https://graph.facebook.com/v18.0",
|
|
433
|
+
headers: {
|
|
434
|
+
Authorization: `Bearer ${accessToken}`,
|
|
435
|
+
},
|
|
436
|
+
responseType: "arraybuffer",
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// ✅ Factory para Facebook token refresh
|
|
441
|
+
export const newFacebookInstance = () => {
|
|
442
|
+
return axios.create({
|
|
443
|
+
baseURL: "https://graph.facebook.com/v18.0/oauth",
|
|
444
|
+
headers: { "Content-Type": "application/json" },
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**REGRA**: Nunca crie axios instances ad-hoc. Use factories existentes ou crie nova factory.
|
|
450
|
+
|
|
451
|
+
### 3. Event Emitter Pattern (Webhooks)
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// src/useCases/events/NatyEvents.ts
|
|
455
|
+
import { EventEmitter } from "events";
|
|
456
|
+
|
|
457
|
+
export class NatyEvents extends EventEmitter {
|
|
458
|
+
constructor() {
|
|
459
|
+
super();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
emitMessage(data: WhatsappResponse): void {
|
|
463
|
+
this.emit("message", data);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
emitConnection(event: connectionEvent): void {
|
|
467
|
+
this.emit("connection", event);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
emitError(error: Error): void {
|
|
471
|
+
this.emit("error", error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ✅ Consumer usage
|
|
476
|
+
sdk.on("message", (data: WhatsappResponse) => {
|
|
477
|
+
console.log("New message:", data.id);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
sdk.on("connection", (event) => {
|
|
481
|
+
console.log("Connection event:", event.type);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
sdk.on("error", (error) => {
|
|
485
|
+
console.error("SDK error:", error.message);
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### 4. Interface Implementation Pattern
|
|
490
|
+
|
|
491
|
+
**REGRA**: Interfaces definem contratos, classes implementam com Try wrapper
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// 1. Definir interface
|
|
495
|
+
// src/interfaces/IConnection.ts
|
|
496
|
+
export interface IConnection {
|
|
497
|
+
getSingle(id: string): Promise<Either<Error, ConnectionEntity>>;
|
|
498
|
+
insert(data: ConnectionInsertData): Promise<Either<Error, ConnectionEntity>>;
|
|
499
|
+
update(
|
|
500
|
+
id: string,
|
|
501
|
+
data: ConnectionUpdateData,
|
|
502
|
+
): Promise<Either<Error, ConnectionEntity>>;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 2. Implementar com Try wrapper
|
|
506
|
+
// src/useCases/connection/index.ts
|
|
507
|
+
export class Connections implements IConnection {
|
|
508
|
+
getSingle = (id: string) => Try(connectionMutations.getSingle, id);
|
|
509
|
+
|
|
510
|
+
insert = (data: ConnectionInsertData) =>
|
|
511
|
+
Try(connectionMutations.insert, data);
|
|
512
|
+
|
|
513
|
+
update = (id: string, data: ConnectionUpdateData) =>
|
|
514
|
+
Try(connectionMutations.update, id, data);
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 🌐 Regras de Negócio WhatsApp
|
|
521
|
+
|
|
522
|
+
### 1. Janela de 24 Horas
|
|
523
|
+
|
|
524
|
+
**REGRA CRÍTICA**: Mensagens free-form só podem ser enviadas dentro de 24h da última mensagem do cliente.
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// Fora da janela de 24h: OBRIGATÓRIO usar template
|
|
528
|
+
const result = await sdk.message.send_text_template({
|
|
529
|
+
companyId,
|
|
530
|
+
to: phoneNumber,
|
|
531
|
+
template: {
|
|
532
|
+
name: "hello_world",
|
|
533
|
+
language: { code: "pt_BR" },
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Dentro da janela: Pode usar mensagem livre
|
|
538
|
+
const result = await sdk.message.send_text_message({
|
|
539
|
+
companyId,
|
|
540
|
+
to: phoneNumber,
|
|
541
|
+
bodyText: "Mensagem personalizada",
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 2. Multi-Tenancy (companyId OBRIGATÓRIO)
|
|
546
|
+
|
|
547
|
+
**REGRA**: TODO método que acessa dados DEVE receber `companyId` para isolamento de tenant.
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
// ✅ CORRETO - companyId presente
|
|
551
|
+
await sdk.connection.getSingle(connectionId, { companyId });
|
|
552
|
+
|
|
553
|
+
await sdk.message.send_text_message({
|
|
554
|
+
companyId, // ✅ Obrigatório
|
|
555
|
+
to: phoneNumber,
|
|
556
|
+
bodyText: "Hello",
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// ❌ ERRADO - Esqueceu companyId
|
|
560
|
+
await sdk.connection.getSingle(connectionId); // ❌ Faltando companyId
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Pattern em mutations:**
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// src/services/mutations/connection.ts
|
|
567
|
+
export const connectionMutations = {
|
|
568
|
+
getSingle: async (id: string, companyId?: string) => {
|
|
569
|
+
const params = companyId ? { companyId } : {};
|
|
570
|
+
const { data } = await api.get(`/connections/${id}`, { params });
|
|
571
|
+
return throwSuccess(new ConnectionEntity(data));
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### 3. Token Refresh Automático
|
|
577
|
+
|
|
578
|
+
**REGRA**: SDK gerencia refresh automático. Ao criar `WhatsappResponse`, token é renovado se necessário.
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
// src/useCases/message/whatsappResponse.ts
|
|
582
|
+
export class WhatsappResponse {
|
|
583
|
+
private async getApiInstanceToken() {
|
|
584
|
+
// 1. Busca connection
|
|
585
|
+
const result = await this.getConnection({
|
|
586
|
+
companyId: this.companyId,
|
|
587
|
+
phoneNumberId: this.phone_number_id,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
if (result.isError) return result.isError;
|
|
591
|
+
|
|
592
|
+
// 2. Exchange para long-lived token se necessário
|
|
593
|
+
const connection = result.isSuccess;
|
|
594
|
+
const tokenResult = await this.exchangeForLongLivedToken(connection);
|
|
595
|
+
|
|
596
|
+
// 3. Atualiza no banco
|
|
597
|
+
await this.updateConnectionToken(connection.id, tokenResult.access_token);
|
|
598
|
+
|
|
599
|
+
// 4. Cria axios instance com token atualizado
|
|
600
|
+
this._axiosInstance = newAxiosInstance({
|
|
601
|
+
phone_number_id: this.phone_number_id,
|
|
602
|
+
accessToken: tokenResult.access_token,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### 4. Formato de Phone Number
|
|
609
|
+
|
|
610
|
+
**REGRA**: Sempre use formato E.164 (`+5511999999999`)
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
// ✅ CORRETO - E.164 format
|
|
614
|
+
const phoneNumber = "+5511987654321";
|
|
615
|
+
|
|
616
|
+
// ❌ ERRADO - Formatos inválidos
|
|
617
|
+
const phoneNumber = "11987654321"; // ❌ Sem código do país
|
|
618
|
+
const phoneNumber = "(11) 98765-4321"; // ❌ Formatação
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Nota**: SDK não valida formato. Aplicação consumidora é responsável pela validação.
|
|
622
|
+
|
|
623
|
+
### 5. Webhook Deduplication
|
|
624
|
+
|
|
625
|
+
**REGRA**: Use `message.id` para evitar processar duplicatas (Meta pode enviar o mesmo webhook múltiplas vezes).
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// ✅ CORRETO - Deduplicação com Set/Cache
|
|
629
|
+
const processedIds = new Set<string>();
|
|
630
|
+
|
|
631
|
+
sdk.on("message", async (data: WhatsappResponse) => {
|
|
632
|
+
if (processedIds.has(data.id)) {
|
|
633
|
+
console.log("Duplicate webhook ignored:", data.id);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
processedIds.add(data.id);
|
|
638
|
+
|
|
639
|
+
// Processar mensagem...
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 6. Rate Limits Meta (Informativo)
|
|
644
|
+
|
|
645
|
+
| Tier | Limite Diário | Obs |
|
|
646
|
+
| ------ | ------------- | -------------------- |
|
|
647
|
+
| Tier 1 | 1,000 msgs | Conta nova |
|
|
648
|
+
| Tier 2 | 10,000 msgs | Após verificação |
|
|
649
|
+
| Tier 3 | 100,000 msgs | Volume alto aprovado |
|
|
650
|
+
|
|
651
|
+
**SDK não implementa throttling**. Aplicação consumidora deve gerenciar rate limits se necessário.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## 🔧 Guidelines de Integração
|
|
656
|
+
|
|
657
|
+
### Criar Nova Mutation
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
// 1. Adicionar em src/services/mutations/messages.ts
|
|
661
|
+
export const messageMutations = {
|
|
662
|
+
// ... existing mutations
|
|
663
|
+
|
|
664
|
+
deleteMessage: async (messageId: string, companyId: string) => {
|
|
665
|
+
const { data } = await api.delete(`/messages/${messageId}`, {
|
|
666
|
+
params: { companyId },
|
|
667
|
+
});
|
|
668
|
+
return throwSuccess(data);
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Criar Novo Use Case
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
// 2. Adicionar método na interface src/interfaces/IMessage.ts
|
|
677
|
+
export interface IMessage {
|
|
678
|
+
// ... existing methods
|
|
679
|
+
deleteMessage(
|
|
680
|
+
messageId: string,
|
|
681
|
+
companyId: string,
|
|
682
|
+
): Promise<Either<Error, any>>;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 3. Implementar em src/useCases/messages/index.ts
|
|
686
|
+
export class Messages implements IMessage {
|
|
687
|
+
// ... existing methods
|
|
688
|
+
|
|
689
|
+
deleteMessage = (messageId: string, companyId: string) =>
|
|
690
|
+
Try(messageMutations.deleteMessage, messageId, companyId);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 4. Se for public API, adicionar no SDK principal
|
|
694
|
+
// src/useCases/sdk/index.ts
|
|
695
|
+
export class NatyMeta extends NatyEvents {
|
|
696
|
+
message: IMessage;
|
|
697
|
+
|
|
698
|
+
constructor() {
|
|
699
|
+
super();
|
|
700
|
+
this.message = new Messages();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Estender Entity
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
// 1. Adicionar campo em src/entities/connection.ts
|
|
709
|
+
export class ConnectionEntity {
|
|
710
|
+
// ... existing readonly fields
|
|
711
|
+
readonly newField: string; // ✅ Sempre readonly
|
|
712
|
+
readonly createdAt?: Date;
|
|
713
|
+
|
|
714
|
+
constructor(data: any) {
|
|
715
|
+
Object.assign(this, data);
|
|
716
|
+
this.id = data.id || genId("connection");
|
|
717
|
+
|
|
718
|
+
// ✅ Transformações no constructor
|
|
719
|
+
if (data.createdAt) {
|
|
720
|
+
this.createdAt = new Date(data.createdAt);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 2. Atualizar interface se necessário
|
|
726
|
+
// src/interfaces/IConnection.ts
|
|
727
|
+
export interface IConnectionData {
|
|
728
|
+
// ... existing fields
|
|
729
|
+
newField?: string;
|
|
730
|
+
createdAt?: string | Date;
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Criar Novo Message Builder
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
// src/elements/carousel.ts
|
|
738
|
+
export class Carousel {
|
|
739
|
+
private header: string = "";
|
|
740
|
+
private cards: any[] = [];
|
|
741
|
+
|
|
742
|
+
setHeader(text: string): this {
|
|
743
|
+
this.header = text;
|
|
744
|
+
return this;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
addCard(card: { title: string; body: string; image?: string }): this {
|
|
748
|
+
this.cards.push(card);
|
|
749
|
+
return this;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
build(): object {
|
|
753
|
+
if (!this.header) throw new Error("Header is required");
|
|
754
|
+
if (this.cards.length === 0) throw new Error("At least one card required");
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
type: "carousel",
|
|
758
|
+
header: { text: this.header },
|
|
759
|
+
cards: this.cards,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Exportar em src/elements/index.ts
|
|
765
|
+
export * from "./carousel";
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Configurar Axios Interceptor
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
// src/configs/axiosInterceptors.ts
|
|
772
|
+
|
|
773
|
+
// ✅ Adicionar novo header globalmente
|
|
774
|
+
export const updateInterceptor = (headers: Record<string, string>) => {
|
|
775
|
+
Object.assign(configInterceptors, headers);
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// Usage:
|
|
779
|
+
updateInterceptor({
|
|
780
|
+
"x-access-token": appToken,
|
|
781
|
+
"x-custom-header": "value",
|
|
782
|
+
});
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## 🧪 Testing Guidelines
|
|
788
|
+
|
|
789
|
+
### Configuração
|
|
790
|
+
|
|
791
|
+
```javascript
|
|
792
|
+
// jest.config.js
|
|
793
|
+
module.exports = {
|
|
794
|
+
preset: "ts-jest",
|
|
795
|
+
testEnvironment: "node",
|
|
796
|
+
testMatch: ["**/*.spec.ts", "**/*.test.ts"],
|
|
797
|
+
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
|
|
798
|
+
};
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Naming Conventions
|
|
802
|
+
|
|
803
|
+
- Test files: `*.spec.ts` ou `*.test.ts`
|
|
804
|
+
- Location: `Tests/` folder ou colocated com source
|
|
805
|
+
- Suite: `describe('EntityName ou functionName', () => {})`
|
|
806
|
+
- Cases: `it('should do something specific', () => {})`
|
|
807
|
+
|
|
808
|
+
### Testes de Either Returns
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
// Tests/entities/connection.spec.ts
|
|
812
|
+
import { Connections } from "../../src/useCases/connection";
|
|
813
|
+
import { ConnectionEntity } from "../../src/Entities";
|
|
814
|
+
|
|
815
|
+
describe("Connections", () => {
|
|
816
|
+
let connections: Connections;
|
|
817
|
+
|
|
818
|
+
beforeEach(() => {
|
|
819
|
+
connections = new Connections();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("should return ConnectionEntity on success", async () => {
|
|
823
|
+
const result = await connections.getSingle("CONNECTION_123");
|
|
824
|
+
|
|
825
|
+
expect(result.isError).toBeUndefined();
|
|
826
|
+
expect(result.isSuccess).toBeDefined();
|
|
827
|
+
expect(result.isSuccess).toBeInstanceOf(ConnectionEntity);
|
|
828
|
+
expect(result.isSuccess?.id).toBe("CONNECTION_123");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("should return error on failure", async () => {
|
|
832
|
+
const result = await connections.getSingle("INVALID_ID");
|
|
833
|
+
|
|
834
|
+
expect(result.isSuccess).toBeUndefined();
|
|
835
|
+
expect(result.isError).toBeDefined();
|
|
836
|
+
expect(result.isError).toHaveProperty("message");
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Mocking External APIs
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
// ✅ Mock axios
|
|
845
|
+
jest.mock("../../src/services/axiosInstances", () => ({
|
|
846
|
+
api: {
|
|
847
|
+
get: jest.fn(),
|
|
848
|
+
post: jest.fn(),
|
|
849
|
+
put: jest.fn(),
|
|
850
|
+
delete: jest.fn(),
|
|
851
|
+
},
|
|
852
|
+
}));
|
|
853
|
+
|
|
854
|
+
import { api } from "../../src/services/axiosInstances";
|
|
855
|
+
|
|
856
|
+
describe("ConnectionMutations", () => {
|
|
857
|
+
it("should call API with correct params", async () => {
|
|
858
|
+
const mockData = { id: "CONNECTION_123", phone_number_id: "123456" };
|
|
859
|
+
(api.get as jest.Mock).mockResolvedValue({ data: mockData });
|
|
860
|
+
|
|
861
|
+
const result = await connectionMutations.getSingle("CONNECTION_123");
|
|
862
|
+
|
|
863
|
+
expect(api.get).toHaveBeenCalledWith("/connections/CONNECTION_123");
|
|
864
|
+
expect(result.isSuccess).toEqual(expect.objectContaining(mockData));
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
### Testing Builders
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
describe("List Builder", () => {
|
|
873
|
+
it("should build valid list structure", () => {
|
|
874
|
+
const list = new List()
|
|
875
|
+
.setBody("Choose option")
|
|
876
|
+
.setButtonText("Select")
|
|
877
|
+
.addSection("Section 1")
|
|
878
|
+
.addItem({ id: "item1", title: "Item 1" })
|
|
879
|
+
.build();
|
|
880
|
+
|
|
881
|
+
expect(list).toHaveProperty("type", "list");
|
|
882
|
+
expect(list).toHaveProperty("body");
|
|
883
|
+
expect(list).toHaveProperty("action.button", "Select");
|
|
884
|
+
expect(list).toHaveProperty("action.sections");
|
|
885
|
+
expect(list.action.sections).toHaveLength(1);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("should throw error if required fields missing", () => {
|
|
889
|
+
expect(() => {
|
|
890
|
+
new List().build(); // Missing body
|
|
891
|
+
}).toThrow();
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## ❌ Anti-Patterns (EVITAR)
|
|
899
|
+
|
|
900
|
+
### Error Handling
|
|
901
|
+
|
|
902
|
+
```typescript
|
|
903
|
+
// ❌ NUNCA use throw diretamente
|
|
904
|
+
function badFunction() {
|
|
905
|
+
if (error) throw new Error("Failed");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// ✅ USE Either
|
|
909
|
+
function goodFunction() {
|
|
910
|
+
if (error) return throwError(new Error("Failed"));
|
|
911
|
+
return throwSuccess(data);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ❌ NUNCA acesse .isSuccess sem checar .isError
|
|
915
|
+
const result = await operation();
|
|
916
|
+
console.log(result.isSuccess.id); // ❌ Crashea se houver erro!
|
|
917
|
+
|
|
918
|
+
// ✅ SEMPRE cheque primeiro
|
|
919
|
+
if (result.isError) return;
|
|
920
|
+
console.log(result.isSuccess.id); // ✅ Type-safe
|
|
921
|
+
|
|
922
|
+
// ❌ NUNCA use try/catch direto (use Try wrapper)
|
|
923
|
+
try {
|
|
924
|
+
await externalCall();
|
|
925
|
+
} catch (err) {
|
|
926
|
+
// ❌ Inconsistente
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ✅ USE Try wrapper
|
|
930
|
+
const result = await Try(externalCall);
|
|
931
|
+
if (result.isError) {
|
|
932
|
+
/* handle */
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### Mutabilidade
|
|
937
|
+
|
|
938
|
+
```typescript
|
|
939
|
+
// ❌ NUNCA mute entities
|
|
940
|
+
connection.phone_number_id = "new_value"; // ❌ Fields são readonly
|
|
941
|
+
|
|
942
|
+
// ✅ Crie nova instância
|
|
943
|
+
const updated = new ConnectionEntity({
|
|
944
|
+
...connection,
|
|
945
|
+
phone_number_id: "new_value",
|
|
946
|
+
});
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Axios Instances
|
|
950
|
+
|
|
951
|
+
```typescript
|
|
952
|
+
// ❌ NUNCA crie axios ad-hoc
|
|
953
|
+
const api = axios.create({ baseURL: "..." }); // ❌ Inconsistente
|
|
954
|
+
|
|
955
|
+
// ✅ USE factories
|
|
956
|
+
const api = newAxiosInstance({ phone_number_id, accessToken });
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### Multi-Tenancy
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// ❌ NUNCA esqueça companyId
|
|
963
|
+
await connection.getSingle(id); // ❌ Quebra isolamento
|
|
964
|
+
|
|
965
|
+
// ✅ SEMPRE inclua companyId
|
|
966
|
+
await connection.getSingle(id, { companyId });
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Imports
|
|
970
|
+
|
|
971
|
+
```typescript
|
|
972
|
+
// ❌ NUNCA importe tudo
|
|
973
|
+
import * as everything from "./module"; // ❌ Poluição de namespace
|
|
974
|
+
|
|
975
|
+
// ✅ Seja específico
|
|
976
|
+
import { ConnectionEntity, MessageEntity } from "./entities";
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### Hardcoding
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
// ❌ NUNCA hardcode URLs ou tokens
|
|
983
|
+
const url = "https://graph.facebook.com/v18.0"; // ❌
|
|
984
|
+
|
|
985
|
+
// ✅ USE variáveis de ambiente
|
|
986
|
+
const url = process.env.META_API_URL || "https://graph.facebook.com/v18.0";
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
---
|
|
990
|
+
|
|
991
|
+
## 📚 Exemplos Completos End-to-End
|
|
992
|
+
|
|
993
|
+
### Exemplo 1: Criar Método para Enviar Reação a Mensagem
|
|
994
|
+
|
|
995
|
+
**Objetivo**: Adicionar funcionalidade `send_reaction` para reagir a mensagens.
|
|
996
|
+
|
|
997
|
+
#### Passo 1: Criar Mutation
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
// src/services/mutations/messages.ts
|
|
1001
|
+
export const messageMutations = {
|
|
1002
|
+
// ... existing methods
|
|
1003
|
+
|
|
1004
|
+
sendReaction: async (params: {
|
|
1005
|
+
phone_number_id: string;
|
|
1006
|
+
accessToken: string;
|
|
1007
|
+
to: string;
|
|
1008
|
+
message_id: string;
|
|
1009
|
+
emoji: string;
|
|
1010
|
+
}) => {
|
|
1011
|
+
const axiosInstance = newAxiosInstance({
|
|
1012
|
+
phone_number_id: params.phone_number_id,
|
|
1013
|
+
accessToken: params.accessToken,
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const payload = {
|
|
1017
|
+
messaging_product: "whatsapp",
|
|
1018
|
+
recipient_type: "individual",
|
|
1019
|
+
to: params.to,
|
|
1020
|
+
type: "reaction",
|
|
1021
|
+
reaction: {
|
|
1022
|
+
message_id: params.message_id,
|
|
1023
|
+
emoji: params.emoji,
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const { data } = await axiosInstance.post("/messages", payload);
|
|
1028
|
+
return throwSuccess(data);
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
#### Passo 2: Adicionar Interface
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
// src/interfaces/IMessage.ts
|
|
1037
|
+
export interface IMessage {
|
|
1038
|
+
// ... existing methods
|
|
1039
|
+
|
|
1040
|
+
send_reaction(params: {
|
|
1041
|
+
companyId: string;
|
|
1042
|
+
to: string;
|
|
1043
|
+
message_id: string;
|
|
1044
|
+
emoji: string;
|
|
1045
|
+
}): Promise<Either<Error, any>>;
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
#### Passo 3: Implementar Use Case
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
// src/useCases/messages/index.ts
|
|
1053
|
+
export class Messages implements IMessage {
|
|
1054
|
+
// ... existing methods
|
|
1055
|
+
|
|
1056
|
+
send_reaction = async (params: {
|
|
1057
|
+
companyId: string;
|
|
1058
|
+
to: string;
|
|
1059
|
+
message_id: string;
|
|
1060
|
+
emoji: string;
|
|
1061
|
+
}) => {
|
|
1062
|
+
// 1. Obter connection
|
|
1063
|
+
const connectionResult = await this.getConnection({
|
|
1064
|
+
companyId: params.companyId,
|
|
1065
|
+
to: params.to,
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
if (connectionResult.isError) return connectionResult;
|
|
1069
|
+
const connection = connectionResult.isSuccess;
|
|
1070
|
+
|
|
1071
|
+
// 2. Chamar mutation com Try wrapper
|
|
1072
|
+
return Try(messageMutations.sendReaction, {
|
|
1073
|
+
phone_number_id: connection.phone_number_id,
|
|
1074
|
+
accessToken: connection.accessToken,
|
|
1075
|
+
to: params.to,
|
|
1076
|
+
message_id: params.message_id,
|
|
1077
|
+
emoji: params.emoji,
|
|
1078
|
+
});
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
#### Passo 4: Usar na Aplicação
|
|
1084
|
+
|
|
1085
|
+
```typescript
|
|
1086
|
+
// Consumer code
|
|
1087
|
+
const result = await sdk.message.send_reaction({
|
|
1088
|
+
companyId: "COMPANY_123",
|
|
1089
|
+
to: "+5511987654321",
|
|
1090
|
+
message_id: "wamid.HBgLNT...",
|
|
1091
|
+
emoji: "👍",
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
if (result.isError) {
|
|
1095
|
+
console.error("Failed to send reaction:", result.isError);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
console.log("Reaction sent:", result.isSuccess);
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
### Exemplo 2: Criar Handler para Novo Tipo de Webhook
|
|
1103
|
+
|
|
1104
|
+
**Objetivo**: Adicionar handler para eventos de status de mensagem.
|
|
1105
|
+
|
|
1106
|
+
#### Passo 1: Criar Route Handler
|
|
1107
|
+
|
|
1108
|
+
```typescript
|
|
1109
|
+
// src/routes/webhooks/methods/messageStatus.ts
|
|
1110
|
+
import { Request, Response } from "express";
|
|
1111
|
+
import { NatyEvents } from "../../../useCases/events/NatyEvents";
|
|
1112
|
+
|
|
1113
|
+
export const messageStatusHandler = (events: NatyEvents) => {
|
|
1114
|
+
return async (req: Request, res: Response) => {
|
|
1115
|
+
try {
|
|
1116
|
+
const { entry } = req.body;
|
|
1117
|
+
|
|
1118
|
+
for (const change of entry[0].changes) {
|
|
1119
|
+
const { statuses } = change.value;
|
|
1120
|
+
|
|
1121
|
+
if (!statuses) continue;
|
|
1122
|
+
|
|
1123
|
+
for (const status of statuses) {
|
|
1124
|
+
// Emitir evento
|
|
1125
|
+
events.emit("message_status", {
|
|
1126
|
+
id: status.id,
|
|
1127
|
+
status: status.status, // sent, delivered, read
|
|
1128
|
+
timestamp: status.timestamp,
|
|
1129
|
+
recipient_id: status.recipient_id,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
res.sendStatus(200);
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
console.error("Message status webhook error:", error);
|
|
1137
|
+
res.sendStatus(500);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
};
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
#### Passo 2: Registrar Route
|
|
1144
|
+
|
|
1145
|
+
```typescript
|
|
1146
|
+
// src/routes/webhooks/index.ts
|
|
1147
|
+
import { messageStatusHandler } from "./methods/messageStatus";
|
|
1148
|
+
|
|
1149
|
+
export const setupWebhookRoutes = (app: Express, events: NatyEvents) => {
|
|
1150
|
+
// ... existing routes
|
|
1151
|
+
|
|
1152
|
+
app.post("/webhooks/message-status", messageStatusHandler(events));
|
|
1153
|
+
};
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
#### Passo 3: Adicionar Type
|
|
1157
|
+
|
|
1158
|
+
```typescript
|
|
1159
|
+
// src/types/whatsappTypes.ts
|
|
1160
|
+
export type MessageStatusEvent = {
|
|
1161
|
+
id: string;
|
|
1162
|
+
status: "sent" | "delivered" | "read" | "failed";
|
|
1163
|
+
timestamp: string;
|
|
1164
|
+
recipient_id: string;
|
|
1165
|
+
};
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
#### Passo 4: Usar na Aplicação
|
|
1169
|
+
|
|
1170
|
+
```typescript
|
|
1171
|
+
// Consumer code
|
|
1172
|
+
sdk.on("message_status", (event: MessageStatusEvent) => {
|
|
1173
|
+
console.log(`Message ${event.id} is now ${event.status}`);
|
|
1174
|
+
|
|
1175
|
+
// Atualizar status no banco de dados
|
|
1176
|
+
if (event.status === "read") {
|
|
1177
|
+
markAsRead(event.id);
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
### Exemplo 3: Estender ConnectionEntity com Metadata
|
|
1183
|
+
|
|
1184
|
+
**Objetivo**: Adicionar campo `metadata` para dados customizados.
|
|
1185
|
+
|
|
1186
|
+
#### Passo 1: Atualizar Entity
|
|
1187
|
+
|
|
1188
|
+
```typescript
|
|
1189
|
+
// src/entities/connection.ts
|
|
1190
|
+
export class ConnectionEntity {
|
|
1191
|
+
// ... existing readonly fields
|
|
1192
|
+
readonly metadata?: Record<string, any>; // ✅ Novo campo
|
|
1193
|
+
|
|
1194
|
+
constructor(data: any) {
|
|
1195
|
+
Object.assign(this, data);
|
|
1196
|
+
this.id = data.id || genId("connection");
|
|
1197
|
+
|
|
1198
|
+
// ✅ Parse metadata se for string
|
|
1199
|
+
if (typeof data.metadata === "string") {
|
|
1200
|
+
try {
|
|
1201
|
+
this.metadata = JSON.parse(data.metadata);
|
|
1202
|
+
} catch {
|
|
1203
|
+
this.metadata = {};
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
#### Passo 2: Atualizar Interface
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
// src/interfaces/IConnection.ts
|
|
1214
|
+
export interface IConnectionData {
|
|
1215
|
+
// ... existing fields
|
|
1216
|
+
metadata?: Record<string, any>;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
export interface IConnection {
|
|
1220
|
+
// ... existing methods
|
|
1221
|
+
|
|
1222
|
+
updateMetadata(
|
|
1223
|
+
id: string,
|
|
1224
|
+
metadata: Record<string, any>,
|
|
1225
|
+
companyId: string,
|
|
1226
|
+
): Promise<Either<Error, ConnectionEntity>>;
|
|
1227
|
+
}
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
#### Passo 3: Adicionar Mutation
|
|
1231
|
+
|
|
1232
|
+
```typescript
|
|
1233
|
+
// src/services/mutations/connection.ts
|
|
1234
|
+
export const connectionMutations = {
|
|
1235
|
+
// ... existing methods
|
|
1236
|
+
|
|
1237
|
+
updateMetadata: async (
|
|
1238
|
+
id: string,
|
|
1239
|
+
metadata: Record<string, any>,
|
|
1240
|
+
companyId: string,
|
|
1241
|
+
) => {
|
|
1242
|
+
const { data } = await api.put(
|
|
1243
|
+
`/connections/${id}`,
|
|
1244
|
+
{ metadata },
|
|
1245
|
+
{ params: { companyId } },
|
|
1246
|
+
);
|
|
1247
|
+
return throwSuccess(new ConnectionEntity(data));
|
|
1248
|
+
},
|
|
1249
|
+
};
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
#### Passo 4: Implementar Use Case
|
|
1253
|
+
|
|
1254
|
+
```typescript
|
|
1255
|
+
// src/useCases/connection/index.ts
|
|
1256
|
+
export class Connections implements IConnection {
|
|
1257
|
+
// ... existing methods
|
|
1258
|
+
|
|
1259
|
+
updateMetadata = (
|
|
1260
|
+
id: string,
|
|
1261
|
+
metadata: Record<string, any>,
|
|
1262
|
+
companyId: string,
|
|
1263
|
+
) => Try(connectionMutations.updateMetadata, id, metadata, companyId);
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
#### Passo 5: Usar na Aplicação
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
// Consumer code
|
|
1271
|
+
const result = await sdk.connection.updateMetadata(
|
|
1272
|
+
"CONNECTION_123",
|
|
1273
|
+
{
|
|
1274
|
+
department: "Sales",
|
|
1275
|
+
responsible: "john@company.com",
|
|
1276
|
+
custom_field: "value",
|
|
1277
|
+
},
|
|
1278
|
+
"COMPANY_456",
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
if (result.isError) {
|
|
1282
|
+
console.error("Failed to update metadata:", result.isError);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
console.log("Metadata updated:", result.isSuccess.metadata);
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
## 📖 Referências à Documentação
|
|
1292
|
+
|
|
1293
|
+
### Documentação Principal
|
|
1294
|
+
|
|
1295
|
+
- [Visão Geral](../docs/01-visao-geral.md) - Propósito e escopo do SDK
|
|
1296
|
+
- [Contexto de Negócio](../docs/02-contexto-negocio.md) - Domínio WhatsApp Business
|
|
1297
|
+
- [Arquitetura](../docs/03-arquitetura.md) - Detalhes das 4 camadas
|
|
1298
|
+
- [Fluxos Funcionais](../docs/04-fluxos-funcionais.md) - Sequências de operações
|
|
1299
|
+
- [Integrações](../docs/05-integracoes.md) - APIs externas (Meta, Naty)
|
|
1300
|
+
- [Entidades](../docs/06-entidades.md) - Domain models documentados
|
|
1301
|
+
- [Guia Prático](../docs/07-guia-pratico.md) - Exemplos de uso
|
|
1302
|
+
- [Troubleshooting](../docs/08-troubleshooting.md) - Problemas comuns
|
|
1303
|
+
|
|
1304
|
+
### Arquivos Chave de Referência
|
|
1305
|
+
|
|
1306
|
+
- [src/index.ts](../src/index.ts) - API pública exportada
|
|
1307
|
+
- [src/interfaces/ISdk.ts](../src/interfaces/ISdk.ts) - Contrato principal do SDK
|
|
1308
|
+
- [src/errors/Either.ts](../src/errors/Either.ts) - Implementação do Either monad
|
|
1309
|
+
- [src/utils/tryCatch.ts](../src/utils/tryCatch.ts) - Try wrapper
|
|
1310
|
+
- [src/useCases/sdk/index.ts](../src/useCases/sdk/index.ts) - Classe NatyMeta
|
|
1311
|
+
- [tsconfig.json](../tsconfig.json) - Configuração TypeScript
|
|
1312
|
+
- [jest.config.js](../jest.config.js) - Configuração de testes
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1316
|
+
## 🎯 Checklist para Novos Desenvolvedores
|
|
1317
|
+
|
|
1318
|
+
Ao contribuir com o SDK, verifique:
|
|
1319
|
+
|
|
1320
|
+
- [ ] Todos métodos async retornam `Either<Error, T>`
|
|
1321
|
+
- [ ] Usou `Try` wrapper ao invés de try/catch
|
|
1322
|
+
- [ ] Entities têm campos `readonly`
|
|
1323
|
+
- [ ] Incluiu `companyId` em todas operações que acessam dados
|
|
1324
|
+
- [ ] Nomes seguem convenções (interfaces com `I`, types com `Type`, etc.)
|
|
1325
|
+
- [ ] Criou barrel export (`index.ts`) se adicionou novo módulo
|
|
1326
|
+
- [ ] Usou factory para axios instances (não criou ad-hoc)
|
|
1327
|
+
- [ ] Builders usam fluent API (retornam `this`)
|
|
1328
|
+
- [ ] Adicionou type annotations explícitas
|
|
1329
|
+
- [ ] Testou com Jest (coverage > 70%)
|
|
1330
|
+
- [ ] Documentou parâmetros complexos com JSDoc
|
|
1331
|
+
- [ ] Validou formato E.164 se aceita phone numbers (ou documentou)
|
|
1332
|
+
- [ ] Tratou casos de erro com parseError
|
|
1333
|
+
- [ ] Evitou hardcode de URLs/tokens (usou env vars)
|
|
1334
|
+
|
|
1335
|
+
---
|
|
1336
|
+
|
|
1337
|
+
## 🚀 Comandos Úteis
|
|
1338
|
+
|
|
1339
|
+
```bash
|
|
1340
|
+
# Desenvolvimento
|
|
1341
|
+
npm run dev # Inicia com nodemon (hot reload)
|
|
1342
|
+
|
|
1343
|
+
# Build
|
|
1344
|
+
npm run build # Compila TypeScript para dist/
|
|
1345
|
+
|
|
1346
|
+
# Testes
|
|
1347
|
+
npm run test # Executa Jest em watch mode
|
|
1348
|
+
npm run test:ci # Executa testes uma vez (CI/CD)
|
|
1349
|
+
|
|
1350
|
+
# Linting
|
|
1351
|
+
npm run lint # ESLint check
|
|
1352
|
+
npm run lint:fix # ESLint auto-fix
|
|
1353
|
+
|
|
1354
|
+
# Type Check
|
|
1355
|
+
npm run type-check # TypeScript compiler check (sem emitir arquivos)
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
---
|
|
1359
|
+
|
|
1360
|
+
## � Logging Guidelines
|
|
1361
|
+
|
|
1362
|
+
### Logger Integration
|
|
1363
|
+
|
|
1364
|
+
O SDK suporta logging configurável via injeção de dependência. Um logger customizado pode ser fornecido ao inicializar o SDK, caso contrário, um console logger padrão é usado.
|
|
1365
|
+
|
|
1366
|
+
#### Logger Interface
|
|
1367
|
+
|
|
1368
|
+
```typescript
|
|
1369
|
+
// src/interfaces/ILogger.ts
|
|
1370
|
+
export interface ILogger {
|
|
1371
|
+
log(
|
|
1372
|
+
level: "debug" | "info" | "warn" | "error",
|
|
1373
|
+
message: string,
|
|
1374
|
+
meta?: Record<string, any>,
|
|
1375
|
+
): void;
|
|
1376
|
+
}
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
#### Onde Usar Logger
|
|
1380
|
+
|
|
1381
|
+
| Contexto | Nível | Quando Usar |
|
|
1382
|
+
| ---------------------- | ----- | ------------------------------------------------- |
|
|
1383
|
+
| **SDK Initialization** | info | NatyMeta constructor, connect() |
|
|
1384
|
+
| **HTTP Requests** | debug | Axios interceptors (automático) |
|
|
1385
|
+
| **HTTP Responses** | debug | Axios interceptors (automático) |
|
|
1386
|
+
| **HTTP Errors** | error | Axios interceptors (automático) |
|
|
1387
|
+
| **Token Refresh** | info | WhatsappResponse.getApiInstanceToken() |
|
|
1388
|
+
| **Message Sent** | info | Após envio bem-sucedido (send_text_message, etc.) |
|
|
1389
|
+
| **Message Failed** | error | Catch blocks em métodos de envio |
|
|
1390
|
+
| **Webhook Received** | info | Webhook handlers (messages, connection) |
|
|
1391
|
+
| **Webhook Error** | error | Catch blocks em webhook handlers |
|
|
1392
|
+
| **Event Emitted** | debug | NatyEvents.emit() |
|
|
1393
|
+
| **Operation Failed** | error | Try wrapper catch block |
|
|
1394
|
+
|
|
1395
|
+
#### Padrões de Uso
|
|
1396
|
+
|
|
1397
|
+
```typescript
|
|
1398
|
+
// ✅ CORRETO - Info para operações principais
|
|
1399
|
+
this.logger.log("info", "Sending text message", {
|
|
1400
|
+
to: this.clientNumber,
|
|
1401
|
+
textLength: text.length,
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// ✅ CORRETO - Debug para detalhes técnicos
|
|
1405
|
+
this.logger.log("debug", "HTTP Request", {
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
url: "/messages",
|
|
1408
|
+
hasData: true,
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// ✅ CORRETO - Error com contexto útil
|
|
1412
|
+
this.logger.log("error", "Failed to send message", {
|
|
1413
|
+
to: this.clientNumber,
|
|
1414
|
+
error: err.message,
|
|
1415
|
+
code: err.code,
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// ❌ ERRADO - Logging sem contexto
|
|
1419
|
+
this.logger.log("error", "Error occurred");
|
|
1420
|
+
|
|
1421
|
+
// ❌ ERRADO - Expor dados sensíveis
|
|
1422
|
+
this.logger.log("debug", "Token", {
|
|
1423
|
+
accessToken: token, // ❌ Nunca logue tokens
|
|
1424
|
+
});
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
#### Segurança
|
|
1428
|
+
|
|
1429
|
+
**REGRA**: Nunca logue dados sensíveis diretamente. Use sanitização:
|
|
1430
|
+
|
|
1431
|
+
```typescript
|
|
1432
|
+
// ✅ CORRETO - Headers sanitizados
|
|
1433
|
+
function sanitizeHeaders(headers: any): any {
|
|
1434
|
+
const sanitized = { ...headers };
|
|
1435
|
+
if (sanitized.Authorization) {
|
|
1436
|
+
sanitized.Authorization = "Bearer [REDACTED]";
|
|
1437
|
+
}
|
|
1438
|
+
if (sanitized["x-access-token"]) {
|
|
1439
|
+
sanitized["x-access-token"] = "[REDACTED]";
|
|
1440
|
+
}
|
|
1441
|
+
return sanitized;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
logger.log("debug", "HTTP Request", {
|
|
1445
|
+
headers: sanitizeHeaders(config.headers),
|
|
1446
|
+
});
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
#### Acesso ao Logger
|
|
1450
|
+
|
|
1451
|
+
**Context Global:**
|
|
1452
|
+
|
|
1453
|
+
```typescript
|
|
1454
|
+
import { getGlobalLogger } from "../utils/loggerContext";
|
|
1455
|
+
|
|
1456
|
+
const logger = getGlobalLogger();
|
|
1457
|
+
logger.log("info", "Operation started");
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
**Injetado via Constructor:**
|
|
1461
|
+
|
|
1462
|
+
```typescript
|
|
1463
|
+
export class WhatsappResponse {
|
|
1464
|
+
private logger: ILogger;
|
|
1465
|
+
|
|
1466
|
+
constructor(..., logger?: ILogger) {
|
|
1467
|
+
this.logger = logger || getGlobalLogger();
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
#### Criando Adapter para Logger Externo
|
|
1473
|
+
|
|
1474
|
+
```typescript
|
|
1475
|
+
// Exemplo: Adapter para Pino
|
|
1476
|
+
export function createPinoAdapter(pinoLogger: any): ILogger {
|
|
1477
|
+
return {
|
|
1478
|
+
log(level, message, meta) {
|
|
1479
|
+
if (meta) {
|
|
1480
|
+
pinoLogger[level](meta, message);
|
|
1481
|
+
} else {
|
|
1482
|
+
pinoLogger[level](message);
|
|
1483
|
+
}
|
|
1484
|
+
},
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
#### Best Practices
|
|
1490
|
+
|
|
1491
|
+
1. **Use níveis apropriados**:
|
|
1492
|
+
- `debug`: Detalhes técnicos (HTTP, tokens refresh, state changes)
|
|
1493
|
+
- `info`: Operações principais (mensagens enviadas, webhooks recebidos)
|
|
1494
|
+
- `warn`: Situações anormais mas não críticas
|
|
1495
|
+
- `error`: Falhas que impedem operação
|
|
1496
|
+
|
|
1497
|
+
2. **Inclua contexto útil**:
|
|
1498
|
+
- IDs relevantes (`companyId`, `messageId`, `connectionId`)
|
|
1499
|
+
- Parâmetros de entrada (sanitizados)
|
|
1500
|
+
- Estado relevante antes/depois
|
|
1501
|
+
- Stack trace em errors (via meta)
|
|
1502
|
+
|
|
1503
|
+
3. **Evite noise excessivo**:
|
|
1504
|
+
- Não logue em loops high-frequency
|
|
1505
|
+
- Use `debug` para logs verbose
|
|
1506
|
+
- Considere sampling para operações muito frequentes
|
|
1507
|
+
|
|
1508
|
+
4. **Estruture metadata consistentemente**:
|
|
1509
|
+
|
|
1510
|
+
```typescript
|
|
1511
|
+
// ✅ CORRETO - Estrutura consistente
|
|
1512
|
+
logger.log("info", "Message sent", {
|
|
1513
|
+
operation: "send_message",
|
|
1514
|
+
to: phoneNumber,
|
|
1515
|
+
messageType: "text",
|
|
1516
|
+
messageId: response.id,
|
|
1517
|
+
duration: Date.now() - startTime,
|
|
1518
|
+
});
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
5. **Logger é opcional**: SDK deve funcionar sem logger customizado (fallback para console)
|
|
1522
|
+
|
|
1523
|
+
---
|
|
1524
|
+
|
|
1525
|
+
## �💡 Dicas Finais
|
|
1526
|
+
|
|
1527
|
+
1. **Leia a documentação em docs/** antes de fazer mudanças significativas
|
|
1528
|
+
2. **Consulte código existente** para entender patterns na prática
|
|
1529
|
+
3. **Either é obrigatório** - não há exceções a essa regra
|
|
1530
|
+
4. **Multi-tenancy é crítico** - nunca esqueça companyId
|
|
1531
|
+
5. **Imutabilidade ajuda debug** - entities readonly previnem bugs sutis
|
|
1532
|
+
6. **Type safety é seu amigo** - não use `any` sem justificativa forte
|
|
1533
|
+
7. **Teste seus changes** - Jest deve passar em todos os casos
|
|
1534
|
+
8. **Documente APIs públicas** - JSDoc para parâmetros não óbvios
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
**Versão**: 1.0.0
|
|
1539
|
+
**Última Atualização**: Fevereiro 2026
|
|
1540
|
+
**Mantido por**: Time @natyapp/meta
|